From 581a36245c84850616cfd837177f0fd39e85f06d Mon Sep 17 00:00:00 2001 From: kspi Date: Wed, 4 Jul 2012 22:43:31 +0300 Subject: [PATCH 001/870] Correct Lithuanian short date format. --- django/conf/locale/lt/formats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/conf/locale/lt/formats.py b/django/conf/locale/lt/formats.py index c28600b6d6..4784a1bab5 100644 --- a/django/conf/locale/lt/formats.py +++ b/django/conf/locale/lt/formats.py @@ -9,7 +9,7 @@ TIME_FORMAT = 'H:i:s' # DATETIME_FORMAT = # YEAR_MONTH_FORMAT = # MONTH_DAY_FORMAT = -SHORT_DATE_FORMAT = 'Y.m.d' +SHORT_DATE_FORMAT = 'Y-m-d' # SHORT_DATETIME_FORMAT = # FIRST_DAY_OF_WEEK = From 7055e20d24638d646457686177be9a60362f1129 Mon Sep 17 00:00:00 2001 From: Dan Loewenherz Date: Fri, 7 Sep 2012 11:18:01 -0400 Subject: [PATCH 002/870] wrap long words in field labels on admin forms, closes #18755 Otherwise, words overlap into the fields themselves, which makes the labels unreadable. --- django/contrib/admin/static/admin/css/forms.css | 1 + 1 file changed, 1 insertion(+) diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css index efec04b670..4885f62566 100644 --- a/django/contrib/admin/static/admin/css/forms.css +++ b/django/contrib/admin/static/admin/css/forms.css @@ -65,6 +65,7 @@ form ul.inline li { padding: 3px 10px 0 0; float: left; width: 8em; + word-wrap: break-word; } .aligned ul label { From 50fd96497b2129b8a350f789dfdc8ce0b0df4988 Mon Sep 17 00:00:00 2001 From: Pablo Recio Date: Fri, 14 Dec 2012 20:22:58 +0000 Subject: [PATCH 003/870] Added 'license' value to the setup.py fixes #19430 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 333d57ac70..f4545b6a0c 100644 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ setup( author_email = 'foundation@djangoproject.com', description = 'A high-level Python Web framework that encourages rapid development and clean, pragmatic design.', download_url = 'https://www.djangoproject.com/m/releases/1.4/Django-1.4.tar.gz', + license = "BSD", packages = packages, cmdclass = cmdclasses, data_files = data_files, From ac8eb82abb23f7ae50ab85100619f13257b03526 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Mon, 17 Dec 2012 10:35:36 +0100 Subject: [PATCH 004/870] Fixed typo in WidthRatioNode's error. --- django/template/defaulttags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index aca2f41f2d..eb102b4c84 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -453,7 +453,7 @@ class WidthRatioNode(Node): except VariableDoesNotExist: return '' except (ValueError, TypeError): - raise TemplateSyntaxError("widthratio final argument must be an number") + raise TemplateSyntaxError("widthratio final argument must be a number") try: value = float(value) max_value = float(max_value) From 55972ee5c799c75f2d3a320a46297076aaae614a Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 18 Dec 2012 09:56:30 +0100 Subject: [PATCH 005/870] Fixed #19441 -- Created PostgreSQL varchar index when unique=True MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Dylan Verheul for the report and Anssi Kääriäinen for the review. --- .../db/backends/postgresql_psycopg2/creation.py | 8 ++++---- tests/regressiontests/indexes/models.py | 6 ++++++ tests/regressiontests/indexes/tests.py | 16 +++++++++++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/django/db/backends/postgresql_psycopg2/creation.py b/django/db/backends/postgresql_psycopg2/creation.py index ca389b9046..90304aa566 100644 --- a/django/db/backends/postgresql_psycopg2/creation.py +++ b/django/db/backends/postgresql_psycopg2/creation.py @@ -41,7 +41,8 @@ class DatabaseCreation(BaseDatabaseCreation): return '' def sql_indexes_for_field(self, model, f, style): - if f.db_index and not f.unique: + output = [] + if f.db_index: qn = self.connection.ops.quote_name db_table = model._meta.db_table tablespace = f.db_tablespace or model._meta.db_tablespace @@ -60,7 +61,8 @@ class DatabaseCreation(BaseDatabaseCreation): "(%s%s)" % (style.SQL_FIELD(qn(f.column)), opclass) + "%s;" % tablespace_sql) - output = [get_index_sql('%s_%s' % (db_table, f.column))] + if not f.unique: + output = [get_index_sql('%s_%s' % (db_table, f.column))] # Fields with database column types of `varchar` and `text` need # a second index that specifies their operator class, which is @@ -73,8 +75,6 @@ class DatabaseCreation(BaseDatabaseCreation): elif db_type.startswith('text'): output.append(get_index_sql('%s_%s_like' % (db_table, f.column), ' text_pattern_ops')) - else: - output = [] return output def set_autocommit(self): diff --git a/tests/regressiontests/indexes/models.py b/tests/regressiontests/indexes/models.py index 9758377f99..47ba5896a8 100644 --- a/tests/regressiontests/indexes/models.py +++ b/tests/regressiontests/indexes/models.py @@ -9,3 +9,9 @@ class Article(models.Model): index_together = [ ["headline", "pub_date"], ] + + +class IndexedArticle(models.Model): + headline = models.CharField(max_length=100, db_index=True) + body = models.TextField(db_index=True) + slug = models.CharField(max_length=40, unique=True, db_index=True) diff --git a/tests/regressiontests/indexes/tests.py b/tests/regressiontests/indexes/tests.py index 0dac881fa9..f3a32a44bb 100644 --- a/tests/regressiontests/indexes/tests.py +++ b/tests/regressiontests/indexes/tests.py @@ -1,8 +1,9 @@ from django.core.management.color import no_style from django.db import connections, DEFAULT_DB_ALIAS from django.test import TestCase +from django.utils.unittest import skipUnless -from .models import Article +from .models import Article, IndexedArticle class IndexesTests(TestCase): @@ -10,3 +11,16 @@ class IndexesTests(TestCase): connection = connections[DEFAULT_DB_ALIAS] index_sql = connection.creation.sql_indexes_for_model(Article, no_style()) self.assertEqual(len(index_sql), 1) + + @skipUnless(connections[DEFAULT_DB_ALIAS].vendor == 'postgresql', + "This is a postgresql-specific issue") + def test_postgresql_text_indexes(self): + """Test creation of PostgreSQL-specific text indexes (#12234)""" + connection = connections[DEFAULT_DB_ALIAS] + index_sql = connection.creation.sql_indexes_for_model(IndexedArticle, no_style()) + self.assertEqual(len(index_sql), 5) + self.assertIn('("headline" varchar_pattern_ops)', index_sql[1]) + self.assertIn('("body" text_pattern_ops)', index_sql[3]) + # unique=True and db_index=True should only create the varchar-specific + # index (#19441). + self.assertIn('("slug" varchar_pattern_ops)', index_sql[4]) From 7df03268a467a9aec9c4c574c85317a738ca33ae Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 18 Dec 2012 06:39:23 -0500 Subject: [PATCH 006/870] Fixed #17312 - Warned about database side effects in tests. Thanks jcspray for the suggestion. --- docs/topics/testing.txt | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index 8c11e32a55..b4645c236b 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -115,8 +115,8 @@ Here is an example :class:`unittest.TestCase` subclass:: class AnimalTestCase(unittest.TestCase): def setUp(self): - self.lion = Animal.objects.create(name="lion", sound="roar") - self.cat = Animal.objects.create(name="cat", sound="meow") + self.lion = Animal(name="lion", sound="roar") + self.cat = Animal(name="cat", sound="meow") def test_animals_can_speak(self): """Animals that can speak are correctly identified""" @@ -139,6 +139,18 @@ For more details about :mod:`unittest`, see the Python documentation. .. _suggested organization: http://docs.python.org/library/unittest.html#organizing-tests +.. warning:: + + If your tests rely on database access such as creating or querying models, + be sure to create your test classes as subclasses of + :class:`django.test.TestCase` rather than :class:`unittest.TestCase`. + + In the example above, we instantiate some models but do not save them to + the database. Using :class:`unittest.TestCase` avoids the cost of running + each test in a transaction and flushing the database, but for most + applications the scope of tests you will be able to write this way will + be fairly limited, so it's easiest to use :class:`django.test.TestCase`. + Writing doctests ---------------- @@ -343,7 +355,7 @@ This convenience method sets up the test database, and puts other Django features into modes that allow for repeatable testing. The call to :meth:`~django.test.utils.setup_test_environment` is made -automatically as part of the setup of `./manage.py test`. You only +automatically as part of the setup of ``./manage.py test``. You only need to manually invoke this method if you're not using running your tests via Django's test runner. @@ -1191,6 +1203,8 @@ Normal Python unit test classes extend a base class of :width: 508 :height: 391 + Hierarchy of Django unit testing classes + Regardless of the version of Python you're using, if you've installed :mod:`unittest2`, :mod:`django.utils.unittest` will point to that library. @@ -1385,6 +1399,7 @@ attribute:: def test_my_stuff(self): # Here self.client is an instance of MyTestClient... + call_some_test_code() .. _topics-testing-fixtures: From 31f49f1396c4cc565017066e9f17c6b850a89687 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 18 Dec 2012 07:04:17 -0500 Subject: [PATCH 007/870] Fixed #19442 - Clarified that raw SQL must be committed. Thanks startup.canada for the suggestion. --- docs/topics/db/transactions.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 65944abb8b..e3c2cadf6d 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -199,7 +199,8 @@ Requirements for transaction handling Django requires that every transaction that is opened is closed before the completion of a request. If you are using :func:`autocommit` (the default commit mode) or :func:`commit_on_success`, this will be done -for you automatically. However, if you are manually managing +for you automatically (with the exception of :ref:`executing custom SQL +`). However, if you are manually managing transactions (using the :func:`commit_manually` decorator), you must ensure that the transaction is either committed or rolled back before a request is completed. From 6534a95ac3142ff79f8152b0d5dcbf9330d8abde Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 18 Dec 2012 06:52:30 -0500 Subject: [PATCH 008/870] Fixed #19470 - Clarified widthratio example. Thanks orblivion for the suggestion. --- django/template/defaulttags.py | 8 ++++---- docs/ref/templates/builtins.txt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index aca2f41f2d..b5c8cf2d36 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -1319,11 +1319,11 @@ def widthratio(parser, token): For example:: - + - Above, if ``this_value`` is 175 and ``max_value`` is 200, the image in - the above example will be 88 pixels wide (because 175/200 = .875; - .875 * 100 = 87.5 which is rounded up to 88). + If ``this_value`` is 175, ``max_value`` is 200, and ``max_width`` is 100, + the image in the above example will be 88 pixels wide + (because 175/200 = .875; .875 * 100 = 87.5 which is rounded up to 88). """ bits = token.contents.split() if len(bits) != 4: diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 57ef0cfb27..dd288ababc 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -1079,11 +1079,11 @@ value to a maximum value, and then applies that ratio to a constant. For example:: Bar + height="10" width="{% widthratio this_value max_value max_width %}" /> -Above, if ``this_value`` is 175 and ``max_value`` is 200, the image in the -above example will be 88 pixels wide (because 175/200 = .875; .875 * 100 = 87.5 -which is rounded up to 88). +If ``this_value`` is 175, ``max_value`` is 200, and ``max_width`` is 100, the +image in the above example will be 88 pixels wide +(because 175/200 = .875; .875 * 100 = 87.5 which is rounded up to 88). .. templatetag:: with From abd0f304b162b3120b1c7321fbfc3090e5f3c92c Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Wed, 19 Dec 2012 15:07:52 -0300 Subject: [PATCH 009/870] Added PASSWORD_HASHERS to settings reference document. --- docs/ref/settings.txt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index daa4ee9a46..135cddae25 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1450,6 +1450,25 @@ format has higher precedence and will be applied instead. See also :setting:`DECIMAL_SEPARATOR`, :setting:`THOUSAND_SEPARATOR` and :setting:`USE_THOUSAND_SEPARATOR`. +.. setting:: PASSWORD_HASHERS + +PASSWORD_HASHERS +---------------- + +.. versionadded:: 1.4 + +See :ref:`auth_password_storage`. + +Default:: + + ('django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.BCryptPasswordHasher', + 'django.contrib.auth.hashers.SHA1PasswordHasher', + 'django.contrib.auth.hashers.MD5PasswordHasher', + 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', + 'django.contrib.auth.hashers.CryptPasswordHasher',) + .. setting:: PASSWORD_RESET_TIMEOUT_DAYS PASSWORD_RESET_TIMEOUT_DAYS From 3989ce52ef78840eefe01541628daa220191c0ad Mon Sep 17 00:00:00 2001 From: Patryk Zawadzki Date: Wed, 19 Dec 2012 19:12:08 +0100 Subject: [PATCH 010/870] Fixed #18172 -- Made models with __iter__ usable in ModelMultipleChoiceField Thanks to Patryk Zawadzki for the patch. --- django/forms/models.py | 4 +++- tests/modeltests/model_forms/models.py | 15 +++++++++++++++ tests/modeltests/model_forms/tests.py | 16 +++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/django/forms/models.py b/django/forms/models.py index e9b71ccf26..0913a4e8b8 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -1033,6 +1033,8 @@ class ModelMultipleChoiceField(ModelChoiceField): return qs def prepare_value(self, value): - if hasattr(value, '__iter__') and not isinstance(value, six.text_type): + if (hasattr(value, '__iter__') and + not isinstance(value, six.text_type) and + not hasattr(value, '_meta')): return [super(ModelMultipleChoiceField, self).prepare_value(v) for v in value] return super(ModelMultipleChoiceField, self).prepare_value(value) diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index 132da2d318..383a10584b 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -263,3 +263,18 @@ class FlexibleDatePost(models.Model): slug = models.CharField(max_length=50, unique_for_year='posted', blank=True) subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True) posted = models.DateField(blank=True, null=True) + +@python_2_unicode_compatible +class Colour(models.Model): + name = models.CharField(max_length=50) + + def __iter__(self): + for number in xrange(5): + yield number + + def __str__(self): + return self.name + +class ColourfulItem(models.Model): + name = models.CharField(max_length=50) + colours = models.ManyToManyField(Colour) diff --git a/tests/modeltests/model_forms/tests.py b/tests/modeltests/model_forms/tests.py index 9699b155c0..38fc9d058c 100644 --- a/tests/modeltests/model_forms/tests.py +++ b/tests/modeltests/model_forms/tests.py @@ -19,7 +19,8 @@ from .models import (Article, ArticleStatus, BetterWriter, BigInt, Book, Category, CommaSeparatedInteger, CustomFieldForExclusionModel, DerivedBook, DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle, ImprovedArticleWithParentLink, Inventory, Post, Price, - Product, TextFile, Writer, WriterProfile, test_images) + Product, TextFile, Writer, WriterProfile, Colour, ColourfulItem, + test_images) if test_images: from .models import ImageFile, OptionalImageFile @@ -174,6 +175,10 @@ class PriceFormWithoutQuantity(forms.ModelForm): model = Price exclude = ('quantity',) +class ColourfulItemForm(forms.ModelForm): + class Meta: + model = ColourfulItem + class ModelFormBaseTest(TestCase): def test_base_form(self): @@ -1518,3 +1523,12 @@ class OldFormForXTests(TestCase): ['name']) self.assertHTMLEqual(six.text_type(CustomFieldForExclusionForm()), '''''') + + def test_iterable_model_m2m(self) : + colour = Colour.objects.create(name='Blue') + form = ColourfulItemForm() + self.maxDiff = 1024 + self.assertHTMLEqual(form.as_p(), """

+

Hold down "Control", or "Command" on a Mac, to select more than one.

""") From c04c03daa3620dc5106740a976738ada35203ab5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 20 Dec 2012 16:10:19 +0800 Subject: [PATCH 011/870] Fixed #19401 -- Ensure that swappable model references are case insensitive. This is necessary because get_model() checks are case insensitive, and if the swapable check isn't, the swappable logic gets tied up in knots with models that are partially swapped out. Thanks to chris@cogdon.org for the report and extensive analysis, and Preston for his work on the draft patch. --- django/db/models/options.py | 19 ++++++++++++++++--- .../regressiontests/swappable_models/tests.py | 12 ++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/django/db/models/options.py b/django/db/models/options.py index f74d9ffff8..45b32b0ea4 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -213,12 +213,25 @@ class Options(object): """ Has this model been swapped out for another? If so, return the model name of the replacement; otherwise, return None. + + For historical reasons, model name lookups using get_model() are + case insensitive, so we make sure we are case insensitive here. """ if self.swappable: - model_label = '%s.%s' % (self.app_label, self.object_name) + model_label = '%s.%s' % (self.app_label, self.object_name.lower()) swapped_for = getattr(settings, self.swappable, None) - if swapped_for not in (None, model_label): - return swapped_for + if swapped_for: + try: + swapped_label, swapped_object = swapped_for.split('.') + except ValueError: + # setting not in the format app_label.model_name + # raising ImproperlyConfigured here causes problems with + # test cleanup code - instead it is raised in get_user_model + # or as part of validation. + return swapped_for + + if '%s.%s' % (swapped_label, swapped_object.lower()) not in (None, model_label): + return swapped_for return None swapped = property(_swapped) diff --git a/tests/regressiontests/swappable_models/tests.py b/tests/regressiontests/swappable_models/tests.py index 75644e32aa..089b391416 100644 --- a/tests/regressiontests/swappable_models/tests.py +++ b/tests/regressiontests/swappable_models/tests.py @@ -9,6 +9,8 @@ from django.db.models.loading import cache from django.test import TestCase from django.test.utils import override_settings +from regressiontests.swappable_models.models import Article + class SwappableModelTests(TestCase): def setUp(self): @@ -44,3 +46,13 @@ class SwappableModelTests(TestCase): for ct in ContentType.objects.all()] self.assertIn(('swappable_models', 'alternatearticle'), apps_models) self.assertNotIn(('swappable_models', 'article'), apps_models) + + @override_settings(TEST_ARTICLE_MODEL='swappable_models.article') + def test_case_insensitive(self): + "Model names are case insensitive. Check that model swapping honors this." + try: + Article.objects.all() + except AttributeError: + self.fail('Swappable model names should be case insensitive.') + + self.assertIsNone(Article._meta.swapped) From 3dcd435a0eb4380a3846e9c65140df480975687e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Thu, 20 Dec 2012 21:25:48 +0200 Subject: [PATCH 012/870] Fixed #19500 -- Solved a regression in join reuse The ORM didn't reuse joins for direct foreign key traversals when using chained filters. For example: qs.filter(fk__somefield=1).filter(fk__somefield=2)) produced two joins. As a bonus, reverse onetoone filters can now reuse joins correctly The regression was caused by the join() method refactor in commit 68847135bc9acb2c51c2d36797d0a85395f0cd35 Thanks for Simon Charette for spotting some issues with the first draft of the patch. --- django/db/models/sql/compiler.py | 8 ++-- django/db/models/sql/constants.py | 3 -- django/db/models/sql/expressions.py | 5 +-- django/db/models/sql/query.py | 39 +++++++++---------- django/db/models/sql/subqueries.py | 1 - .../generic_relations_regress/tests.py | 8 +++- tests/regressiontests/queries/tests.py | 33 ++++++++++++++++ 7 files changed, 65 insertions(+), 32 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 4d846fb438..4a12d74452 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -6,7 +6,7 @@ from django.db.backends.util import truncate_name from django.db.models.constants import LOOKUP_SEP from django.db.models.query_utils import select_related_descend from django.db.models.sql.constants import (SINGLE, MULTI, ORDER_DIR, - GET_ITERATOR_CHUNK_SIZE, REUSE_ALL, SelectInfo) + GET_ITERATOR_CHUNK_SIZE, SelectInfo) from django.db.models.sql.datastructures import EmptyResultSet from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.query import get_order_dir, Query @@ -317,7 +317,7 @@ class SQLCompiler(object): for name in self.query.distinct_fields: parts = name.split(LOOKUP_SEP) - field, col, alias, _, _ = self._setup_joins(parts, opts, None) + field, col, alias, _, _ = self._setup_joins(parts, opts) col, alias = self._final_join_removal(col, alias) result.append("%s.%s" % (qn(alias), qn2(col))) return result @@ -450,7 +450,7 @@ class SQLCompiler(object): if not alias: alias = self.query.get_initial_alias() field, target, opts, joins, _ = self.query.setup_joins( - pieces, opts, alias, REUSE_ALL) + pieces, opts, alias) # We will later on need to promote those joins that were added to the # query afresh above. joins_to_promote = [j for j in joins if self.query.alias_refcount[j] < 2] @@ -688,7 +688,7 @@ class SQLCompiler(object): int_opts = int_model._meta alias = self.query.join( (alias, int_opts.db_table, lhs_col, int_opts.pk.column), - promote=True, + promote=True ) alias_chain.append(alias) alias = self.query.join( diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py index 1c34f70169..9f82f426ed 100644 --- a/django/db/models/sql/constants.py +++ b/django/db/models/sql/constants.py @@ -44,6 +44,3 @@ ORDER_DIR = { 'ASC': ('ASC', 'DESC'), 'DESC': ('DESC', 'ASC'), } - -# A marker for join-reusability. -REUSE_ALL = object() diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py index af7e45e74e..a4c1d85c65 100644 --- a/django/db/models/sql/expressions.py +++ b/django/db/models/sql/expressions.py @@ -1,10 +1,9 @@ from django.core.exceptions import FieldError from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import FieldDoesNotExist -from django.db.models.sql.constants import REUSE_ALL class SQLEvaluator(object): - def __init__(self, expression, query, allow_joins=True, reuse=REUSE_ALL): + def __init__(self, expression, query, allow_joins=True, reuse=None): self.expression = expression self.opts = query.get_meta() self.cols = [] @@ -54,7 +53,7 @@ class SQLEvaluator(object): field_list, query.get_meta(), query.get_initial_alias(), self.reuse) col, _, join_list = query.trim_joins(source, join_list, path) - if self.reuse is not None and self.reuse != REUSE_ALL: + if self.reuse is not None: self.reuse.update(join_list) self.cols.append((node, (join_list[-1], col))) except FieldDoesNotExist: diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index ff56211c5d..a030112e75 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -20,7 +20,7 @@ from django.db.models.fields import FieldDoesNotExist from django.db.models.loading import get_model from django.db.models.sql import aggregates as base_aggregates_module from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE, - ORDER_PATTERN, REUSE_ALL, JoinInfo, SelectInfo, PathInfo) + ORDER_PATTERN, JoinInfo, SelectInfo, PathInfo) from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode, @@ -891,7 +891,7 @@ class Query(object): """ return len([1 for count in self.alias_refcount.values() if count]) - def join(self, connection, reuse=REUSE_ALL, promote=False, + def join(self, connection, reuse=None, promote=False, outer_if_first=False, nullable=False, join_field=None): """ Returns an alias for the join in 'connection', either reusing an @@ -902,10 +902,9 @@ class Query(object): lhs.lhs_col = table.col - The 'reuse' parameter can be used in three ways: it can be REUSE_ALL - which means all joins (matching the connection) are reusable, it can - be a set containing the aliases that can be reused, or it can be None - which means a new join is always created. + The 'reuse' parameter can be either None which means all joins + (matching the connection) are reusable, or it can be a set containing + the aliases that can be reused. If 'promote' is True, the join type for the alias will be LOUTER (if the alias previously existed, the join type will be promoted from INNER @@ -926,10 +925,8 @@ class Query(object): """ lhs, table, lhs_col, col = connection existing = self.join_map.get(connection, ()) - if reuse == REUSE_ALL: + if reuse is None: reuse = existing - elif reuse is None: - reuse = set() else: reuse = [a for a in existing if a in reuse] for alias in reuse: @@ -1040,7 +1037,7 @@ class Query(object): # then we need to explore the joins that are required. field, source, opts, join_list, path = self.setup_joins( - field_list, opts, self.get_initial_alias(), REUSE_ALL) + field_list, opts, self.get_initial_alias()) # Process the join chain to see if it can be trimmed col, _, join_list = self.trim_joins(source, join_list, path) @@ -1441,7 +1438,7 @@ class Query(object): raise MultiJoin(multijoin_pos + 1) return path, final_field, target - def setup_joins(self, names, opts, alias, can_reuse, allow_many=True, + def setup_joins(self, names, opts, alias, can_reuse=None, allow_many=True, allow_explicit_fk=False): """ Compute the necessary table joins for the passage through the fields @@ -1450,9 +1447,9 @@ class Query(object): the table to start the joining from. The 'can_reuse' defines the reverse foreign key joins we can reuse. It - can be sql.constants.REUSE_ALL in which case all joins are reusable - or a set of aliases that can be reused. Note that Non-reverse foreign - keys are always reusable. + can be None in which case all joins are reusable or a set of aliases + that can be reused. Note that non-reverse foreign keys are always + reusable when using setup_joins(). If 'allow_many' is False, then any reverse foreign key seen will generate a MultiJoin exception. @@ -1485,8 +1482,9 @@ class Query(object): else: nullable = True connection = alias, opts.db_table, from_field.column, to_field.column - alias = self.join(connection, reuse=can_reuse, nullable=nullable, - join_field=join_field) + reuse = None if direct or to_field.unique else can_reuse + alias = self.join(connection, reuse=reuse, + nullable=nullable, join_field=join_field) joins.append(alias) return final_field, target, opts, joins, path @@ -1643,7 +1641,7 @@ class Query(object): try: for name in field_names: field, target, u2, joins, u3 = self.setup_joins( - name.split(LOOKUP_SEP), opts, alias, REUSE_ALL, allow_m2m, + name.split(LOOKUP_SEP), opts, alias, None, allow_m2m, True) final_alias = joins[-1] col = target.column @@ -1729,8 +1727,9 @@ class Query(object): else: opts = self.model._meta if not self.select: - count = self.aggregates_module.Count((self.join((None, opts.db_table, None, None)), opts.pk.column), - is_summary=True, distinct=True) + count = self.aggregates_module.Count( + (self.join((None, opts.db_table, None, None)), opts.pk.column), + is_summary=True, distinct=True) else: # Because of SQL portability issues, multi-column, distinct # counts need a sub-query -- see get_count() for details. @@ -1934,7 +1933,7 @@ class Query(object): opts = self.model._meta alias = self.get_initial_alias() field, col, opts, joins, extra = self.setup_joins( - start.split(LOOKUP_SEP), opts, alias, REUSE_ALL) + start.split(LOOKUP_SEP), opts, alias) select_col = self.alias_map[joins[1]].lhs_join_col select_alias = alias diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 39d1ee0116..6072804697 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -232,7 +232,6 @@ class DateQuery(Query): field_name.split(LOOKUP_SEP), self.get_meta(), self.get_initial_alias(), - False ) except FieldError: raise FieldDoesNotExist("%s has no field named '%s'" % ( diff --git a/tests/regressiontests/generic_relations_regress/tests.py b/tests/regressiontests/generic_relations_regress/tests.py index 4c0f024433..262c2e4917 100644 --- a/tests/regressiontests/generic_relations_regress/tests.py +++ b/tests/regressiontests/generic_relations_regress/tests.py @@ -72,5 +72,11 @@ class GenericRelationTests(TestCase): Q(notes__note__icontains=r'other note')) self.assertTrue(org_contact in qs) - + def test_join_reuse(self): + qs = Person.objects.filter( + addresses__street='foo' + ).filter( + addresses__street='bar' + ) + self.assertEqual(str(qs.query).count('JOIN'), 2) diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py index 75e27769b4..9270955877 100644 --- a/tests/regressiontests/queries/tests.py +++ b/tests/regressiontests/queries/tests.py @@ -2418,3 +2418,36 @@ class ReverseJoinTrimmingTest(TestCase): qs = Tag.objects.filter(annotation__tag=t.pk) self.assertIn('INNER JOIN', str(qs.query)) self.assertEquals(list(qs), []) + +class JoinReuseTest(TestCase): + """ + Test that the queries reuse joins sensibly (for example, direct joins + are always reused). + """ + def test_fk_reuse(self): + qs = Annotation.objects.filter(tag__name='foo').filter(tag__name='bar') + self.assertEqual(str(qs.query).count('JOIN'), 1) + + def test_fk_reuse_select_related(self): + qs = Annotation.objects.filter(tag__name='foo').select_related('tag') + self.assertEqual(str(qs.query).count('JOIN'), 1) + + def test_fk_reuse_annotation(self): + qs = Annotation.objects.filter(tag__name='foo').annotate(cnt=Count('tag__name')) + self.assertEqual(str(qs.query).count('JOIN'), 1) + + def test_fk_reuse_disjunction(self): + qs = Annotation.objects.filter(Q(tag__name='foo') | Q(tag__name='bar')) + self.assertEqual(str(qs.query).count('JOIN'), 1) + + def test_fk_reuse_order_by(self): + qs = Annotation.objects.filter(tag__name='foo').order_by('tag__name') + self.assertEqual(str(qs.query).count('JOIN'), 1) + + def test_revo2o_reuse(self): + qs = Detail.objects.filter(member__name='foo').filter(member__name='foo') + self.assertEqual(str(qs.query).count('JOIN'), 1) + + def test_revfk_noreuse(self): + qs = Author.objects.filter(report__name='r4').filter(report__name='r1') + self.assertEqual(str(qs.query).count('JOIN'), 2) From d407164c0499c234ec043f5720b3209311b2f4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sat, 25 Aug 2012 14:13:37 +0300 Subject: [PATCH 013/870] Fixed #18854 -- Join promotion in disjunction cases The added promotion logic is based on promoting any joins used in only some of the childs of an OR clause unless the join existed before the OR clause addition. --- django/db/models/sql/query.py | 106 +++++++++---------- tests/regressiontests/queries/models.py | 18 ++++ tests/regressiontests/queries/tests.py | 130 +++++++++++++++++++++++- 3 files changed, 195 insertions(+), 59 deletions(-) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index a030112e75..c71bc634aa 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -772,17 +772,37 @@ class Query(object): unref_amount = cur_refcount - to_counts.get(alias, 0) self.unref_alias(alias, unref_amount) - def promote_unused_aliases(self, initial_refcounts, used_aliases): + def promote_disjunction(self, aliases_before, alias_usage_counts, + num_childs): """ - Given a "before" copy of the alias_refcounts dictionary (as - 'initial_refcounts') and a collection of aliases that may have been - changed or created, works out which aliases have been created since - then and which ones haven't been used and promotes all of those - aliases, plus any children of theirs in the alias tree, to outer joins. + This method is to be used for promoting joins in ORed filters. + + The principle for promotion is: any alias which is used (it is in + alias_usage_counts), is not used by every child of the ORed filter, + and isn't pre-existing needs to be promoted to LOUTER join. + + Some examples (assume all joins used are nullable): + - existing filter: a__f1=foo + - add filter: b__f1=foo|b__f2=foo + In this case we should not promote either of the joins (using INNER + doesn't remove results). We correctly avoid join promotion, because + a is not used in this branch, and b is used two times. + + - add filter a__f1=foo|b__f2=foo + In this case we should promote both a and b, otherwise they will + remove results. We will also correctly do that as both aliases are + used, and in addition both are used only once while there are two + filters. + + - existing: a__f1=bar + - add filter: a__f2=foo|b__f2=foo + We will not promote a as it is previously used. If the join results + in null, the existing filter can't succeed. + + The above (and some more) are tested in queries.DisjunctionPromotionTests """ - for alias in self.tables: - if alias in used_aliases and (alias not in initial_refcounts or - self.alias_refcount[alias] == initial_refcounts[alias]): + for alias, use_count in alias_usage_counts.items(): + if use_count < num_childs and alias not in aliases_before: self.promote_joins([alias]) def change_aliases(self, change_map): @@ -1150,16 +1170,12 @@ class Query(object): can_reuse) return - table_promote = False - join_promote = False - if (lookup_type == 'isnull' and value is True and not negate and len(join_list) > 1): # If the comparison is against NULL, we may need to use some left # outer joins when creating the join chain. This is only done when # needed, as it's less efficient at the database level. self.promote_joins(join_list) - join_promote = True # Process the join list to see if we can remove any inner joins from # the far end (fewer tables in a query is better). Note that join @@ -1167,39 +1183,6 @@ class Query(object): # information available when reusing joins. col, alias, join_list = self.trim_joins(target, join_list, path) - if connector == OR: - # Some joins may need to be promoted when adding a new filter to a - # disjunction. We walk the list of new joins and where it diverges - # from any previous joins (ref count is 1 in the table list), we - # make the new additions (and any existing ones not used in the new - # join list) an outer join. - join_it = iter(join_list) - table_it = iter(self.tables) - next(join_it), next(table_it) - unconditional = False - for join in join_it: - table = next(table_it) - # Once we hit an outer join, all subsequent joins must - # also be promoted, regardless of whether they have been - # promoted as a result of this pass through the tables. - unconditional = (unconditional or - self.alias_map[join].join_type == self.LOUTER) - if join == table and self.alias_refcount[join] > 1: - # We have more than one reference to this join table. - # This means that we are dealing with two different query - # subtrees, so we don't need to do any join promotion. - continue - join_promote = join_promote or self.promote_joins([join], unconditional) - if table != join: - table_promote = self.promote_joins([table]) - # We only get here if we have found a table that exists - # in the join list, but isn't on the original tables list. - # This means we've reached the point where we only have - # new tables, so we can break out of this promotion loop. - break - self.promote_joins(join_it, join_promote) - self.promote_joins(table_it, table_promote or join_promote) - if having_clause or force_having: if (alias, col) not in self.group_by: self.group_by.append((alias, col)) @@ -1256,33 +1239,36 @@ class Query(object): subtree = True else: subtree = False - connector = AND + connector = q_object.connector + if connector == OR: + alias_usage_counts = dict() + aliases_before = set(self.tables) if q_object.connector == OR and not force_having: force_having = self.need_force_having(q_object) for child in q_object.children: - if connector == OR: - refcounts_before = self.alias_refcount.copy() if force_having: self.having.start_subtree(connector) else: self.where.start_subtree(connector) + if connector == OR: + refcounts_before = self.alias_refcount.copy() if isinstance(child, Node): self.add_q(child, used_aliases, force_having=force_having) else: self.add_filter(child, connector, q_object.negated, can_reuse=used_aliases, force_having=force_having) + if connector == OR: + used = alias_diff(refcounts_before, self.alias_refcount) + for alias in used: + alias_usage_counts[alias] = alias_usage_counts.get(alias, 0) + 1 if force_having: self.having.end_subtree() else: self.where.end_subtree() - if connector == OR: - # Aliases that were newly added or not used at all need to - # be promoted to outer joins if they are nullable relations. - # (they shouldn't turn the whole conditional into the empty - # set just because they don't match anything). - self.promote_unused_aliases(refcounts_before, used_aliases) - connector = q_object.connector + if connector == OR: + self.promote_disjunction(aliases_before, alias_usage_counts, + len(q_object.children)) if q_object.negated: self.where.negate() if subtree: @@ -2005,3 +1991,11 @@ def is_reverse_o2o(field): expected to be some sort of relation field or related object. """ return not hasattr(field, 'rel') and field.field.unique + +def alias_diff(refcounts_before, refcounts_after): + """ + Given the before and after copies of refcounts works out which aliases + have been added to the after copy. + """ + return set(t for t in refcounts_after + if refcounts_after[t] > refcounts_before.get(t, 0)) diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index 73b9762150..16583e891c 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -421,3 +421,21 @@ class Responsibility(models.Model): def __str__(self): return self.description + +# Models for disjunction join promotion low level testing. +class FK1(models.Model): + f1 = models.TextField() + f2 = models.TextField() + +class FK2(models.Model): + f1 = models.TextField() + f2 = models.TextField() + +class FK3(models.Model): + f1 = models.TextField() + f2 = models.TextField() + +class BaseA(models.Model): + a = models.ForeignKey(FK1, null=True) + b = models.ForeignKey(FK2, null=True) + c = models.ForeignKey(FK3, null=True) diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py index 9270955877..e3e515025c 100644 --- a/tests/regressiontests/queries/tests.py +++ b/tests/regressiontests/queries/tests.py @@ -8,8 +8,8 @@ import sys from django.conf import settings from django.core.exceptions import FieldError from django.db import DatabaseError, connection, connections, DEFAULT_DB_ALIAS -from django.db.models import Count -from django.db.models.query import Q, ITER_CHUNK_SIZE, EmptyQuerySet +from django.db.models import Count, F, Q +from django.db.models.query import ITER_CHUNK_SIZE, EmptyQuerySet from django.db.models.sql.where import WhereNode, EverythingNode, NothingNode from django.db.models.sql.datastructures import EmptyResultSet from django.test import TestCase, skipUnlessDBFeature @@ -24,7 +24,7 @@ from .models import (Annotation, Article, Author, Celebrity, Child, Cover, Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory, SpecialCategory, OneToOneCategory, NullableName, ProxyCategory, SingleObject, RelatedObject, ModelA, ModelD, Responsibility, Job, - JobResponsibilities) + JobResponsibilities, BaseA) class BaseQuerysetTest(TestCase): @@ -2451,3 +2451,127 @@ class JoinReuseTest(TestCase): def test_revfk_noreuse(self): qs = Author.objects.filter(report__name='r4').filter(report__name='r1') self.assertEqual(str(qs.query).count('JOIN'), 2) + +class DisjunctionPromotionTests(TestCase): + def test_disjunction_promotion1(self): + # Pre-existing join, add two ORed filters to the same join, + # all joins can be INNER JOINS. + qs = BaseA.objects.filter(a__f1='foo') + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + qs = qs.filter(Q(b__f1='foo') | Q(b__f2='foo')) + self.assertEqual(str(qs.query).count('INNER JOIN'), 2) + # Reverse the order of AND and OR filters. + qs = BaseA.objects.filter(Q(b__f1='foo') | Q(b__f2='foo')) + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + qs = qs.filter(a__f1='foo') + self.assertEqual(str(qs.query).count('INNER JOIN'), 2) + + def test_disjunction_promotion2(self): + qs = BaseA.objects.filter(a__f1='foo') + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + # Now we have two different joins in an ORed condition, these + # must be OUTER joins. The pre-existing join should remain INNER. + qs = qs.filter(Q(b__f1='foo') | Q(c__f2='foo')) + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 2) + # Reverse case. + qs = BaseA.objects.filter(Q(b__f1='foo') | Q(c__f2='foo')) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 2) + qs = qs.filter(a__f1='foo') + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 2) + + def test_disjunction_promotion3(self): + qs = BaseA.objects.filter(a__f2='bar') + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + # The ANDed a__f2 filter allows us to use keep using INNER JOIN + # even inside the ORed case. If the join to a__ returns nothing, + # the ANDed filter for a__f2 can't be true. + qs = qs.filter(Q(a__f1='foo') | Q(b__f2='foo')) + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1) + + @unittest.expectedFailure + def test_disjunction_promotion3_failing(self): + # Now the ORed filter creates LOUTER join, but we do not have + # logic to unpromote it for the AND filter after it. The query + # results will be correct, but we have one LOUTER JOIN too much + # currently. + qs = BaseA.objects.filter( + Q(a__f1='foo') | Q(b__f2='foo')).filter(a__f2='bar') + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1) + + def test_disjunction_promotion4(self): + qs = BaseA.objects.filter(Q(a=1) | Q(a=2)) + self.assertEqual(str(qs.query).count('JOIN'), 0) + qs = qs.filter(a__f1='foo') + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + qs = BaseA.objects.filter(a__f1='foo') + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + qs = qs.filter(Q(a=1) | Q(a=2)) + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + + def test_disjunction_promotion5(self): + qs = BaseA.objects.filter(Q(a=1) | Q(a=2)) + # Note that the above filters on a force the join to an + # inner join even if it is trimmed. + self.assertEqual(str(qs.query).count('JOIN'), 0) + qs = qs.filter(Q(a__f1='foo') | Q(b__f1='foo')) + # So, now the a__f1 join doesn't need promotion. + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1) + + @unittest.expectedFailure + def test_disjunction_promotion5_failing(self): + qs = BaseA.objects.filter(Q(a__f1='foo') | Q(b__f1='foo')) + # Now the join to a is created as LOUTER + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 0) + # The below filter should force the a to be inner joined. But, + # this is failing as we do not have join unpromotion logic. + qs = BaseA.objects.filter(Q(a=1) | Q(a=2)) + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1) + + def test_disjunction_promotion6(self): + qs = BaseA.objects.filter(Q(a=1) | Q(a=2)) + self.assertEqual(str(qs.query).count('JOIN'), 0) + qs = BaseA.objects.filter(Q(a__f1='foo') & Q(b__f1='foo')) + self.assertEqual(str(qs.query).count('INNER JOIN'), 2) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 0) + + qs = BaseA.objects.filter(Q(a__f1='foo') & Q(b__f1='foo')) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 0) + self.assertEqual(str(qs.query).count('INNER JOIN'), 2) + qs = qs.filter(Q(a=1) | Q(a=2)) + self.assertEqual(str(qs.query).count('INNER JOIN'), 2) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 0) + + def test_disjunction_promotion7(self): + qs = BaseA.objects.filter(Q(a=1) | Q(a=2)) + self.assertEqual(str(qs.query).count('JOIN'), 0) + qs = BaseA.objects.filter(Q(a__f1='foo') | (Q(b__f1='foo') & Q(a__f1='bar'))) + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1) + qs = BaseA.objects.filter( + (Q(a__f1='foo') | Q(b__f1='foo')) & (Q(a__f1='bar') | Q(c__f1='foo')) + ) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 3) + self.assertEqual(str(qs.query).count('INNER JOIN'), 0) + qs = BaseA.objects.filter( + (Q(a__f1='foo') | (Q(a__f1='bar')) & (Q(b__f1='bar') | Q(c__f1='foo'))) + ) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 2) + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + + def test_disjunction_promotion_fexpression(self): + qs = BaseA.objects.filter(Q(a__f1=F('b__f1')) | Q(b__f1='foo')) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1) + self.assertEqual(str(qs.query).count('INNER JOIN'), 1) + qs = BaseA.objects.filter(Q(a__f1=F('c__f1')) | Q(b__f1='foo')) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 3) + qs = BaseA.objects.filter(Q(a__f1=F('b__f1')) | Q(a__f2=F('b__f2')) | Q(c__f1='foo')) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 3) + qs = BaseA.objects.filter(Q(a__f1=F('c__f1')) | (Q(pk=1) & Q(pk=2))) + self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 2) + self.assertEqual(str(qs.query).count('INNER JOIN'), 0) From 4007c8f6eb2067ba18da24c4a0fb97d392a76817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Thu, 20 Dec 2012 22:50:06 +0200 Subject: [PATCH 014/870] Fixed a regression in distinct_on Caused by regression fix for #19500. --- django/db/models/sql/compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 4a12d74452..d0e791ba20 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -317,7 +317,7 @@ class SQLCompiler(object): for name in self.query.distinct_fields: parts = name.split(LOOKUP_SEP) - field, col, alias, _, _ = self._setup_joins(parts, opts) + field, col, alias, _, _ = self._setup_joins(parts, opts, None) col, alias = self._final_join_removal(col, alias) result.append("%s.%s" % (qn(alias), qn2(col))) return result From e277301c2c96915ba1408d57a866bc65e51de95a Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 21 Dec 2012 10:01:03 +0100 Subject: [PATCH 015/870] Fixed #19387 -- Preserved SafeData status in contrib.messages Thanks Anton Baklanov for the report and the patch. --- django/contrib/messages/storage/cookie.py | 9 +++++++-- django/contrib/messages/tests/cookie.py | 19 +++++++++++++++++++ django/contrib/messages/tests/session.py | 14 ++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/django/contrib/messages/storage/cookie.py b/django/contrib/messages/storage/cookie.py index 6b5b016234..619c69249e 100644 --- a/django/contrib/messages/storage/cookie.py +++ b/django/contrib/messages/storage/cookie.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib.messages.storage.base import BaseStorage, Message from django.http import SimpleCookie from django.utils.crypto import salted_hmac, constant_time_compare +from django.utils.safestring import SafeData, mark_safe from django.utils import six @@ -15,7 +16,9 @@ class MessageEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, Message): - message = [self.message_key, obj.level, obj.message] + # Using 0/1 here instead of False/True to produce more compact json + is_safedata = 1 if isinstance(obj.message, SafeData) else 0 + message = [self.message_key, is_safedata, obj.level, obj.message] if obj.extra_tags: message.append(obj.extra_tags) return message @@ -30,7 +33,9 @@ class MessageDecoder(json.JSONDecoder): def process_messages(self, obj): if isinstance(obj, list) and obj: if obj[0] == MessageEncoder.message_key: - return Message(*obj[1:]) + if obj[1]: + obj[3] = mark_safe(obj[3]) + return Message(*obj[2:]) return [self.process_messages(item) for item in obj] if isinstance(obj, dict): return dict([(key, self.process_messages(value)) diff --git a/django/contrib/messages/tests/cookie.py b/django/contrib/messages/tests/cookie.py index e0668ab604..77e4ece091 100644 --- a/django/contrib/messages/tests/cookie.py +++ b/django/contrib/messages/tests/cookie.py @@ -6,6 +6,7 @@ from django.contrib.messages.storage.cookie import (CookieStorage, MessageEncoder, MessageDecoder) from django.contrib.messages.storage.base import Message from django.test.utils import override_settings +from django.utils.safestring import SafeData, mark_safe def set_cookie_data(storage, messages, invalid=False, encode_empty=False): @@ -132,3 +133,21 @@ class CookieTest(BaseTest): value = encoder.encode(messages) decoded_messages = json.loads(value, cls=MessageDecoder) self.assertEqual(messages, decoded_messages) + + def test_safedata(self): + """ + Tests that a message containing SafeData is keeping its safe status when + retrieved from the message storage. + """ + def encode_decode(data): + message = Message(constants.DEBUG, data) + encoded = storage._encode(message) + decoded = storage._decode(encoded) + return decoded.message + + storage = self.get_storage() + + self.assertIsInstance( + encode_decode(mark_safe("Hello Django!")), SafeData) + self.assertNotIsInstance( + encode_decode("Hello Django!"), SafeData) diff --git a/django/contrib/messages/tests/session.py b/django/contrib/messages/tests/session.py index 741f53136d..e162f49fc2 100644 --- a/django/contrib/messages/tests/session.py +++ b/django/contrib/messages/tests/session.py @@ -1,5 +1,8 @@ +from django.contrib.messages import constants from django.contrib.messages.tests.base import BaseTest +from django.contrib.messages.storage.base import Message from django.contrib.messages.storage.session import SessionStorage +from django.utils.safestring import SafeData, mark_safe def set_session_data(storage, messages): @@ -36,3 +39,14 @@ class SessionTest(BaseTest): set_session_data(storage, example_messages) # Test that the message actually contains what we expect. self.assertEqual(list(storage), example_messages) + + def test_safedata(self): + """ + Tests that a message containing SafeData is keeping its safe status when + retrieved from the message storage. + """ + storage = self.get_storage() + + message = Message(constants.DEBUG, mark_safe("Hello Django!")) + set_session_data(storage, [message]) + self.assertIsInstance(list(storage)[0].message, SafeData) From 52a2588df69e5252bee98e76e8d3a2aa37bce23c Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 21 Dec 2012 15:52:06 -0500 Subject: [PATCH 016/870] Fixed #19506 - Remove 'mysite' prefix in model example. Thanks Mike O'Connor for the report. --- docs/topics/db/models.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index 2f1676ac1a..cfa794ca92 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -66,13 +66,13 @@ those models. Do this by editing your settings file and changing the your ``models.py``. For example, if the models for your application live in the module -``mysite.myapp.models`` (the package structure that is created for an +``myapp.models`` (the package structure that is created for an application by the :djadmin:`manage.py startapp ` script), :setting:`INSTALLED_APPS` should read, in part:: INSTALLED_APPS = ( #... - 'mysite.myapp', + 'myapp', #... ) From d19109fd37e75ccf29d2ca64370102753dbc7c5b Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Fri, 21 Dec 2012 21:59:06 -0300 Subject: [PATCH 017/870] Fixed #19497 -- Refactored testing docs. Thanks Tim Graham for the review and suggestions. --- docs/index.txt | 6 +- .../contributing/writing-code/unit-tests.txt | 4 +- docs/intro/contributing.txt | 2 +- docs/intro/tutorial05.txt | 2 +- docs/misc/api-stability.txt | 2 +- docs/ref/django-admin.txt | 6 +- docs/ref/settings.txt | 6 +- docs/ref/signals.txt | 2 +- docs/releases/0.96.txt | 2 +- docs/releases/1.1-alpha-1.txt | 2 +- docs/releases/1.1-beta-1.txt | 2 +- docs/releases/1.1.txt | 4 +- docs/topics/index.txt | 2 +- docs/topics/install.txt | 2 +- .../django_unittest_classes_hierarchy.graffle | 0 .../django_unittest_classes_hierarchy.pdf | Bin .../django_unittest_classes_hierarchy.svg | 0 docs/topics/testing/advanced.txt | 429 ++++++++ docs/topics/testing/doctests.txt | 81 ++ docs/topics/testing/index.txt | 111 ++ .../{testing.txt => testing/overview.txt} | 985 ++++-------------- 21 files changed, 849 insertions(+), 801 deletions(-) rename docs/topics/{ => testing}/_images/django_unittest_classes_hierarchy.graffle (100%) rename docs/topics/{ => testing}/_images/django_unittest_classes_hierarchy.pdf (100%) rename docs/topics/{ => testing}/_images/django_unittest_classes_hierarchy.svg (100%) create mode 100644 docs/topics/testing/advanced.txt create mode 100644 docs/topics/testing/doctests.txt create mode 100644 docs/topics/testing/index.txt rename docs/topics/{testing.txt => testing/overview.txt} (75%) diff --git a/docs/index.txt b/docs/index.txt index 9fea8ff3f2..ab00da271c 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -180,7 +180,11 @@ testing of Django applications: :doc:`Overview ` | :doc:`Adding custom commands ` -* **Testing:** :doc:`Overview ` +* **Testing:** + :doc:`Overview ` | + :doc:`Writing and running tests ` | + :doc:`Advanced topics ` | + :doc:`Doctests ` * **Deployment:** :doc:`Overview ` | diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 4e702ff83e..afef554a8c 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -15,8 +15,8 @@ The tests cover: We appreciate any and all contributions to the test suite! The Django tests all use the testing infrastructure that ships with Django for -testing applications. See :doc:`Testing Django applications ` -for an explanation of how to write new tests. +testing applications. See :doc:`Testing Django applications +` for an explanation of how to write new tests. .. _running-unit-tests: diff --git a/docs/intro/contributing.txt b/docs/intro/contributing.txt index a343814c02..c94038bc56 100644 --- a/docs/intro/contributing.txt +++ b/docs/intro/contributing.txt @@ -281,7 +281,7 @@ correctly in a couple different situations. computer programming, so there's lots of information out there: * A good first look at writing tests for Django can be found in the - documentation on :doc:`Testing Django applications`. + documentation on :doc:`Testing Django applications `. * Dive Into Python (a free online book for beginning Python developers) includes a great `introduction to Unit Testing`__. * After reading those, if you want something a little meatier to sink diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt index 163b7cdd0f..d1f95176ed 100644 --- a/docs/intro/tutorial05.txt +++ b/docs/intro/tutorial05.txt @@ -632,7 +632,7 @@ a piece of code, it usually means that code should be refactored or removed. Coverage will help to identify dead code. See :ref:`topics-testing-code-coverage` for details. -:doc:`Testing Django applications ` has comprehensive +:doc:`Testing Django applications ` has comprehensive information about testing. .. _Selenium: http://seleniumhq.org/ diff --git a/docs/misc/api-stability.txt b/docs/misc/api-stability.txt index 4f232e795b..a13cb5de69 100644 --- a/docs/misc/api-stability.txt +++ b/docs/misc/api-stability.txt @@ -71,7 +71,7 @@ of 1.0. This includes these APIs: external template tags. Before adding any such tags, we'll ensure that Django raises an error if it tries to load tags with duplicate names. -- :doc:`Testing ` +- :doc:`Testing ` - :doc:`django-admin utility `. diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 306db8439e..6ab3b1d133 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1036,7 +1036,7 @@ test .. django-admin:: test -Runs tests for all installed models. See :doc:`/topics/testing` for more +Runs tests for all installed models. See :doc:`/topics/testing/index` for more information. .. django-admin-option:: --failfast @@ -1072,7 +1072,7 @@ For example, this command:: ...would perform the following steps: -1. Create a test database, as described in :doc:`/topics/testing`. +1. Create a test database, as described in :ref:`the-test-database`. 2. Populate the test database with fixture data from the given fixtures. (For more on fixtures, see the documentation for ``loaddata`` above.) 3. Runs the Django development server (as in ``runserver``), pointed at @@ -1080,7 +1080,7 @@ For example, this command:: This is useful in a number of ways: -* When you're writing :doc:`unit tests ` of how your views +* When you're writing :doc:`unit tests ` of how your views act with certain fixture data, you can use ``testserver`` to interact with the views in a Web browser, manually. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 135cddae25..5ecc221039 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -562,7 +562,7 @@ If the default value (``None``) is used with the SQLite database engine, the tests will use a memory resident database. For all other database engines the test database will use the name ``'test_' + DATABASE_NAME``. -See :doc:`/topics/testing`. +See :ref:`the-test-database`. .. setting:: TEST_CREATE @@ -1982,9 +1982,7 @@ TEST_RUNNER Default: ``'django.test.simple.DjangoTestSuiteRunner'`` The name of the class to use for starting the test suite. See -:doc:`/topics/testing`. - -.. _Testing Django Applications: ../testing/ +:ref:`other-testing-frameworks`. .. setting:: THOUSAND_SEPARATOR diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 0db540370d..3315f9781b 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -476,7 +476,7 @@ Test signals .. module:: django.test.signals :synopsis: Signals sent during testing. -Signals only sent when :doc:`running tests `. +Signals only sent when :ref:`running tests `. setting_changed --------------- diff --git a/docs/releases/0.96.txt b/docs/releases/0.96.txt index a608629957..a00f878df3 100644 --- a/docs/releases/0.96.txt +++ b/docs/releases/0.96.txt @@ -220,7 +220,7 @@ supported :doc:`serialization formats `, that will be loaded into your database at the start of your tests. This makes testing with real data much easier. -See :doc:`the testing documentation ` for the full details. +See :doc:`the testing documentation ` for the full details. Improvements to the admin interface ----------------------------------- diff --git a/docs/releases/1.1-alpha-1.txt b/docs/releases/1.1-alpha-1.txt index 10b0d5d71e..c8ac56cf48 100644 --- a/docs/releases/1.1-alpha-1.txt +++ b/docs/releases/1.1-alpha-1.txt @@ -51,7 +51,7 @@ Performance improvements .. currentmodule:: django.test -Tests written using Django's :doc:`testing framework ` now run +Tests written using Django's :doc:`testing framework ` now run dramatically faster (as much as 10 times faster in many cases). This was accomplished through the introduction of transaction-based tests: when diff --git a/docs/releases/1.1-beta-1.txt b/docs/releases/1.1-beta-1.txt index 9bac3a53f1..1555a9464a 100644 --- a/docs/releases/1.1-beta-1.txt +++ b/docs/releases/1.1-beta-1.txt @@ -102,7 +102,7 @@ Testing improvements .. currentmodule:: django.test.client A couple of small but very useful improvements have been made to the -:doc:`testing framework `: +:doc:`testing framework `: * The test :class:`Client` now can automatically follow redirects with the ``follow`` argument to :meth:`Client.get` and :meth:`Client.post`. This diff --git a/docs/releases/1.1.txt b/docs/releases/1.1.txt index 852644dee4..84af7fc1d9 100644 --- a/docs/releases/1.1.txt +++ b/docs/releases/1.1.txt @@ -264,14 +264,14 @@ Testing improvements -------------------- A few notable improvements have been made to the :doc:`testing framework -`. +`. Test performance improvements ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. currentmodule:: django.test -Tests written using Django's :doc:`testing framework ` now run +Tests written using Django's :doc:`testing framework ` now run dramatically faster (as much as 10 times faster in many cases). This was accomplished through the introduction of transaction-based tests: when diff --git a/docs/topics/index.txt b/docs/topics/index.txt index 72f5090b15..82c5859b2c 100644 --- a/docs/topics/index.txt +++ b/docs/topics/index.txt @@ -13,7 +13,7 @@ Introductions to all the key parts of Django you'll need to know: templates class-based-views/index files - testing + testing/index auth cache conditional-view-processing diff --git a/docs/topics/install.txt b/docs/topics/install.txt index b71033f319..0c3767d3e1 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -135,7 +135,7 @@ table once ``syncdb`` has created it. After creating a database user with these permissions, you'll specify the details in your project's settings file, see :setting:`DATABASES` for details. -If you're using Django's :doc:`testing framework` to test +If you're using Django's :doc:`testing framework` to test database queries, Django will need permission to create a test database. .. _PostgreSQL: http://www.postgresql.org/ diff --git a/docs/topics/_images/django_unittest_classes_hierarchy.graffle b/docs/topics/testing/_images/django_unittest_classes_hierarchy.graffle similarity index 100% rename from docs/topics/_images/django_unittest_classes_hierarchy.graffle rename to docs/topics/testing/_images/django_unittest_classes_hierarchy.graffle diff --git a/docs/topics/_images/django_unittest_classes_hierarchy.pdf b/docs/topics/testing/_images/django_unittest_classes_hierarchy.pdf similarity index 100% rename from docs/topics/_images/django_unittest_classes_hierarchy.pdf rename to docs/topics/testing/_images/django_unittest_classes_hierarchy.pdf diff --git a/docs/topics/_images/django_unittest_classes_hierarchy.svg b/docs/topics/testing/_images/django_unittest_classes_hierarchy.svg similarity index 100% rename from docs/topics/_images/django_unittest_classes_hierarchy.svg rename to docs/topics/testing/_images/django_unittest_classes_hierarchy.svg diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt new file mode 100644 index 0000000000..0674b2e41b --- /dev/null +++ b/docs/topics/testing/advanced.txt @@ -0,0 +1,429 @@ +======================= +Advanced testing topics +======================= + +The request factory +=================== + +.. module:: django.test.client + +.. class:: RequestFactory + +The :class:`~django.test.client.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 +restricted subset of the test client API: + +* It only has access to the HTTP methods :meth:`~Client.get()`, + :meth:`~Client.post()`, :meth:`~Client.put()`, + :meth:`~Client.delete()`, :meth:`~Client.head()` and + :meth:`~Client.options()`. + +* These methods accept all the same arguments *except* for + ``follows``. Since this is just a factory for producing + requests, it's up to you to handle the response. + +* It does not support middleware. Session and authentication + attributes must be supplied by the test itself if required + for the view to function properly. + +Example +------- + +The following is a simple unit test using the request factory:: + + from django.utils import unittest + from django.test.client import RequestFactory + + class SimpleTest(unittest.TestCase): + def setUp(self): + # Every test needs access to the request factory. + self.factory = RequestFactory() + + def test_details(self): + # Create an instance of a GET request. + request = self.factory.get('/customer/details') + + # Test my_view() as if it were deployed at /customer/details + response = my_view(request) + self.assertEqual(response.status_code, 200) + +.. _topics-testing-advanced-multidb: + +Tests and multiple databases +============================ + +.. _topics-testing-masterslave: + +Testing master/slave configurations +----------------------------------- + +If you're testing a multiple database configuration with master/slave +replication, this strategy of creating test databases poses a problem. +When the test databases are created, there won't be any replication, +and as a result, data created on the master won't be seen on the +slave. + +To compensate for this, Django allows you to define that a database is +a *test mirror*. Consider the following (simplified) example database +configuration:: + + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'myproject', + 'HOST': 'dbmaster', + # ... plus some other settings + }, + 'slave': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'myproject', + 'HOST': 'dbslave', + 'TEST_MIRROR': 'default' + # ... plus some other settings + } + } + +In this setup, we have two database servers: ``dbmaster``, described +by the database alias ``default``, and ``dbslave`` described by the +alias ``slave``. As you might expect, ``dbslave`` has been configured +by the database administrator as a read slave of ``dbmaster``, so in +normal activity, any write to ``default`` will appear on ``slave``. + +If Django created two independent test databases, this would break any +tests that expected replication to occur. However, the ``slave`` +database has been configured as a test mirror (using the +:setting:`TEST_MIRROR` setting), indicating that under testing, +``slave`` should be treated as a mirror of ``default``. + +When the test environment is configured, a test version of ``slave`` +will *not* be created. Instead the connection to ``slave`` +will be redirected to point at ``default``. As a result, writes to +``default`` will appear on ``slave`` -- but because they are actually +the same database, not because there is data replication between the +two databases. + +.. _topics-testing-creation-dependencies: + +Controlling creation order for test databases +--------------------------------------------- + +By default, Django will always create the ``default`` database first. +However, no guarantees are made on the creation order of any other +databases in your test setup. + +If your database configuration requires a specific creation order, you +can specify the dependencies that exist using the +:setting:`TEST_DEPENDENCIES` setting. Consider the following +(simplified) example database configuration:: + + DATABASES = { + 'default': { + # ... db settings + 'TEST_DEPENDENCIES': ['diamonds'] + }, + 'diamonds': { + # ... db settings + }, + 'clubs': { + # ... db settings + 'TEST_DEPENDENCIES': ['diamonds'] + }, + 'spades': { + # ... db settings + 'TEST_DEPENDENCIES': ['diamonds','hearts'] + }, + 'hearts': { + # ... db settings + 'TEST_DEPENDENCIES': ['diamonds','clubs'] + } + } + +Under this configuration, the ``diamonds`` database will be created first, +as it is the only database alias without dependencies. The ``default`` and +``clubs`` alias will be created next (although the order of creation of this +pair is not guaranteed); then ``hearts``; and finally ``spades``. + +If there are any circular dependencies in the +:setting:`TEST_DEPENDENCIES` definition, an ``ImproperlyConfigured`` +exception will be raised. + +Running tests outside the test runner +===================================== + +If you want to run tests outside of ``./manage.py test`` -- for example, +from a shell prompt -- you will need to set up the test +environment first. Django provides a convenience method to do this:: + + >>> from django.test.utils import setup_test_environment + >>> setup_test_environment() + +This convenience method sets up the test database, and puts other +Django features into modes that allow for repeatable testing. + +The call to :meth:`~django.test.utils.setup_test_environment` is made +automatically as part of the setup of ``./manage.py test``. You only +need to manually invoke this method if you're not using running your +tests via Django's test runner. + +.. _other-testing-frameworks: + +Using different testing frameworks +================================== + +Clearly, :mod:`doctest` and :mod:`unittest` are not the only Python testing +frameworks. While Django doesn't provide explicit support for alternative +frameworks, it does provide a way to invoke tests constructed for an +alternative framework as if they were normal Django tests. + +When you run ``./manage.py test``, Django looks at the :setting:`TEST_RUNNER` +setting to determine what to do. By default, :setting:`TEST_RUNNER` points to +``'django.test.simple.DjangoTestSuiteRunner'``. This class defines the default Django +testing behavior. This behavior involves: + +#. Performing global pre-test setup. + +#. Looking for unit tests and doctests in the ``models.py`` and + ``tests.py`` files in each installed application. + +#. Creating the test databases. + +#. Running ``syncdb`` to install models and initial data into the test + databases. + +#. Running the unit tests and doctests that are found. + +#. Destroying the test databases. + +#. Performing global post-test teardown. + +If you define your own test runner class and point :setting:`TEST_RUNNER` at +that class, Django will execute your test runner whenever you run +``./manage.py test``. In this way, it is possible to use any test framework +that can be executed from Python code, or to modify the Django test execution +process to satisfy whatever testing requirements you may have. + +.. _topics-testing-test_runner: + +Defining a test runner +---------------------- + +.. currentmodule:: django.test.simple + +A test runner is a class defining a ``run_tests()`` method. Django ships +with a ``DjangoTestSuiteRunner`` class that defines the default Django +testing behavior. This class defines the ``run_tests()`` entry point, +plus a selection of other methods that are used to by ``run_tests()`` to +set up, execute and tear down the test suite. + +.. class:: DjangoTestSuiteRunner(verbosity=1, interactive=True, failfast=True, **kwargs) + + ``verbosity`` determines the amount of notification and debug information + that will be printed to the console; ``0`` is no output, ``1`` is normal + output, and ``2`` is verbose output. + + If ``interactive`` is ``True``, the test suite has permission to ask the + user for instructions when the test suite is executed. An example of this + behavior would be asking for permission to delete an existing test + database. If ``interactive`` is ``False``, the test suite must be able to + run without any manual intervention. + + If ``failfast`` is ``True``, the test suite will stop running after the + first test failure is detected. + + Django will, from time to time, extend the capabilities of + the test runner by adding new arguments. The ``**kwargs`` declaration + allows for this expansion. If you subclass ``DjangoTestSuiteRunner`` or + write your own test runner, ensure accept and handle the ``**kwargs`` + parameter. + + .. versionadded:: 1.4 + + Your test runner may also define additional command-line options. + If you add an ``option_list`` attribute to a subclassed test runner, + those options will be added to the list of command-line options that + the :djadmin:`test` command can use. + +Attributes +~~~~~~~~~~ + +.. attribute:: DjangoTestSuiteRunner.option_list + + .. versionadded:: 1.4 + + This is the tuple of ``optparse`` options which will be fed into the + management command's ``OptionParser`` for parsing arguments. See the + documentation for Python's ``optparse`` module for more details. + +Methods +~~~~~~~ + +.. method:: DjangoTestSuiteRunner.run_tests(test_labels, extra_tests=None, **kwargs) + + Run the test suite. + + ``test_labels`` is a list of strings describing the tests to be run. A test + label can take one of three forms: + + * ``app.TestCase.test_method`` -- Run a single test method in a test + case. + * ``app.TestCase`` -- Run all the test methods in a test case. + * ``app`` -- Search for and run all tests in the named application. + + If ``test_labels`` has a value of ``None``, the test runner should run + search for tests in all the applications in :setting:`INSTALLED_APPS`. + + ``extra_tests`` is a list of extra ``TestCase`` instances to add to the + suite that is executed by the test runner. These extra tests are run + in addition to those discovered in the modules listed in ``test_labels``. + + This method should return the number of tests that failed. + +.. method:: DjangoTestSuiteRunner.setup_test_environment(**kwargs) + + Sets up the test environment ready for testing. + +.. method:: DjangoTestSuiteRunner.build_suite(test_labels, extra_tests=None, **kwargs) + + Constructs a test suite that matches the test labels provided. + + ``test_labels`` is a list of strings describing the tests to be run. A test + label can take one of three forms: + + * ``app.TestCase.test_method`` -- Run a single test method in a test + case. + * ``app.TestCase`` -- Run all the test methods in a test case. + * ``app`` -- Search for and run all tests in the named application. + + If ``test_labels`` has a value of ``None``, the test runner should run + search for tests in all the applications in :setting:`INSTALLED_APPS`. + + ``extra_tests`` is a list of extra ``TestCase`` instances to add to the + suite that is executed by the test runner. These extra tests are run + in addition to those discovered in the modules listed in ``test_labels``. + + Returns a ``TestSuite`` instance ready to be run. + +.. method:: DjangoTestSuiteRunner.setup_databases(**kwargs) + + Creates the test databases. + + Returns a data structure that provides enough detail to undo the changes + that have been made. This data will be provided to the ``teardown_databases()`` + function at the conclusion of testing. + +.. method:: DjangoTestSuiteRunner.run_suite(suite, **kwargs) + + Runs the test suite. + + Returns the result produced by the running the test suite. + +.. method:: DjangoTestSuiteRunner.teardown_databases(old_config, **kwargs) + + Destroys the test databases, restoring pre-test conditions. + + ``old_config`` is a data structure defining the changes in the + database configuration that need to be reversed. It is the return + value of the ``setup_databases()`` method. + +.. method:: DjangoTestSuiteRunner.teardown_test_environment(**kwargs) + + Restores the pre-test environment. + +.. method:: DjangoTestSuiteRunner.suite_result(suite, result, **kwargs) + + Computes and returns a return code based on a test suite, and the result + from that test suite. + + +Testing utilities +----------------- + +.. module:: django.test.utils + :synopsis: Helpers to write custom test runners. + +To assist in the creation of your own test runner, Django provides a number of +utility methods in the ``django.test.utils`` module. + +.. function:: setup_test_environment() + + Performs any global pre-test setup, such as the installing the + instrumentation of the template rendering system and setting up + the dummy email outbox. + +.. function:: teardown_test_environment() + + Performs any global post-test teardown, such as removing the black + magic hooks into the template system and restoring normal email + services. + +.. currentmodule:: django.db.connection.creation + +The creation module of the database backend (``connection.creation``) +also provides some utilities that can be useful during testing. + +.. function:: create_test_db([verbosity=1, autoclobber=False]) + + Creates a new test database and runs ``syncdb`` against it. + + ``verbosity`` has the same behavior as in ``run_tests()``. + + ``autoclobber`` describes the behavior that will occur if a + database with the same name as the test database is discovered: + + * If ``autoclobber`` is ``False``, the user will be asked to + approve destroying the existing database. ``sys.exit`` is + called if the user does not approve. + + * If autoclobber is ``True``, the database will be destroyed + without consulting the user. + + Returns the name of the test database that it created. + + ``create_test_db()`` has the side effect of modifying the value of + :setting:`NAME` in :setting:`DATABASES` to match the name of the test + database. + +.. function:: destroy_test_db(old_database_name, [verbosity=1]) + + Destroys the database whose name is the value of :setting:`NAME` in + :setting:`DATABASES`, and sets :setting:`NAME` to the value of + ``old_database_name``. + + The ``verbosity`` argument has the same behavior as for + :class:`~django.test.simple.DjangoTestSuiteRunner`. + +.. _topics-testing-code-coverage: + +Integration with coverage.py +============================ + +Code coverage describes how much source code has been tested. It shows which +parts of your code are being exercised by tests and which are not. It's an +important part of testing applications, so it's strongly recommended to check +the coverage of your tests. + +Django can be easily integrated with `coverage.py`_, a tool for measuring code +coverage of Python programs. First, `install coverage.py`_. Next, run the +following from your project folder containing ``manage.py``:: + + coverage run --source='.' manage.py test myapp + +This runs your tests and collects coverage data of the executed files in your +project. You can see a report of this data by typing following command:: + + coverage report + +Note that some Django code was executed while running tests, but it is not +listed here because of the ``source`` flag passed to the previous command. + +For more options like annotated HTML listings detailing missed lines, see the +`coverage.py`_ docs. + +.. _coverage.py: http://nedbatchelder.com/code/coverage/ +.. _install coverage.py: http://pypi.python.org/pypi/coverage diff --git a/docs/topics/testing/doctests.txt b/docs/topics/testing/doctests.txt new file mode 100644 index 0000000000..5036e946a9 --- /dev/null +++ b/docs/topics/testing/doctests.txt @@ -0,0 +1,81 @@ +=================== +Django and doctests +=================== + +Doctests use Python's standard :mod:`doctest` module, which searches your +docstrings for statements that resemble a session of the Python interactive +interpreter. A full explanation of how :mod:`doctest` works is out of the scope +of this document; read Python's official documentation for the details. + +.. admonition:: What's a **docstring**? + + A good explanation of docstrings (and some guidelines for using them + effectively) can be found in :pep:`257`: + + A docstring is a string literal that occurs as the first statement in + a module, function, class, or method definition. Such a docstring + becomes the ``__doc__`` special attribute of that object. + + For example, this function has a docstring that describes what it does:: + + def add_two(num): + "Return the result of adding two to the provided number." + return num + 2 + + Because tests often make great documentation, putting tests directly in + your docstrings is an effective way to document *and* test your code. + +As with unit tests, for a given Django application, the test runner looks for +doctests in two places: + +* The ``models.py`` file. You can define module-level doctests and/or a + doctest for individual models. It's common practice to put + application-level doctests in the module docstring and model-level + doctests in the model docstrings. + +* A file called ``tests.py`` in the application directory -- i.e., the + directory that holds ``models.py``. This file is a hook for any and all + doctests you want to write that aren't necessarily related to models. + +This example doctest is equivalent to the example given in the unittest section +above:: + + # models.py + + from django.db import models + + class Animal(models.Model): + """ + An animal that knows how to make noise + + # Create some animals + >>> lion = Animal.objects.create(name="lion", sound="roar") + >>> cat = Animal.objects.create(name="cat", sound="meow") + + # Make 'em speak + >>> lion.speak() + 'The lion says "roar"' + >>> cat.speak() + 'The cat says "meow"' + """ + name = models.CharField(max_length=20) + sound = models.CharField(max_length=20) + + def speak(self): + return 'The %s says "%s"' % (self.name, self.sound) + +When you :ref:`run your tests `, the test runner will find this +docstring, notice that portions of it look like an interactive Python session, +and execute those lines while checking that the results match. + +In the case of model tests, note that the test runner takes care of creating +its own test database. That is, any test that accesses a database -- by +creating and saving model instances, for example -- will not affect your +production database. However, the database is not refreshed between doctests, +so if your doctest requires a certain state you should consider flushing the +database or loading a fixture. (See the section on :ref:`fixtures +` for more on this.) Note that to use this feature, +the database user Django is connecting as must have ``CREATE DATABASE`` +rights. + +For more details about :mod:`doctest`, see the Python documentation. diff --git a/docs/topics/testing/index.txt b/docs/topics/testing/index.txt new file mode 100644 index 0000000000..0345b72703 --- /dev/null +++ b/docs/topics/testing/index.txt @@ -0,0 +1,111 @@ +================= +Testing in Django +================= + +.. toctree:: + :hidden: + + overview + doctests + advanced + +Automated testing is an extremely useful bug-killing tool for the modern +Web developer. You can use a collection of tests -- a **test suite** -- to +solve, or avoid, a number of problems: + +* When you're writing new code, you can use tests to validate your code + works as expected. + +* When you're refactoring or modifying old code, you can use tests to + ensure your changes haven't affected your application's behavior + unexpectedly. + +Testing a Web application is a complex task, because a Web application is made +of several layers of logic -- from HTTP-level request handling, to form +validation and processing, to template rendering. With Django's test-execution +framework and assorted utilities, you can simulate requests, insert test data, +inspect your application's output and generally verify your code is doing what +it should be doing. + +The best part is, it's really easy. + +Unit tests v. doctests +====================== + +There are two primary ways to write tests with Django, corresponding to the +two test frameworks that ship in the Python standard library. The two +frameworks are: + +* **Unit tests** -- tests that are expressed as methods on a Python class + that subclasses :class:`unittest.TestCase` or Django's customized + :class:`TestCase`. For example:: + + import unittest + + class MyFuncTestCase(unittest.TestCase): + def testBasic(self): + a = ['larry', 'curly', 'moe'] + self.assertEqual(my_func(a, 0), 'larry') + self.assertEqual(my_func(a, 1), 'curly') + +* **Doctests** -- tests that are embedded in your functions' docstrings and + are written in a way that emulates a session of the Python interactive + interpreter. For example:: + + def my_func(a_list, idx): + """ + >>> a = ['larry', 'curly', 'moe'] + >>> my_func(a, 0) + 'larry' + >>> my_func(a, 1) + 'curly' + """ + return a_list[idx] + +Which should I use? +------------------- + +Because Django supports both of the standard Python test frameworks, it's up to +you and your tastes to decide which one to use. You can even decide to use +*both*. + +For developers new to testing, however, this choice can seem confusing. Here, +then, are a few key differences to help you decide which approach is right for +you: + +* If you've been using Python for a while, :mod:`doctest` will probably feel + more "pythonic". It's designed to make writing tests as easy as possible, + so it requires no overhead of writing classes or methods. You simply put + tests in docstrings. This has the added advantage of serving as + documentation (and correct documentation, at that!). However, while + doctests are good for some simple example code, they are not very good if + you want to produce either high quality, comprehensive tests or high + quality documentation. Test failures are often difficult to debug + as it can be unclear exactly why the test failed. Thus, doctests should + generally be avoided and used primarily for documentation examples only. + +* The :mod:`unittest` framework will probably feel very familiar to + developers coming from Java. :mod:`unittest` is inspired by Java's JUnit, + so you'll feel at home with this method if you've used JUnit or any test + framework inspired by JUnit. + +* If you need to write a bunch of tests that share similar code, then + you'll appreciate the :mod:`unittest` framework's organization around + classes and methods. This makes it easy to abstract common tasks into + common methods. The framework also supports explicit setup and/or cleanup + routines, which give you a high level of control over the environment + in which your test cases are run. + +* If you're writing tests for Django itself, you should use :mod:`unittest`. + +Where to go from here +===================== + +As unit tests are preferred in Django, we treat them in detail in the +:doc:`overview` document. + +:doc:`doctests` describes Django-specific features when using doctests. + +You can also use any *other* Python test framework, Django provides an API and +tools for that kind of integration. They are described in the +:ref:`other-testing-frameworks` section of :doc:`advanced`. diff --git a/docs/topics/testing.txt b/docs/topics/testing/overview.txt similarity index 75% rename from docs/topics/testing.txt rename to docs/topics/testing/overview.txt index b4645c236b..5f64789019 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing/overview.txt @@ -5,69 +5,17 @@ Testing Django applications .. module:: django.test :synopsis: Testing tools for Django applications. -Automated testing is an extremely useful bug-killing tool for the modern -Web developer. You can use a collection of tests -- a **test suite** -- to -solve, or avoid, a number of problems: +.. seealso:: -* When you're writing new code, you can use tests to validate your code - works as expected. + The :doc:`testing tutorial ` and the + :doc:`advanced testing topics `. -* When you're refactoring or modifying old code, you can use tests to - ensure your changes haven't affected your application's behavior - unexpectedly. - -Testing a Web application is a complex task, because a Web application is made -of several layers of logic -- from HTTP-level request handling, to form -validation and processing, to template rendering. With Django's test-execution -framework and assorted utilities, you can simulate requests, insert test data, -inspect your application's output and generally verify your code is doing what -it should be doing. - -The best part is, it's really easy. - -This document is split into two primary sections. First, we explain how to -write tests with Django. Then, we explain how to run them. +This document is split into two primary sections. First, we explain how to write +tests with Django. Then, we explain how to run them. Writing tests ============= -There are two primary ways to write tests with Django, corresponding to the -two test frameworks that ship in the Python standard library. The two -frameworks are: - -* **Unit tests** -- tests that are expressed as methods on a Python class - that subclasses :class:`unittest.TestCase` or Django's customized - :class:`TestCase`. For example:: - - import unittest - - class MyFuncTestCase(unittest.TestCase): - def testBasic(self): - a = ['larry', 'curly', 'moe'] - self.assertEqual(my_func(a, 0), 'larry') - self.assertEqual(my_func(a, 1), 'curly') - -* **Doctests** -- tests that are embedded in your functions' docstrings and - are written in a way that emulates a session of the Python interactive - interpreter. For example:: - - def my_func(a_list, idx): - """ - >>> a = ['larry', 'curly', 'moe'] - >>> my_func(a, 0) - 'larry' - >>> my_func(a, 1) - 'curly' - """ - return a_list[idx] - -We'll discuss choosing the appropriate test framework later, however, most -experienced developers prefer unit tests. You can also use any *other* Python -test framework, as we'll explain in a bit. - -Writing unit tests ------------------- - Django's unit tests use a Python standard library module: :mod:`unittest`. This module defines tests in class-based approach. @@ -151,122 +99,6 @@ For more details about :mod:`unittest`, see the Python documentation. applications the scope of tests you will be able to write this way will be fairly limited, so it's easiest to use :class:`django.test.TestCase`. -Writing doctests ----------------- - -Doctests use Python's standard :mod:`doctest` module, which searches your -docstrings for statements that resemble a session of the Python interactive -interpreter. A full explanation of how :mod:`doctest` works is out of the scope -of this document; read Python's official documentation for the details. - -.. admonition:: What's a **docstring**? - - A good explanation of docstrings (and some guidelines for using them - effectively) can be found in :pep:`257`: - - A docstring is a string literal that occurs as the first statement in - a module, function, class, or method definition. Such a docstring - becomes the ``__doc__`` special attribute of that object. - - For example, this function has a docstring that describes what it does:: - - def add_two(num): - "Return the result of adding two to the provided number." - return num + 2 - - Because tests often make great documentation, putting tests directly in - your docstrings is an effective way to document *and* test your code. - -As with unit tests, for a given Django application, the test runner looks for -doctests in two places: - -* The ``models.py`` file. You can define module-level doctests and/or a - doctest for individual models. It's common practice to put - application-level doctests in the module docstring and model-level - doctests in the model docstrings. - -* A file called ``tests.py`` in the application directory -- i.e., the - directory that holds ``models.py``. This file is a hook for any and all - doctests you want to write that aren't necessarily related to models. - -This example doctest is equivalent to the example given in the unittest section -above:: - - # models.py - - from django.db import models - - class Animal(models.Model): - """ - An animal that knows how to make noise - - # Create some animals - >>> lion = Animal.objects.create(name="lion", sound="roar") - >>> cat = Animal.objects.create(name="cat", sound="meow") - - # Make 'em speak - >>> lion.speak() - 'The lion says "roar"' - >>> cat.speak() - 'The cat says "meow"' - """ - name = models.CharField(max_length=20) - sound = models.CharField(max_length=20) - - def speak(self): - return 'The %s says "%s"' % (self.name, self.sound) - -When you :ref:`run your tests `, the test runner will find this -docstring, notice that portions of it look like an interactive Python session, -and execute those lines while checking that the results match. - -In the case of model tests, note that the test runner takes care of creating -its own test database. That is, any test that accesses a database -- by -creating and saving model instances, for example -- will not affect your -production database. However, the database is not refreshed between doctests, -so if your doctest requires a certain state you should consider flushing the -database or loading a fixture. (See the section on fixtures, below, for more -on this.) Note that to use this feature, the database user Django is connecting -as must have ``CREATE DATABASE`` rights. - -For more details about :mod:`doctest`, see the Python documentation. - -Which should I use? -------------------- - -Because Django supports both of the standard Python test frameworks, it's up to -you and your tastes to decide which one to use. You can even decide to use -*both*. - -For developers new to testing, however, this choice can seem confusing. Here, -then, are a few key differences to help you decide which approach is right for -you: - -* If you've been using Python for a while, :mod:`doctest` will probably feel - more "pythonic". It's designed to make writing tests as easy as possible, - so it requires no overhead of writing classes or methods. You simply put - tests in docstrings. This has the added advantage of serving as - documentation (and correct documentation, at that!). However, while - doctests are good for some simple example code, they are not very good if - you want to produce either high quality, comprehensive tests or high - quality documentation. Test failures are often difficult to debug - as it can be unclear exactly why the test failed. Thus, doctests should - generally be avoided and used primarily for documentation examples only. - -* The :mod:`unittest` framework will probably feel very familiar to - developers coming from Java. :mod:`unittest` is inspired by Java's JUnit, - so you'll feel at home with this method if you've used JUnit or any test - framework inspired by JUnit. - -* If you need to write a bunch of tests that share similar code, then - you'll appreciate the :mod:`unittest` framework's organization around - classes and methods. This makes it easy to abstract common tasks into - common methods. The framework also supports explicit setup and/or cleanup - routines, which give you a high level of control over the environment - in which your test cases are run. - -* If you're writing tests for Django itself, you should use :mod:`unittest`. - .. _running-tests: Running tests @@ -341,23 +173,7 @@ be reported, and any test databases created by the run will not be destroyed. flag areas in your code that aren't strictly wrong but could benefit from a better implementation. -Running tests outside the test runner -------------------------------------- - -If you want to run tests outside of ``./manage.py test`` -- for example, -from a shell prompt -- you will need to set up the test -environment first. Django provides a convenience method to do this:: - - >>> from django.test.utils import setup_test_environment - >>> setup_test_environment() - -This convenience method sets up the test database, and puts other -Django features into modes that allow for repeatable testing. - -The call to :meth:`~django.test.utils.setup_test_environment` is made -automatically as part of the setup of ``./manage.py test``. You only -need to manually invoke this method if you're not using running your -tests via Django's test runner. +.. _the-test-database: The test database ----------------- @@ -400,100 +216,9 @@ advanced settings. your tests. *It is a bad idea to have such import-time database queries in your code* anyway - rewrite your code so that it doesn't do this. -.. _topics-testing-masterslave: +.. seealso:: -Testing master/slave configurations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you're testing a multiple database configuration with master/slave -replication, this strategy of creating test databases poses a problem. -When the test databases are created, there won't be any replication, -and as a result, data created on the master won't be seen on the -slave. - -To compensate for this, Django allows you to define that a database is -a *test mirror*. Consider the following (simplified) example database -configuration:: - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'myproject', - 'HOST': 'dbmaster', - # ... plus some other settings - }, - 'slave': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'myproject', - 'HOST': 'dbslave', - 'TEST_MIRROR': 'default' - # ... plus some other settings - } - } - -In this setup, we have two database servers: ``dbmaster``, described -by the database alias ``default``, and ``dbslave`` described by the -alias ``slave``. As you might expect, ``dbslave`` has been configured -by the database administrator as a read slave of ``dbmaster``, so in -normal activity, any write to ``default`` will appear on ``slave``. - -If Django created two independent test databases, this would break any -tests that expected replication to occur. However, the ``slave`` -database has been configured as a test mirror (using the -:setting:`TEST_MIRROR` setting), indicating that under testing, -``slave`` should be treated as a mirror of ``default``. - -When the test environment is configured, a test version of ``slave`` -will *not* be created. Instead the connection to ``slave`` -will be redirected to point at ``default``. As a result, writes to -``default`` will appear on ``slave`` -- but because they are actually -the same database, not because there is data replication between the -two databases. - -.. _topics-testing-creation-dependencies: - -Controlling creation order for test databases -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, Django will always create the ``default`` database first. -However, no guarantees are made on the creation order of any other -databases in your test setup. - -If your database configuration requires a specific creation order, you -can specify the dependencies that exist using the -:setting:`TEST_DEPENDENCIES` setting. Consider the following -(simplified) example database configuration:: - - DATABASES = { - 'default': { - # ... db settings - 'TEST_DEPENDENCIES': ['diamonds'] - }, - 'diamonds': { - # ... db settings - }, - 'clubs': { - # ... db settings - 'TEST_DEPENDENCIES': ['diamonds'] - }, - 'spades': { - # ... db settings - 'TEST_DEPENDENCIES': ['diamonds','hearts'] - }, - 'hearts': { - # ... db settings - 'TEST_DEPENDENCIES': ['diamonds','clubs'] - } - } - -Under this configuration, the ``diamonds`` database will be created first, -as it is the only database alias without dependencies. The ``default`` and -``clubs`` alias will be created next (although the order of creation of this -pair is not guaranteed); then ``hearts``; and finally ``spades``. - -If there are any circular dependencies in the -:setting:`TEST_DEPENDENCIES` definition, an ``ImproperlyConfigured`` -exception will be raised. + The :ref:`advanced multi-db testing topics `. Order in which tests are executed --------------------------------- @@ -610,36 +335,6 @@ to a faster hashing algorithm:: Don't forget to also include in :setting:`PASSWORD_HASHERS` any hashing algorithm used in fixtures, if any. -.. _topics-testing-code-coverage: - -Integration with coverage.py ----------------------------- - -Code coverage describes how much source code has been tested. It shows which -parts of your code are being exercised by tests and which are not. It's an -important part of testing applications, so it's strongly recommended to check -the coverage of your tests. - -Django can be easily integrated with `coverage.py`_, a tool for measuring code -coverage of Python programs. First, `install coverage.py`_. Next, run the -following from your project folder containing ``manage.py``:: - - coverage run --source='.' manage.py test myapp - -This runs your tests and collects coverage data of the executed files in your -project. You can see a report of this data by typing following command:: - - coverage report - -Note that some Django code was executed while running tests, but it is not -listed here because of the ``source`` flag passed to the previous command. - -For more options like annotated HTML listings detailing missed lines, see the -`coverage.py`_ docs. - -.. _coverage.py: http://nedbatchelder.com/code/coverage/ -.. _install coverage.py: http://pypi.python.org/pypi/coverage - Testing tools ============= @@ -1136,60 +831,12 @@ The following is a simple unit test using the test client:: # Check that the rendered context contains 5 customers. self.assertEqual(len(response.context['customers']), 5) -The request factory -------------------- +.. seealso:: -.. class:: RequestFactory - -The :class:`~django.test.client.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 -restricted subset of the test client API: - -* It only has access to the HTTP methods :meth:`~Client.get()`, - :meth:`~Client.post()`, :meth:`~Client.put()`, - :meth:`~Client.delete()`, :meth:`~Client.head()` and - :meth:`~Client.options()`. - -* These methods accept all the same arguments *except* for - ``follows``. Since this is just a factory for producing - requests, it's up to you to handle the response. - -* It does not support middleware. Session and authentication - attributes must be supplied by the test itself if required - for the view to function properly. - -Example -~~~~~~~ - -The following is a simple unit test using the request factory:: - - from django.utils import unittest - from django.test.client import RequestFactory - - class SimpleTest(unittest.TestCase): - def setUp(self): - # Every test needs access to the request factory. - self.factory = RequestFactory() - - def test_details(self): - # Create an instance of a GET request. - request = self.factory.get('/customer/details') - - # Test my_view() as if it were deployed at /customer/details - response = my_view(request) - self.assertEqual(response.status_code, 200) - -Test cases ----------- + :class:`django.test.client.RequestFactory` Provided test case classes -~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------- .. currentmodule:: django.test @@ -1208,37 +855,39 @@ Normal Python unit test classes extend a base class of Regardless of the version of Python you're using, if you've installed :mod:`unittest2`, :mod:`django.utils.unittest` will point to that library. -TestCase -^^^^^^^^ +SimpleTestCase +~~~~~~~~~~~~~~ -.. class:: TestCase() +.. class:: SimpleTestCase() -This class provides some additional capabilities that can be useful for testing -Web sites. +.. versionadded:: 1.4 -Converting a normal :class:`unittest.TestCase` to a Django :class:`TestCase` is -easy: Just change the base class of your test from `'unittest.TestCase'` to -`'django.test.TestCase'`. All of the standard Python unit test functionality -will continue to be available, but it will be augmented with some useful -additions, including: +A very thin subclass of :class:`unittest.TestCase`, it extends it with some +basic functionality like: -* Automatic loading of fixtures. +* Saving and restoring the Python warning machinery state. +* Checking that a callable :meth:`raises a certain exception `. +* :meth:`Testing form field rendering `. +* Testing server :ref:`HTML responses for the presence/lack of a given fragment `. +* The ability to run tests with :ref:`modified settings ` -* Wraps each test in a transaction. +If you need any of the other more complex and heavyweight Django-specific +features like: -* Creates a TestClient instance. +* Using the :attr:`~TestCase.client` :class:`~django.test.client.Client`. +* Testing or using the ORM. +* Database :attr:`~TestCase.fixtures`. +* Custom test-time :attr:`URL maps `. +* Test :ref:`skipping based on database backend features `. +* The remaining specialized :ref:`assert* ` methods. -* Django-specific assertions for testing for things like redirection and form - errors. +then you should use :class:`~django.test.TransactionTestCase` or +:class:`~django.test.TestCase` instead. -.. versionchanged:: 1.5 - The order in which tests are run has changed. See `Order in which tests are - executed`_. - -``TestCase`` inherits from :class:`~django.test.TransactionTestCase`. +``SimpleTestCase`` inherits from :class:`django.utils.unittest.TestCase`. TransactionTestCase -^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~ .. class:: TransactionTestCase() @@ -1309,36 +958,185 @@ to test the effects of commit and rollback: Using ``reset_sequences = True`` will slow down the test, since the primary key reset is an relatively expensive database operation. -SimpleTestCase -^^^^^^^^^^^^^^ +TestCase +~~~~~~~~ -.. class:: SimpleTestCase() +.. class:: TestCase() + +This class provides some additional capabilities that can be useful for testing +Web sites. + +Converting a normal :class:`unittest.TestCase` to a Django :class:`TestCase` is +easy: Just change the base class of your test from `'unittest.TestCase'` to +`'django.test.TestCase'`. All of the standard Python unit test functionality +will continue to be available, but it will be augmented with some useful +additions, including: + +* Automatic loading of fixtures. + +* Wraps each test in a transaction. + +* Creates a TestClient instance. + +* Django-specific assertions for testing for things like redirection and form + errors. + +.. versionchanged:: 1.5 + The order in which tests are run has changed. See `Order in which tests are + executed`_. + +``TestCase`` inherits from :class:`~django.test.TransactionTestCase`. + +.. _live-test-server: + +LiveServerTestCase +~~~~~~~~~~~~~~~~~~ .. versionadded:: 1.4 -A very thin subclass of :class:`unittest.TestCase`, it extends it with some -basic functionality like: +.. class:: LiveServerTestCase() -* Saving and restoring the Python warning machinery state. -* Checking that a callable :meth:`raises a certain exception `. -* :meth:`Testing form field rendering `. -* Testing server :ref:`HTML responses for the presence/lack of a given fragment `. -* The ability to run tests with :ref:`modified settings ` +``LiveServerTestCase`` does basically the same as +:class:`~django.test.TransactionTestCase` with one extra feature: it launches a +live Django server in the background on setup, and shuts it down on teardown. +This allows the use of automated test clients other than the +:ref:`Django dummy client ` such as, for example, the Selenium_ +client, to execute a series of functional tests inside a browser and simulate a +real user's actions. -If you need any of the other more complex and heavyweight Django-specific -features like: +By default the live server's address is `'localhost:8081'` and the full URL +can be accessed during the tests with ``self.live_server_url``. If you'd like +to change the default address (in the case, for example, where the 8081 port is +already taken) then you may pass a different one to the :djadmin:`test` command +via the :djadminopt:`--liveserver` option, for example: -* Using the :attr:`~TestCase.client` :class:`~django.test.client.Client`. -* Testing or using the ORM. -* Database :attr:`~TestCase.fixtures`. -* Custom test-time :attr:`URL maps `. -* Test :ref:`skipping based on database backend features `. -* The remaining specialized :ref:`assert* ` methods. +.. code-block:: bash -then you should use :class:`~django.test.TransactionTestCase` or -:class:`~django.test.TestCase` instead. + ./manage.py test --liveserver=localhost:8082 -``SimpleTestCase`` inherits from :class:`django.utils.unittest.TestCase`. +Another way of changing the default server address is by setting the +`DJANGO_LIVE_TEST_SERVER_ADDRESS` environment variable somewhere in your +code (for example, in a :ref:`custom test runner`): + +.. code-block:: python + + import os + os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost:8082' + +In the case where the tests are run by multiple processes in parallel (for +example, in the context of several simultaneous `continuous integration`_ +builds), the processes will compete for the same address, and therefore your +tests might randomly fail with an "Address already in use" error. To avoid this +problem, you can pass a comma-separated list of ports or ranges of ports (at +least as many as the number of potential parallel processes). For example: + +.. code-block:: bash + + ./manage.py test --liveserver=localhost:8082,8090-8100,9000-9200,7041 + +Then, during test execution, each new live test server will try every specified +port until it finds one that is free and takes it. + +.. _continuous integration: http://en.wikipedia.org/wiki/Continuous_integration + +To demonstrate how to use ``LiveServerTestCase``, let's write a simple Selenium +test. First of all, you need to install the `selenium package`_ into your +Python path: + +.. code-block:: bash + + pip install selenium + +Then, add a ``LiveServerTestCase``-based test to your app's tests module +(for example: ``myapp/tests.py``). The code for this test may look as follows: + +.. code-block:: python + + from django.test import LiveServerTestCase + from selenium.webdriver.firefox.webdriver import WebDriver + + class MySeleniumTests(LiveServerTestCase): + fixtures = ['user-data.json'] + + @classmethod + def setUpClass(cls): + cls.selenium = WebDriver() + super(MySeleniumTests, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + cls.selenium.quit() + super(MySeleniumTests, cls).tearDownClass() + + def test_login(self): + self.selenium.get('%s%s' % (self.live_server_url, '/login/')) + username_input = self.selenium.find_element_by_name("username") + username_input.send_keys('myuser') + password_input = self.selenium.find_element_by_name("password") + password_input.send_keys('secret') + self.selenium.find_element_by_xpath('//input[@value="Log in"]').click() + +Finally, you may run the test as follows: + +.. code-block:: bash + + ./manage.py test myapp.MySeleniumTests.test_login + +This example will automatically open Firefox then go to the login page, enter +the credentials and press the "Log in" button. Selenium offers other drivers in +case you do not have Firefox installed or wish to use another browser. The +example above is just a tiny fraction of what the Selenium client can do; check +out the `full reference`_ for more details. + +.. _Selenium: http://seleniumhq.org/ +.. _selenium package: http://pypi.python.org/pypi/selenium +.. _full reference: http://selenium-python.readthedocs.org/en/latest/api.html +.. _Firefox: http://www.mozilla.com/firefox/ + +.. note:: + + ``LiveServerTestCase`` makes use of the :doc:`staticfiles contrib app + ` so you'll need to have your project configured + accordingly (in particular by setting :setting:`STATIC_URL`). + +.. note:: + + When using an in-memory SQLite database to run the tests, the same database + connection will be shared by two threads in parallel: the thread in which + the live server is run and the thread in which the test case is run. It's + important to prevent simultaneous database queries via this shared + connection by the two threads, as that may sometimes randomly cause the + tests to fail. So you need to ensure that the two threads don't access the + database at the same time. In particular, this means that in some cases + (for example, just after clicking a link or submitting a form), you might + need to check that a response is received by Selenium and that the next + page is loaded before proceeding with further test execution. + Do this, for example, by making Selenium wait until the `` HTML tag + is found in the response (requires Selenium > 2.13): + + .. code-block:: python + + def test_login(self): + from selenium.webdriver.support.wait import WebDriverWait + timeout = 2 + ... + self.selenium.find_element_by_xpath('//input[@value="Log in"]').click() + # Wait until the response is received + WebDriverWait(self.selenium, timeout).until( + lambda driver: driver.find_element_by_tag_name('body')) + + The tricky thing here is that there's really no such thing as a "page load," + especially in modern Web apps that generate HTML dynamically after the + server generates the initial document. So, simply checking for the presence + of `` in the response might not necessarily be appropriate for all + use cases. Please refer to the `Selenium FAQ`_ and + `Selenium documentation`_ for more information. + + .. _Selenium FAQ: http://code.google.com/p/selenium/wiki/FrequentlyAskedQuestions#Q:_WebDriver_fails_to_find_elements_/_Does_not_block_on_page_loa + .. _Selenium documentation: http://seleniumhq.org/docs/04_webdriver_advanced.html#explicit-waits + +Test cases features +------------------- Default test client ~~~~~~~~~~~~~~~~~~~ @@ -1638,7 +1436,7 @@ Emptying the test outbox If you use Django's custom ``TestCase`` class, the test runner will clear the contents of the test email outbox at the start of each test case. -For more detail on email services during tests, see `Email services`_. +For more detail on email services during tests, see `Email services`_ below. .. _assertions: @@ -1984,376 +1782,3 @@ under MySQL with MyISAM tables):: @skipUnlessDBFeature('supports_transactions') def test_transaction_behavior(self): # ... conditional test code - -Live test server ----------------- - -.. versionadded:: 1.4 - -.. currentmodule:: django.test - -.. class:: LiveServerTestCase() - -``LiveServerTestCase`` does basically the same as -:class:`~django.test.TransactionTestCase` with one extra feature: it launches a -live Django server in the background on setup, and shuts it down on teardown. -This allows the use of automated test clients other than the -:ref:`Django dummy client ` such as, for example, the Selenium_ -client, to execute a series of functional tests inside a browser and simulate a -real user's actions. - -By default the live server's address is `'localhost:8081'` and the full URL -can be accessed during the tests with ``self.live_server_url``. If you'd like -to change the default address (in the case, for example, where the 8081 port is -already taken) then you may pass a different one to the :djadmin:`test` command -via the :djadminopt:`--liveserver` option, for example: - -.. code-block:: bash - - ./manage.py test --liveserver=localhost:8082 - -Another way of changing the default server address is by setting the -`DJANGO_LIVE_TEST_SERVER_ADDRESS` environment variable somewhere in your -code (for example, in a :ref:`custom test runner`): - -.. code-block:: python - - import os - os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost:8082' - -In the case where the tests are run by multiple processes in parallel (for -example, in the context of several simultaneous `continuous integration`_ -builds), the processes will compete for the same address, and therefore your -tests might randomly fail with an "Address already in use" error. To avoid this -problem, you can pass a comma-separated list of ports or ranges of ports (at -least as many as the number of potential parallel processes). For example: - -.. code-block:: bash - - ./manage.py test --liveserver=localhost:8082,8090-8100,9000-9200,7041 - -Then, during test execution, each new live test server will try every specified -port until it finds one that is free and takes it. - -.. _continuous integration: http://en.wikipedia.org/wiki/Continuous_integration - -To demonstrate how to use ``LiveServerTestCase``, let's write a simple Selenium -test. First of all, you need to install the `selenium package`_ into your -Python path: - -.. code-block:: bash - - pip install selenium - -Then, add a ``LiveServerTestCase``-based test to your app's tests module -(for example: ``myapp/tests.py``). The code for this test may look as follows: - -.. code-block:: python - - from django.test import LiveServerTestCase - from selenium.webdriver.firefox.webdriver import WebDriver - - class MySeleniumTests(LiveServerTestCase): - fixtures = ['user-data.json'] - - @classmethod - def setUpClass(cls): - cls.selenium = WebDriver() - super(MySeleniumTests, cls).setUpClass() - - @classmethod - def tearDownClass(cls): - cls.selenium.quit() - super(MySeleniumTests, cls).tearDownClass() - - def test_login(self): - self.selenium.get('%s%s' % (self.live_server_url, '/login/')) - username_input = self.selenium.find_element_by_name("username") - username_input.send_keys('myuser') - password_input = self.selenium.find_element_by_name("password") - password_input.send_keys('secret') - self.selenium.find_element_by_xpath('//input[@value="Log in"]').click() - -Finally, you may run the test as follows: - -.. code-block:: bash - - ./manage.py test myapp.MySeleniumTests.test_login - -This example will automatically open Firefox then go to the login page, enter -the credentials and press the "Log in" button. Selenium offers other drivers in -case you do not have Firefox installed or wish to use another browser. The -example above is just a tiny fraction of what the Selenium client can do; check -out the `full reference`_ for more details. - -.. _Selenium: http://seleniumhq.org/ -.. _selenium package: http://pypi.python.org/pypi/selenium -.. _full reference: http://selenium-python.readthedocs.org/en/latest/api.html -.. _Firefox: http://www.mozilla.com/firefox/ - -.. note:: - - ``LiveServerTestCase`` makes use of the :doc:`staticfiles contrib app - ` so you'll need to have your project configured - accordingly (in particular by setting :setting:`STATIC_URL`). - -.. note:: - - When using an in-memory SQLite database to run the tests, the same database - connection will be shared by two threads in parallel: the thread in which - the live server is run and the thread in which the test case is run. It's - important to prevent simultaneous database queries via this shared - connection by the two threads, as that may sometimes randomly cause the - tests to fail. So you need to ensure that the two threads don't access the - database at the same time. In particular, this means that in some cases - (for example, just after clicking a link or submitting a form), you might - need to check that a response is received by Selenium and that the next - page is loaded before proceeding with further test execution. - Do this, for example, by making Selenium wait until the `` HTML tag - is found in the response (requires Selenium > 2.13): - - .. code-block:: python - - def test_login(self): - from selenium.webdriver.support.wait import WebDriverWait - timeout = 2 - ... - self.selenium.find_element_by_xpath('//input[@value="Log in"]').click() - # Wait until the response is received - WebDriverWait(self.selenium, timeout).until( - lambda driver: driver.find_element_by_tag_name('body')) - - The tricky thing here is that there's really no such thing as a "page load," - especially in modern Web apps that generate HTML dynamically after the - server generates the initial document. So, simply checking for the presence - of `` in the response might not necessarily be appropriate for all - use cases. Please refer to the `Selenium FAQ`_ and - `Selenium documentation`_ for more information. - - .. _Selenium FAQ: http://code.google.com/p/selenium/wiki/FrequentlyAskedQuestions#Q:_WebDriver_fails_to_find_elements_/_Does_not_block_on_page_loa - .. _Selenium documentation: http://seleniumhq.org/docs/04_webdriver_advanced.html#explicit-waits - -Using different testing frameworks -================================== - -Clearly, :mod:`doctest` and :mod:`unittest` are not the only Python testing -frameworks. While Django doesn't provide explicit support for alternative -frameworks, it does provide a way to invoke tests constructed for an -alternative framework as if they were normal Django tests. - -When you run ``./manage.py test``, Django looks at the :setting:`TEST_RUNNER` -setting to determine what to do. By default, :setting:`TEST_RUNNER` points to -``'django.test.simple.DjangoTestSuiteRunner'``. This class defines the default Django -testing behavior. This behavior involves: - -#. Performing global pre-test setup. - -#. Looking for unit tests and doctests in the ``models.py`` and - ``tests.py`` files in each installed application. - -#. Creating the test databases. - -#. Running ``syncdb`` to install models and initial data into the test - databases. - -#. Running the unit tests and doctests that are found. - -#. Destroying the test databases. - -#. Performing global post-test teardown. - -If you define your own test runner class and point :setting:`TEST_RUNNER` at -that class, Django will execute your test runner whenever you run -``./manage.py test``. In this way, it is possible to use any test framework -that can be executed from Python code, or to modify the Django test execution -process to satisfy whatever testing requirements you may have. - -.. _topics-testing-test_runner: - -Defining a test runner ----------------------- - -.. currentmodule:: django.test.simple - -A test runner is a class defining a ``run_tests()`` method. Django ships -with a ``DjangoTestSuiteRunner`` class that defines the default Django -testing behavior. This class defines the ``run_tests()`` entry point, -plus a selection of other methods that are used to by ``run_tests()`` to -set up, execute and tear down the test suite. - -.. class:: DjangoTestSuiteRunner(verbosity=1, interactive=True, failfast=True, **kwargs) - - ``verbosity`` determines the amount of notification and debug information - that will be printed to the console; ``0`` is no output, ``1`` is normal - output, and ``2`` is verbose output. - - If ``interactive`` is ``True``, the test suite has permission to ask the - user for instructions when the test suite is executed. An example of this - behavior would be asking for permission to delete an existing test - database. If ``interactive`` is ``False``, the test suite must be able to - run without any manual intervention. - - If ``failfast`` is ``True``, the test suite will stop running after the - first test failure is detected. - - Django will, from time to time, extend the capabilities of - the test runner by adding new arguments. The ``**kwargs`` declaration - allows for this expansion. If you subclass ``DjangoTestSuiteRunner`` or - write your own test runner, ensure accept and handle the ``**kwargs`` - parameter. - - .. versionadded:: 1.4 - - Your test runner may also define additional command-line options. - If you add an ``option_list`` attribute to a subclassed test runner, - those options will be added to the list of command-line options that - the :djadmin:`test` command can use. - -Attributes -~~~~~~~~~~ - -.. attribute:: DjangoTestSuiteRunner.option_list - - .. versionadded:: 1.4 - - This is the tuple of ``optparse`` options which will be fed into the - management command's ``OptionParser`` for parsing arguments. See the - documentation for Python's ``optparse`` module for more details. - -Methods -~~~~~~~ - -.. method:: DjangoTestSuiteRunner.run_tests(test_labels, extra_tests=None, **kwargs) - - Run the test suite. - - ``test_labels`` is a list of strings describing the tests to be run. A test - label can take one of three forms: - - * ``app.TestCase.test_method`` -- Run a single test method in a test - case. - * ``app.TestCase`` -- Run all the test methods in a test case. - * ``app`` -- Search for and run all tests in the named application. - - If ``test_labels`` has a value of ``None``, the test runner should run - search for tests in all the applications in :setting:`INSTALLED_APPS`. - - ``extra_tests`` is a list of extra ``TestCase`` instances to add to the - suite that is executed by the test runner. These extra tests are run - in addition to those discovered in the modules listed in ``test_labels``. - - This method should return the number of tests that failed. - -.. method:: DjangoTestSuiteRunner.setup_test_environment(**kwargs) - - Sets up the test environment ready for testing. - -.. method:: DjangoTestSuiteRunner.build_suite(test_labels, extra_tests=None, **kwargs) - - Constructs a test suite that matches the test labels provided. - - ``test_labels`` is a list of strings describing the tests to be run. A test - label can take one of three forms: - - * ``app.TestCase.test_method`` -- Run a single test method in a test - case. - * ``app.TestCase`` -- Run all the test methods in a test case. - * ``app`` -- Search for and run all tests in the named application. - - If ``test_labels`` has a value of ``None``, the test runner should run - search for tests in all the applications in :setting:`INSTALLED_APPS`. - - ``extra_tests`` is a list of extra ``TestCase`` instances to add to the - suite that is executed by the test runner. These extra tests are run - in addition to those discovered in the modules listed in ``test_labels``. - - Returns a ``TestSuite`` instance ready to be run. - -.. method:: DjangoTestSuiteRunner.setup_databases(**kwargs) - - Creates the test databases. - - Returns a data structure that provides enough detail to undo the changes - that have been made. This data will be provided to the ``teardown_databases()`` - function at the conclusion of testing. - -.. method:: DjangoTestSuiteRunner.run_suite(suite, **kwargs) - - Runs the test suite. - - Returns the result produced by the running the test suite. - -.. method:: DjangoTestSuiteRunner.teardown_databases(old_config, **kwargs) - - Destroys the test databases, restoring pre-test conditions. - - ``old_config`` is a data structure defining the changes in the - database configuration that need to be reversed. It is the return - value of the ``setup_databases()`` method. - -.. method:: DjangoTestSuiteRunner.teardown_test_environment(**kwargs) - - Restores the pre-test environment. - -.. method:: DjangoTestSuiteRunner.suite_result(suite, result, **kwargs) - - Computes and returns a return code based on a test suite, and the result - from that test suite. - - -Testing utilities ------------------ - -.. module:: django.test.utils - :synopsis: Helpers to write custom test runners. - -To assist in the creation of your own test runner, Django provides a number of -utility methods in the ``django.test.utils`` module. - -.. function:: setup_test_environment() - - Performs any global pre-test setup, such as the installing the - instrumentation of the template rendering system and setting up - the dummy email outbox. - -.. function:: teardown_test_environment() - - Performs any global post-test teardown, such as removing the black - magic hooks into the template system and restoring normal email - services. - -.. currentmodule:: django.db.connection.creation - -The creation module of the database backend (``connection.creation``) -also provides some utilities that can be useful during testing. - -.. function:: create_test_db([verbosity=1, autoclobber=False]) - - Creates a new test database and runs ``syncdb`` against it. - - ``verbosity`` has the same behavior as in ``run_tests()``. - - ``autoclobber`` describes the behavior that will occur if a - database with the same name as the test database is discovered: - - * If ``autoclobber`` is ``False``, the user will be asked to - approve destroying the existing database. ``sys.exit`` is - called if the user does not approve. - - * If autoclobber is ``True``, the database will be destroyed - without consulting the user. - - Returns the name of the test database that it created. - - ``create_test_db()`` has the side effect of modifying the value of - :setting:`NAME` in :setting:`DATABASES` to match the name of the test - database. - -.. function:: destroy_test_db(old_database_name, [verbosity=1]) - - Destroys the database whose name is the value of :setting:`NAME` in - :setting:`DATABASES`, and sets :setting:`NAME` to the value of - ``old_database_name``. - - The ``verbosity`` argument has the same behavior as for - :class:`~django.test.simple.DjangoTestSuiteRunner`. From d9a0b6ab36b6beb4251acf1de1fdf255e1223e02 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 17 Dec 2012 10:47:38 +0100 Subject: [PATCH 018/870] Fixed #19487 -- Used str in the test client's WSGI environ. This regression was introduced by the unicode_literals patch. The WSGI spec mandates that environ contains native strings. --- django/test/client.py | 46 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/django/test/client.py b/django/test/client.py index 6d12321075..a3c04bb20d 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -21,7 +21,7 @@ from django.http import SimpleCookie, HttpRequest, QueryDict from django.template import TemplateDoesNotExist from django.test import signals from django.utils.functional import curry -from django.utils.encoding import force_bytes +from django.utils.encoding import force_bytes, force_str from django.utils.http import urlencode from django.utils.importlib import import_module from django.utils.itercompat import is_iterable @@ -205,15 +205,15 @@ class RequestFactory(object): # See http://www.python.org/dev/peps/pep-3333/#environ-variables environ = { 'HTTP_COOKIE': self.cookies.output(header='', sep='; '), - 'PATH_INFO': '/', - 'REMOTE_ADDR': '127.0.0.1', - 'REQUEST_METHOD': 'GET', - 'SCRIPT_NAME': '', - 'SERVER_NAME': 'testserver', - 'SERVER_PORT': '80', - 'SERVER_PROTOCOL': 'HTTP/1.1', + 'PATH_INFO': str('/'), + 'REMOTE_ADDR': str('127.0.0.1'), + 'REQUEST_METHOD': str('GET'), + 'SCRIPT_NAME': str(''), + 'SERVER_NAME': str('testserver'), + 'SERVER_PORT': str('80'), + 'SERVER_PROTOCOL': str('HTTP/1.1'), 'wsgi.version': (1, 0), - 'wsgi.url_scheme': 'http', + 'wsgi.url_scheme': str('http'), 'wsgi.input': FakePayload(b''), 'wsgi.errors': self.errors, 'wsgi.multiprocess': True, @@ -241,21 +241,21 @@ class RequestFactory(object): return force_bytes(data, encoding=charset) def _get_path(self, parsed): + path = force_str(parsed[2]) # If there are parameters, add them if parsed[3]: - return unquote(parsed[2] + ";" + parsed[3]) - else: - return unquote(parsed[2]) + path += str(";") + force_str(parsed[3]) + return unquote(path) def get(self, path, data={}, **extra): "Construct a GET request." parsed = urlparse(path) r = { - 'CONTENT_TYPE': 'text/html; charset=utf-8', + 'CONTENT_TYPE': str('text/html; charset=utf-8'), 'PATH_INFO': self._get_path(parsed), - 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], - 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': urlencode(data, doseq=True) or force_str(parsed[4]), + 'REQUEST_METHOD': str('GET'), } r.update(extra) return self.request(**r) @@ -271,8 +271,8 @@ class RequestFactory(object): 'CONTENT_LENGTH': len(post_data), 'CONTENT_TYPE': content_type, 'PATH_INFO': self._get_path(parsed), - 'QUERY_STRING': parsed[4], - 'REQUEST_METHOD': 'POST', + 'QUERY_STRING': force_str(parsed[4]), + 'REQUEST_METHOD': str('POST'), 'wsgi.input': FakePayload(post_data), } r.update(extra) @@ -283,10 +283,10 @@ class RequestFactory(object): parsed = urlparse(path) r = { - 'CONTENT_TYPE': 'text/html; charset=utf-8', + 'CONTENT_TYPE': str('text/html; charset=utf-8'), 'PATH_INFO': self._get_path(parsed), - 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], - 'REQUEST_METHOD': 'HEAD', + 'QUERY_STRING': urlencode(data, doseq=True) or force_str(parsed[4]), + 'REQUEST_METHOD': str('HEAD'), } r.update(extra) return self.request(**r) @@ -312,13 +312,13 @@ class RequestFactory(object): data = force_bytes(data, settings.DEFAULT_CHARSET) r = { 'PATH_INFO': self._get_path(parsed), - 'QUERY_STRING': parsed[4], - 'REQUEST_METHOD': method, + 'QUERY_STRING': force_str(parsed[4]), + 'REQUEST_METHOD': str(method), } if data: r.update({ 'CONTENT_LENGTH': len(data), - 'CONTENT_TYPE': content_type, + 'CONTENT_TYPE': str(content_type), 'wsgi.input': FakePayload(data), }) r.update(extra) From 1e4a27d08790c96f657d2e960c8142d1ca69aede Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 17 Dec 2012 10:49:26 +0100 Subject: [PATCH 019/870] Fixed #19468 -- Decoded request.path correctly on Python 3. Thanks aliva for the report and claudep for the feedback. --- django/contrib/staticfiles/handlers.py | 3 +- django/core/handlers/base.py | 43 ++++++++++++++++--------- django/core/handlers/wsgi.py | 2 +- django/test/client.py | 6 +++- tests/regressiontests/handlers/tests.py | 3 +- tests/regressiontests/requests/tests.py | 11 +++++++ 6 files changed, 49 insertions(+), 19 deletions(-) diff --git a/django/contrib/staticfiles/handlers.py b/django/contrib/staticfiles/handlers.py index 9067a0e75e..5174586ba1 100644 --- a/django/contrib/staticfiles/handlers.py +++ b/django/contrib/staticfiles/handlers.py @@ -6,6 +6,7 @@ except ImportError: # Python 2 from urlparse import urlparse from django.conf import settings +from django.core.handlers.base import get_path_info from django.core.handlers.wsgi import WSGIHandler from django.contrib.staticfiles import utils @@ -67,6 +68,6 @@ class StaticFilesHandler(WSGIHandler): return super(StaticFilesHandler, self).get_response(request) def __call__(self, environ, start_response): - if not self._should_handle(environ['PATH_INFO']): + if not self._should_handle(get_path_info(environ)): return self.application(environ, start_response) return super(StaticFilesHandler, self).__call__(environ, start_response) diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 0caf6b29fa..023947585e 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -5,10 +5,14 @@ import sys import types from django import http +from django.conf import settings +from django.core import exceptions +from django.core import urlresolvers from django.core import signals from django.utils.encoding import force_text from django.utils.importlib import import_module from django.utils import six +from django.views import debug logger = logging.getLogger('django.request') @@ -32,8 +36,6 @@ class BaseHandler(object): Must be called after the environment is fixed (see __call__ in subclasses). """ - from django.conf import settings - from django.core import exceptions self._view_middleware = [] self._template_response_middleware = [] self._response_middleware = [] @@ -75,9 +77,6 @@ class BaseHandler(object): def get_response(self, request): "Returns an HttpResponse object for the given HttpRequest" - from django.core import exceptions, urlresolvers - from django.conf import settings - try: # Setup default url resolver for this thread, this code is outside # the try/except so we don't get a spurious "unbound local @@ -147,7 +146,6 @@ class BaseHandler(object): 'request': request }) if settings.DEBUG: - from django.views import debug response = debug.technical_404_response(request, e) else: try: @@ -204,8 +202,6 @@ class BaseHandler(object): caused by anything, so assuming something like the database is always available would be an error. """ - from django.conf import settings - if settings.DEBUG_PROPAGATE_EXCEPTIONS: raise @@ -218,7 +214,6 @@ class BaseHandler(object): ) if settings.DEBUG: - from django.views import debug return debug.technical_500_response(request, *exc_info) # If Http500 handler is not installed, re-raise last exception @@ -238,6 +233,20 @@ class BaseHandler(object): response = func(request, response) return response + +def get_path_info(environ): + """ + Returns the HTTP request's PATH_INFO as a unicode string. + """ + path_info = environ.get('PATH_INFO', str('/')) + # Under Python 3, strings in environ are decoded with ISO-8859-1; + # re-encode to recover the original bytestring provided by the webserver. + if six.PY3: + path_info = path_info.encode('iso-8859-1') + # It'd be better to implement URI-to-IRI decoding, see #19508. + return path_info.decode('utf-8') + + def get_script_name(environ): """ Returns the equivalent of the HTTP request's SCRIPT_NAME environment @@ -246,7 +255,6 @@ def get_script_name(environ): from the client's perspective), unless the FORCE_SCRIPT_NAME setting is set (to anything). """ - from django.conf import settings if settings.FORCE_SCRIPT_NAME is not None: return force_text(settings.FORCE_SCRIPT_NAME) @@ -255,9 +263,14 @@ def get_script_name(environ): # rewrites. Unfortunately not every Web server (lighttpd!) passes this # information through all the time, so FORCE_SCRIPT_NAME, above, is still # needed. - script_url = environ.get('SCRIPT_URL', '') - if not script_url: - script_url = environ.get('REDIRECT_URL', '') + script_url = environ.get('SCRIPT_URL', environ.get('REDIRECT_URL', str(''))) if script_url: - return force_text(script_url[:-len(environ.get('PATH_INFO', ''))]) - return force_text(environ.get('SCRIPT_NAME', '')) + script_name = script_url[:-len(environ.get('PATH_INFO', str('')))] + else: + script_name = environ.get('SCRIPT_NAME', str('')) + # Under Python 3, strings in environ are decoded with ISO-8859-1; + # re-encode to recover the original bytestring provided by the webserver. + if six.PY3: + script_name = script_name.encode('iso-8859-1') + # It'd be better to implement URI-to-IRI decoding, see #19508. + return script_name.decode('utf-8') diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 4c0710549a..426679ca7b 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -128,7 +128,7 @@ class LimitedStream(object): class WSGIRequest(http.HttpRequest): def __init__(self, environ): script_name = base.get_script_name(environ) - path_info = force_text(environ.get('PATH_INFO', '/')) + path_info = base.get_path_info(environ) if not path_info or path_info == script_name: # Sometimes PATH_INFO exists, but is empty (e.g. accessing # the SCRIPT_NAME URL without a trailing slash). We really need to diff --git a/django/test/client.py b/django/test/client.py index a3c04bb20d..015ee1309a 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -245,7 +245,11 @@ class RequestFactory(object): # If there are parameters, add them if parsed[3]: path += str(";") + force_str(parsed[3]) - return unquote(path) + path = unquote(path) + # WSGI requires latin-1 encoded strings. See get_path_info(). + if six.PY3: + path = path.encode('utf-8').decode('iso-8859-1') + return path def get(self, path, data={}, **extra): "Construct a GET request." diff --git a/tests/regressiontests/handlers/tests.py b/tests/regressiontests/handlers/tests.py index 8676a448d9..9cd5816219 100644 --- a/tests/regressiontests/handlers/tests.py +++ b/tests/regressiontests/handlers/tests.py @@ -1,6 +1,7 @@ from django.core.handlers.wsgi import WSGIHandler from django.test import RequestFactory from django.test.utils import override_settings +from django.utils import six from django.utils import unittest class HandlerTests(unittest.TestCase): @@ -22,7 +23,7 @@ class HandlerTests(unittest.TestCase): def test_bad_path_info(self): """Tests for bug #15672 ('request' referenced before assignment)""" environ = RequestFactory().get('/').environ - environ['PATH_INFO'] = b'\xed' + environ['PATH_INFO'] = '\xed' handler = WSGIHandler() response = handler(environ, lambda *a, **k: None) self.assertEqual(response.status_code, 400) diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py index adf824dff7..bb7f925e87 100644 --- a/tests/regressiontests/requests/tests.py +++ b/tests/regressiontests/requests/tests.py @@ -11,6 +11,7 @@ from django.core.handlers.wsgi import WSGIRequest, LimitedStream from django.http import HttpRequest, HttpResponse, parse_cookie, build_request_repr, UnreadablePostError from django.test.client import FakePayload from django.test.utils import override_settings, str_prefix +from django.utils import six from django.utils import unittest from django.utils.http import cookie_date, urlencode from django.utils.timezone import utc @@ -57,6 +58,16 @@ class RequestsTests(unittest.TestCase): self.assertEqual(build_request_repr(request, path_override='/otherpath/', GET_override={'a': 'b'}, POST_override={'c': 'd'}, COOKIES_override={'e': 'f'}, META_override={'g': 'h'}), str_prefix("")) + def test_wsgirequest_path_info(self): + def wsgi_str(path_info): + path_info = path_info.encode('utf-8') # Actual URL sent by the browser (bytestring) + if six.PY3: + path_info = path_info.decode('iso-8859-1') # Value in the WSGI environ dict (native string) + return path_info + # Regression for #19468 + request = WSGIRequest({'PATH_INFO': wsgi_str("/سلام/"), 'REQUEST_METHOD': 'get', 'wsgi.input': BytesIO(b'')}) + self.assertEqual(request.path, "/سلام/") + def test_parse_cookie(self): self.assertEqual(parse_cookie('invalid@key=true'), {}) From 0dc3fc954f53d5b03b864e63b309acfdbb40dbf9 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 22 Dec 2012 16:00:15 +0100 Subject: [PATCH 020/870] Fixed #19509 -- Fixed crypt/bcrypt non-ascii password encoding Also systematically added non-ascii passwords in hashers test suite. Thanks Vaal for the report. --- django/contrib/auth/hashers.py | 12 ++-- django/contrib/auth/tests/hashers.py | 89 ++++++++++++++-------------- 2 files changed, 52 insertions(+), 49 deletions(-) diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index c628059d34..b3631a6b8c 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -8,7 +8,7 @@ from django.conf import settings from django.test.signals import setting_changed from django.utils import importlib from django.utils.datastructures import SortedDict -from django.utils.encoding import force_bytes +from django.utils.encoding import force_bytes, force_str from django.core.exceptions import ImproperlyConfigured from django.utils.crypto import ( pbkdf2, constant_time_compare, get_random_string) @@ -275,14 +275,16 @@ class BCryptPasswordHasher(BasePasswordHasher): def encode(self, password, salt): bcrypt = self._load_library() - data = bcrypt.hashpw(password, salt) + # Need to reevaluate the force_bytes call once bcrypt is supported on + # Python 3 + data = bcrypt.hashpw(force_bytes(password), salt) return "%s$%s" % (self.algorithm, data) def verify(self, password, encoded): algorithm, data = encoded.split('$', 1) assert algorithm == self.algorithm bcrypt = self._load_library() - return constant_time_compare(data, bcrypt.hashpw(password, data)) + return constant_time_compare(data, bcrypt.hashpw(force_bytes(password), data)) def safe_summary(self, encoded): algorithm, empty, algostr, work_factor, data = encoded.split('$', 4) @@ -395,7 +397,7 @@ class CryptPasswordHasher(BasePasswordHasher): def encode(self, password, salt): crypt = self._load_library() assert len(salt) == 2 - data = crypt.crypt(password, salt) + data = crypt.crypt(force_str(password), salt) # we don't need to store the salt, but Django used to do this return "%s$%s$%s" % (self.algorithm, '', data) @@ -403,7 +405,7 @@ class CryptPasswordHasher(BasePasswordHasher): crypt = self._load_library() algorithm, salt, data = encoded.split('$', 2) assert algorithm == self.algorithm - return constant_time_compare(data, crypt.crypt(password, data)) + return constant_time_compare(data, crypt.crypt(force_str(password), data)) def safe_summary(self, encoded): algorithm, salt, data = encoded.split('$', 2) diff --git a/django/contrib/auth/tests/hashers.py b/django/contrib/auth/tests/hashers.py index d867a57d98..e2a3537695 100644 --- a/django/contrib/auth/tests/hashers.py +++ b/django/contrib/auth/tests/hashers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.conf.global_settings import PASSWORD_HASHERS as default_hashers @@ -25,63 +26,63 @@ class TestUtilsHashPass(unittest.TestCase): load_hashers(password_hashers=default_hashers) def test_simple(self): - encoded = make_password('letmein') + encoded = make_password('lètmein') self.assertTrue(encoded.startswith('pbkdf2_sha256$')) self.assertTrue(is_password_usable(encoded)) - self.assertTrue(check_password('letmein', encoded)) - self.assertFalse(check_password('letmeinz', encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) def test_pkbdf2(self): - encoded = make_password('letmein', 'seasalt', 'pbkdf2_sha256') - self.assertEqual(encoded, -'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=') + encoded = make_password('lètmein', 'seasalt', 'pbkdf2_sha256') + self.assertEqual(encoded, + 'pbkdf2_sha256$10000$seasalt$CWWFdHOWwPnki7HvkcqN9iA2T3KLW1cf2uZ5kvArtVY=') self.assertTrue(is_password_usable(encoded)) - self.assertTrue(check_password('letmein', encoded)) - self.assertFalse(check_password('letmeinz', encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256") def test_sha1(self): - encoded = make_password('letmein', 'seasalt', 'sha1') - self.assertEqual(encoded, -'sha1$seasalt$fec3530984afba6bade3347b7140d1a7da7da8c7') + encoded = make_password('lètmein', 'seasalt', 'sha1') + self.assertEqual(encoded, + 'sha1$seasalt$cff36ea83f5706ce9aa7454e63e431fc726b2dc8') self.assertTrue(is_password_usable(encoded)) - self.assertTrue(check_password('letmein', encoded)) - self.assertFalse(check_password('letmeinz', encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) self.assertEqual(identify_hasher(encoded).algorithm, "sha1") def test_md5(self): - encoded = make_password('letmein', 'seasalt', 'md5') + encoded = make_password('lètmein', 'seasalt', 'md5') self.assertEqual(encoded, - 'md5$seasalt$f5531bef9f3687d0ccf0f617f0e25573') + 'md5$seasalt$3f86d0d3d465b7b458c231bf3555c0e3') self.assertTrue(is_password_usable(encoded)) - self.assertTrue(check_password('letmein', encoded)) - self.assertFalse(check_password('letmeinz', encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) self.assertEqual(identify_hasher(encoded).algorithm, "md5") def test_unsalted_md5(self): - encoded = make_password('letmein', 'seasalt', 'unsalted_md5') - self.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7') + encoded = make_password('lètmein', 'seasalt', 'unsalted_md5') + self.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43') self.assertTrue(is_password_usable(encoded)) - self.assertTrue(check_password('letmein', encoded)) - self.assertFalse(check_password('letmeinz', encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_md5") @skipUnless(crypt, "no crypt module to generate password.") def test_crypt(self): - encoded = make_password('letmein', 'ab', 'crypt') - self.assertEqual(encoded, 'crypt$$abN/qM.L/H8EQ') + encoded = make_password('lètmei', 'ab', 'crypt') + self.assertEqual(encoded, 'crypt$$ab1Hv2Lg7ltQo') self.assertTrue(is_password_usable(encoded)) - self.assertTrue(check_password('letmein', encoded)) - self.assertFalse(check_password('letmeinz', encoded)) + self.assertTrue(check_password('lètmei', encoded)) + self.assertFalse(check_password('lètmeiz', encoded)) self.assertEqual(identify_hasher(encoded).algorithm, "crypt") @skipUnless(bcrypt, "py-bcrypt not installed") def test_bcrypt(self): - encoded = make_password('letmein', hasher='bcrypt') + encoded = make_password('lètmein', hasher='bcrypt') self.assertTrue(is_password_usable(encoded)) self.assertTrue(encoded.startswith('bcrypt$')) - self.assertTrue(check_password('letmein', encoded)) - self.assertFalse(check_password('letmeinz', encoded)) + self.assertTrue(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt") def test_unusable(self): @@ -90,46 +91,46 @@ class TestUtilsHashPass(unittest.TestCase): self.assertFalse(check_password(None, encoded)) self.assertFalse(check_password(UNUSABLE_PASSWORD, encoded)) self.assertFalse(check_password('', encoded)) - self.assertFalse(check_password('letmein', encoded)) - self.assertFalse(check_password('letmeinz', encoded)) + self.assertFalse(check_password('lètmein', encoded)) + self.assertFalse(check_password('lètmeinz', encoded)) self.assertRaises(ValueError, identify_hasher, encoded) def test_bad_algorithm(self): def doit(): - make_password('letmein', hasher='lolcat') + make_password('lètmein', hasher='lolcat') self.assertRaises(ValueError, doit) self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash") def test_bad_encoded(self): - self.assertFalse(is_password_usable('letmein_badencoded')) + self.assertFalse(is_password_usable('lètmein_badencoded')) self.assertFalse(is_password_usable('')) def test_low_level_pkbdf2(self): hasher = PBKDF2PasswordHasher() - encoded = hasher.encode('letmein', 'seasalt') - self.assertEqual(encoded, -'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=') - self.assertTrue(hasher.verify('letmein', encoded)) + encoded = hasher.encode('lètmein', 'seasalt') + self.assertEqual(encoded, + 'pbkdf2_sha256$10000$seasalt$CWWFdHOWwPnki7HvkcqN9iA2T3KLW1cf2uZ5kvArtVY=') + self.assertTrue(hasher.verify('lètmein', encoded)) def test_low_level_pbkdf2_sha1(self): hasher = PBKDF2SHA1PasswordHasher() - encoded = hasher.encode('letmein', 'seasalt') - self.assertEqual(encoded, -'pbkdf2_sha1$10000$seasalt$91JiNKgwADC8j2j86Ije/cc4vfQ=') - self.assertTrue(hasher.verify('letmein', encoded)) + encoded = hasher.encode('lètmein', 'seasalt') + self.assertEqual(encoded, + 'pbkdf2_sha1$10000$seasalt$oAfF6vgs95ncksAhGXOWf4Okq7o=') + self.assertTrue(hasher.verify('lètmein', encoded)) def test_upgrade(self): self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm) for algo in ('sha1', 'md5'): - encoded = make_password('letmein', hasher=algo) + encoded = make_password('lètmein', hasher=algo) state = {'upgraded': False} def setter(password): state['upgraded'] = True - self.assertTrue(check_password('letmein', encoded, setter)) + self.assertTrue(check_password('lètmein', encoded, setter)) self.assertTrue(state['upgraded']) def test_no_upgrade(self): - encoded = make_password('letmein') + encoded = make_password('lètmein') state = {'upgraded': False} def setter(): state['upgraded'] = True @@ -139,7 +140,7 @@ class TestUtilsHashPass(unittest.TestCase): def test_no_upgrade_on_incorrect_pass(self): self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm) for algo in ('sha1', 'md5'): - encoded = make_password('letmein', hasher=algo) + encoded = make_password('lètmein', hasher=algo) state = {'upgraded': False} def setter(): state['upgraded'] = True From 38f725da547f07baaa791acfe2c116a7fc6b02fe Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 22 Dec 2012 12:08:22 -0300 Subject: [PATCH 021/870] Better name for a new testing documentation link. --- docs/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.txt b/docs/index.txt index ab00da271c..e8e7eadb23 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -181,7 +181,7 @@ testing of Django applications: :doc:`Adding custom commands ` * **Testing:** - :doc:`Overview ` | + :doc:`Introduction ` | :doc:`Writing and running tests ` | :doc:`Advanced topics ` | :doc:`Doctests ` From 9d62220e008c5e1b03e3026aaa97afac2e4eb67b Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 22 Dec 2012 19:01:55 +0100 Subject: [PATCH 022/870] Fixed #15516 -- Updated the ticket life cycle diagram. --- docs/internals/_images/djangotickets.png | Bin 52003 -> 0 bytes docs/internals/_images/triage_process.graffle | 2652 +++++++++++++++++ docs/internals/_images/triage_process.pdf | Bin 0 -> 70123 bytes docs/internals/_images/triage_process.svg | 3 + .../contributing/triaging-tickets.txt | 6 +- 5 files changed, 2658 insertions(+), 3 deletions(-) delete mode 100644 docs/internals/_images/djangotickets.png create mode 100644 docs/internals/_images/triage_process.graffle create mode 100644 docs/internals/_images/triage_process.pdf create mode 100644 docs/internals/_images/triage_process.svg diff --git a/docs/internals/_images/djangotickets.png b/docs/internals/_images/djangotickets.png deleted file mode 100644 index 34a2a41852579920166184b48338361438c11274..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52003 zcmbsRc|4Wv8#NAJq|7oUWk`~cjD=(hWlS1m$V{TlLnLD&QY3T85D86)$}EJGF-e&v zQ)JGV-`e;0dEU?OegA#;ANQx5ZSU(k&+|Brb*#0HD@<2Mjh<#V4M7m}8tSTN34&aQ zAjoQ{$ng`C4f3P-pB_6E6lD_v~dYNV1R&(~}bzYD}U$&Ln!1RaNdnnSqn3TxcuX z;?N$Rr*)>9hTE?;q~98%+|si4{?p&jYsZCuoX>S29=!~zYjP{N;MA}zb@LpbhD6)z z5+O4YvrE=f4toiO)6%h5D$Q1BNBh1WlD_sgt9RAI-*Q2oU_So5kBB1ImLB3Xf1zqj z_KCyDj`2{{Yok*fwGKQx&a=_-d=0$CG0VY|&pb=X)Aq)g-jY&%Z7FyA2N~zKYb9*# zN2`iT<0wDK-d`+>p#LB}keo$MpD%7+f-AB8`OoR?0^Qx)Dlh--HvN3u)qm{NrRwD1 zH<7pdrWwc?qlbGZ6L%4{v-C1c0(72vjEdVSe^|V&WD21at=jB2zZD5(nnvSQ^@uHJU|t zMt)oIkMEkMU!@wjHEec~yqUP%Xf?>lBbcv5-6+?@`dFU)?pDT4YB{gJKiAUV6cs)p zqv7(9H1N1+^egttv4Ymw^QVs;*JSgZRMu?H>?kDs$;z|BbrqubE)oO}p`m*GyjSw? zlJUJdANG#-^r&);7&Q`Hp(p{5sa3 zbjyNWiHDJjBIB>}1rJL6gFJA|Sof(4ex@o)87zzcS6XoW|KkU@zgJvozh>EB{*0C< zN?l^lK75BVg9EKctrtIA_+TnCWo)M;1x3i=q0JdGinN5=xSZ#0A-cCIr8x5q=&U&C zZg_oC*(bMOna+-nIZT^>cykNUU@f84#t^8)^E1Qt>Anv~^9_WV!_1Rw_6(#mY*S>N ziXrE23_Tv9&3{gSl_pr3Kf2CR{LVGp+}_mUdlNG<2Uuyy22X19N6)5n2P=<1YhKb* zr@)7r+lCbf%d(GR4Ri(0>+5H?*`{YDeG%QtxTZQK7ooF!P9%f&@Ufc3rYZO3Q1$~=sRrKM}Wg@_KP>yqy4P+Q0ut_qp@(fOpL zq&C&8T&%^ZQ>O?*#)*2E(}?o~4`XPYt~>2fV@_sVcDgPP)ZjF(NZR8>9RE)=v5D<7BwqGRhKp_{b4@P8r=uEf53n525f%{O;cjbh7dm+Gs@c_EHY{gF4nx0P)#!Yixw2lBE~UTo0LOOH zY4oO~rlw|NW9xWwD#-uW&!2Q0QlEbOSZhu>R%?0n>$AaGFS>2R-;1v9+O?}G?$8#( zoSlt>vlU*-|gZ(!*{vNsdl^}9+bJsUEcF}zpthr4_j5ep-Se_YtW>{Ek z-+goEuUW5D-=$qXW!I*R^!4@EPbWK7tbF+V`CheoW>%I)wm~SZnC&>QcT;;C<~t&O!wIkq($tnd13d z^EhKJW2rMZCO+PObs=ZpA$8}upBmheT@iQu|E(NI`h3CiG%J}<(7?q2%A>3kjG?AV zCMG6l&a`!8pEfJ=ib_r0P8hs#?0x4ps`E(honx=8W#b=`fZ9|K?LSeg3VCMjtrMEoQEC>C&aO zRi5e+k4YI$onu0d{T1btYO6C{+Y}y6Hrzl$DB1D-`}bp>(_h-!@(T;sW(HM!eK&^d zBTpyGxZnyZH~(>Hz8rBLZ_jFsK5)^{kc=3p_)$_)VpwRipPzqXVuGA_5FNco(Qm!i zv!9yc=GxysO-`R18q|f^M2+9-t}cy@jTxLhd)B}}fa|HdyL-mt$7_p|JGr?<+&)nf zZ@m{M7REbr&R@Y^cl~~FrLb4hrR~MO1AfN;mS>CVCmnr`^eK2rSxI$>glgJlN!HcX ziHnN|2M2%s`jrSkdiw@V_kvsun(*|{@UPeerI#EBDP2FmZ>zc)YT znwyuCV|V4sN1xn^ii&V9_3z0qb>7bw1X0z!KUf!Uc=-v#ziykUq9bz$q$0VFNnQCO zz&=V4yLazyWt$8at$3B6@8ayNuAxD;sdKo#u8y8VijHs|Yu!spNy!$zxHNUq@+xnX zfUNBIk00;US(36+A$06Vn+J6Z(rmfp>JW-z+%n2vgm* zZCich9%%^)A)(d&&nt_QUHkX%=a6=MT~rkN^}yVUj{tqS=O0rgC4x{=Q_IE|R8;&! zvV5hjq*OIKT;FZK^unan-1d+C_iGFp<8E4)CcHduOi%YcT=Lc0JgeoX)s=hY+3Mmy z(-oE^e6)gs0yE_vIWL@_ndRAG#ed6I*vD5la73`Q($ewu!&AMb(u#`yxQp4@+1fjv zfBFsy3SM9O-Tg84VA8{fVsz00a;`rc^7LOyNlB3t5nRXE*x3_OQcTRu%#4kV_4Nc&E*KRrTkIs z@T1^TQ*9W_7IN~L-mUjn=0|Z<6n&N%6^Ev#wf_Jun{a}H?<$tGx3||KvFK_SB2MSw z;l{>Bjnk)3AJx|sacHpTaj|k3=U=PuQ2g4voZHKFlO>76$-nfNbqig5Vr{l6Z`8uw z5i|dd9n8!K{NbNJRkJ%%`eHdw2H{ zAqk1Ov9`z0n}wA4`esyy$!mzK2x*Z$jC_7BSgvl zVrb=ktmDj{>z7*|1#dqY$({1x!C5o2)5(gr*5{f;Jv=;QWo4};uCFi85`<9f?wvc| zTxwzyilynA7?`c9s(SE%+2EkKcvQ`xt)1QR`aC;S=46>Dud8*XgA)@*y1FP1JT6xx z5`8xR`Eo~Q=i~_H-$xBy{PmebHl=ycdCXl_?ydWH`%$-FwpXtX*8w67e6wnKB#%&^ zs_?$^csS1m5_wPpQ zH?Lg1s&a{6f%li|J7kZ)Gc#1w)ZJy*CG#ycG{Pz>6lO!+E!e_6{;jV7fP6|2-@^8) zxOi!1CNwN8QPxAq6u0s;C#O1k$hWHcz_Uvi>Nh$gxaD`Vu}LAiZv35Ye5W8ETvjG` z`SNJgKDGCHSE;LCuWS6e$05D+r$4o6gmk*PyN~kopUjv=21C7O`4OL>FP=y=ZI5Q~HQFAhsehKGkwmeGcfC8^gR4Sn?J(E>$U zTpZh7w?oIXJHnbH4(~n%6kwyl9QLI>XV8IOgiqse#Ac+*T{j}2wzd{=B$@c>=FOY9 zn2+PR?NzKwe9W_6Wk^pb;{WDb6t7>uo|BVv`jOn3%om3hFK?*+cq%yCGN(3wL@+;i z5ADc#U@={NeLxby_4(dXPry`!3KptX>~77Vfq_A&DldD!dE$o;AC3oYbCKa3|LV6k z^#&1c#eTuSK>cwWa8)KPEd@C(E$z0TGkSW#76v%@(T#i}5AJau1^S?>5tEjF6EA9J z$-acMKiyZ(q2Mw6fu63W`H_67btCgir#Kx8O)z_i@5)GgzyD_WI&DI9v>P_;#~Y{7 zk&zAD2ha$PrX2V7{u`}}!lq?jI55kp`_v-nsHgzG41nTV6gTf$B&4SH<0CevdygU9 z>1y5@i^OX7R`?!s_j9>+&C%Zemg|+WDC|*V@&Up+A2%yUhH>6SWmmE0Lro=Jxjb*st8& z+`2zb@kgf@|7dR3iF)$Fw0!rTJ=?R|0kUpWB2^ELrE2p_A3372o_m5vR#`2e{NRU_u<3IbLUtp9=qmlA@1C{gJfM| z6I@IiFu&K8qN6?f6G03~cel080^8E=6wy9?y3%VlBYA1$G!IME1tTrn(jE4iPtNa7 zC0Fi_Vbsjd&Al@J>vOGWXk_GRq+vA$dkqbZU%!62Dw~`;7mYGoy59x|{Y_zEG|NN@ z^Nw33`4_DIh&H-Nc{rMRNF>_Z*$KM5%If8iIdn=|>l06tD4m_%Sq?(!t{dWMsJ{CL3k3yYzjkySyFSNP)}@r=Zf|cNuaU(>#fw7< zwD;%t9=-BLx^r9m+gm=Je0{8Wk;k#GY$bJbU9GcxOr(KHdwBlC;NT#3pgIy5r?sZW z3c;Lr;rW%$gMvKiuQc=4Vk*a2dMw{Q04LI+q#|)2t^?`1$y1+uPeaI#?7< z&zuJS15m5^l5-#9of+UN02SPm-^S`7WtjIto~vu3&&GcJ+P0^u$AFHC zG;x0(+xznMpjA5$4%=Me9pjg$uy>aTtX71zVeR}?iR^p5Dva+?uaoPZQV-M;# zKRgm{puZ}7csKpFGBkC+f7_Q?BMi@48pElL~ZPvJ0z=lUQa=d&zzK&>(U#GZd!WdHM1W%G=2Z z&h9T?tQ{TuTn=8LAiwEEg!}Bi7$e7>2CP#gZ)}hPN#D3*by5Ql@IK@Un>q>JX(l9Z~FZt zrKQbY+cL@3A(V%I{X*^-;MBohU%0>)RQ>eDi@tm-%ER$dQZIV)t?H3R61AT^fBsz6 zJ0*@;Q6s@Z8C=+3tPYqBhwg%{+))Dj7JtH&1Q@u@fATr)i&9sOV!d5cU2iReQV}IX z`Om*R78x^EGwW>5o!X;`m!taPh28#~^7bx?j*g}o<|J$jk8jVs=pcm#>x!x>#n+qE zKM@)ImHv^9%(5U2_0-fx5qFmP%Q*7h-rij=Ol~awP6Y4xx8J{I#P##%&*`jd;9Zo{ zXGoyz?%i53VR?C9v=lSHLE3efdW!Nly>(;HKEveh->Gv<~dZ7o-b?1&9 z-9LUea&(6qz6?N*(n|X)pTmUS;0i*%l`hG0Re=7 z)coJSmn9M}T)2SPMz$%6i~EXWpWYkx_^}mAu7$;3k%l@-rR=wR5owa64US3Yl`}S#RWxdzON}X_M&ZJTk4b9C8U`f<=Z)ae*=IQz7 z^=mo?21hj`?0y8&MN!d(?2MsL+d>t{S!IvO$S?*s-^3r zz;AI;cE*C0lG!>bcOoJHeT5Gl^6>O@a&(M4XS#Yoddr{Cse4rJpX3e7nKR=z3pA;jYG|zp+biVsu#V&JR>?xpCu$np!=m zS*=tZKIYFU$GY0v=K!k%)%ecx4g<1{qKTv17F2Ei?Z*#*e;1i#bU7e#`-y;Cw{Dq~ zUL!NS`3&gA$j}fp7z+6|Zo{H0ozE}63B9K^S(kaqGUX;}xO!as^RWvBF;Z>(Dq6fa zE$kt)UUEH3k)LfmU-^D}auYd5N?N)}kf$G8eE%fx%B$C}w4PLExo4OgCy$JddTy*P zuCA`4O+}-{*y4e9j%>>|YisK=zm4zh?FE^cnOQLn>>=g;n|?C`)iW+iN=nzRNrhOT zOmQ7`wZ49R1^g#SH8H{CY`+ofPG*&U@;=KmZllc>>`P#0!(QgTHq|!b|GF!UT3fa2 zzYEo+dyb^GC+eEkm8)wsG|%?Bz8+v)!A{*e+}?P5f5BQ`spodt)8s_jJfFh9=#cth zd7v~gvHPmL*Kgd=5{<>0+yxUv3`|X>#Ki#zgfwb~^)L&w9TXI_`||V*2#3s$@%EKC zH&@r>?EWUydf49@n2nh;;AWIapxcTy>Q(k$w z>j=&5nxmf6J-@pP!IL>3{|br`++bQ(7Q!bsI=W_bUeR~;ysmEAvPaeDqb`XFLmoTS zwx)|5+_-mOpWmmMa&&ZbdRoNI^-;|rLZH60(*s)tCFf9q4Tznr z_e<`>Tk~T=dg*c}+uGW6baW10cy`~+9^auB!Rg}adL^d{RVzCyD>y0n23$vKCp1yNLM_7_}u zqxE~=HPuT~pgiZ-?Q^=s>V14l(}S5aKdCR%1$cPBHGk5JOn zi><6w462s#Sr#MAQM=^0Bdg7gORv$33Numxqk(hv@$pG3F*15$tfx0JIjI$6sRBu3 zkB_|Zckbxr<{>@v(Mx_}1hKNRf~!HHs;;gk0_I0sZg_h$aLKxZ(@=^Fegbw1jH=NU z`oI8z2R;T&qZ*vh0utYyc6qD?`b+lN(+5vQ_72R=%;@qp2TFac{T?`XNzlWij74p3 z_r%4X=o_0a=x$_B%57_T2?d@uHEt zB)9##7^teIcKh~iaKJNOW#u>i{u&(xYb>sy;0Jma=Qwuxor9H?iktyjoT#YT%KKHtd`I(nwSMxwB_cgP;qP zm{;FKVtpO+D7QWzf|k$udYi(*coN97mRJGti9UH*X}%$9AG!*$A{QSYx$wG&ivIte zo_P!)^=~**@!#s?OXm4CynXFYrKh{QF=_z5;_>6hpyKi6FOA>L&5bmXx(+lV3JQHS zHN&7jC>c0;yk(6m{d{}gy3EX?x=A_peX^DS5&ds@I1*pY(r;Ks^Si9}GI1Fd0MP}+ z66n-}OLOXZOI9-v+#pVOP*_-4NXQ#F{9C@&{QNu+sfAZ`_ipkwwdLQ1E$4fKH&s^B zr(4`kSN{8R@0z}-uz;ws$5iR!8~>w>%twEms5TlOA4lTkp5`G-|G!=UNgFzzrwKU9 z0Tu@C?uUh2`7a%!;D3IA+r{18XLe}obiEg<5YBkpxcm0)k!WwA5hornDzFZN=t`I) zp3Qt0V^M=uWuKaU$kK|h__uo1lBW?&drM3Gn}_@d4s73}uwMG7f`y6#qTb)Xf8!l* zDW^kB;yZ94``I(PO+lf(=X6e;x)&b)SXTh&6RFj!_u?Bzlr##)-{Otdb1{PFbdqJn zK}qvWro^HisMy&20@Xs&)!n9kJUd=o_74rV~}}Bp1HX+ z+YUi&5`BF8cK!XmD%Mi(M_fU4mE+u_ln+quxBe)x_?bK~Ej_)us)~4RA1UbeAbY=; z9CxBNzs}(e@Zm>~9z}ChQ~!*{$d;E;!Vn7)-B>I3>Fd`E6^~x{J_B8Z3<;T0)9D?T z*Y)cgOFx`UlBdtFr0bkM?Y0s>w@!He`4q?C5pweTv&3e}QSs!E#Ny#h1V%X9At6yw z=_4Cvi=q|Fmgr*7oH>K;3%^3|^bYNz!sk=j9qBF#Oo@~(nH%hoNuaE;Gcmc~`=F*t zBntcT^72O6LM$KxpinPF=pN|U5%8p5#u-`qL33m z&_2z}(-KZdJ?827`&&MK()ZTIN{c%dMgfRK$g--zqeuhJY9EWgTag>4l7zNxK`uCd z0QNQe0Qj#j&+J{N7x5P{`gedPkS8N6t8uyEbac zDj9|S%^Od3^-x`bFSz*5&REgGduW+yXdwLToQ`bmh#FVl#l^J&MP6E3+SAjs$JFV= z-Cej>XcH`9s#sR&uViFoIL_$Pw-KLG6%}P%pPQWxlx(rEuz=nidGB6|#HBmyNulef zSu;fUF^54gdj9O0fiT;;IOXE0nQll>Sok4-&4%gB6tUB(3i7T$)sG!JCXg1bTg9~B zido~q2%q@2v;4`gZo9a+IFD{&I5V+-udG$Rfpd18f$-Ae;trbNw;v;)4p=-|ZOk8G!_`S|hUw(Z<>p4Tnd0V;X`h498@_UMsY;{yY_Mn*z#33&?1 z%96CZZxo#7;XH-M@;3=2Z)!-Ft@#exft7dq;VgdLpBg)7Q~{z|&wZk=XD1w-=3=kt`Dv z7?2nJ8(+H{4pX$ZwMA7rpMYd@)dmeQvwnzX*8As0uEGKue9SI8=xbgf7i&5-I7G|a zXr+=3k@LOavF+q8?8KRI0u?3ex9+w3-mj67ttT9o&z(GZ(lXy`@O`jzf$EdrIycWe zK6&EgNd@$se%Wc{(>V3a{aX_?9$eJ4Opb~oJ0$~jkWPJoBurmM;4rW4sj56H${a?b zv{0vw-K9JxzJRGF2pyb0w3cOZF9u58Mj2S2t*ov-$oT18vI0Bgy|y$JK(-~c z`3=|ct=tKz?-D&WG>%cDV?*8ys~hUuS?(>s$87g8Mlg4JD|wr2YrNL4bLWK?5ldp^ zJ9g}7jOo>Ih7^Geb{+CPd_{ZYz1_8FWN^JdVrt)J-QfCHsHg0X#^lN4$LDl)o&Qdo zWM~^1RgeDpBXvG(vy9*R+o&kS{Tb-%fLhmAHaVrNWi4UoXw2SEdudq6FDMwM%Inbc_Ke?l7Zx3wVDeKkXmyNyVHhGo z06it800`rtXP~yA|1W`G1#LA;_#lk}qF6TNQ9=T`k^YxvigLfp!OKAbqA^wvwoRh6 zf%I{^29@|Jl!-T#{24(?+duuoA5V5cMLLZpD407zP*4!J9q2YIWDU&>)rBj$M9zD` z*3|Tos?BAJjB|F4t;{vALPE9;Jy49H916}l__ec>Y4j+^?%i-yT4C-OcKUKG+mSKX`yxEWg~s{kAmO;8Dw{KSWj78a_5Q0vCS*S+L^sZ@c2U zGZGmjgX49I!~pid`Sf3KP-z{99UE4DcT394m;e6#8zco%A8HKpDN%(RAThd-+OC2r zmS}`m%A51fi_WNj{sX?n=Wc}9QhRNbo_-C!H{jit^78VI_I60{ui~ao;;aB-DAB&i zBsujUWxsj_E2p#nK~wH7`7K1+G(@kq#ztzJ53cU+_T#+j?5+TmU7ei{-(Dg9x1;6W zg_a5yF7ho~H25B%Y6DGNHs+;Jeu%K%v2$mhPs+uIh+2pqMOJMkKPq@z84PL4YI1XO ziW*NarmGJ4kO#vLDIgT-rV3*s+=nAS?lJFZbiGskBy=RpqeX&VEGvrfirYKmt5>hW zDTSAt+k8QR1U>z|X*YT%-`kG7;}4$nu0Ct_Isw*4b9Sqqz?Z>EH~2%f`D?!TgY88P zhN6SlulW_R#XZJVw@N#XeUqe62za6^;4E2$r3?u<>FZk#@5aK;kj*@)fzB^q;3}mF zh6Q6DwP12$;-d>$U6dUg&BzyIJxGK|Xy|vA&uR0!)r8P1r{7mj2MV6t^rZoYnx z|IhgqW05zo>K#7Jx__!}<}ghytwX}X5)u*v1AibN z8H;$1wWe!UoSlNl(piRcY$;=+VnGM7aqr~gj`1mjC}nr4k8@ic<|+8g=CP8UU0o_# zxkz<*f#_986=kFX#y1e2_G)1y2=-5;g~jX05tvnw&mtmp;8z)_dU5R20XLasoIg2W zd-M{mju(|xgbU+de_@GNwQTBRz7l~q;LYlO&Aj=jfBiea?quPTf zEPeOxj6e*6_1-Q?_z3GdIy%6hH8ouYc7(U|d1`8Edivg=>Hs}bX$ECnBV3T1RZzfO zBA-*TuYAvcil?2ueNXIx;0G@j78dmKF8I#Y?L-`G426rz$;pY)f$BZ2qw`^L9}ZK0 z(N%p1hbeIEe4Os5Q)G)QYPQh?OCU`n!stD>tz%^K)}- zhm0~`z9dnTDv~=xkyFaO7g6W2yd=|y-}*8V6i5*W-GicRq!mv#JAiv)GPn``vTK7Po^bWY$*Q08%ja&o8{ z{!oyUaegZ*+Cyb$&K{&Fj1N6>E3+<$0aSQ@Ih~`S46<2swRaI-jv}Bn*yLa!N`yy;haN(`2tqq%XB1n)! z5W~0^umZzyRUrp`UwNtbU6(9EtkzOISp)u*88(%4U97Z4a*&EF>qN}fFx8uH%-N@~ z>Uh~6-raM8GFyD`SwZeekGH3?RFk*RTn(MI6muP>|48vdmpM}XqVC6JnONua7_l(j zxRyuEOjH!8e7VynQXjv5ecsHhvdKnbKc5M@7B@GyA6OIE;1oQkjV(38?5mbNRo`SF3P|_ym(} z{~*Ucp?iNa)_rtOIJgJgeM7@jeZQHKxsO^}B~P>y3vgh=y0vDUqz0g-R@qmH9Y4rp zo%LZ#+(N_>^=r!(lF}Gh`$xJtUqE@_%nY5vsoq5JVeFx0)Ty^* z$F>NPy<_K(vmS|@Kq)vLH~=I!M!ivrasjJS!vM|ju8Ja9$51EPL*VT<4&8E0=EmK~ znVGQ9js;O=H{OrVV-wif*uD8N(*ql}+6zop`$GhTIY`|vKu2g+GzXB{F0nj6U? ze4&Hmi`%YzH&xC1-B-Q_g%rI?QDI>zT&zn|KPc(g)65-r-lCA$Z-QBZb`{ysWc#Xi z!<>=&Ep|-1RPps$?_~~c70QS(I(3S^xWjQ{x9!nG&v^x8bAuS?kCx0Y zXl*C&FWXKPofu#qM16m;m^?E(dnYUFr{tq7(`*w=?losje0}Ak8#yIyK7pa6A{fK# z0s{jfa^j0Yqycz%T)S3*>w+S|CSnZX<2p2u+FDh*8W=ql5W8?r1k>(>{D1K1(ZrfT z`<}P?N%rmWX-y|RLSJM$NIqirH1*O|zJ2p%GV|UKVse=pib@UjcvV#mfQmf#M0?y^ zr?i5EApaAqgZ&>gT5k&?(;H7!@cr1*0+$gjHMNGewmVpKSJzd5X;|mZUbs*U8WzUk zd;{YP7p|dgJQ2<|)nB>Uo~6|*`DKw6Ef=WU5&uU!1UuAh*M|Bk|9*|5m=^dV$TjX2 z!fSkSmz2E$U<53ElYjoe`s=LcnXN{C?j+;rTjb32vE78ZrDcKZh$co^;9>6RF_ucA zCyfsHdUza(Z}NqQD=?6ZFbAcF2@$mR{NmYC8C(JnA`Aot!*5VdwJ7(s_pnKadkA;g@&zdtCm z#ri{$@=IKS%w#{rA+=&xTGmf&(^zu%w_ z>tc&#J_bS%1zq312}u5T4O6LAsyyD{H}-=&l$YOVO;?fEipz0bSX}g9pRGqRluB~A zaz(S}YF2jkUpU2B%8LzQ%&6m?e)#@P_*7qcY50vkd1!B`;#S*;fd8C);O{hk9*lEN z&peycJ|4P5Fi0t#p-nBY;1e|4&`_$^j{W>7l4vyeB9smaj)!U1d7fH4K6B4fj7MXX z^oLMYOJ3V0{+~q>0ft862ZO}<;L^4Ga1j`HnvBt_|M%7f2yi-T&(srd-XzD$hwTyY zrFnhf_E?93k&%e;TUb`$lDufEw=)^~4EuAe3MoDJg*>e)RQmKiL-% z8d}xCbnvWAcflpx{q2vY2GVH`j)GLGFEe2qgw4iXwtVgFrE4m0Vic4;owcG~1;?O( zFIAm?rERwD9_Dm58ztboim*$vp;kgmy29tJ z^w9~AchoYpm!7)M!NCDZMFI}49xImryjQYsg(Z1-syaLMI(v`v{B&8Fy99er&kj1W zRwFL*bRONf>!ZzFd`+tEm*JKLyn;G*zBg)mD~$)$t&D{~{r<3Ts>g+m6@x(>2VOuQ zq#7+ofs+$x%L3=qM~O4cZmOPJYFR&}Zxsxe|1xeyHF1rIYuDqRpmDJfj2U8QwdQ`<;?pMo(mdzgGdlQ%#Z*^Vie zFv`#fSl6LRAH^b@82){`NK={bniP@t`t@P3#2CB5OI$o3 z=&h5>J_Toem!wTqJ@^LAZcP`kSq%*h5pT8-Pjhp>mt8MB`*5NTFd4S-{<^voom}G; zX}%s*s<#6QNV9Q}5}&_%MG{F+$cX@~8LBxd!Bvnmxzkqqkj7yB0RY$c0<1!~TUlCS zJss(HckX563O@OGeT4Vqqyn5|)%KeEU|0 zAR_D6AXkAt2%+cLM#x|yCn#u3cpYfKL-9>8i|hxo5D|b3h>F6&kpw?u&a}h=fhk20AN;2t-fE9%A`P7? zQ=>-)5n?iQP33VLl)XoAIOdubafrmE!1}n!gzyCXHMmEdqrQ1gI|3Xk&cS^QTV<0xTR3!_J*MK~Y??u|cn&bgfkWh zPJ(;`ADGa=r-f}JBT$?{#fh3#Sc|dS)>Lrb&n~>V@QYeoevV(yldMgD*=NfUxt{Z@ zZ;=%8^#8=Jo%urU7W2(WYk6(0W|l2c+wsbk3|)*w$wGwQO7v9ta@4#+1u5wMec<#X zf-n&N3d=kkx&NEZi%m(P=g`p6d6Ad*111qTYXAeVFCXDkrlBB*Mbol26p}J*jc_S} zq18H^05W3dPPKGhOv+fysHg|YrR>O-zAZ~ArFcc>mbPAP+vlVlzmu|gu2EHAzffnr zzgTJ+NdkIbgU`p_Uao*)2ZvYtF@3>_GK;1Ot>U0nk0ATbm(h)z($uvhuHcOR3=X0O z=Nk|KP#JyaKTCs*+9hEf`r?I1pGV_9=^ zu0tl=XH88VK{-AU5o~}^hmr*k0SHWZI-wc#T!~FfOG6FlsjsJu49BrX)M0#L9TITZ zet0fP_It=*1GDWdEl$H9?xCzrwR`zKj*8T+hUT;qi?832|4{s>I8qm=3?v#MTtv>qWlJ+SyC1uQrJOE9AN3MQ* zXN*-s52y~*x)`Mxn6vQmxqot&JmUY!Ja0cDX0NKYTcz$YM|Uax2{ zPjutd4Alj`X0rDMW=XsGqSRAZN4*cXc3j6ejX-k|HL1{L;ZDV!`sDQflD8!tf7Cp+ zam1+xdUG@zJ=^YdqlmT)n$H4QLxK5^D8rG@oes}%cYFR$cIH{KOo_1m(q>A#Lwt z3x~DG#LzHjB7%o;lXA}e0`K!%vVlqvw!R-RInHxl$IdQCiAUE1WZVo67C;_O4eZu> zf3u`pHJ=VU%h<|EAFB@hzg~dbW5*!1fg7JNZr?P)To zxCiQdKgXY`8c}=5YdM7}xpR_!!G`0!i&R&z#wIW+BOVbF;6YMz*V^+)#TAQ53!fK8 z9_hBJU_Bkubi1-(ibR^Mu7@2HJoH)3LJaKqVGQu>L(q=7+`VEAfxy! z{Wd&w=n!`#2JFGOc|dhSq7DcMhP*LQm7#KhmsomJa)Jv1e zX@9V_{{-vkbQqcH#Q}z5C-3pmN3I4O$^}O00U?^ z&=bXU$l#$3zhJZpt~Lkf_7r4kVjt&3Nuw}LqPCQzBnH|ow0_+|XMz^v6@Z2fx>3R$ zi-Hku7z+R=*l2>WE?~D*RIHGSKzWiAP=cd$vOPy=qtcR$O(`Ie>xPPol{hDGG9lr=>b6G@MkslBEZ*615S*NX z&vKPbAyfsBeJLp^fa-&9-q+QQAZjp>wgMB+`61mNUlCFAHkH6f&47PMeTCz}EN1hD zyN+~zww4If+<%JUEwz|-W#xx_w?3iY?J%Ewv%jXStZZq?8>}pT_;64Duzzi&-U66Xz}&6dw)J6NNJt3a z&trtbM!KvFc36sD?N1lO?lbJ z_n$u5L-+&f7r8z$c$Xu^b-2$9xhO?oV@sM^-}wW5e7VUcC2=0$G94Hdumd0U*-K0DN z=`bY)E>UaN(UK5T_KBYIyW%&OxLLM%iXK)>JKQ>g8LUuszE$kU{cJb=c3Ks#Sk0wN z41CcV1R;_5^O$46LnBe2#9VR{d|nV{Imag__njVnk?Gl&sm!00mlsV&OicW|FZTN4 ziN>_1aHZEtuvxINvfc^_K}z=x@7p!;J7jEpJZ}r(zLwKX-*(Q!)0<;#j+b$2%XQhj zJNP|8q%*NSd>GS8rG*~F^Csy~Fd)Rew#)DQH!D63kIe}M$BBq$M4K0Obt*b&GW34_;XV=nCMfOm zI}>dUjYbeM7`C5I$L z6g&AwR~#!3P~wT9!l*H_C=6&2UJ&JY;U5@r$Nqk{Db(vN70JrXyrq*p?80yvgqs}q z%gjvG{pAzG{}s%bDku!V*dpoac)|%-Q2)h=*j(={W|;Gk;j9%dUAgkTi`-xOzoMK@ z0qgQWSr!HI4}=Gk{yh{osYsPO!)^R(UNbHkX4v?ZFPMQ@d!nN@^1rVLfSUAa?Z)-% z@8aT+6~3bRKprWrurh&B<{nfv6ye%g*vT+SijFjPAvZDz?QEbfIUGk=6qq`_?n5nA z@cj<64wi?Hc?T;i(2wGcCDLP9`u(~NhJ}W{MQFfr2E&3XFV1&2f&~N(CkxBti5H7! zyrbvSbWU~cL^Ri)kSJxg1YpUBdr0t#^I$>q?_*%`JBBS^Wn}QKcp$hU`XBvtgRX@q6g)G? zC-)6xB1ryZJO%;4;==ha?Y8G3R-yF?+s>S#lie|1TwILf0LX%k!=oag8AJWTsm3Pf zPL#Dnaz#T)xfvhqgHznmHLj7aBWVAl_!L+d8aa7+d?lWGLK6&h;Cy`m_yzl{DW`Sv zWC(0(nx(;2;$mXRgqOXIOI<{DE5s9UY9NQ=r7Q3ZcLwq3-}<}aVyU~=L8u%oy*IYF z&`?wYfh-{*0n3D;M3O~|)F8^D&2qd#932y?bGC)l?9(Cb^A!M3j6n-!cqLu5+jEv@Zu1!ixx!0^EWFjb-^#8v- zubT;P;$OE)wkakFTy|%<&isNI|(C{#3Mu1Ej&BeS& zx*BV0_Muqd!7QMhpFVr`(^&>m7oPgzqph``{n=$AsQMSAAeqCehZ`S`x%-piTbABc4i;T?C{f0&of-9~|Y~dspXwcb6HlEjE2uig9bSpzch}Sjr|e&R`agDJpKFrN;xcI?GiS%iGCb-6N-LpcDgLr+0HFj=imlL}U%#$l@(t7vK}0ux`urKL zI`$9{Fr+b|E|P=~Sq*+`g5Zs6fNTN39~^Ioa-?_I73zVL#^K(@$th>?|Il?N&{(%! z+y9f0BvB&DkkCM=Bo&eh$xz*;WR@nC=7B;KN+=Scl;#1=noSKVr8$w%K#4RDitqRD ze&1(3-?!Fxt#{q)e%_nQb)En7+~?lMK8|A-M*0+OIGxqrcHmeKk2T~>Eb8~qpCcUJ zQ~CtVn)N*+<1Lt-_#qvsL0}hfT4V%3;QO;riGYD+3u={@kf82QN&>AT=fmJ&;N-bq zEU9u~>yyL^0@pB=Y4f&sZ{OnXGy)}!&2FT&XS+@YHK)0A3h;{aareI|Tc#cRKiwY! zS`V64`}XY|U*B4ES#YeD4_;UQ6{;m-dtUNISW|#Kzz;W(kGMa9zMbc}Ug7mxx<5C@ zQlW)2y%d2oi!As^oK_?R<9bPv&C(|lprgO z4L;n)rU#5Fo(GI4>D*$sH)-gYJFmAzq^6cF2A$XwxzyQtS7$=(OtfO>g37jtQ+d< zt<^Sh+Z-Mq?b1m^k)u6jD^o3riO1b;2b}ERuOH~n!O8t%SFzt_+vhloSaNuMX@17r zpe3t`BxA;0g+cTq#4>gj5qoOcqxSc*{z3p>I9KAQZqMbv3nZ+qVPo*1dX_ zl$?BXQTunkguMWif?hF~u-F=70q5Z_h`UseFpD&2V-nqmb#cXVy*~6nEEjHVkg?!W zrx@`oVmh|PZZc62VH!C?%JhX`SsbT+?<;81N7~c?QC^GdSy8Y*h1P7zWQUyT?WZp> zSbK4vi1thFUb=c`ZM!DN70Z|JH1Qkj;-VOTeK_zNA{_qj8#zk$(6l?ZZVfUu6_pUY zrS49p7+X5hBw7Phj+{HE)@#K70=Vk}8sH7x-)j%pWc*>(zXDb7=j6;>EhpRt0NTT)i z!A;*i7k?gE7jpM!2=dHo_oE}MtX@2PIC;{fC7gNV`IpX~{SNJjc0GyPuStfzbZ&^i zQ%&x@fsY=TRNWdDUi9|!VyEWC8(-9K?>9HgvWLzVDm2_`eqHVE#RZgD@(TV6$^>$B zRyL%#7;0)3bt|tnvc*b!%6n>9R1~9aY(P+x3e6YQ+OqMsZ|~k!0u*59o;{e&z|-t1 z2-ox^1nywk+F0F0J$p9O%u?TBWF>5F(6+ZtY%5ekBO}SU$s>Zh$NNsFwZ9%6X zQNt32SK^jh;LRC4pO;5C9zGmxU5JoR#4o*lTl~tfGg+qTZf+x1eR@T2T}vs({ErL` zGJ3liR-v9=UYh;izkAouzyNfb|40f1Fd=e^1g#MxW{ep#6QZi|xcy~n7muh%zEbk7 z7MEnn%9c5;fA&N;`4ybK?{9MD^z?O|)%xGKb;})m#m&uFHI^el$ji&ifJU2~&+(U~S00k1A2Zb|Q z#L>#1y|v-|Hs$3n%K`I(g|uo=LjGP8)Y)EMy9&Hnl*=b3kqNfl4Q=)F@#%sClXron z+tWvn`WP80IA70b-~N0_O~Z?|j>YjBD2jkzIi6}gdMsbI%xBdJ? zOV1gz6l0_W0rIKaV2>Fgm92%CHI+h;EZN$J(kSxEC%8d>uq(h_q?vbtuRZFlxA_%=fuk6zT_W}tD`!~G5pk2s+w z4|Mi4{=k4`U-V<1?THUC@S>VL!=~ef|IqE7Bi%`*p!WCs8C7#5u%SNZRK zf9Q2s1v>+-4~$nD{-U#HczX0gPuW1TZV@@-8}8nf5XPDxkzWi-V3V@)*TG#QU2ETF z>CYW)8$&#x=aQN_ZQ;V4N0l(cH@te~Pc@krOEv1R?MQBf#a3Y8&_&kS+^Vw5Px=fB z{qu8u#l(BglJU~TSFg5Z3j~2cGi(^2@Buh+kRXlxT&GU0YgkNoxnIA2qnLNV?@+B`?P3RGJgDrXQ#*K zja3*bvgrpV8+r7gAft_P^nm((}i#0wMx{Ra3VJs zE6|=fyU+*$>^~_hGhhB5I~8o-SYkm?h-1cI~=rs8`Z| zS`1BKk#e7vwFJaC-+71Wh!MXjcgYo~Bp|(t7{a7UBp-Bp;guQ~%mzXu64Lw{vE)tc zjvdct?OG#!H@Y$CMXt|iI_lHYcAiW(W}y}o6pv5Zl{UTd}x3^ML|njJKTPfgPq-vU%xneno~gR)#Xi~ zvT)efV5)%a(t1t!$k^cUC&(3GW_eAYzM2XINsM}?&qev)J!B-NC$F=P4JCm^h9W2Y zfF4DCx?3mLJJ;oA5C96uw0ZNVm<`-!Nl6c5)ds<>R#H*|E8)5J&9roKRMMXo^H0P3 z_phqHJl-|9&trw0>eR4!Xz~Sud?3^5#5o&h`QtulZIXMG8 z&P%m+*e3{RH(yp)Beoa)B@g~i+Ogw2B~(I!;y?d9!e6zrFV75mwLSIZrPb}nuPho< z>8g`5uv0Q$rQ+eku9sgM!Pvzp@eKFN()@Lkj&ANe-Q6AEi1NVYPEK)&i8NDRyg~s8s&ud6E+_7Tuoe};v+35KXv{8|hWb2Yll>$~VS~X0dmPCQ3@|dn_UecC znguN_7q0D&n9)8s^YP<2^Hi4xQMw-DBIJ8HTfyQ~b_jnBZ8+!XQT!q{n5g#H zyqw&g91fRmTjF`hyP2X6>L z=#N1O7m5RY-;xrs142z*+9kzzen{D)N2jScI2R-}B;yi7$eJ}k7Nx^a*1dfTlO}`W z_4E9wsV9_ldiPdVQ+tc_$LfEEFw?K`M zY98tcRaSwJ|cPyq^T2t9d=J{e=s|1#58KxcpotRzJ{j!&ONb9I=V@uZGCa z3jrsm;$Oex;UABeWvOLZ=V1)wKN=OrbxqgbR}R_N4erm-qpx2+~hGuN}CcsCd|Y|g)_;TAOb z>pm~c+Q{Xgrmil2s?yR<0YAPpHwOg;2?C%GLWZ44LD2P7Mwt&Eju*i3W5*i7YBakh zaah9D_h1O*<>n^aaZ!!Vj3n8JtG*LbO!==7S$tTD>9ly!TB?~CG z(@G&2Xf|)_V3VJv|0QyLV)P67ahK2g)bfjPZ0PZRzTS(za4?OYypbVYO{a`oM(~L2 z(jMHNywrS>KUb~4|57}luv&l>d$8~! z&@^yzcS=M}O>ip=Xuq}o38uS67h*9BLd8X1^wfGvug)@;-d7zxn|gNM!fj`_!+X(7 z>mX<)Heb0qT6@^A@)s{OzqdH)B?4bi5Dg7{s#UzCL-z$&3WruLAw`2~NQHak@Zq)S z1l!F?d~#P}r~Isot~!DcZe2g{5SK_wS@+D|dJ2A~Q)C2+X{thAd6`Sm(BKJhX2J7c z51g$e1PWm8Nh8!0T$)s@K;T%VAizh$<>xnuW4}o4L92nRR3RrFoSDqGyW#Z7IH}-f zKh+w0d2b3j@IcA|{xhdu5b{1vaB@<}4r%dj;nKv`0cOsKplV^E#q}=LK?P@feQato zQW!Y5|GU>;Y^?N`k}y_Z>=VbCrVy3Ct~M?fke-81s~=nOwTeZPinZwGapATE#jZp;|Xo^2VdeQ)+Dq6-0y zqdCFt?9!ug?~vc+*7x~de2KLtYzGGqcl5jW;K68JrM_L64b!@Q_sTAABQ_pCbLR4r zx;V@ZVr+lCN_56|L!zD7^riO5z2h?FazXo>usVA6`fMdH(QLA<#Mk25Gi4#XU8M1e z7xr?EAF>q2M%^CT&g0Qotq`sK#FO$E@1Q|k;dD*7R9Iojck&kIgX2o>+<7y6cWqtW zcQE{>9co$u~m!8Y*q^BXdF@a4AFU|1lugLo3Z zT3VLGAO7B0A@as?uGBI&PYE8XJEa_tFZg?djg4`YzlTBe7Q4I-g9@%;XzAeKfMuo_ zdOFK;UPUEJtmeqp z!-1fSi~yLoA?#t2x99ZoBy0<4BEASgU@HhbTz0~1y6i$2+F`17(L{k%0u|29n}-h^ zi25E>;M)ms{sXSB7_1<+2eeqXZM`lZw%%|L z4cff}CN@JrK0Ap9sq;!7zN>1!cgsNP>buK58-Fe`2BupO7on>f|dG{ z14?&r$3<1>SCyBGdL&%nBkXh+H@Jc8V#+}CJjdgLijd1rqH@VUSO$s)-Y~=Hv|g zP=osug&vKM-dS{yK%k-`BV`+s}GdY}n`{g577WUPgz zMU)4ARPE8j2uIZw?aGS%N%%n_vQyK+PLW8}$dcN=1xV)XIO+`=FyJURM%UrEg>yA` zYRV#4CoL3l@xFaIq}UlWphzlOGwfot^40kV%AYx%od02t^X%9w9>Fb!e>xqD*DIc6 zZeh_1+{2xkH?uFWRnO9Ji{kpo50P>o+THLr#-#f3n)lAvl{PKC+qP@`e9Q zAXsxJ2vS6tp);RNUafColLF~+?Y^8WeDTPaJIks0uM z>P}LrsxS?qX!>j}lPLSGf`HXb^yba4Jp)uUHRCHvtO~XyB^9eIN0Kl7*^L-^<@cwK zWUCjZNjy0wEr{k&m#bBVL;qg4p5|24z}(V*j92%Y-c`5T?|@@p(oUb)IK5A0W(K#& zzI`)oZ6k4*I#XUve-RYk&*Jjc3vX0}F!x9D!Vd&pY#qt~nihw$vs<|gICdDI!(e`@ zkXA~AH<(9EF9Rnp{XTu@i(bO0p1v$;HfO}u^HsfHm~ua|Gu9r7_;$eS>ifsjM+TdD z^&Fp9un43#Z!x7ZF0}6MN3yetVRBRTKTaX;Veh|*L<-f}hsad#m-x%hn|B1v8{XH+ zlPAN9Bm_J|XyuVD!a|~n(|mSjbnK&@B3_qzmVZ z?%p(jIII*>)FOd+2#Lg&_wOTvle=uld8QkC0;bzcvL!+A99BxYHOU|eMmc>o)gL7{ zZlE=$e#x9DdYenMNku^!=?4g{V)GKR?$+(wS1eoRfiF^6hi*bXcQfLSag>k-b$V{s zPgspaC=bDp&K6cJxpf(5KfzdAykrSGt5bi`%M9?s9Wg77*A>RmIUfVfND`lOaxfMu zd)TTVBd~1AHoK8$ErOF^^KprHY||0-p|Km*EPJUcadxT@Q8h(mMbbn@QJ5d26*&A^ zwQ3*S_Tl=iu}D9L*+0l&1~E#sd%uuwxrTufmVhmT2S3Db_2wLmez|CD$BqTa%%S>v zBmeB=ED@Q)=?$xOU~yywIXEO$Ryt}aN?+PC&mcBt&9X}6e(QEmJXAS-UZq9y#pXvv zQUX28nn~t0QEPwWzhK5a+c{O{`7;iH+;MPN=i@C6~kTeKEkEBerG;k-XRjlN9Nd4=j^cVSHsQr=g!wK_*jlJChcOu9KC=eaaj-XN`<;OKbt*fER_#4V6N&^%I61!oafdi25r z&;hiyed-%!Gf$rU46s?WT26N3qZp;GN7Sc`)tmC9qC&k}H;l#lHFdo^de#@K)fZh= znv@ixioA`32F=M6m-7wbkT^14W{QNQ+le8WF{6o4S;{5g!KTRc1B)bh>@xw`C z6fHhqJE$G>SLB}D-P~bi)_}@B7#R-rC8;m)Fu>8n-*Ai#@%Q%8;eSC;)e*c48NN%c zM7W{~eISwD+Mxp$`?R_u*TZQz!e7Mio}5%7erzA18pA`;S3~YKH$d**g9mTGm-g}! zHGGwgQ5!a(BOtdE#mZ;S7)?823pe7~GeS>~SsgZAoenz!pHKQ#KXw$%U&0&*c&=b= z9IsVq0N_m0PnQ?UDk>Tn=>Zj%0ZU}XvoBKAW3);SFjwW*ylDnnQ=_cqcT!kzZC)OB zx=ZvZlpT=jXV0AZmCuKTl7!xf+UFPR>*}^+@=s+57@(K93eOlm80irI)BNh1|3zWs zDpieibP8eb=jOWD+rQ;4=!vozq7&EIm*HqUWTd5ma$CPVaOR<23<#XOuyfmX{A5im}rbs~iw#LLdja-S6d|aQIo!);0U9#afaXwiIhl@(a z^3#D{uU~Z9n&IZvZ~Vh4vGd7CGD9(qUur?_pYgZdRZzMqY+Tk>TOIl+g-i3GR5z5HJxE5#lNeRQw83h;4kCOV`rJc<4 zs(W{Vl!<8LJoN&;gbvm4YkW{NCNV9jGHgvw=F6Jr7o#5>jNP>9Ww)=-^A=?>^n<}Q zq*uRgZN%zO;dwmo7*SB)*!a3-+lFV`SjUK;b_8COKY@^1X!GVqez-GU{G*G8huhDb z*we4^kmKQlokqnvbEg2da?ot&%lg`2WFxJdfDdWDhPzAFe=xa6a7+H;->f7*P*WgfP+WJi@m^Xep8J-@J8;`*U~PusgBgstf1OM{{-t`$&)*005jNA=DWf7}S&Q zxP5b}>iC;G9e?w+yv^?C=h|j9x4auWthMivs*w-(B0S9cS;u|F?cR4yNzJ{G^;mrlLg3-#~c(*UgLKj{hnprtyW zww1o8q1(^^S*1tb@|mIe1sXB7g#%t)_`F!ruyex4Vd2y4kL@*mfpqKALx;Kh4wjCJ z?SKBl1u%IWE_COPR#fhiuAZ=M+v}>d=+8?80U)~dmD{cq`w5a~w z>heqR>dNIBWRITbLjd4aRC+Q+ZkT$m3Yd?&Gtcy7$<${PUv;U7kg#4y8@LeCbmB-n<@e zVCY`FpT+{S`ge^wi8rLI>wUi3ZL~iz?ERUYX0Gb%RX;;>133W&U6i<_TX>3j?C7a0 z%8hzl4vIY;zrw!jF_Y*;G&`C|)Zrnp)g{A!%)YjyElDW*<{25}ZLCUIBI(^@<{xO> z^!%{FR?0V| zs3n6&;=1<~E z`}GxW-#l0Kb=F#u?Zg(}&Zke^__37 zFIh5r-PH)`2Rp3yC`aEbGQTZeTRU)2;>&{jxvHB^L3Bc%RBkT^AdyTMzJGrQPJahC z+h~cOt#$X2BP{SdvSuFm(TN@&KynSFA(9qBsXRJ(9-+%bvHEB}sen%8#WyK{B1T4626cnJInfP!+>_32JHUAmgp)3Q}qeWr_YMtZsi_vifi^I4*A%xVF=gwz7<1E_`&s6r6Rw$8f8%olFnKK(j& zk(I^FCZ&ufH1|6kcYZIqI>?-!*0Cc;vL+s49{|4+UfK&~vrmz`@%@@grNNeF&+BRP zO8j_bn3KXMrC^MCmT+7GSNxnDmMf8DanhtgLxyxyRxYx9#EQDO*xA{we9*eVQRh}k z3FhM6e-102Eu6C`E+!4wck0v^aEb1xwX=FEMDaY)FT%++qcoJbP{`D+J(-!+Uv6lZ z^jTjTkkzd3n-n&qr*Ewi1Pq{6b}b)WtXRT}%uyJUY_PSI-nO&qkrHC?X-V`wHez%K z2X+qxGt5YB=ie~c$HEX2DDJpu$WmzDDxDr9TU2}JnpsOZ3CtT;6 zGEY}egVcoQfoE+OzUDgWSgs!263x<%WoKK?x}%Y_wx>xe=g6dH<)>FMad8S>1=l*X z-{!D}?aR%|nX&x&N|{IO>K%5(Qe(oe)+;bk8hTcE&d_R(~zpAi4TtvgoC&2&Fmk* zR=zWISpEeNhb_FvOfFp2m80g^l^KwMfk){l6;B)a0=+qpjaDW^Kei#I?cmtaj*faa z2gO@MK-?4)LoT-QS%QyDz@tM)jx;niu}Sc27Vw<>EJ~~4j1&_X- z=xvg$@fEfsJ$ARn4_zghYNk4$1y!x1>xh#s5KT01sbxOw@a#pcMKOI!b%KPb;zbwz zItyB+#Y$60s+{(al@j0c4p|_H>b3J)H}Vs+HY%2Q+W*o;M<>Wy%L{^?N7mI&z4gp_ ztk(vCw=zqLRQ=%w`Ccg+r!Qajq%9Cu;yUK)R<-eM?=MR0wyk>6F4^g8*L|a8i>+es z&ywcS!AjoJvWj#nc0*iVV%Cn7l-O3cm)F*-=Un$JY8nzHm_3t@zON-!tH~P1x>J~2 zEPFQ@0LxYV(;~tEcodiFZ-1&z!la+7 zdc30K$Ky*^te`c7RV>dmzM6f*NHfTFs144ZK25((`Scjg(&;akjq~a$;Vb0IHpD*J z^h`slwoQE2mWZs`PQ8?eJq0%7uz7m+`s*Zp8O-n0sii;_qen*^oX|2`rPQNqYpqP2 zRNB+glx$nbg0xyS#mAb!F-zXQJ-)S*G+UOxPFk`H$bwea^~xFjz1}F-)fq40pW@9` zdqsR~QC!2!u!w&)c?gFZOnl(L_yX_SX+u8{f3`!-I_+xH`)TXvujl4jo$b~5N4_{x zl4G$FM^bxyl(y$QH#L3c=__j_wV<}Ix@O_=Kj6OuXgIp?T_<>1izEF zt~3BQU8n(Z(tYh>Iq)FqQ28J#Lhs9n<@`C8kwRr-N!f%qF%@ZpFL;OlJ0eBa}wySSi{ zrvpaN6R`L-G_1GcOI2~rq6iMomOt^_7!zrR(wwcbeD#;tv1xy_*zKIYxK z5YWN-TlIH^n65tYjvwSMjQ%J}olpbtc*nhu4K%v|aKsIJ`SMzH_9A<7;=~@uPxdAu z6qh|Es;EYEuf&d`ckfM=V6jx0NWqc7gODzyk?c1nPAxnoH*;K9lQ(?_3`j~yNTG|8 zyA#eMx>>9~aeljbc#xALPJu)dt~{|Oni0-L5{Q8VIXH~ks1IG~tN~f` zfKjH=D-#W=-0se?R1`hs_UvK0j^R;hye|_=Fl+BNqmlOB_b*>;vlX;wi0d#P&c-Yl zW~j_-X8$JX;Em`?Q%rsQ{8@ef$ygiWE`%t~oQTmD zod+Kt9qU6q4a1d4D54>IYp>BO@1x35H%@Lqn>3@{fq&8sp;}4m_YmXeIu+_6ku;07q$@s;1GlsHH&0gbEFX zI2MwQgj=WYkFQ^^qcGtqRkp*QQ4g$&;_OA5L)=UAguH?(W=tOfJ=&#Ls=+UddW&b3+Bf``t1ae4QOXt1c#h{ls zjSD7>_2$ST2@WmiLu;@lO{drob78(!Z9BI#)A{AwOTt8y^j|Y zb#a0J__J5qoQQY)MLiSork%Rq|q(hy1Bi$ zC$xqRoyw-8RV0*^m#6O9MNAs>_rJ?aeQ6i2+KvQC(aMe2A&KeA3kpfH9wwMkra!OZsZC}3i zmYqAr%^(|TJSuyE&O~?jd6Xt};Hhkg6`UyeBtkxZ!e!Wi&25k_MtML!3Rd*)ov&%C zP2jNJy~UXt`BhYWInz%&L^pCdZjmt@pBfEqJ3stZvZ1?tr%vsU=*K(7X$+I}8h{_d zh!K^Ag}y0HT{Sg7)Yh6hZWez>0#B^A(QDUUhG?{Y{TL6Ao`ROX{&l(_ckUQ5oNDSI z2IaEfm?4HRve_CQ7*_&-^@G_MB_rkFHUrAbPHm#=1q?Fe1aykp3Az6=NWA`SY*ju=aePBXK{o4#Au^b; z+h^8qgnKS z$@{4A1VKaL08CMYp`-%jl$n|E!l>GJg|_}AkEB9#i(3fG(aTG0Pl;2@jQ(<4&U4_f z;;#OOlR(x^b8op@8%hP=Xg2T<&BCIhefa|%UdkGgHH{2yO%F~_+PYPA z?&TXw3KWxcClE}I7%_r>hYyJtL+2w5%CUp{gst+)yOts<1J#k*v}PRrIcjY(A3hu1 zR@&JJsYECp0_zJoo~G#Cl~zh-e66wQ!Q;m$MqwX_&#;){ zbYP%^1x76Q@!8EV>ySUJ9Ml1RkUA6Oo6?}@NYL`-y?0Cg>O-5f9V2q>91b(xg)^{OgIKbi*L&`TeOqVTvKLz87rO9&dwm!+L-cz zF;;3qPaB}GKMU%Q+Y~9K7%8tJX^yfe7*UUhOnGv<5ccgG>WyVTdR!QrWGEZ1s+{TC zbB`A(Bm1>$*RHG^FKiGo7~(lFc0vAyQ<~X?fByMO@RAl#)6xg>FVklnZrLV^X9}&5 z+{?(5&WtMj55=}Fe9xXeFi(8u&+jCJWm#x&X`nYdb0)c{X7B#isrUK~8S?1P9fixD zC;U6eyD_p3P=)Dioj%DC9HS#(4UZCgs%&jtr5L9JUKf^6aCPmHj1-ynCMXroMts2E zO#As)FR_jEn)!SXnWfi^{SUDfA%uF`h_3lOZgNXxY!IVgHZ-VfXxu)c^9G2bsfiH@ zA@C-+X{&4F67X2YmKD$_H6R1KE2=@;Qk;lHj?UK!`Bsef;cQMye2RHa&q>lz>wq% z2al=hQu0$ubUocoRy$(#{^-s{(6I!LPLP7V!CD2uIEX|M|0UzCD{2bYw?^(ZX?wVf zZYxdJ#dkx<8b2-QrKx5WrMg{qsyLeit%>Wa0NM565S&?`k?qB5sE9<8bT;zQ4vLA& zfH3Qj0i^TP#dP2~0WPT^zV%rTqm9+H7|}j-MYqHfy3BQgdd=9$d+Z>0xaV#VhbPfD zEn2;t+uNo$JplY=6lLP9cg3ZN%-Y7)y+a20)&|kAC;MM``R6qP0$>z#AE~)C9~Bae z>02YszkfgCo1uAwerrz4R{#9Xi9qYQ;bWG9u=Z)e)oSq zH5K(zAZ3;a^rSKzskb^b3P-#$_)VG=rXXw)M5YKHD$ychK3a3yn>>d zn9BR>LulJ@u1NZvDsOtW$-H`}XK-<3F@T&1tk0Py=1{l(dNc%i zOm)*Ihv4W_-VW^7@5I@&*aci!`c`ShtpO_Y$T^7RydiriS8#<=NWQKTLrWZB~O+^)o-Q>G(Mqf5s+J1m)Wq!>5L6 z+*c&mTDPu2n=nGC;VOOe>Jz}JA?;Q;&mR1Ylv;Y_07(A*i+EP(2 z=d*`igf`N|?)^F*yd7+)nKZfw65I(cK_iQi=~B5-K!Qo1%gaNpM;)WDe>V900uHu> zZwe~``4h~EM@{#fMEC3oC=DbT9R$(kg%NH{jROEijtNAN{oyCqFV#Jb@MDm0&CFCR z?vOlEg~Ih#mW{-`?y?7VU}t$tC0VI|g${r_(YC)dS4C2D{1-dbUot#@p7aaP&qDkd zE$MK^vyUI&adPj84DV}=c9)hF3sU@v13e`K`ejIhL2oDmW!&M;PKOUq36UOKGe2^m z`7mv~fM?!JLh9SEe}9IXdq1KFH;tYUEnc?#=fesnsp8Bq(LW5z)S!hk7x)3vRad@^ z4u3dN&w%uR6zFOd8<_@M;ggj7-g>e+*7ER+(L;q_h&=m=3KSpNJa9HvpAxIbW|)z( zRuG9^TB;1f;AUfi_m^+q7Kh3OZ*qIG&1ScEnndoOpnuUW_Qz9(W!<(4E01|Cy{>sj z_yv8)S_=n#WK9r1^crjMyLQxS8E8B)VGqi4lM1NV-vN1}J*M&*SS zcbTLE4V3N)*2T}Bt%k3`zmcE5daR`6E$!rVqoN|1y$Oy;Sbnv(3NY-3{|R)Z6l8E2 zP@N2%-7c~;fHNz4U9kYv$6FZA`40(%jKCQ65tniX+_0<%4p0JyH!8p2mSCt(P*5|d8SPLj#Paxn0E<8vuyc#EHH$ zXF{>VwRTZJb(4Ky;Cn7xyn{><{MIW>9N(o|@0?@^w|xu^OZ)eXD-jlTzyEfrQcUs4 zhOmggY%Ryz;R}0TtbP^AxtKLe->Xs9OKQ+x=(G zJoh9X&g@K-07NBB_UlhF*FTuF>BXtFFmfoo+AcFg@9H_m<@3H?0%8 z!!N9^vu^WE2?>tV{37w^pX%k_1Z<9%txip`KJBhw4ONWk`MQ01*oTnbcjPNa{ygZ{ zMYceGhAP9lo$AvLt>3qH*zBJZYIb~d6(N8}Aw&UDrUCXLpG3sx;15(t5J$;;`ZPA} zXzq=gZql{=Z?+Q*3=K!3uMD~ycgPD!Nq)@6g<7A|+L`$2(#!a06_TX!T5%*2ax=-7o*@=TEpsZvx=XQQC86-sIj&t~&uNMLf{*(Coi&Aa5I|lsI0~ zp-y2zfyD3CIe+ZWeLg*^{``mN0-DbMzkald#P#I}Cc(b`(qS@2tWshsQbG_0z4^Vr zMaO(~)ZAd!N6n zsF!$dP0Kf;B1?w^Ri=O4d0D^PfekG1}HhI>N`- zS8}G|qDxa9Z}$9>m1}wQ{0Gtoz7cn4Q1F24mI0aDEehD`={RRD{E2KyuUZ2u3qL*% zID1ac2R8?ijOBS%{qn2)vkW&6b9o!*38HK_P zM%RFaF^|FM>6zB)f4`gYs9BZ&{C9!A1`z1t=8oOPv{^wxf$RpTm=JI8>>NrX9pCsG zC+8VClX7QRWZ$|r)$jqO6P=YXJ)6)=QzhgYF3OJiehJPg={*fDTb(YgWzEZXwKVK= z3StGlm&%R9Y9&3rl8=l$2B)xetd=O-L5n{2>_Z@?566XdnPmUcr9Y7Ew-1=6^Fz@y z=2mOZD&ECgUf-dlA`YE<*Go+N`X6!M0D=Ni#3(1L!-@1Olp#A8*|vH z_wL^pMKX4FvMci9#%6h3TKxz-5q6jm#$^FMGU?DmzDaGZFZ~Mp8L{~vp4-*p2j<}c zM{2>sg>5;Ze>ysO%Vt^~@8!Ajv_S`{LckvC)Ty{h&zUm}uX>A&IOTn>P|bd+BdZpz zN+(WycHB?qtT*0sj`%9`40u??w&O91RbuGLvo$aWX@b={&ik8^*3_Xs$IqT^dv_!U z_>KO36lT~JNwzKB1<(f7=68Y7J1No1k$sCj zYHMq&@$4smsYQk)IHKT%nvcYB{YXg31l5C+-;r_PA_Zc_q+T}v?g}Je>J1}fG(&%P zxom7^cJ1;AQ9yf@hAAw086jxJiYSX4=#ETHKf!u0Dqih9Oc1i&3($Q5%)CzC9vdGo zmSwBI_WM2v@aZce#YNk_!-e-(d3vmNR7|TBaY$PF(<5!U{|I)c%cWQtWv_4hU1wFf zcn3wlTPaa!BM=_%@1wLo`5=h`!QvMU#(88u#HRQ>;f)3u_ zX5}+@3tx)ZKN`*Z^72x03N2Kfo_~ksK!@c!m^;A;HADC87WogV>jtu401Tn$EUY?} zSfWcUM{ec2RzW7*8l00?xYr1?wCCmJ_WLGMF?d~Cebw8TBWHYfsI~U>Mfv&Nbr#{MG>|_$8^?buZj?VaHU9637 zm(XQkw6OlG&!aVTAKWx{<|MTQ4JsI7-16%ehF2P&T@Ot*OB*@ozV7YMYg`V58vIY{ zu})RdywsjyzCY>$v}zMXiqQMIFzfZpE}MNh;2mR<-?7GX;f7_qu1|w|KU!xCOd81h zV6_?=8lbw2ro+IK!W|@?7Z?j)>Jh!ziXbx;E&~mRJ7>b}927ow4I969cI-RF-L7cf zd!$xpSxBIm1j-7$jq`J#-n}C>|J<;%E%4R2WuWbxKp+JX@dC~3m1(wa-@ex@rPR2K zE1f?c_f~tG+dX1|gKnl<&)On-g&@^yC!a^Y5lBTxvHZUMhjHx!QED1Py=B;MliCkA zy0gH`MJv3z_zu8Snp~1s%?UT2$Gv-_cplH5Q8i&~NTq3VcW7k~jiO?g!C{Iaqm)yk zJAX#;lHTkQ>TTMXlB}0=@Qt;0xh{kI_I*oz9t!A0%z(W-?)G8W@?uCp>@i7~ zwqvBx0|ph4Rw~nxq16Fot9>R`b$j%tP5t}!g=AQyrMcb)RzJN`6sUN`qMNjLb-gDm z^)uSG7HGtayxZ4zG5vPInwpC~&5dKDm;U-PM{Yf}>P%nXbscqmdRT`%S{1a;QWY#q=dU;s^)E+NU)IZWl%_=U&UI4< z-MD{$@uS24&_&5VcE0wXlNt~HMipjh$HmdOXNdEL)jGJ((gq2I_O*WfRm6&6`JS^v z3ma1$FL=|Y#OoLD=+jsh<>wrTQ4=TEQj>O95nqM7A^? z0D7ko=l;rY(!4$|N3orZ^v{4h>2tijt#`YyIqGhwqS<#h+N)Rgo;{|}{8EuMrWj=!OUJd8fqiY1|pV&UgzknZK*e3((tjnFz+cw2}JYM?e*D|&$^ciCFlRu^X z>Hmr1mpOAfjab@grPP`{N`3xUDuf2moN67@s!7drr6mNQeXcKn7G}DKbz@lX>o;!- zJ8H^gS%|3?(39xPvfXQK%*c1ueycOcy|B|jF|pyog<$x!XMvw8 zho;4?O*oR4iI>}AFbo=X;B@bDZs}z{iRp0m13nID>;bc=pTFkcRd^Y)u(2Y?*bgssk>2Cp`q#FC5ew)0fMMVJQw z$aeNiyTrIxN3`dB4sCqC%1!x6l}xj;?2#~o@%<0V8e~ey_dA#=DcLD4*>21!Pg}!1 z_47|#=2btM;(pLzdd{)--A?N)nA~NkN_YP){oJ|)wHu-}{hnNOpnaI`=hj74cP8j- zN3Qw!{b!NB)9!;4%4!z*rXZ9eaA4q_{YM$SK9Qj&MbUkv{P*< zINZlTt%>vIB-c)OH!kx?i}kA2tKo;eg+IyeLl~dXf!IimO29?7Rs*T=C{2wMHhCHm9?i6 zsVttw`kFJ2o_77iv#;WZzUyb&urqxH6RyOZRF%!57YqIe2red{2L#F zbJw?XH-~HLgdY`lp^A#w$0k(D?xuJpVuAHhHymKX#7aUP$>G|ymc#tQUopfSW(F;Z zZ~5WvCf>DM?)R%lJ?{1%5sgC~Cj1FCk(~Q(D6mzM#knM z{l40xLM+>;=73wM>1kB^`_J_8sUgYc2#c+chR23L+DwIv?J)$r*X7M;dp%Fau9j3( zxWlHQV-4Iw!+{nO8L~*dwXq{11J~45GW0k4M@_omHTL$IZW*AMy9qWKb^+O{EJC`wI)+4IiS5&?D9K2y# zDa5qGLLD6P#|G~AzvxPGLl5_3!>9=9OVp;&i9=L+W;ihsMB&bER9}~W47yD-QSJSJuPS6?8<;CvcK9qoyY_i0}YF%FwVxoRd0H6#kmR3^4Ox95WE9` zLQL-``48To6YgazSa1LMO>OtlqhX2@r%Y)Mh=1Ss=8HDhCsh?qD?%^s1G2l>8-r3E zsiV!(E&<*|mWg;5|B*ks_~sm@(l1(coPrv;HpCz3S_t>l(0*1G-je92sdI4q)JF9Xn~0a#bEbXQ~PK4vH_vD^YDOS`UkEc z$UWG@)>*n2OxASTyH6s&>ltJ zMrhfZUR73>xqtuI1%V4Mc1hi2t0cER#8PwC07WTRFE8g9FU1^#)AqVVlu6Zx6@;>F z0WEPjd!#qcdsxxwZI(fXTpwLK;I0#s@Ow~v@w94X?~-oO)Gj3hw0msUJH4xSe~0;7 zNAT-^LrkCK??IMc^_EA}2gVAs2mU=W_VaQ-ZFBE6Jz`shk=*$t7Xy)`tG{BRi%Tw@ zq3kucbqx&}M;PppSELYDA}xFw^r|o|a}ej``LULq>}-rKw;b??-%Xx7)2C-SwK8j^ zXoHr42+t!Ek$mC-tAs7d!vdAwUjU~d?DZP9c<*WLQnQaMM{uav^Sa+Uq?<1)gyb+V z{GmG^II~SH-#PJXY_F6LBYcZx8K3N2LYE7LCZxWSD@!#0z9JLOo%_j_f_|-NGs1J$ zu0pb0_*NOy9&lJXeP3etUOE5BpJgZJXGpb65rndB;pV8MNhwzVk4Ln3iML7KY z7sraEzcK*FxPg-@r|azdZD6vET~5A2pjf#M^Yu4$%gAyyyLPqhP!rPfjRa@*#EJJY z)KauTrYX~Out+pWYmbwf61HJx>Lr#lG+Ht8+&MnXIc&oS#-q%K z4I^?YNpazt@X)r{J-mNkr_Pn4mB`aP&uIJj0}p6IH-G`65!#$erF=Xy!v^f+vfSNm>E|@i| z4u2ze_x?&T1{eC&xTxOL4_j9K{=DK0t_ZyqnZB|8ljn&zg&Qg*< zGp>MT+7uVZAXbxJHA`txE?c(m-s~hrr;mStpdr3+F`()IGxIpVw8O&A9Xk?p59j1; zubiJ|5xP7>0=k>9w0FASoP`TD)zs$r_=GE3g#0j0E5(xX!m@_smGT-p%qnw?QAIPhjN@-@8 zUPxCe?Ayvb&+|Ae_VQv<_or{)l0(mK-6cGpUKMaREL%Yk&R0>W5N6J7T+C!Qu6 zw0+Rzp5~D*24GPOpUCGbOx_YcHtV!mPIkvZ#+3Ce)5mC?KG$&#oDyQv^S3Qq#`NB5 zotd*-)(yStYGZ!2!+IO6()T`m_v^gr@=YVgceNuSqzx!Ypq&xlDci1Zc6VLm>l zjA14!G#C}=TRI$HCyk=m2DPSmrY)#x%_XPQuU`*i^e96^7P)+Z?7f+UrEW1kkniY0 zqzw{9Wmyeq9~2e)jc01S;fuZUP)$J&bW6*aDo<)Lxi;mdqs}Zx#~ctG7RaB0(=Z9n zr*cRFsW0n_ul9C?vf@b$y zhwnF|e1=HG^o||2kJah5n2|jefAIrwd$w&$^1L>o&b46EmMu=OyvPzUKI(`EAnxps zVJy%EcZ8F@iK6*s{lA2>t{|ntPpp@{lOYfu&SQQ)` z+3`Hyug^Z`1>7z_e4nMId zC@innuP^X6*J;;0%gDTvj5IBUOcoxIL!rUX;iGnXJ zF5Z5ohBtykLR@Lz%BDj~sBB6LyQHv(E|1y7iMXHs?s?GPK8h;VFg%)}Ab{)l!mmf) z5xs6zSTpAHsxhMDt_~_eZO6Zhb&vRBe>)&(%HSU&0>gJZVwRTy2&4bQPC%_!EL2)p ztg5X08Mj>w?yYDU=}y{9=z0KJILo#ULt+TNWSo_pyD@81&az{qelOpUA#Z)Cw|EXS z&zfyhYDZgU7-b&AtpSJ-a=YFxTjTyU@)kF{7!wC-Kt&CQl4%6rVL}F?50Fd%u-~H5 z16W2ckG|3pZa>VTM#)-8y*Lq|Ry5IrYu&1wWodmlGasuV2qH_E(ikJ18!o z0s;JE1c;#e>4mincxd1bVJ!ikB?3Q<33y}iZV4SD+Xhs7&AxwR1xNY<4wUS`39 z;k%t66})yG>jLtyoOVrcWMoNR-q-ooIOT)!s;HSBC@8q|?AZm}wgJ6BLz4UwRAo5L znws8Wv>1_tRwxN0UQ&65g<;$bAt5G|_^aB~ZKau?-{#bp9Tx9Bp*CE82Q(jDNh2wT z)|$7o7cU-VD}`DI7UGx9;W}7`7wrFDYee=?#{l|_u#=Kqd zDM8&TG8mV8A^-_1B#!8R=dF_GJ$X(r&`kBGJHb9CIpL83y9N$4*^@qCv_wb%XGO2& z^x9~fN3`eQ)?_QC)M9jO$*A;@y_1*3dEW0+;{lHf{^Xb5zc0XitKPiG_;1oT$Y^B` z^0&Zj-9((9@3|(N@iGJ-LgwvPD>YLi0Cdrd07LDbvX;s!mbC@lb1MqWMmIq$v#_GC z3H^`G^3Td6A3f4x5-d!otQ|Y}^WS>@*j2lUnWkuhe#fls;Yn|p2B1RQ$4*_kPpBlf zQI@%v1bhE0MBd#Sc{YpQDJo)B8O$1ei7sqFG6$-88wu+0UN$(ZPgIhNf6gAkJg+;% zg}Wl}Hm?3+xr;0xcNu+Pqt)9|W{jLHm<~|eFI=__YqI1kjh&HDTbbb~lOS@zhk;or z0DeJFN>p|H0s>0FqbW4N=7}G=iFgPy@I`ETHN)x9T=lWB5`~0L){F}ov~MH*fof_!a9}+!^y*QkU_TPAI2X$y3!$ zcW}rE9KVrIaVdSp?Dt>i?RHl_`yF`Hbc4af6C_2bpCEz_8k7Oa<5Mx3=!p9Bt3Myi z&G;ckZe06IE{u0ac|nm4w8VgXKjY#V)LwMPqG~XIT5>nNFe%jKRTIwE7e2I(p$*Vw z#}yF`wEwAx0gw18wmm$R$05Ad3;-gKuXQG}(r{&I9>WAUQ{+#x1keuR6@H0NtE+!? z|8qxi|1F)1iUQSoXaAINiX04~*T0HOBrJN{I}A&6_lQZ6AP4pE2-UMMVW*8>KlZFkv*%>~TefFP8;e8a2%#JUAHE4A)!Fzd2QH zf<0{CDK3xHExu2+q~s5PdxjW*vqIM~JR~G7a6DMc0e&wuafWvc8L|!YotTSlO-+Ne zw8oX@wlAA-#pY(Y*hZ7K7oKEiaP$ujZfPs}G;7=vuUJXcX!#6TOyKrgFEjEgyN~RQ ziB}bS`}&4~{?Ll8J2ymYa=T0B@L4kxt~3;4@}yf?!9S}^X8j%;rLgFJyvi_JsbJ@S z16P8G{1nR`(aL^xd6k*VNu??nmvAFixf-w2)tPmhj=u|Qd0jY2OJ&%F;s;|2HTNBr z*D|oq&E2u1s;QGhPqrI<&O-Dl0q-?J{EA|q=W$Ao9XdkV2y6@>jzUxNS)*mSY+nbE z##8r$6=$r)H9Uk9tBd#vU;(^b<=J?V)-RfFrw_|3ey}MQjqZ+#OJa~lK_B!vjRVv?Ol!G!U)FT2u zifS7tswrKCZbhQTD0CQ+%*L4jtox~!iBLGor!G{vni+WEUTe@EK1g)9S9uiM*3mH~ zKYzKGm-gGRz52jy&>jCKM?OBJ=I&jL7^eQL)JY}O)inpDO=o8*`3cG*S9@Dq9UxUS zD$pmpw5WuUo~!FXO*cAk(+-XxG<+x$X6F3jY`_5}eq*?fEYy7VARV?2$eUCCR(=1E z&A%GEI`qD2)hjt7FMpTcjj|XmG41%Mb4Je-?y|0cVssR(xfmF1$e|NY7cq>mjt zAN%Q^6+S&@5xX>;WG(K4pEazUfu zQ`S07LNj%${Fmfn>G8^?Xyy7R$vTgYG0mCll==Re>+U&670c(19PZ4TuO*emV$j&F z=-9{FiG6G|M{EeroV|sZ`9%#qzX=6nQ{gta6MqFyZ&c9*=r_Y`$q}wYgB) z)QPa#gOO|+8jjZON;X;m*F#5++${YGL>V352rgTX*|T-doX@%PL5@U+xBt$lv#`Y| z_=NJ?6)@K+tln}r!}fd}vqUMbK1pk%okTFYhqD3MBg5U}rq7yqSh33{hj^Oz@4hSg zaEhG7w{pv=1;n6f~z$4iwFz|XqFr3J;vN9Beup`Vhlw}5{rb7FTlpmmsv;V@0qyh-Jb6KJ@bZ-oi zro^7d-3JprL;u@uYCAd}^m*$*&cjwV|I?e`Q}$Ft)O(Qu4)FK5d=DLJ`l|^dIqgkY z0VH_Hic%5d?xb!4X^E1ox(PgcmeB^phPyiX%gcLzQE+d8oG^r zE0vKT(m}_Lj2ih9!VF58nbOPPCYX^RUEs*XQqCm;z)u4xK!?Uy9e$8SnGYf*mBx*; z=kSsUV(C)NNn+C}>DWo_X3tJ@Ho(o>*j9@0L!#5SZxXt%-*DTh_VXwSjt?CQJDfVi zJoc0M%@Kc^a6|M`_>49)E5fk`lJ8`*zGv1~OJd}n9&xbl!Mch&6mrIz_jUeMnYw8} zC$p}~&*jx}63NgrK0h=%_>h+b3k7!YqdRwGGwiXUNA_~~@P_k_|Lp+!r#rED<27BluohWztjia2c`YzmMn7z}ugHobZ{T$e z%au?M26%g?fikeEW79TCbu%Ww(+^Qt5^a6YiAc9McQtACzZfW>-;>8duj~PS%bq5x z{FoKfg<%W%HM2_*r$ln<=mzo%{Eo?|yevLbcWut5kkUn?Gq3~<0P|ofoJTqdS+QP( zY>Ds8r!q5y=?b%M&ZT4friYSi&e7@h<}^njWVx4yHlOsaFj!%$Fj^G!wWRT164Tqi z|JHi=SwleSqMw!SD>Kzx~8-K0&YGoy6u;4kV!%g-V@4DN@wW4q}ed_0A(rJHL z>E#DtPwd><7>uvSi)a{?N|_@dv3x1YH3urG1HNi=37Ys=FXmt5ep@} zi;IF-3>N@(jWGSfUb4|{Kjq=Mk`fB0^>?fiohfdMo+Il{!p4p-sGg}>-^j%ubuImPGl24TO8+3V6%j~1Y>P=^AXE)T-H|eW$ zfEbg#*}|_%s&?O`)5IQpnqo}o`4-;}+1j^iL~iE2C#7&RBxeO*fI?D0kw~;Rd5w*) zEv*Dko5sZtsFp;cC@Q|Ygb@SZXsuc2|B4{7aL>bRR!3)_{E#GB0}YCE=O>HaE+#$Y z!tPq3xSJe3U<9AmD)OY?%{?V#(NwA7UN7mWkxsV-+!eJXKXTkbD***Y9TBIYVtvRq zc-q`IjctwR09E#q7$D@K;Fro|`+^B3tT!c?`BKLMM?x6I8uD$7rKd-mn`JtpPk zCCth(zx{YXpS^1XCU_SR14>I_5Uc)e4`@g^N(_&*EY-5k{Q_V}wF(2689xd-nY(v0 znzRc+7-AswHeOI^s*rT1CGX(9xWe!ghc zBNc^eLmmpi5M%Ax6@4_t{5|&?wQzcL#)tbm(o>B09IrpFB`0drwe%kNg&BaCDWzC7 zoUiLEE5A1#J{A(4_6C9pbNS2H7geSCx-x%C>fVx5;6f|x^IiU6HGjX1aZ_3`6ZPEm;=k7=fP z1>#HdCaSYB>E4N}nSLuf1K;iW=q%d0>t5mv!Smb0_&EVI>)5sCP>Lr0Vxmju0D*r! zbLJtxY}TDN=~~o!T+6Sv&uv}L4hf=2MG?j^0oG3U-GR)VbQZNXn#Ie{W3+sHT%eM|537cXB9!;iq# z)nfS*^Xdq?lolGy^vNIsp^K7L-M`ONb)j_R5;}5ZCP_p=inBouGyzH*el6%Y84%#Z z**~p5_A5ExH;87?S`r2Daj8tegA5H#CQZ_R)*NhZ9u|MOIh5W$>R`S?7L4kx-Yz*k}s$`{9ylO@BT|RYvTtAHiyvBsvFkKfEfd&j=Ur5 z0|Wjz&F0BhmWCmq0q}lmm`rxT1lr6f+zp1@;gFjE5$V8o05_lx`)D z0HE4k39Z1)$|@&jn92pIxRdqZE`8n+4(xmBl1yVK1o3H7JG#gJ0E*aPBU4E`rfl=f zws&Feg9j1S%0Muk{r5R<@Z_A*$#O87Dz(F*!F8Zi+{bf${8+|iBj(gzt0a*?o;(m4 zkIsXHlx;^Fy2*$ZJoAf)!T4bl)d7=_bD~RZmW2rOq5nsdr^9!2dc&V(+ftNm&Q)cM zH=JTRv5PJ>T@#;E3xFdghrZZw&s3sAv>o0iRV3{$l&k}q%{+Jrz&1&?FXi3gg~`6d zzDm+flRS&eQ2nj`O?8Xt?MVJNs$i9HwHF3V-07F~^RUp&eP>pj<0LkI_FU$DP{*#y~HgiL?Frh+~q47{#PgCm@7Do4U9gNJT@dm61(o9OQT0Nl* zPXkaRtP!jIkf>PokHtfXTgy(;*%LD~Icb7fACbHYfo}kx3r+?JIMX{LufZ~j`0Jsn znpzLB;p_i?63`e&9iyAIt;l9YLLWzM z2Vg80ms&P0ZP2P7c~(Zq$E-y*g(cgT%J^_l3W~W;7A|~5Fg~ccGc=RG*>RUX^VP36b)psa!scnwWcvSIs0un7|7_ zbfX_nphQmGihvcegVp9VZ|W|%C_rVvBacrno$KnF{hu~%ThI{E-f;=12Ww~Xn02#) zrvL2md;Xn2louoLPBWR5e?#mfDp%|a*vNxu(3)c6#B**0k!XB5Cq*Rkic1jEigJ;n zJI(I6J_0T&eCx_^{-t0pP&YVsLXZ0Y`N8%tqPRuLYJ~-6d-xI2jA>4G`7*C{{{<6~ Bxe@>X diff --git a/docs/internals/_images/triage_process.graffle b/docs/internals/_images/triage_process.graffle new file mode 100644 index 0000000000..cd1e89cc3a --- /dev/null +++ b/docs/internals/_images/triage_process.graffle @@ -0,0 +1,2652 @@ + + + + + ActiveLayerIndex + 0 + ApplicationVersion + + com.omnigroup.OmniGrafflePro + 139.16.0.171715 + + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {1118.5799560546875, 782.8900146484375}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + CreationDate + 2012-12-22 15:48:38 +0000 + Creator + Aymeric Augustin + DisplayScale + 1.000 cm = 1.000 cm + GraphDocumentVersion + 8 + GraphicsList + + + Class + LineGraphic + ID + 104 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + -1 + Points + + {98.499995506345428, 441} + {45, 441} + {36, 576} + + Style + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + HeadArrow + 0 + Legacy + + LineType + 2 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 103 + + + + Bounds + {{99, 432}, {18, 18}} + Class + ShapedGraphic + ID + 103 + Shape + Circle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + + + Bounds + {{27, 576}, {342, 36}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + HFlip + YES + ID + 102 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + Text + + Pad + 4 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red102\green102\blue102;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\i\fs24 \cf2 The ticket has a patch which applies cleanly and includes all needed tests and docs. A core developer can commit it as is.} + + VFlip + YES + + + Bounds + {{27, 543.5}, {342, 12}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 100 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\i\fs20 \cf0 For clarity, only the most common transitions are shown.} + VerticalPad + 0 + + + + Class + LineGraphic + ID + 98 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + -1 + Points + + {98.499995506345428, 333} + {45, 333} + {36, 189} + + Style + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + HeadArrow + 0 + Legacy + + LineType + 2 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 97 + + + + Bounds + {{99, 324}, {18, 18}} + Class + ShapedGraphic + ID + 97 + Shape + Circle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + + + Bounds + {{27, 135}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + HFlip + YES + ID + 96 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + Text + + Pad + 4 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red102\green102\blue102;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\i\fs24 \cf2 The ticket is a bug and obviously should be fixed.} + + VFlip + YES + + + Bounds + {{189, 306}, {18, 18}} + Class + ShapedGraphic + ID + 94 + Shape + Circle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + + + Class + LineGraphic + ID + 93 + Points + + {204.18279336665475, 307.78674107223611} + {252, 252} + {252, 189} + + Style + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + HeadArrow + 0 + Legacy + + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 94 + + + + Bounds + {{162, 135}, {180, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + HFlip + YES + ID + 95 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + Text + + Pad + 4 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red102\green102\blue102;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\i\fs24 \cf2 The ticket requires a discussion by the community and a design decision by a core developer.} + + VFlip + YES + + + Bounds + {{387, 279}, {18, 18}} + Class + ShapedGraphic + ID + 91 + Shape + Circle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + + + Class + LineGraphic + ID + 90 + Points + + {396, 278.49999548261451} + {396, 189} + + Style + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 91 + + + + Bounds + {{369, 135}, {198, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + HFlip + YES + ID + 89 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + Text + + Pad + 4 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red102\green102\blue102;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\i\fs24 \cf2 The ticket was already reported, isn't a bug, doesn't provide enough information, or can't be reproduced.} + + VFlip + YES + + + Class + LineGraphic + Head + + ID + 132 + Info + 4 + + ID + 134 + Points + + {342, 342} + {393, 395} + {450, 450} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 16 + + + + Class + LineGraphic + Head + + ID + 132 + + ID + 133 + Points + + {342, 450} + {450, 450} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 17 + + + + Class + LineGraphic + Head + + ID + 10 + + ID + 60 + Points + + {108, 423} + {108, 477} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 11 + + + + Class + LineGraphic + ID + 82 + Points + + {162, 288} + {396, 288} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + 0 + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 12 + Info + 3 + + + + Class + LineGraphic + Head + + ID + 11 + + ID + 54 + Points + + {108, 315} + {108, 369} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 12 + Info + 1 + + + + Class + LineGraphic + Head + + ID + 130 + + ID + 131 + Points + + {162, 504} + {450, 504} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 10 + Info + 3 + + + + Class + LineGraphic + Head + + ID + 11 + + ID + 58 + Points + + {234.0000000000002, 342} + {162, 396} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 16 + + + + Class + LineGraphic + Head + + ID + 11 + + ID + 57 + Points + + {234.0000000000002, 450} + {162, 396} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 17 + + + + Class + LineGraphic + Head + + ID + 17 + + ID + 56 + Points + + {288, 369} + {288, 423} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 16 + + + + Class + LineGraphic + Head + + ID + 16 + + ID + 55 + Points + + {162, 288} + {234.0000000000002, 342} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 12 + + + + Class + LineGraphic + Head + + ID + 135 + Info + 4 + + ID + 136 + Points + + {396, 288} + {450, 405} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 82 + Info + 1 + + + + Class + LineGraphic + Head + + ID + 137 + + ID + 138 + Points + + {396, 288} + {450, 360} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 82 + Info + 1 + + + + Class + LineGraphic + Head + + ID + 139 + + ID + 140 + Points + + {396, 288} + {450, 315} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 82 + Info + 1 + + + + Class + LineGraphic + Head + + ID + 123 + Info + 4 + + ID + 124 + Points + + {396, 288} + {450, 270} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 82 + Info + 1 + + + + Bounds + {{315, 630}, {125.99999999999999, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 128 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + GradientCenter + {0, 0.15238095234285712} + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 development status} + + + + Bounds + {{26.999999999999993, 650}, {108.00000000000001, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + FontInfo + + Font + Helvetica + Size + 12 + + ID + 45 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 2 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red0\green64\blue128;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr + +\f0\b\fs24 \cf2 Committers} + VerticalPad + 0 + + + + Class + LineGraphic + ID + 44 + Points + + {144, 657} + {180, 657} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 2 + + + + + Bounds + {{26.999999999999993, 632}, {108.00000000000001, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + FontInfo + + Font + Helvetica + Size + 12 + + ID + 43 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 2 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red0\green128\blue0;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr + +\f0\b\fs24 \cf2 Ticket triagers } + VerticalPad + 0 + + + + Class + LineGraphic + ID + 42 + Points + + {144, 639} + {180, 639} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 2 + + + + + Bounds + {{315, 648}, {125.99999999999999, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 129 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + GradientCenter + {0, 0.15238095234285712} + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 in progress} + + + + Bounds + {{441, 630}, {125.99999999999999, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 125 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 stopped} + + + + Bounds + {{441, 648}, {125.99999999999999, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 127 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0.501961 + r + 0 + + GradientCenter + {0, 0.15238095234285712} + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 completed} + + + + Class + LineGraphic + ID + 36 + Points + + {423, 234} + {567, 234} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + Class + LineGraphic + ID + 33 + Points + + {27, 234} + {369, 234} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + Bounds + {{450, 441}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 132 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 wontfix} + + + + Bounds + {{450, 396}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 135 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 worksforme} + + + + Bounds + {{450, 351}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 137 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 needsinfo} + + + + Bounds + {{450, 306}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 139 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 invalid} + + + + Bounds + {{450, 495}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 130 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0.501961 + r + 0 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 fixed} + + + + Bounds + {{450, 261}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 123 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 duplicate} + + + + Bounds + {{234, 423}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 17 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + + stroke + + CornerRadius + 5 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Someday\ +/\ +Mabye} + + + + Bounds + {{234, 315}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 16 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + + stroke + + CornerRadius + 5 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Design\ +Decision\ +Needed} + + + + Bounds + {{54, 261}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 12 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + + stroke + + CornerRadius + 5 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Unreviewed} + + + + Bounds + {{54, 369}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 11 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + + stroke + + CornerRadius + 5 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Accepted} + + + + Bounds + {{54, 477}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 10 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + + stroke + + CornerRadius + 5 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Ready for Checkin} + + + + Bounds + {{27, 207}, {342, 351}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 13 + + ID + 99 + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs28 \cf0 Open tickets\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\fs12 \cf0 \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\fs24 \cf0 triage state} + + TextPlacement + 0 + + + Bounds + {{423, 207}, {144, 351}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 15 + + ID + 32 + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs28 \cf0 Closed tickets\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\fs12 \cf0 \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\fs24 \cf0 resolution} + + TextPlacement + 0 + + + Bounds + {{315, 630}, {252, 36}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 126 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + GradientCenter + {0, 0.15238095234285712} + + stroke + + Draws + NO + + + + + Bounds + {{27, 630}, {180, 36}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 88 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + GradientCenter + {0, 0.15238095234285712} + + stroke + + Draws + NO + + + + + GridInfo + + ShowsGrid + YES + SnapsToGrid + YES + + GuidesLocked + NO + GuidesVisible + YES + HPages + 2 + ImageCounter + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Calque 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2012-12-22 18:00:58 +0000 + Modifier + Aymeric Augustin + NotesVisible + NO + Orientation + 2 + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSHorizonalPagination + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG + + NSLeftMargin + + float + 18 + + NSPaperSize + + size + {595.28997802734375, 841.8900146484375} + + NSPrintReverseOrientation + + int + 0 + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + PrintOnePage + + ReadOnly + NO + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canevas 1 + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UniqueID + 1 + UseEntirePage + + VPages + 1 + WindowInfo + + CurrentSheet + 0 + ExpandedCanvases + + Frame + {{1, 4}, {1190, 874}} + ListView + + OutlineWidth + 142 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 120 + VisibleRegion + {{0, 50.450449800270746}, {950.45043820152921, 662.1621536285536}} + Zoom + 1.1100000143051147 + ZoomValues + + + Canevas 1 + 1.1100000143051147 + 1.0499999523162842 + + + + + diff --git a/docs/internals/_images/triage_process.pdf b/docs/internals/_images/triage_process.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a157fa89604774cad811707530b1d64ddd91699c GIT binary patch literal 70123 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 Io9_|*535HOsQ>@~ literal 0 HcmV?d00001 diff --git a/docs/internals/_images/triage_process.svg b/docs/internals/_images/triage_process.svg new file mode 100644 index 0000000000..363ba41aef --- /dev/null +++ b/docs/internals/_images/triage_process.svg @@ -0,0 +1,3 @@ + + +2012-12-22 18:00ZCanevas 1Calque 1Closed ticketsresolutionOpen ticketstriage stateReady for CheckinAcceptedUnreviewedDesignDecisionNeededSomeday/Mabyeduplicatefixedinvalidneedsinfoworksformewontfixcompletedstoppedin progressTicket triagers Committersdevelopment statusThe ticket was already reported, isn't a bug, doesn't provide enough information, or can't be reproduced.The ticket requires a discussion by the community and a design decision by a core developer.The ticket is a bug and obviously should be fixed.For clarity, only the most common transitions are shown.The ticket has a patch which applies cleanly and includes all needed tests and docs. A core developer can commit it as is. diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index 84f70fd731..19298c55fb 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -50,9 +50,9 @@ attribute easily tells us what and who each ticket is waiting on. Since a picture is worth a thousand words, let's start there: -.. image:: /internals/_images/djangotickets.png - :height: 451 - :width: 590 +.. image:: /internals/_images/triage_process.* + :height: 564 + :width: 580 :alt: Django's ticket triage workflow We've got two roles in this diagram: From dc704516c240011a9aeda17f631ade35c65cda58 Mon Sep 17 00:00:00 2001 From: Ian Clelland Date: Thu, 27 Sep 2012 16:49:10 -0700 Subject: [PATCH 023/870] Add assertInHTML method to TestCase --- django/test/testcases.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/django/test/testcases.py b/django/test/testcases.py index 3af6b8c346..9e92bf5bb5 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -393,6 +393,20 @@ class SimpleTestCase(ut2.TestCase): safe_repr(dom1, True), safe_repr(dom2, True)) self.fail(self._formatMessage(msg, standardMsg)) + def assertInHTML(self, needle, haystack, count = None, msg_prefix=''): + needle = assert_and_parse_html(self, needle, None, + 'First argument is not valid HTML:') + haystack = assert_and_parse_html(self, haystack, None, + 'Second argument is not valid HTML:') + real_count = haystack.count(needle) + if count is not None: + self.assertEqual(real_count, count, + msg_prefix + "Found %d instances of '%s' in response" + " (expected %d)" % (real_count, needle, count)) + else: + self.assertTrue(real_count != 0, + msg_prefix + "Couldn't find '%s' in response" % needle) + def assertXMLEqual(self, xml1, xml2, msg=None): """ Asserts that two XML snippets are semantically the same. From 089d9ca1df43fb36eaa857e2d617a94a82c6b906 Mon Sep 17 00:00:00 2001 From: Ian Clelland Date: Thu, 27 Sep 2012 16:52:24 -0700 Subject: [PATCH 024/870] Add assertJSONEqual method to TestCase --- django/test/testcases.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/django/test/testcases.py b/django/test/testcases.py index 9e92bf5bb5..6f9bc0e724 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -407,6 +407,18 @@ class SimpleTestCase(ut2.TestCase): self.assertTrue(real_count != 0, msg_prefix + "Couldn't find '%s' in response" % needle) + def assertJSONEqual(self, raw, expected_data, msg=None): + try: + data = json.loads(raw) + except ValueError: + self.fail("First argument is not valid JSON: %r" % raw) + if isinstance(expected_data, six.string_types): + try: + expected_data = json.loads(expected_data) + except ValueError: + self.fail("Second argument is not valid JSON: %r" % expected_data) + self.assertEqual(data, expected_data, msg=msg) + def assertXMLEqual(self, xml1, xml2, msg=None): """ Asserts that two XML snippets are semantically the same. From 8d35fd4c327e05b63c72a1c1e9a4a68de4dddcf0 Mon Sep 17 00:00:00 2001 From: Ian Clelland Date: Fri, 28 Sep 2012 00:20:01 -0700 Subject: [PATCH 025/870] Use new TestCase methods for equality comparisons --- .../formtools/tests/wizard/cookiestorage.py | 2 +- tests/modeltests/field_subclassing/tests.py | 6 +++++- tests/modeltests/fixtures/tests.py | 19 +++++++++++-------- .../regressiontests/admin_changelist/tests.py | 7 +++---- .../regressiontests/fixtures_regress/tests.py | 4 ++-- tests/regressiontests/i18n/tests.py | 2 +- .../m2m_through_regress/tests.py | 6 +++--- 7 files changed, 26 insertions(+), 20 deletions(-) diff --git a/django/contrib/formtools/tests/wizard/cookiestorage.py b/django/contrib/formtools/tests/wizard/cookiestorage.py index d450f47861..3f26b85f7d 100644 --- a/django/contrib/formtools/tests/wizard/cookiestorage.py +++ b/django/contrib/formtools/tests/wizard/cookiestorage.py @@ -43,5 +43,5 @@ class TestCookieStorage(TestStorage, TestCase): storage.init_data() storage.update_response(response) unsigned_cookie_data = cookie_signer.unsign(response.cookies[storage.prefix].value) - self.assertEqual(json.loads(unsigned_cookie_data), + self.assertJSONEqual(unsigned_cookie_data, {"step_files": {}, "step": None, "extra_data": {}, "step_data": {}}) diff --git a/tests/modeltests/field_subclassing/tests.py b/tests/modeltests/field_subclassing/tests.py index 0ec317dea5..9331ff2f3f 100644 --- a/tests/modeltests/field_subclassing/tests.py +++ b/tests/modeltests/field_subclassing/tests.py @@ -61,7 +61,11 @@ class CustomField(TestCase): # Serialization works, too. stream = serializers.serialize("json", MyModel.objects.all()) - self.assertEqual(stream, '[{"pk": %d, "model": "field_subclassing.mymodel", "fields": {"data": "12", "name": "m"}}]' % m1.pk) + self.assertJSONEqual(stream, [{ + "pk": m1.pk, + "model": "field_subclassing.mymodel", + "fields": {"data": "12", "name": "m"} + }]) obj = list(serializers.deserialize("json", stream))[0] self.assertEqual(obj.object, m) diff --git a/tests/modeltests/fixtures/tests.py b/tests/modeltests/fixtures/tests.py index 415ed6dcf2..103612198e 100644 --- a/tests/modeltests/fixtures/tests.py +++ b/tests/modeltests/fixtures/tests.py @@ -22,7 +22,7 @@ class TestCaseFixtureLoadingTests(TestCase): ]) -class FixtureLoadingTests(TestCase): +class DumpDataAssertMixin(object): def _dumpdata_assert(self, args, output, format='json', natural_keys=False, use_base_manager=False, exclude_list=[]): @@ -34,7 +34,15 @@ class FixtureLoadingTests(TestCase): 'use_base_manager': use_base_manager, 'exclude': exclude_list}) command_output = new_io.getvalue().strip() - self.assertEqual(command_output, output) + if format == "json": + self.assertJSONEqual(command_output, output) + elif format == "xml": + self.assertXMLEqual(command_output, output) + else: + self.assertEqual(command_output, output) + + +class FixtureLoadingTests(DumpDataAssertMixin, TestCase): def test_initial_data(self): # syncdb introduces 1 initial data object from initial_data.json. @@ -290,12 +298,7 @@ class FixtureLoadingTests(TestCase): News StoriesLatest news storiesPoker has no place on ESPN2006-06-16T12:00:00Time to reform copyright2006-06-16T13:00:00copyrightfixturesarticle3lawfixturesarticle3Django ReinhardtStephane GrappelliPrinceAchieving self-awareness of Python programs""", format='xml', natural_keys=True) -class FixtureTransactionTests(TransactionTestCase): - def _dumpdata_assert(self, args, output, format='json'): - new_io = six.StringIO() - management.call_command('dumpdata', *args, **{'format': format, 'stdout': new_io}) - command_output = new_io.getvalue().strip() - self.assertEqual(command_output, output) +class FixtureTransactionTests(DumpDataAssertMixin, TransactionTestCase): @skipUnlessDBFeature('supports_forward_references') def test_format_discovery(self): diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py index e8d4cbff16..7a3a5c0e03 100644 --- a/tests/regressiontests/admin_changelist/tests.py +++ b/tests/regressiontests/admin_changelist/tests.py @@ -122,12 +122,11 @@ class ChangeListTests(TestCase): table_output = template.render(context) # make sure that hidden fields are in the correct place hiddenfields_div = '
' % new_child.id - self.assertFalse(table_output.find(hiddenfields_div) == -1, - 'Failed to find hidden fields in: %s' % table_output) + self.assertInHTML(hiddenfields_div, table_output, msg_prefix='Failed to find hidden fields') + # make sure that list editable fields are rendered in divs correctly editable_name_field = '' - self.assertFalse('%s' % editable_name_field == -1, - 'Failed to find "name" list_editable field in: %s' % table_output) + self.assertInHTML('%s' % editable_name_field, table_output, msg_prefix='Failed to find "name" list_editable field') def test_result_list_editable(self): """ diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py index 988c5acd0c..6791143473 100644 --- a/tests/regressiontests/fixtures_regress/tests.py +++ b/tests/regressiontests/fixtures_regress/tests.py @@ -358,7 +358,7 @@ class TestFixtures(TestCase): format='json', stdout=stdout ) - self.assertEqual( + self.assertJSONEqual( stdout.getvalue(), """[{"pk": %d, "model": "fixtures_regress.widget", "fields": {"name": "grommet"}}]""" % widget.pk @@ -519,7 +519,7 @@ class NaturalKeyFixtureTests(TestCase): use_natural_keys=True, stdout=stdout, ) - self.assertEqual( + self.assertJSONEqual( stdout.getvalue(), """[{"pk": 2, "model": "fixtures_regress.store", "fields": {"main": null, "name": "Amazon"}}, {"pk": 3, "model": "fixtures_regress.store", "fields": {"main": null, "name": "Borders"}}, {"pk": 4, "model": "fixtures_regress.person", "fields": {"name": "Neal Stephenson"}}, {"pk": 1, "model": "fixtures_regress.book", "fields": {"stores": [["Amazon"], ["Borders"]], "name": "Cryptonomicon", "author": ["Neal Stephenson"]}}]""" ) diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index dcc288e600..44d84f9143 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -634,7 +634,7 @@ class FormattingTests(TestCase): self.assertEqual(datetime.datetime(2009, 12, 31, 6, 0, 0), form6.cleaned_data['date_added']) with self.settings(USE_THOUSAND_SEPARATOR=True): # Checking for the localized "products_delivered" field - self.assertTrue('' in form6.as_ul()) + self.assertInHTML('', form6.as_ul()) def test_iter_format_modules(self): """ diff --git a/tests/regressiontests/m2m_through_regress/tests.py b/tests/regressiontests/m2m_through_regress/tests.py index 17a4525442..5ac10462fa 100644 --- a/tests/regressiontests/m2m_through_regress/tests.py +++ b/tests/regressiontests/m2m_through_regress/tests.py @@ -73,12 +73,12 @@ class M2MThroughTestCase(TestCase): out = StringIO() management.call_command("dumpdata", "m2m_through_regress", format="json", stdout=out) - self.assertEqual(out.getvalue().strip(), """[{"pk": %(m_pk)s, "model": "m2m_through_regress.membership", "fields": {"person": %(p_pk)s, "price": 100, "group": %(g_pk)s}}, {"pk": %(p_pk)s, "model": "m2m_through_regress.person", "fields": {"name": "Bob"}}, {"pk": %(g_pk)s, "model": "m2m_through_regress.group", "fields": {"name": "Roll"}}]""" % pks) + self.assertJSONEqual(out.getvalue().strip(), """[{"pk": %(m_pk)s, "model": "m2m_through_regress.membership", "fields": {"person": %(p_pk)s, "price": 100, "group": %(g_pk)s}}, {"pk": %(p_pk)s, "model": "m2m_through_regress.person", "fields": {"name": "Bob"}}, {"pk": %(g_pk)s, "model": "m2m_through_regress.group", "fields": {"name": "Roll"}}]""" % pks) out = StringIO() management.call_command("dumpdata", "m2m_through_regress", format="xml", indent=2, stdout=out) - self.assertEqual(out.getvalue().strip(), """ + self.assertXMLEqual(out.getvalue().strip(), """ @@ -232,4 +232,4 @@ class ThroughLoadDataTestCase(TestCase): "Check that sequences on an m2m_through are created for the through model, not a phantom auto-generated m2m table. Refs #11107" out = StringIO() management.call_command("dumpdata", "m2m_through_regress", format="json", stdout=out) - self.assertEqual(out.getvalue().strip(), """[{"pk": 1, "model": "m2m_through_regress.usermembership", "fields": {"price": 100, "group": 1, "user": 1}}, {"pk": 1, "model": "m2m_through_regress.person", "fields": {"name": "Guido"}}, {"pk": 1, "model": "m2m_through_regress.group", "fields": {"name": "Python Core Group"}}]""") + self.assertJSONEqual(out.getvalue().strip(), """[{"pk": 1, "model": "m2m_through_regress.usermembership", "fields": {"price": 100, "group": 1, "user": 1}}, {"pk": 1, "model": "m2m_through_regress.person", "fields": {"name": "Guido"}}, {"pk": 1, "model": "m2m_through_regress.group", "fields": {"name": "Python Core Group"}}]""") From 585aa11d233b7e3e40fe45fa69ef045d8f282345 Mon Sep 17 00:00:00 2001 From: Ian Clelland Date: Fri, 28 Sep 2012 00:25:08 -0700 Subject: [PATCH 026/870] Use HTML parser to compare html snippets --- tests/regressiontests/admin_inlines/tests.py | 53 ++++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py index 5bb6077bff..3c868012fa 100644 --- a/tests/regressiontests/admin_inlines/tests.py +++ b/tests/regressiontests/admin_inlines/tests.py @@ -131,19 +131,17 @@ class TestInline(TestCase): response = self.client.get('/admin/admin_inlines/capofamiglia/add/') self.assertContains(response, - '') + '', html=True) self.assertContains(response, - '') + '', html=True) self.assertContains(response, '', html=True) self.assertContains(response, - '') + '', html=True) self.assertContains(response, - '') + '', html=True) self.assertContains(response, '', html=True) @@ -326,7 +324,8 @@ class TestInlinePermissions(TestCase): # Add permission on inner2s, so we get the inline self.assertContains(response, '

Inner2s

') self.assertContains(response, 'Add another Inner2') - self.assertContains(response, 'value="3" id="id_inner2_set-TOTAL_FORMS"') + self.assertContains(response, '', html=True) def test_inline_change_m2m_add_perm(self): permission = Permission.objects.get(codename='add_book', content_type=self.book_ct) @@ -345,8 +344,10 @@ class TestInlinePermissions(TestCase): # We have change perm on books, so we can add/change/delete inlines self.assertContains(response, '

Author-book relationships

') self.assertContains(response, 'Add another Author-Book Relationship') - self.assertContains(response, 'value="4" id="id_Author_books-TOTAL_FORMS"') - self.assertContains(response, '', html=True) + self.assertContains(response, '' % self.author_book_auto_m2m_intermediate_id, html=True) self.assertContains(response, 'id="id_Author_books-0-DELETE"') def test_inline_change_fk_add_perm(self): @@ -357,8 +358,10 @@ class TestInlinePermissions(TestCase): self.assertContains(response, '

Inner2s

') self.assertContains(response, 'Add another Inner2') # 3 extra forms only, not the existing instance form - self.assertContains(response, 'value="3" id="id_inner2_set-TOTAL_FORMS"') - self.assertNotContains(response, '', html=True) + self.assertNotContains(response, '' % self.inner2_id, html=True) def test_inline_change_fk_change_perm(self): permission = Permission.objects.get(codename='change_inner2', content_type=self.inner_ct) @@ -367,10 +370,13 @@ class TestInlinePermissions(TestCase): # Change permission on inner2s, so we can change existing but not add new self.assertContains(response, '

Inner2s

') # Just the one form for existing instances - self.assertContains(response, 'value="1" id="id_inner2_set-TOTAL_FORMS"') - self.assertContains(response, '', html=True) + self.assertContains(response, '' % self.inner2_id, html=True) # max-num 0 means we can't add new ones - self.assertContains(response, 'value="0" id="id_inner2_set-MAX_NUM_FORMS"') + self.assertContains(response, '', html=True) def test_inline_change_fk_add_change_perm(self): permission = Permission.objects.get(codename='add_inner2', content_type=self.inner_ct) @@ -381,9 +387,10 @@ class TestInlinePermissions(TestCase): # Add/change perm, so we can add new and change existing self.assertContains(response, '

Inner2s

') # One form for existing instance and three extra for new - self.assertContains(response, 'value="4" id="id_inner2_set-TOTAL_FORMS"') - self.assertContains(response, '', html=True) + self.assertContains(response, '' % self.inner2_id, html=True) def test_inline_change_fk_change_del_perm(self): permission = Permission.objects.get(codename='change_inner2', content_type=self.inner_ct) @@ -394,8 +401,10 @@ class TestInlinePermissions(TestCase): # Change/delete perm on inner2s, so we can change/delete existing self.assertContains(response, '

Inner2s

') # One form for existing instance only, no new - self.assertContains(response, 'value="1" id="id_inner2_set-TOTAL_FORMS"') - self.assertContains(response, '', html=True) + self.assertContains(response, '' % self.inner2_id, html=True) self.assertContains(response, 'id="id_inner2_set-0-DELETE"') @@ -410,8 +419,10 @@ class TestInlinePermissions(TestCase): # All perms on inner2s, so we can add/change/delete self.assertContains(response, '

Inner2s

') # One form for existing instance only, three for new - self.assertContains(response, 'value="4" id="id_inner2_set-TOTAL_FORMS"') - self.assertContains(response, '', html=True) + self.assertContains(response, '' % self.inner2_id, html=True) self.assertContains(response, 'id="id_inner2_set-0-DELETE"') From b9fc70141abd4c812b03df50452a0d3ed8cd62d2 Mon Sep 17 00:00:00 2001 From: Ian Clelland Date: Fri, 28 Sep 2012 00:27:38 -0700 Subject: [PATCH 027/870] Don't rely on dictionary ordering in tests --- .../regressiontests/fixtures_regress/tests.py | 19 +++++++++++-------- tests/regressiontests/forms/tests/forms.py | 12 +++++------- tests/regressiontests/httpwrappers/tests.py | 17 ++++++++--------- tests/regressiontests/mail/tests.py | 12 +++++++++++- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py index 6791143473..61dc4460df 100644 --- a/tests/regressiontests/fixtures_regress/tests.py +++ b/tests/regressiontests/fixtures_regress/tests.py @@ -18,6 +18,7 @@ from django.utils.encoding import force_text from django.utils._os import upath from django.utils import six from django.utils.six import PY3, StringIO +import json from .models import (Animal, Stuff, Absolute, Parent, Child, Article, Widget, Store, Person, Book, NKChild, RefToNKChild, Circle1, Circle2, Circle3, @@ -334,15 +335,17 @@ class TestFixtures(TestCase): # between different Python versions. data = re.sub('0{6,}\d', '', data) - lion_json = '{"pk": 1, "model": "fixtures_regress.animal", "fields": {"count": 3, "weight": 1.2, "name": "Lion", "latin_name": "Panthera leo"}}' - emu_json = '{"pk": 10, "model": "fixtures_regress.animal", "fields": {"count": 42, "weight": 1.2, "name": "Emu", "latin_name": "Dromaius novaehollandiae"}}' - platypus_json = '{"pk": %d, "model": "fixtures_regress.animal", "fields": {"count": 2, "weight": 2.2, "name": "Platypus", "latin_name": "Ornithorhynchus anatinus"}}' - platypus_json = platypus_json % animal.pk + animals_data = sorted([ + {"pk": 1, "model": "fixtures_regress.animal", "fields": {"count": 3, "weight": 1.2, "name": "Lion", "latin_name": "Panthera leo"}}, + {"pk": 10, "model": "fixtures_regress.animal", "fields": {"count": 42, "weight": 1.2, "name": "Emu", "latin_name": "Dromaius novaehollandiae"}}, + {"pk": animal.pk, "model": "fixtures_regress.animal", "fields": {"count": 2, "weight": 2.2, "name": "Platypus", "latin_name": "Ornithorhynchus anatinus"}}, + ], key=lambda x: x["pk"]) + + data = sorted(json.loads(data), key=lambda x: x["pk"]) + + self.maxDiff = 1024 + self.assertEqual(data, animals_data) - self.assertEqual(len(data), len('[%s]' % ', '.join([lion_json, emu_json, platypus_json]))) - self.assertTrue(lion_json in data) - self.assertTrue(emu_json in data) - self.assertTrue(platypus_json in data) def test_proxy_model_included(self): """ diff --git a/tests/regressiontests/forms/tests/forms.py b/tests/regressiontests/forms/tests/forms.py index 1c83ed04d4..ade06845f8 100644 --- a/tests/regressiontests/forms/tests/forms.py +++ b/tests/regressiontests/forms/tests/forms.py @@ -11,6 +11,7 @@ from django.test import TestCase from django.test.utils import str_prefix from django.utils.datastructures import MultiValueDict, MergeDict from django.utils.safestring import mark_safe +from django.utils import six class Person(Form): @@ -136,11 +137,7 @@ class FormsTestCase(TestCase): self.assertEqual(p.errors['first_name'], ['This field is required.']) self.assertEqual(p.errors['birthday'], ['This field is required.']) self.assertFalse(p.is_valid()) - self.assertHTMLEqual(p.errors.as_ul(), '
  • first_name
    • This field is required.
  • birthday
    • This field is required.
') - self.assertEqual(p.errors.as_text(), """* first_name - * This field is required. -* birthday - * This field is required.""") + self.assertDictEqual(p.errors, {'birthday': ['This field is required.'], 'first_name': ['This field is required.']}) self.assertEqual(p.cleaned_data, {'last_name': 'Lennon'}) self.assertEqual(p['first_name'].errors, ['This field is required.']) self.assertHTMLEqual(p['first_name'].errors.as_ul(), '
  • This field is required.
') @@ -1495,7 +1492,7 @@ class FormsTestCase(TestCase): form = UserRegistration(auto_id=False) if form.is_valid(): - return 'VALID: %r' % form.cleaned_data + return 'VALID: %r' % sorted(six.iteritems(form.cleaned_data)) t = Template('
\n\n{{ form }}\n
\n\n
') return t.render(Context({'form': form})) @@ -1520,7 +1517,8 @@ class FormsTestCase(TestCase): """) # Case 3: POST with valid data (the success message).) - self.assertHTMLEqual(my_function('POST', {'username': 'adrian', 'password1': 'secret', 'password2': 'secret'}), str_prefix("VALID: {'username': %(_)s'adrian', 'password1': %(_)s'secret', 'password2': %(_)s'secret'}")) + self.assertEqual(my_function('POST', {'username': 'adrian', 'password1': 'secret', 'password2': 'secret'}), + str_prefix("VALID: [('password1', %(_)s'secret'), ('password2', %(_)s'secret'), ('username', %(_)s'adrian')]")) def test_templates_with_forms(self): class UserRegistration(Form): diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index a601e541c6..d26a728e7b 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -130,15 +130,14 @@ class QueryDictTests(unittest.TestCase): self.assertTrue(q.has_key('foo')) self.assertTrue('foo' in q) - self.assertEqual(sorted(list(six.iteritems(q))), - [('foo', 'another'), ('name', 'john')]) - self.assertEqual(sorted(list(six.iterlists(q))), - [('foo', ['bar', 'baz', 'another']), ('name', ['john'])]) - self.assertEqual(sorted(list(six.iterkeys(q))), - ['foo', 'name']) - self.assertEqual(sorted(list(six.itervalues(q))), - ['another', 'john']) - self.assertEqual(len(q), 2) + self.assertListEqual(sorted(list(six.iteritems(q))), + [('foo', 'another'), ('name', 'john')]) + self.assertListEqual(sorted(list(six.iterlists(q))), + [('foo', ['bar', 'baz', 'another']), ('name', ['john'])]) + self.assertListEqual(sorted(list(six.iterkeys(q))), + ['foo', 'name']) + self.assertListEqual(sorted(list(six.itervalues(q))), + ['another', 'john']) q.update({'foo': 'hello'}) self.assertEqual(q['foo'], 'hello') diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index b798cb21aa..a8fbf20fd6 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -90,7 +90,17 @@ class MailTests(TestCase): """ headers = {"date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"} email = EmailMessage('subject', 'content', 'from@example.com', ['to@example.com'], headers=headers) - self.assertEqual(email.message().as_string(), 'Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nSubject: subject\nFrom: from@example.com\nTo: to@example.com\ndate: Fri, 09 Nov 2001 01:08:47 -0000\nMessage-ID: foo\n\ncontent') + + self.assertEqual(sorted(email.message().items()), [ + ('Content-Transfer-Encoding', '7bit'), + ('Content-Type', 'text/plain; charset="utf-8"'), + ('From', 'from@example.com'), + ('MIME-Version', '1.0'), + ('Message-ID', 'foo'), + ('Subject', 'subject'), + ('To', 'to@example.com'), + ('date', 'Fri, 09 Nov 2001 01:08:47 -0000'), + ]) def test_from_header(self): """ From 6b9f130278f98b3a15f7ad1959269c200e084f03 Mon Sep 17 00:00:00 2001 From: Ian Clelland Date: Thu, 27 Sep 2012 21:30:13 -0700 Subject: [PATCH 028/870] Sort HTML attributes on generated forms --- django/forms/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/forms/util.py b/django/forms/util.py index 9b1bcebe33..724f256e40 100644 --- a/django/forms/util.py +++ b/django/forms/util.py @@ -20,7 +20,7 @@ def flatatt(attrs): The result is passed through 'mark_safe'. """ - return format_html_join('', ' {0}="{1}"', attrs.items()) + return format_html_join('', ' {0}="{1}"', sorted(attrs.items())) @python_2_unicode_compatible class ErrorDict(dict): From c31c2c92b8ef0b58394ac55fa4f61acc07091047 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Mon, 24 Dec 2012 01:32:04 +0000 Subject: [PATCH 029/870] Made admin generated changelist URLs independent of dict ordering --- django/contrib/admin/views/main.py | 2 +- tests/regressiontests/admin_filters/tests.py | 4 ++-- tests/regressiontests/admin_views/tests.py | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 5033ba98bc..be7067ff61 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -158,7 +158,7 @@ class ChangeList(object): del p[k] else: p[k] = v - return '?%s' % urlencode(p) + return '?%s' % urlencode(sorted(p.items())) def get_results(self, request): paginator = self.model_admin.get_paginator(request, self.query_set, self.list_per_page) diff --git a/tests/regressiontests/admin_filters/tests.py b/tests/regressiontests/admin_filters/tests.py index 4a3e6135c3..8a23f1c9d5 100644 --- a/tests/regressiontests/admin_filters/tests.py +++ b/tests/regressiontests/admin_filters/tests.py @@ -534,13 +534,13 @@ class ListFiltersTests(TestCase): choices = list(filterspec.choices(changelist)) self.assertEqual(choices[3]['display'], 'the 2000\'s') self.assertEqual(choices[3]['selected'], True) - self.assertEqual(choices[3]['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk) + self.assertEqual(choices[3]['query_string'], '?author__id__exact=%s&publication-decade=the+00s' % self.alfred.pk) filterspec = changelist.get_filters(request)[0][0] self.assertEqual(force_text(filterspec.title), 'Verbose Author') choice = select_by(filterspec.choices(changelist), "display", "alfred") self.assertEqual(choice['selected'], True) - self.assertEqual(choice['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk) + self.assertEqual(choice['query_string'], '?author__id__exact=%s&publication-decade=the+00s' % self.alfred.pk) def test_listfilter_without_title(self): """ diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 669695ea9b..0d1e986cfa 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -3545,14 +3545,14 @@ class DateHierarchyTests(TestCase): def assert_contains_month_link(self, response, date): self.assertContains( - response, '?release_date__year=%d&release_date__month=%d"' % ( - date.year, date.month)) + response, '?release_date__month=%d&release_date__year=%d"' % ( + date.month, date.year)) def assert_contains_day_link(self, response, date): self.assertContains( - response, '?release_date__year=%d&' - 'release_date__month=%d&release_date__day=%d"' % ( - date.year, date.month, date.day)) + response, '?release_date__day=%d&' + 'release_date__month=%d&release_date__year=%d"' % ( + date.day, date.month, date.year)) def test_empty(self): """ From 1ae64e96c161229a74efc4235917dcaae7e9cd05 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Mon, 24 Dec 2012 01:33:44 +0000 Subject: [PATCH 030/870] Fixed a dependence on set-ordering in tests --- tests/regressiontests/admin_filters/tests.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/regressiontests/admin_filters/tests.py b/tests/regressiontests/admin_filters/tests.py index 8a23f1c9d5..11f792e07a 100644 --- a/tests/regressiontests/admin_filters/tests.py +++ b/tests/regressiontests/admin_filters/tests.py @@ -83,11 +83,11 @@ class DepartmentListFilterLookupWithNonStringValue(SimpleListFilter): parameter_name = 'department' def lookups(self, request, model_admin): - return set([ + return sorted(set([ (employee.department.id, # Intentionally not a string (Refs #19318) employee.department.code) for employee in model_admin.queryset(request).all() - ]) + ])) def queryset(self, request, queryset): if self.value(): @@ -683,9 +683,9 @@ class ListFiltersTests(TestCase): filterspec = changelist.get_filters(request)[0][-1] self.assertEqual(force_text(filterspec.title), 'department') choices = list(filterspec.choices(changelist)) - self.assertEqual(choices[2]['display'], 'DEV') - self.assertEqual(choices[2]['selected'], True) - self.assertEqual(choices[2]['query_string'], '?department=%s' % self.john.pk) + self.assertEqual(choices[1]['display'], 'DEV') + self.assertEqual(choices[1]['selected'], True) + self.assertEqual(choices[1]['query_string'], '?department=%s' % self.john.pk) def test_fk_with_to_field(self): """ From 8bc410b44536e03ee38a0087256faf367dd98dd9 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Mon, 24 Dec 2012 02:11:32 +0000 Subject: [PATCH 031/870] Fixed HTML comparisons of class="foo bar" and class="bar foo" in tests Refs #17758 --- django/test/html.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/django/test/html.py b/django/test/html.py index 274810cab4..2f6e4c9481 100644 --- a/django/test/html.py +++ b/django/test/html.py @@ -182,6 +182,14 @@ class Parser(HTMLParser): self.handle_endtag(tag) def handle_starttag(self, tag, attrs): + # Special case handling of 'class' attribute, so that comparisons of DOM + # instances are not sensitive to ordering of classes. + attrs = [ + (name, " ".join(sorted(value.split(" ")))) + if name == "class" + else (name, value) + for name, value in attrs + ] element = Element(tag, attrs) self.current.append(element) if tag not in self.SELF_CLOSING_TAGS: From e8f07f0378cef5a133028c502ff4064b91cd0611 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 24 Dec 2012 11:25:58 +0100 Subject: [PATCH 032/870] Fixed a randomly failing test under Python 3. Refs #17758. --- tests/regressiontests/multiple_database/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regressiontests/multiple_database/tests.py b/tests/regressiontests/multiple_database/tests.py index a903ea11a0..d2a5058206 100644 --- a/tests/regressiontests/multiple_database/tests.py +++ b/tests/regressiontests/multiple_database/tests.py @@ -1596,7 +1596,7 @@ class AuthTestCase(TestCase): new_io = StringIO() management.call_command('dumpdata', 'auth', format='json', database='other', stdout=new_io) command_output = new_io.getvalue().strip() - self.assertTrue('"email": "alice@example.com",' in command_output) + self.assertTrue('"email": "alice@example.com"' in command_output) @override_settings(AUTH_PROFILE_MODULE='multiple_database.UserProfile') From 4a71b842662162e0892a9269179421ff2191adba Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Mon, 24 Dec 2012 14:04:02 +0100 Subject: [PATCH 033/870] Fixed #19204 -- Replaced python2-style exception syntax. Thanks to garrison for the report and patch. --- django/utils/unittest/case.py | 16 ++++++++-------- django/utils/unittest/loader.py | 4 ++-- django/utils/unittest/main.py | 2 +- django/utils/unittest/suite.py | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/django/utils/unittest/case.py b/django/utils/unittest/case.py index f906a4cb68..fffd3c2524 100644 --- a/django/utils/unittest/case.py +++ b/django/utils/unittest/case.py @@ -324,7 +324,7 @@ class TestCase(unittest.TestCase): success = False try: self.setUp() - except SkipTest, e: + except SkipTest as e: self._addSkip(result, str(e)) except Exception: result.addError(self, sys.exc_info()) @@ -333,7 +333,7 @@ class TestCase(unittest.TestCase): testMethod() except self.failureException: result.addFailure(self, sys.exc_info()) - except _ExpectedFailure, e: + except _ExpectedFailure as e: addExpectedFailure = getattr(result, 'addExpectedFailure', None) if addExpectedFailure is not None: addExpectedFailure(self, e.exc_info) @@ -349,7 +349,7 @@ class TestCase(unittest.TestCase): warnings.warn("Use of a TestResult without an addUnexpectedSuccess method is deprecated", DeprecationWarning) result.addFailure(self, sys.exc_info()) - except SkipTest, e: + except SkipTest as e: self._addSkip(result, str(e)) except Exception: result.addError(self, sys.exc_info()) @@ -770,16 +770,16 @@ class TestCase(unittest.TestCase): """ try: difference1 = set1.difference(set2) - except TypeError, e: + except TypeError as e: self.fail('invalid type when attempting set difference: %s' % e) - except AttributeError, e: + except AttributeError as e: self.fail('first argument does not support set difference: %s' % e) try: difference2 = set2.difference(set1) - except TypeError, e: + except TypeError as e: self.fail('invalid type when attempting set difference: %s' % e) - except AttributeError, e: + except AttributeError as e: self.fail('second argument does not support set difference: %s' % e) if not (difference1 or difference2): @@ -980,7 +980,7 @@ class TestCase(unittest.TestCase): return _AssertRaisesContext(expected_exception, self, expected_regexp) try: callable_obj(*args, **kwargs) - except expected_exception, exc_value: + except expected_exception as exc_value: if isinstance(expected_regexp, basestring): expected_regexp = re.compile(expected_regexp) if not expected_regexp.search(str(exc_value)): diff --git a/django/utils/unittest/loader.py b/django/utils/unittest/loader.py index 1ca910f019..0bdd106d39 100644 --- a/django/utils/unittest/loader.py +++ b/django/utils/unittest/loader.py @@ -89,7 +89,7 @@ class TestLoader(unittest.TestLoader): if use_load_tests and load_tests is not None: try: return load_tests(self, tests, None) - except Exception, e: + except Exception as e: return _make_failed_load_tests(module.__name__, e, self.suiteClass) return tests @@ -295,7 +295,7 @@ class TestLoader(unittest.TestLoader): else: try: yield load_tests(self, tests, pattern) - except Exception, e: + except Exception as e: yield _make_failed_load_tests(package.__name__, e, self.suiteClass) diff --git a/django/utils/unittest/main.py b/django/utils/unittest/main.py index 0f25a98a22..659310babf 100644 --- a/django/utils/unittest/main.py +++ b/django/utils/unittest/main.py @@ -150,7 +150,7 @@ class TestProgram(object): else: self.testNames = (self.defaultTest,) self.createTests() - except getopt.error, msg: + except getopt.error as msg: self.usageExit(msg) def createTests(self): diff --git a/django/utils/unittest/suite.py b/django/utils/unittest/suite.py index f39569bbc2..da8ac2e2a8 100644 --- a/django/utils/unittest/suite.py +++ b/django/utils/unittest/suite.py @@ -138,7 +138,7 @@ class TestSuite(BaseTestSuite): if setUpClass is not None: try: setUpClass() - except Exception, e: + except Exception as e: if isinstance(result, _DebugResult): raise currentClass._classSetupFailed = True @@ -172,7 +172,7 @@ class TestSuite(BaseTestSuite): if setUpModule is not None: try: setUpModule() - except Exception, e: + except Exception as e: if isinstance(result, _DebugResult): raise result._moduleSetUpFailed = True @@ -203,7 +203,7 @@ class TestSuite(BaseTestSuite): if tearDownModule is not None: try: tearDownModule() - except Exception, e: + except Exception as e: if isinstance(result, _DebugResult): raise errorName = 'tearDownModule (%s)' % previousModule @@ -225,7 +225,7 @@ class TestSuite(BaseTestSuite): if tearDownClass is not None: try: tearDownClass() - except Exception, e: + except Exception as e: if isinstance(result, _DebugResult): raise className = util.strclass(previousClass) From 35d1cd0b28d1d9cd7bffbfbc6cc2e02b58404415 Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Sat, 22 Dec 2012 20:00:08 +0100 Subject: [PATCH 034/870] Fixed #19505 -- A more flexible implementation for customizable admin redirect urls. Work by Julien Phalip. Refs #8001, #18310, #19505. See also 0b908b92a2ca4fb74a103e96bb75c53c05d0a428. --- django/contrib/admin/options.py | 165 ++++++------------ django/contrib/auth/admin.py | 5 +- docs/internals/deprecation.txt | 6 + .../admin_custom_urls/models.py | 63 ++++--- .../admin_custom_urls/tests.py | 79 +++++---- 5 files changed, 144 insertions(+), 174 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 1827d40159..fa6d288f58 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -9,7 +9,7 @@ from django.forms.models import (modelform_factory, modelformset_factory, inlineformset_factory, BaseInlineFormSet) from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets, helpers -from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects, model_format_dict +from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict from django.contrib.admin.templatetags.admin_static import static from django.contrib import messages from django.views.decorators.csrf import csrf_protect @@ -38,6 +38,7 @@ HORIZONTAL, VERTICAL = 1, 2 # returns the - .. versionadded:: 1.4 - For more granular control over the generated markup, you can loop over the radio buttons in the template. Assuming a form ``myform`` with a field ``beatles`` that uses a ``RadioSelect`` as its widget: diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 41cff346ff..31cc6f24f6 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -222,7 +222,4 @@ X-Frame-Options middleware .. class:: XFrameOptionsMiddleware -.. versionadded:: 1.4 - ``XFrameOptionsMiddleware`` was added. - Simple :doc:`clickjacking protection via the X-Frame-Options header `. diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 68abb4f4d2..e9f85e0657 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -804,8 +804,6 @@ for this field is a :class:`~django.forms.TextInput`. .. class:: GenericIPAddressField([protocol=both, unpack_ipv4=False, **options]) -.. versionadded:: 1.4 - An IPv4 or IPv6 address, in string format (e.g. ``192.0.2.30`` or ``2a02:42fe::4``). The default form widget for this field is a :class:`~django.forms.TextInput`. diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index ac20422915..6fd707fdf2 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -209,10 +209,6 @@ Django quotes column and table names behind the scenes. ordering = ['-pub_date', 'author'] - .. versionchanged:: 1.4 - The Django admin honors all elements in the list/tuple; before 1.4, only - the first one was respected. - ``permissions`` --------------- diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index ca2e64a8c5..a8e946f8a5 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -378,16 +378,12 @@ query spans multiple tables, it's possible to get duplicate results when a :meth:`values()` together, be careful when ordering by fields not in the :meth:`values()` call. -.. versionadded:: 1.4 - -As of Django 1.4, you can pass positional arguments (``*fields``) in order to -specify the names of fields to which the ``DISTINCT`` should apply. This -translates to a ``SELECT DISTINCT ON`` SQL query. - -Here's the difference. For a normal ``distinct()`` call, the database compares -*each* field in each row when determining which rows are distinct. For a -``distinct()`` call with specified field names, the database will only compare -the specified field names. +You can pass positional arguments (``*fields``) in order to specify the names +of fields to which the ``DISTINCT`` should apply. This translates to a +``SELECT DISTINCT ON`` SQL query. Here's the difference. For a normal +``distinct()`` call, the database compares *each* field in each row when +determining which rows are distinct. For a ``distinct()`` call with specified +field names, the database will only compare the specified field names. .. note:: This ability to specify field names is only available in PostgreSQL. @@ -740,8 +736,6 @@ prefetch_related .. method:: prefetch_related(*lookups) -.. versionadded:: 1.4 - Returns a ``QuerySet`` that will automatically retrieve, in a single batch, related objects for each of the specified lookups. @@ -1191,8 +1185,6 @@ select_for_update .. method:: select_for_update(nowait=False) -.. versionadded:: 1.4 - Returns a queryset that will lock rows until the end of the transaction, generating a ``SELECT ... FOR UPDATE`` SQL statement on supported databases. @@ -1368,8 +1360,6 @@ bulk_create .. method:: bulk_create(objs, batch_size=None) -.. versionadded:: 1.4 - This method inserts the provided list of objects into the database in an efficient manner (generally only 1 query, no matter how many objects there are):: diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 9e8eec4433..ae1da6cb4b 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -262,8 +262,6 @@ Methods .. method:: HttpRequest.get_signed_cookie(key, default=RAISE_ERROR, salt='', max_age=None) - .. versionadded:: 1.4 - Returns a cookie value for a signed cookie, or raises a :class:`~django.core.signing.BadSignature` exception if the signature is no longer valid. If you provide the ``default`` argument the exception @@ -473,9 +471,6 @@ In addition, ``QueryDict`` has the following methods: It's guaranteed to return a list of some sort unless the default value was no list. - .. versionchanged:: 1.4 - The ``default`` parameter was added. - .. method:: QueryDict.setlist(key, list_) Sets the given key to ``list_`` (unlike ``__setitem__()``). @@ -500,8 +495,6 @@ In addition, ``QueryDict`` has the following methods: .. method:: QueryDict.dict() - .. versionadded:: 1.4 - Returns ``dict`` representation of ``QueryDict``. For every (key, list) pair in ``QueryDict``, ``dict`` will have (key, item), where item is one element of the list, using same logic as :meth:`QueryDict.__getitem__()`:: @@ -705,8 +698,6 @@ Methods .. method:: HttpResponse.set_signed_cookie(key, value='', salt='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=True) - .. versionadded:: 1.4 - Like :meth:`~HttpResponse.set_cookie()`, but :doc:`cryptographic signing ` the cookie before setting it. Use in conjunction with :meth:`HttpRequest.get_signed_cookie`. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index df679e7c1f..bfe283cc68 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -325,8 +325,6 @@ want. See :doc:`/ref/contrib/csrf`. CSRF_COOKIE_PATH ---------------- -.. versionadded:: 1.4 - Default: ``'/'`` The path set on the CSRF cookie. This should either match the URL path of your @@ -341,8 +339,6 @@ its own CSRF cookie. CSRF_COOKIE_SECURE ------------------ -.. versionadded:: 1.4 - Default: ``False`` Whether to use a secure cookie for the CSRF cookie. If this is set to ``True``, @@ -756,11 +752,6 @@ name includes any of the following: * SIGNATURE * TOKEN -.. versionchanged:: 1.4 - - We changed ``'PASSWORD'`` ``'PASS'``. ``'API'``, ``'TOKEN'`` and ``'KEY'`` - were added. - Note that these are *partial* matches. ``'PASS'`` will also match PASSWORD, just as ``'TOKEN'`` will also match TOKENIZED and so on. @@ -1117,8 +1108,6 @@ Available formats are :setting:`DATE_FORMAT`, :setting:`TIME_FORMAT`, IGNORABLE_404_URLS ------------------ -.. versionadded:: 1.4 - Default: ``()`` List of compiled regular expression objects describing URLs that should be @@ -1457,8 +1446,6 @@ See also :setting:`DECIMAL_SEPARATOR`, :setting:`THOUSAND_SEPARATOR` and PASSWORD_HASHERS ---------------- -.. versionadded:: 1.4 - See :ref:`auth_password_storage`. Default:: @@ -1544,8 +1531,6 @@ randomly-generated ``SECRET_KEY`` to each new project. SECURE_PROXY_SSL_HEADER ----------------------- -.. versionadded:: 1.4 - Default: ``None`` A tuple representing a HTTP header/value combination that signifies a request @@ -1677,9 +1662,6 @@ protected cookie data. .. _HTTPOnly: https://www.owasp.org/index.php/HTTPOnly -.. versionchanged:: 1.4 - The default value of the setting was changed from ``False`` to ``True``. - .. setting:: SESSION_COOKIE_NAME SESSION_COOKIE_NAME @@ -1809,8 +1791,6 @@ See also :setting:`DATE_FORMAT` and :setting:`SHORT_DATE_FORMAT`. SIGNING_BACKEND --------------- -.. versionadded:: 1.4 - Default: 'django.core.signing.TimestampSigner' The backend used for signing cookies and other data. @@ -1901,10 +1881,6 @@ A tuple of callables that are used to populate the context in ``RequestContext`` These callables take a request object as their argument and return a dictionary of items to be merged into the context. -.. versionadded:: 1.4 - The ``django.core.context_processors.tz`` context processor - was added in this release. - .. setting:: TEMPLATE_DEBUG TEMPLATE_DEBUG @@ -2029,9 +2005,6 @@ TIME_ZONE Default: ``'America/Chicago'`` -.. versionchanged:: 1.4 - The meaning of this setting now depends on the value of :setting:`USE_TZ`. - A string representing the time zone for this installation, or ``None``. `See available choices`_. (Note that list of available choices lists more than one on the same line; you'll want to use just @@ -2148,8 +2121,6 @@ See also :setting:`DECIMAL_SEPARATOR`, :setting:`NUMBER_GROUPING` and USE_TZ ------ -.. versionadded:: 1.4 - Default: ``False`` A boolean that specifies if datetimes will be timezone-aware by default or not. @@ -2180,8 +2151,6 @@ which sets this header is in use. WSGI_APPLICATION ---------------- -.. versionadded:: 1.4 - Default: ``None`` The full Python path of the WSGI application object that Django's built-in diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index c31c90f4e8..b27a4f87cc 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -480,8 +480,6 @@ Signals only sent when :ref:`running tests `. setting_changed --------------- -.. versionadded:: 1.4 - .. data:: django.test.signals.setting_changed :module: diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index db57d2de96..7c17f0a758 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -221,13 +221,12 @@ straight lookups. Here are some things to keep in mind: self.database_record.delete() sensitive_function.alters_data = True -* .. versionadded:: 1.4 - Occasionally you may want to turn off this feature for other reasons, - and tell the template system to leave a variable un-called no matter - what. To do so, set a ``do_not_call_in_templates`` attribute on the - callable with the value ``True``. The template system then will act as - if your variable is not callable (allowing you to access attributes of - the callable, for example). +* Occasionally you may want to turn off this feature for other reasons, + and tell the template system to leave a variable un-called no matter + what. To do so, set a ``do_not_call_in_templates`` attribute on the + callable with the value ``True``. The template system then will act as + if your variable is not callable (allowing you to access attributes of + the callable, for example). .. _invalid-template-variables: diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 867d1e5cc0..aab53aed0c 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -381,10 +381,6 @@ As you can see, the ``if`` tag may take one or several `` {% elif %}`` clauses, as well as an ``{% else %}`` clause that will be displayed if all previous conditions fail. These clauses are optional. -.. versionadded:: 1.4 - -The ``if`` tag now supports ``{% elif %}`` clauses. - Boolean operators ^^^^^^^^^^^^^^^^^ @@ -743,8 +739,6 @@ escaped, because it's not a format character:: This would display as "It is the 4th of September". -.. versionchanged:: 1.4 - .. note:: The format passed can also be one of the predefined ones @@ -1289,10 +1283,6 @@ Z Time zone offset in seconds. The ``-43200`` to ``4320 UTC is always positive. ================ ======================================== ===================== -.. versionadded:: 1.4 - -The ``e`` and ``o`` format specification characters were added in Django 1.4. - For example:: {{ value|date:"D d M Y" }} @@ -2069,8 +2059,6 @@ If ``value`` is ``"my first post"``, the output will be ``"My First Post"``. truncatechars ^^^^^^^^^^^^^ -.. versionadded:: 1.4 - Truncates a string if it is longer than the specified number of characters. Truncated strings will end with a translatable ellipsis sequence ("..."). @@ -2200,11 +2188,6 @@ It also supports domain-only links ending in one of the original top level domains (``.com``, ``.edu``, ``.gov``, ``.int``, ``.mil``, ``.net``, and ``.org``). For example, ``djangoproject.com`` gets converted. -.. versionchanged:: 1.4 - -Until Django 1.4, only the ``.com``, ``.net`` and ``.org`` suffixes were -supported for domain-only links. - Links can have trailing punctuation (periods, commas, close-parens) and leading punctuation (opening parens), and ``urlize`` will still do the right thing. @@ -2334,8 +2317,6 @@ See :ref:`topic-l10n-templates`. tz ^^ -.. versionadded:: 1.4 - This library provides control over time zone conversions in templates. Like ``l10n``, you only need to load the library using ``{% load tz %}``, but you'll usually also set :setting:`USE_TZ` to ``True`` so that conversion diff --git a/docs/ref/urlresolvers.txt b/docs/ref/urlresolvers.txt index 528f172061..87c0605a11 100644 --- a/docs/ref/urlresolvers.txt +++ b/docs/ref/urlresolvers.txt @@ -71,8 +71,6 @@ You can use ``kwargs`` instead of ``args``. For example:: reverse_lazy() -------------- -.. versionadded:: 1.4 - A lazily evaluated version of `reverse()`_. .. function:: reverse_lazy(viewname, [urlconf=None, args=None, kwargs=None, current_app=None]) diff --git a/docs/ref/urls.txt b/docs/ref/urls.txt index 46332cb42c..5a0b04f9fa 100644 --- a/docs/ref/urls.txt +++ b/docs/ref/urls.txt @@ -114,9 +114,6 @@ value should suffice. See the documentation about :ref:`the 403 (HTTP Forbidden) view ` for more information. -.. versionadded:: 1.4 - ``handler403`` is new in Django 1.4. - handler404 ---------- diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 4ff31591c8..942cac2650 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -138,8 +138,6 @@ results. Instead do:: ``django.utils.dateparse`` ========================== -.. versionadded:: 1.4 - .. module:: django.utils.dateparse :synopsis: Functions to parse datetime objects. @@ -788,8 +786,6 @@ For a complete discussion on the usage of the following see the .. function:: override(language, deactivate=False) - .. versionadded:: 1.4 - A Python context manager that uses :func:`django.utils.translation.activate` to fetch the translation object for a given language, installing it as the translation object for the @@ -812,8 +808,6 @@ For a complete discussion on the usage of the following see the .. function:: get_language_from_request(request, check_path=False) - .. versionchanged:: 1.4 - Analyzes the request to find what language the user wants the system to show. Only languages listed in settings.LANGUAGES are taken into account. If the user requests a sublanguage where we have a main language, we send out the main @@ -838,8 +832,6 @@ For a complete discussion on the usage of the following see the ``django.utils.timezone`` ========================= -.. versionadded:: 1.4 - .. module:: django.utils.timezone :synopsis: Timezone support. diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt index b68d6f2772..0536b03d64 100644 --- a/docs/ref/validators.txt +++ b/docs/ref/validators.txt @@ -115,7 +115,6 @@ to, or in lieu of custom ``field.clean()`` methods. ``validate_ipv6_address`` ------------------------- -.. versionadded:: 1.4 .. data:: validate_ipv6_address @@ -123,7 +122,6 @@ to, or in lieu of custom ``field.clean()`` methods. ``validate_ipv46_address`` -------------------------- -.. versionadded:: 1.4 .. data:: validate_ipv46_address diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index c4736135b0..76fb7d835b 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -522,12 +522,10 @@ The permission_required decorator As in the :func:`~django.contrib.auth.decorators.login_required` decorator, ``login_url`` defaults to :setting:`settings.LOGIN_URL `. - .. versionchanged:: 1.4 - - Added ``raise_exception`` parameter. If given, the decorator will raise - :exc:`~django.core.exceptions.PermissionDenied`, prompting - :ref:`the 403 (HTTP Forbidden) view` instead of - redirecting to the login page. + If the ``raise_exception`` parameter is given, the decorator will raise + :exc:`~django.core.exceptions.PermissionDenied`, prompting :ref:`the 403 + (HTTP Forbidden) view` instead of redirecting to the + login page. Applying permissions to generic views ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -552,8 +550,6 @@ password management. These make use of the :ref:`stock auth forms Django provides no default template for the authentication views - however the template context is documented for each view below. -.. versionadded:: 1.4 - The built-in views all return a :class:`~django.template.response.TemplateResponse` instance, which allows you to easily customize the response data before rendering. For more details, @@ -747,11 +743,10 @@ patterns. that can be used to reset the password, and sending that link to the user's registered email address. - .. versionchanged:: 1.4 - Users flagged with an unusable password (see - :meth:`~django.contrib.auth.models.User.set_unusable_password()` - will not be able to request a password reset to prevent misuse - when using an external authentication source like LDAP. + Users flagged with an unusable password (see + :meth:`~django.contrib.auth.models.User.set_unusable_password()` aren't + allowed to request a password reset to prevent misuse when using an + external authentication source like LDAP. **URL name:** ``password_reset`` @@ -769,8 +764,6 @@ patterns. the subject of the email with the reset password link. Defaults to :file:`registration/password_reset_subject.txt` if not supplied. - .. versionadded:: 1.4 - * ``password_reset_form``: Form that will be used to get the email of the user to reset the password for. Defaults to :class:`~django.contrib.auth.forms.PasswordResetForm`. diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt index e6345aab2e..0f44444416 100644 --- a/docs/topics/auth/passwords.txt +++ b/docs/topics/auth/passwords.txt @@ -13,10 +13,8 @@ work with hashed passwords. How Django stores passwords =========================== -.. versionadded:: 1.4 - Django 1.4 introduces a new flexible password storage system and uses - PBKDF2 by default. Previous versions of Django used SHA1, and other - algorithms couldn't be chosen. +Django provides a flexible password storage system and uses PBKDF2 by default. +Older versions of Django used SHA1, and other algorithms couldn't be chosen. The :attr:`~django.contrib.auth.models.User.password` attribute of a :class:`~django.contrib.auth.models.User` object is a string in this format:: @@ -173,15 +171,12 @@ Manually managing a user's password .. module:: django.contrib.auth.hashers -.. versionadded:: 1.4 The :mod:`django.contrib.auth.hashers` module provides a set of functions to create and validate hashed password. You can use them independently from the ``User`` model. .. function:: check_password(password, encoded) - .. versionadded:: 1.4 - If you'd like to manually authenticate a user by comparing a plain-text password to the hashed password in the database, use the convenience function :func:`django.contrib.auth.hashers.check_password`. It takes two @@ -191,8 +186,6 @@ Manually managing a user's password .. function:: make_password(password[, salt, hashers]) - .. versionadded:: 1.4 - Creates a hashed password in the format used by this application. It takes one mandatory argument: the password in plain-text. Optionally, you can provide a salt and a hashing algorithm to use, if you don't want to use the @@ -206,7 +199,5 @@ Manually managing a user's password .. function:: is_password_usable(encoded_password) - .. versionadded:: 1.4 - Checks if the given string is a hashed password that has a chance of being verified against :func:`django.contrib.auth.hashers.check_password`. diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index a15cf58370..9b3e41d0d4 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -482,8 +482,6 @@ include the name of the active :term:`language` -- see also :ref:`how-django-discovers-language-preference`). This allows you to easily cache multilingual sites without having to create the cache key yourself. -.. versionchanged:: 1.4 - Cache keys also include the active :term:`language ` when :setting:`USE_L10N` is set to ``True`` and the :ref:`current time zone ` when :setting:`USE_TZ` is set to ``True``. diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 046c23bdcd..de898c8373 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -412,14 +412,13 @@ translates (roughly) into the following SQL:: .. _`Keyword Arguments`: http://docs.python.org/tutorial/controlflow.html#keyword-arguments -.. versionchanged:: 1.4 - The field specified in a lookup has to be the name of a model field. - There's one exception though, in case of a - :class:`~django.db.models.ForeignKey` you can specify the field - name suffixed with ``_id``. In this case, the value parameter is expected - to contain the raw value of the foreign model's primary key. For example: +The field specified in a lookup has to be the name of a model field. There's +one exception though, in case of a :class:`~django.db.models.ForeignKey` you +can specify the field name suffixed with ``_id``. In this case, the value +parameter is expected to contain the raw value of the foreign model's primary +key. For example: - >>> Entry.objects.filter(blog_id__exact=4) + >>> Entry.objects.filter(blog_id__exact=4) If you pass an invalid keyword argument, a lookup function will raise ``TypeError``. diff --git a/docs/topics/db/tablespaces.txt b/docs/topics/db/tablespaces.txt index 7fcd5588e7..8bf1d07bca 100644 --- a/docs/topics/db/tablespaces.txt +++ b/docs/topics/db/tablespaces.txt @@ -68,6 +68,3 @@ PostgreSQL and Oracle support tablespaces. SQLite and MySQL don't. When you use a backend that lacks support for tablespaces, Django ignores all tablespace-related options. - -.. versionchanged:: 1.4 - Since Django 1.4, the PostgreSQL backend supports tablespaces. diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index e3c2cadf6d..7716c91681 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -238,9 +238,6 @@ with the PostgreSQL 8, Oracle and MySQL (when using the InnoDB storage engine) backends. Other backends provide the savepoint functions, but they're empty operations -- they don't actually do anything. -.. versionchanged:: 1.4 - Savepoint support for the MySQL backend was added in Django 1.4. - Savepoints aren't especially useful if you are using the default ``autocommit`` behavior of Django. However, if you are using ``commit_on_success`` or ``commit_manually``, each open transaction will build diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 76849c8e23..b5b02581cd 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -139,8 +139,6 @@ As we can see, ``formset.errors`` is a list whose entries correspond to the forms in the formset. Validation was performed for each of the two forms, and the expected error message appears for the second item. -.. versionadded:: 1.4 - We can also check if form data differs from the initial data (i.e. the form was sent without any data):: diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 67d539447c..802150d6c3 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -625,8 +625,6 @@ exclude:: Providing initial values ------------------------ -.. versionadded:: 1.4 - As with regular formsets, it's possible to :ref:`specify initial data ` for forms in the formset by specifying an ``initial`` parameter when instantiating the model formset class returned by diff --git a/docs/topics/http/decorators.txt b/docs/topics/http/decorators.txt index 83d14a0777..25616a44c0 100644 --- a/docs/topics/http/decorators.txt +++ b/docs/topics/http/decorators.txt @@ -39,8 +39,6 @@ a :class:`django.http.HttpResponseNotAllowed` if the conditions are not met. .. function:: require_safe() - .. versionadded:: 1.4 - Decorator to require that a view only accept the GET and HEAD methods. These methods are commonly considered "safe" because they should not have the significance of taking an action other than retrieving the requested diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index dac146bf3e..1832a55267 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -110,8 +110,6 @@ server has permissions to read and write to this location. Using cookie-based sessions --------------------------- -.. versionadded:: 1.4 - To use cookies-based sessions, set the :setting:`SESSION_ENGINE` setting to ``"django.contrib.sessions.backends.signed_cookies"``. The session data will be stored using Django's tools for :doc:`cryptographic signing ` @@ -558,9 +556,6 @@ consistently by all browsers. However, when it is honored, it can be a useful way to mitigate the risk of client side script accessing the protected cookie data. -.. versionchanged:: 1.4 - The default value of the setting was changed from ``False`` to ``True``. - .. _HTTPOnly: https://www.owasp.org/index.php/HTTPOnly SESSION_COOKIE_NAME diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 00c07da6ea..8a07d46f77 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -26,10 +26,9 @@ This mapping can be as short or as long as needed. It can reference other mappings. And, because it's pure Python code, it can be constructed dynamically. -.. versionadded:: 1.4 - Django also provides a way to translate URLs according to the active - language. See the :ref:`internationalization documentation - ` for more information. +Django also provides a way to translate URLs according to the active +language. See the :ref:`internationalization documentation +` for more information. .. _how-django-processes-a-request: @@ -246,9 +245,6 @@ The variables are: * ``handler500`` -- See :data:`django.conf.urls.handler500`. * ``handler403`` -- See :data:`django.conf.urls.handler403`. -.. versionadded:: 1.4 - ``handler403`` is new in Django 1.4. - .. _urlpatterns-view-prefix: The view prefix diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index caa2882f37..9ef521c71d 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -199,8 +199,6 @@ One thing to note about 500 views: The 403 (HTTP Forbidden) view ----------------------------- -.. versionadded:: 1.4 - In the same vein as the 404 and 500 views, Django has a view to handle 403 Forbidden errors. If a view results in a 403 exception then Django will, by default, call the view ``django.views.defaults.permission_denied``. diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt index cefc1667ad..14c81e6665 100644 --- a/docs/topics/i18n/timezones.txt +++ b/docs/topics/i18n/timezones.txt @@ -4,8 +4,6 @@ Time zones ========== -.. versionadded:: 1.4 - .. _time-zones-overview: Overview diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 0b37c25f18..01f168bc10 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -287,8 +287,6 @@ will appear in the ``.po`` file as: msgid "May" msgstr "" -.. versionadded:: 1.4 - Contextual markers are also supported by the :ttag:`trans` and :ttag:`blocktrans` template tags. @@ -507,7 +505,6 @@ It's not possible to mix a template variable inside a string within ``{% trans %}``. If your translations require strings with variables (placeholders), use ``{% blocktrans %}`` instead. -.. versionadded:: 1.4 If you'd like to retrieve a translated string without displaying it, you can use the following syntax:: @@ -533,8 +530,6 @@ or should be used as arguments for other template tags or filters:: {% endfor %}

-.. versionadded:: 1.4 - ``{% trans %}`` also supports :ref:`contextual markers` using the ``context`` keyword: @@ -574,8 +569,6 @@ You can use multiple expressions inside a single ``blocktrans`` tag:: .. note:: The previous more verbose format is still supported: ``{% blocktrans with book|title as book_t and author|title as author_t %}`` -.. versionchanged:: 1.4 - If resolving one of the block arguments fails, blocktrans will fall back to the default language by deactivating the currently active language temporarily with the :func:`~django.utils.translation.deactivate_all` @@ -620,8 +613,6 @@ be retrieved (and stored) beforehand:: This is a URL: {{ the_url }} {% endblocktrans %} -.. versionadded:: 1.4 - ``{% blocktrans %}`` also supports :ref:`contextual markers` using the ``context`` keyword: @@ -1410,8 +1401,6 @@ For example, your :setting:`MIDDLEWARE_CLASSES` might look like this:: ``LocaleMiddleware`` tries to determine the user's language preference by following this algorithm: -.. versionchanged:: 1.4 - * First, it looks for the language prefix in the requested URL. This is only performed when you are using the ``i18n_patterns`` function in your root URLconf. See :ref:`url-internationalization` for more information diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index 652ad397ff..3a5a8cb489 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -504,8 +504,6 @@ logging module. .. class:: CallbackFilter(callback) - .. versionadded:: 1.4 - This filter accepts a callback function (which should accept a single argument, the record to be logged), and calls it for each record that passes through the filter. Handling of that record will not proceed if the callback @@ -542,8 +540,6 @@ logging module. .. class:: RequireDebugFalse() - .. versionadded:: 1.4 - This filter will only pass on records when settings.DEBUG is False. This filter is used as follows in the default :setting:`LOGGING` diff --git a/docs/topics/pagination.txt b/docs/topics/pagination.txt index b504b2a373..17747c22ff 100644 --- a/docs/topics/pagination.txt +++ b/docs/topics/pagination.txt @@ -126,12 +126,6 @@ pages along with any interesting information from the objects themselves:: -.. versionchanged:: 1.4 - Previously, you would need to use - ``{% for contact in contacts.object_list %}``, since the ``Page`` - object was not iterable. - - ``Paginator`` objects ===================== @@ -234,7 +228,6 @@ using :meth:`Paginator.page`. .. class:: Page(object_list, number, paginator) -.. versionadded:: 1.4 A page acts like a sequence of :attr:`Page.object_list` when using ``len()`` or iterating it directly. diff --git a/docs/topics/signing.txt b/docs/topics/signing.txt index 07de97e2f3..0758ce8970 100644 --- a/docs/topics/signing.txt +++ b/docs/topics/signing.txt @@ -5,8 +5,6 @@ Cryptographic signing .. module:: django.core.signing :synopsis: Django's signing framework. -.. versionadded:: 1.4 - The golden rule of Web application security is to never trust data from untrusted sources. Sometimes it can be useful to pass data through an untrusted medium. Cryptographically signed values can be passed through an diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index 0674b2e41b..26dc8ee1ae 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -242,8 +242,6 @@ set up, execute and tear down the test suite. write your own test runner, ensure accept and handle the ``**kwargs`` parameter. - .. versionadded:: 1.4 - Your test runner may also define additional command-line options. If you add an ``option_list`` attribute to a subclassed test runner, those options will be added to the list of command-line options that @@ -254,8 +252,6 @@ Attributes .. attribute:: DjangoTestSuiteRunner.option_list - .. versionadded:: 1.4 - This is the tuple of ``optparse`` options which will be fed into the management command's ``OptionParser`` for parsing arguments. See the documentation for Python's ``optparse`` module for more details. diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 0548f66481..e51741e549 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -860,8 +860,6 @@ SimpleTestCase .. class:: SimpleTestCase() -.. versionadded:: 1.4 - A very thin subclass of :class:`unittest.TestCase`, it extends it with some basic functionality like: @@ -992,8 +990,6 @@ additions, including: LiveServerTestCase ~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.4 - .. class:: LiveServerTestCase() ``LiveServerTestCase`` does basically the same as @@ -1346,8 +1342,6 @@ Overriding settings .. method:: TestCase.settings -.. versionadded:: 1.4 - For testing purposes it's often useful to change a setting temporarily and revert to the original value after running the testing code. For this use case Django provides a standard Python context manager (see :pep:`343`) @@ -1459,8 +1453,6 @@ your test suite. .. method:: SimpleTestCase.assertRaisesMessage(expected_exception, expected_message, callable_obj=None, *args, **kwargs) - .. versionadded:: 1.4 - Asserts that execution of callable ``callable_obj`` raised the ``expected_exception`` exception and that such exception has an ``expected_message`` representation. Any other outcome is reported as a @@ -1469,8 +1461,6 @@ your test suite. .. method:: SimpleTestCase.assertFieldOutput(self, fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value=u'') - .. versionadded:: 1.4 - Asserts that a form field behaves correctly with various inputs. :param fieldclass: the class of the field to be tested. @@ -1495,8 +1485,6 @@ your test suite. that ``text`` appears in the content of the response. If ``count`` is provided, ``text`` must occur exactly ``count`` times in the response. - .. versionadded:: 1.4 - Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with the response content will be based on HTML semantics instead of character-by-character equality. Whitespace is ignored in most cases, @@ -1508,8 +1496,6 @@ your test suite. Asserts that a ``Response`` instance produced the given ``status_code`` and that ``text`` does not appears in the content of the response. - .. versionadded:: 1.4 - Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with the response content will be based on HTML semantics instead of character-by-character equality. Whitespace is ignored in most cases, @@ -1538,8 +1524,6 @@ your test suite. The name is a string such as ``'admin/index.html'``. - .. versionadded:: 1.4 - You can use this as a context manager, like this:: with self.assertTemplateUsed('index.html'): @@ -1552,8 +1536,6 @@ your test suite. Asserts that the template with the given name was *not* used in rendering the response. - .. versionadded:: 1.4 - You can use this as a context manager in the same way as :meth:`~TestCase.assertTemplateUsed`. @@ -1580,12 +1562,6 @@ your test suite. provide an implicit ordering, you can set the ``ordered`` parameter to ``False``, which turns the comparison into a Python set comparison. - .. versionchanged:: 1.4 - The ``ordered`` parameter is new in version 1.4. In earlier versions, - you would need to ensure the queryset is ordered consistently, possibly - via an explicit ``order_by()`` call on the queryset prior to - comparison. - .. versionchanged:: 1.6 The method now checks for undefined order and raises ``ValueError`` if undefined order is spotted. The ordering is seen as undefined if @@ -1612,8 +1588,6 @@ your test suite. .. method:: SimpleTestCase.assertHTMLEqual(html1, html2, msg=None) - .. versionadded:: 1.4 - Asserts that the strings ``html1`` and ``html2`` are equal. The comparison is based on HTML semantics. The comparison takes following things into account: @@ -1643,8 +1617,6 @@ your test suite. .. method:: SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None) - .. versionadded:: 1.4 - Asserts that the strings ``html1`` and ``html2`` are *not* equal. The comparison is based on HTML semantics. See :meth:`~SimpleTestCase.assertHTMLEqual` for details. From 4e5369a5962e59877ade8975620d73485061a706 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 29 Dec 2012 22:19:47 +0100 Subject: [PATCH 072/870] Silenced warnings in the tests of deprecated features. --- django/contrib/auth/tests/models.py | 26 +++++++++------ tests/modeltests/select_related/tests.py | 32 +++++++++++++------ .../multiple_database/tests.py | 7 ++-- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/django/contrib/auth/tests/models.py b/django/contrib/auth/tests/models.py index ca65dee71b..da0e45a55e 100644 --- a/django/contrib/auth/tests/models.py +++ b/django/contrib/auth/tests/models.py @@ -1,3 +1,5 @@ +import warnings + from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import (Group, User, SiteProfileNotAvailable, @@ -17,21 +19,27 @@ class ProfileTestCase(TestCase): # calling get_profile without AUTH_PROFILE_MODULE set del settings.AUTH_PROFILE_MODULE - with six.assertRaisesRegex(self, SiteProfileNotAvailable, - "You need to set AUTH_PROFILE_MODULE in your project"): - user.get_profile() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + with six.assertRaisesRegex(self, SiteProfileNotAvailable, + "You need to set AUTH_PROFILE_MODULE in your project"): + user.get_profile() # Bad syntax in AUTH_PROFILE_MODULE: settings.AUTH_PROFILE_MODULE = 'foobar' - with six.assertRaisesRegex(self, SiteProfileNotAvailable, - "app_label and model_name should be separated by a dot"): - user.get_profile() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + with six.assertRaisesRegex(self, SiteProfileNotAvailable, + "app_label and model_name should be separated by a dot"): + user.get_profile() # module that doesn't exist settings.AUTH_PROFILE_MODULE = 'foo.bar' - with six.assertRaisesRegex(self, SiteProfileNotAvailable, - "Unable to load the profile model"): - user.get_profile() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + with six.assertRaisesRegex(self, SiteProfileNotAvailable, + "Unable to load the profile model"): + user.get_profile() @skipIfCustomUser diff --git a/tests/modeltests/select_related/tests.py b/tests/modeltests/select_related/tests.py index 557a5c318a..27d65fecb1 100644 --- a/tests/modeltests/select_related/tests.py +++ b/tests/modeltests/select_related/tests.py @@ -1,5 +1,7 @@ from __future__ import absolute_import, unicode_literals +import warnings + from django.test import TestCase from .models import Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species @@ -53,7 +55,9 @@ class SelectRelatedTests(TestCase): extra queries """ with self.assertNumQueries(1): - person = Species.objects.select_related(depth=10).get(name="sapiens") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + person = Species.objects.select_related(depth=10).get(name="sapiens") domain = person.genus.family.order.klass.phylum.kingdom.domain self.assertEqual(domain.name, 'Eukaryota') @@ -94,7 +98,9 @@ class SelectRelatedTests(TestCase): """ # Notice: one fewer queries than above because of depth=1 with self.assertNumQueries(expected): - pea = Species.objects.select_related(depth=depth).get(name="sativum") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + pea = Species.objects.select_related(depth=depth).get(name="sativum") self.assertEqual( pea.genus.family.order.klass.phylum.kingdom.domain.name, 'Eukaryota' @@ -113,14 +119,18 @@ class SelectRelatedTests(TestCase): particular level. This can be used on lists as well. """ with self.assertNumQueries(5): - world = Species.objects.all().select_related(depth=2) - orders = [o.genus.family.order.name for o in world] + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + world = Species.objects.all().select_related(depth=2) + orders = [o.genus.family.order.name for o in world] self.assertEqual(sorted(orders), ['Agaricales', 'Diptera', 'Fabales', 'Primates']) def test_select_related_with_extra(self): - s = Species.objects.all().select_related(depth=1)\ - .extra(select={'a': 'select_related_species.id + 10'})[0] + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + s = Species.objects.all().select_related(depth=1)\ + .extra(select={'a': 'select_related_species.id + 10'})[0] self.assertEqual(s.id + 10, s.a) def test_certain_fields(self): @@ -156,7 +166,9 @@ class SelectRelatedTests(TestCase): self.assertEqual(s, 'Diptera') def test_depth_fields_fails(self): - self.assertRaises(TypeError, - Species.objects.select_related, - 'genus__family__order', depth=4 - ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + self.assertRaises(TypeError, + Species.objects.select_related, + 'genus__family__order', depth=4 + ) diff --git a/tests/regressiontests/multiple_database/tests.py b/tests/regressiontests/multiple_database/tests.py index d2a5058206..a79c9c0aa1 100644 --- a/tests/regressiontests/multiple_database/tests.py +++ b/tests/regressiontests/multiple_database/tests.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals import datetime import pickle from operator import attrgetter +import warnings from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType @@ -1612,8 +1613,10 @@ class UserProfileTestCase(TestCase): bob_profile = UserProfile(user=bob, flavor='crunchy frog') bob_profile.save() - self.assertEqual(alice.get_profile().flavor, 'chocolate') - self.assertEqual(bob.get_profile().flavor, 'crunchy frog') + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + self.assertEqual(alice.get_profile().flavor, 'chocolate') + self.assertEqual(bob.get_profile().flavor, 'crunchy frog') class AntiPetRouter(object): # A router that only expresses an opinion on syncdb, From a04df803a590c5bffd9437d9199bc0107ba0e966 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 29 Dec 2012 18:52:50 -0500 Subject: [PATCH 073/870] Removed links to deprecated IGNORABLE_404_STARTS/ENDS settings. refs #19516 and 641acf76e7 --- docs/internals/deprecation.txt | 6 +++--- docs/releases/1.4-alpha-1.txt | 17 +++++++++-------- docs/releases/1.4-beta-1.txt | 17 +++++++++-------- docs/releases/1.4.txt | 17 +++++++++-------- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 74f544c220..c976f5a880 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -227,9 +227,9 @@ these changes. be accessible through their GB-prefixed names (GB is the correct ISO 3166 code for United Kingdom). -* The :setting:`IGNORABLE_404_STARTS` and :setting:`IGNORABLE_404_ENDS` - settings have been superseded by :setting:`IGNORABLE_404_URLS` in - the 1.4 release. They will be removed. +* The ``IGNORABLE_404_STARTS`` and ``IGNORABLE_404_ENDS`` settings have been + superseded by :setting:`IGNORABLE_404_URLS` in the 1.4 release. They will be + removed. * The :doc:`form wizard ` has been refactored to use class-based views with pluggable backends in 1.4. diff --git a/docs/releases/1.4-alpha-1.txt b/docs/releases/1.4-alpha-1.txt index fc19e90384..4086cfdecc 100644 --- a/docs/releases/1.4-alpha-1.txt +++ b/docs/releases/1.4-alpha-1.txt @@ -813,11 +813,12 @@ For more details, see the documentation about Until Django 1.3, it was possible to exclude some URLs from Django's :doc:`404 error reporting` by adding prefixes to -:setting:`IGNORABLE_404_STARTS` and suffixes to :setting:`IGNORABLE_404_ENDS`. +``IGNORABLE_404_STARTS`` and suffixes to ``IGNORABLE_404_ENDS``. In Django 1.4, these two settings are superseded by -:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular expressions. -Django won't send an email for 404 errors on URLs that match any of them. +:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular +expressions. Django won't send an email for 404 errors on URLs that match any +of them. Furthermore, the previous settings had some rather arbitrary default values:: @@ -827,12 +828,12 @@ Furthermore, the previous settings had some rather arbitrary default values:: It's not Django's role to decide if your website has a legacy ``/cgi-bin/`` section or a ``favicon.ico``. As a consequence, the default values of -:setting:`IGNORABLE_404_URLS`, :setting:`IGNORABLE_404_STARTS` and -:setting:`IGNORABLE_404_ENDS` are all now empty. +:setting:`IGNORABLE_404_URLS`, ``IGNORABLE_404_STARTS``, and +``IGNORABLE_404_ENDS`` are all now empty. -If you have customized :setting:`IGNORABLE_404_STARTS` or -:setting:`IGNORABLE_404_ENDS`, or if you want to keep the old default value, -you should add the following lines in your settings file:: +If you have customized ``IGNORABLE_404_STARTS`` or ``IGNORABLE_404_ENDS``, or +if you want to keep the old default value, you should add the following lines +in your settings file:: import re IGNORABLE_404_URLS = ( diff --git a/docs/releases/1.4-beta-1.txt b/docs/releases/1.4-beta-1.txt index 2c84d21b8d..a8732a9e65 100644 --- a/docs/releases/1.4-beta-1.txt +++ b/docs/releases/1.4-beta-1.txt @@ -881,11 +881,12 @@ For more details, see the documentation about Until Django 1.3, it was possible to exclude some URLs from Django's :doc:`404 error reporting` by adding prefixes to -:setting:`IGNORABLE_404_STARTS` and suffixes to :setting:`IGNORABLE_404_ENDS`. +``IGNORABLE_404_STARTS`` and suffixes to ``IGNORABLE_404_ENDS``. In Django 1.4, these two settings are superseded by -:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular expressions. -Django won't send an email for 404 errors on URLs that match any of them. +:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular +expressions. Django won't send an email for 404 errors on URLs that match any +of them. Furthermore, the previous settings had some rather arbitrary default values:: @@ -895,12 +896,12 @@ Furthermore, the previous settings had some rather arbitrary default values:: It's not Django's role to decide if your website has a legacy ``/cgi-bin/`` section or a ``favicon.ico``. As a consequence, the default values of -:setting:`IGNORABLE_404_URLS`, :setting:`IGNORABLE_404_STARTS` and -:setting:`IGNORABLE_404_ENDS` are all now empty. +:setting:`IGNORABLE_404_URLS`, ``IGNORABLE_404_STARTS``, and +``IGNORABLE_404_ENDS`` are all now empty. -If you have customized :setting:`IGNORABLE_404_STARTS` or -:setting:`IGNORABLE_404_ENDS`, or if you want to keep the old default value, -you should add the following lines in your settings file:: +If you have customized ``IGNORABLE_404_STARTS`` or ``IGNORABLE_404_ENDS``, or +if you want to keep the old default value, you should add the following lines +in your settings file:: import re IGNORABLE_404_URLS = ( diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index d700ba8b89..cf53b37f17 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -966,11 +966,12 @@ For more details, see the documentation about Until Django 1.3, it was possible to exclude some URLs from Django's :doc:`404 error reporting` by adding prefixes to -:setting:`IGNORABLE_404_STARTS` and suffixes to :setting:`IGNORABLE_404_ENDS`. +``IGNORABLE_404_STARTS`` and suffixes to ``IGNORABLE_404_ENDS``. In Django 1.4, these two settings are superseded by -:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular expressions. -Django won't send an email for 404 errors on URLs that match any of them. +:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular +expressions. Django won't send an email for 404 errors on URLs that match any +of them. Furthermore, the previous settings had some rather arbitrary default values:: @@ -980,12 +981,12 @@ Furthermore, the previous settings had some rather arbitrary default values:: It's not Django's role to decide if your website has a legacy ``/cgi-bin/`` section or a ``favicon.ico``. As a consequence, the default values of -:setting:`IGNORABLE_404_URLS`, :setting:`IGNORABLE_404_STARTS` and -:setting:`IGNORABLE_404_ENDS` are all now empty. +:setting:`IGNORABLE_404_URLS`, ``IGNORABLE_404_STARTS``, and +``IGNORABLE_404_ENDS`` are all now empty. -If you have customized :setting:`IGNORABLE_404_STARTS` or -:setting:`IGNORABLE_404_ENDS`, or if you want to keep the old default value, -you should add the following lines in your settings file:: +If you have customized ``IGNORABLE_404_STARTS`` or ``IGNORABLE_404_ENDS``, or +if you want to keep the old default value, you should add the following lines +in your settings file:: import re IGNORABLE_404_URLS = ( From 9ef3cab40b1806d99172e94954976413f860a476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Mon, 17 Dec 2012 00:43:33 +0200 Subject: [PATCH 074/870] Made sure join_field is always available in .join() Refs #19385 --- django/db/models/sql/compiler.py | 7 ++++--- django/db/models/sql/query.py | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index d0e791ba20..b0fb933705 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -636,10 +636,10 @@ class SQLCompiler(object): int_opts = int_model._meta continue lhs_col = int_opts.parents[int_model].column + link_field = int_opts.get_ancestor_link(int_model) int_opts = int_model._meta alias = self.query.join((alias, int_opts.db_table, lhs_col, - int_opts.pk.column), - promote=promote) + int_opts.pk.column), promote=promote, join_field=link_field) alias_chain.append(alias) else: alias = root_alias @@ -685,10 +685,11 @@ class SQLCompiler(object): int_opts = int_model._meta continue lhs_col = int_opts.parents[int_model].column + link_field = int_opts.get_ancestor_link(int_model) int_opts = int_model._meta alias = self.query.join( (alias, int_opts.db_table, lhs_col, int_opts.pk.column), - promote=True + promote=True, join_field=link_field ) alias_chain.append(alias) alias = self.query.join( diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index c71bc634aa..841452636b 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -944,6 +944,7 @@ class Query(object): The 'join_field' is the field we are joining along (if any). """ lhs, table, lhs_col, col = connection + assert lhs is None or join_field is not None existing = self.join_map.get(connection, ()) if reuse is None: reuse = existing @@ -1003,8 +1004,9 @@ class Query(object): for field, model in opts.get_fields_with_model(): if model not in seen: link_field = opts.get_ancestor_link(model) - seen[model] = self.join((root_alias, model._meta.db_table, - link_field.column, model._meta.pk.column)) + seen[model] = self.join( + (root_alias, model._meta.db_table, link_field.column, + model._meta.pk.column), join_field=link_field) self.included_inherited_models = seen def remove_inherited_models(self): From 4511aeb6b8b843ee913fb43a37c9686980210948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Mon, 17 Dec 2012 17:09:07 +0200 Subject: [PATCH 075/870] Moved join path generation to Field Refs #19385 --- django/contrib/contenttypes/generic.py | 11 +++ django/db/models/fields/related.py | 52 +++++++++++- django/db/models/related.py | 12 +++ django/db/models/sql/constants.py | 6 -- django/db/models/sql/query.py | 110 ++++--------------------- 5 files changed, 91 insertions(+), 100 deletions(-) diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 6aff07e568..be7a5e5a22 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -11,6 +11,7 @@ from django.db import connection from django.db.models import signals from django.db import models, router, DEFAULT_DB_ALIAS from django.db.models.fields.related import RelatedField, Field, ManyToManyRel +from django.db.models.related import PathInfo from django.forms import ModelForm from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets @@ -160,6 +161,16 @@ class GenericRelation(RelatedField, Field): kwargs['serialize'] = False Field.__init__(self, **kwargs) + def get_path_info(self): + from_field = self.model._meta.pk + opts = self.rel.to._meta + target = opts.get_field_by_name(self.object_id_field_name)[0] + # Note that we are using different field for the join_field + # than from_field or to_field. This is a hack, but we need the + # GenericRelation to generate the extra SQL. + return ([PathInfo(from_field, target, self.model._meta, opts, self, True, False)], + opts, target, self) + def get_choices_default(self): return Field.get_choices(self, include_blank=False) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 90fe69e23c..4b6a5b0aed 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -5,7 +5,7 @@ from django.db.backends import util from django.db.models import signals, get_model from django.db.models.fields import (AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist) -from django.db.models.related import RelatedObject +from django.db.models.related import RelatedObject, PathInfo from django.db.models.query import QuerySet from django.db.models.query_utils import QueryWrapper from django.db.models.deletion import CASCADE @@ -16,7 +16,6 @@ from django.utils.functional import curry, cached_property from django.core import exceptions from django import forms - RECURSIVE_RELATIONSHIP_CONSTANT = 'self' pending_lookups = {} @@ -1004,6 +1003,31 @@ class ForeignKey(RelatedField, Field): ) Field.__init__(self, **kwargs) + def get_path_info(self): + """ + Get path from this field to the related model. + """ + opts = self.rel.to._meta + target = self.rel.get_related_field() + from_opts = self.model._meta + return [PathInfo(self, target, from_opts, opts, self, False, True)], opts, target, self + + def get_reverse_path_info(self): + """ + Get path from the related model to this field's model. + """ + opts = self.model._meta + from_field = self.rel.get_related_field() + from_opts = from_field.model._meta + pathinfos = [PathInfo(from_field, self, from_opts, opts, self, not self.unique, False)] + if from_field.model is self.model: + # Recursive foreign key to self. + target = opts.get_field_by_name( + self.rel.field_name)[0] + else: + target = opts.pk + return pathinfos, opts, target, self + def validate(self, value, model_instance): if self.rel.parent_link: return @@ -1198,6 +1222,30 @@ class ManyToManyField(RelatedField, Field): msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.') self.help_text = string_concat(self.help_text, ' ', msg) + def _get_path_info(self, direct=False): + """ + Called by both direct an indirect m2m traversal. + """ + pathinfos = [] + int_model = self.rel.through + linkfield1 = int_model._meta.get_field_by_name(self.m2m_field_name())[0] + linkfield2 = int_model._meta.get_field_by_name(self.m2m_reverse_field_name())[0] + if direct: + join1infos, _, _, _ = linkfield1.get_reverse_path_info() + join2infos, opts, target, final_field = linkfield2.get_path_info() + else: + join1infos, _, _, _ = linkfield2.get_reverse_path_info() + join2infos, opts, target, final_field = linkfield1.get_path_info() + pathinfos.extend(join1infos) + pathinfos.extend(join2infos) + return pathinfos, opts, target, final_field + + def get_path_info(self): + return self._get_path_info(direct=True) + + def get_reverse_path_info(self): + return self._get_path_info(direct=False) + def get_choices_default(self): return Field.get_choices(self, include_blank=False) diff --git a/django/db/models/related.py b/django/db/models/related.py index a0dcec7132..702853533d 100644 --- a/django/db/models/related.py +++ b/django/db/models/related.py @@ -1,6 +1,15 @@ +from collections import namedtuple + from django.utils.encoding import smart_text from django.db.models.fields import BLANK_CHOICE_DASH +# PathInfo is used when converting lookups (fk__somecol). The contents +# describe the relation in Model terms (model Options and Fields for both +# sides of the relation. The join_field is the field backing the relation. +PathInfo = namedtuple('PathInfo', + 'from_field to_field from_opts to_opts join_field ' + 'm2m direct') + class BoundRelatedObject(object): def __init__(self, related_object, field_mapping, original): self.relation = related_object @@ -67,3 +76,6 @@ class RelatedObject(object): def get_cache_name(self): return "_%s_cache" % self.get_accessor_name() + + def get_path_info(self): + return self.field.get_reverse_path_info() diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py index 9f82f426ed..1764db7fcc 100644 --- a/django/db/models/sql/constants.py +++ b/django/db/models/sql/constants.py @@ -26,12 +26,6 @@ JoinInfo = namedtuple('JoinInfo', 'table_name rhs_alias join_type lhs_alias ' 'lhs_join_col rhs_join_col nullable join_field') -# PathInfo is used when converting lookups (fk__somecol). The contents -# describe the join in Model terms (model Options and Fields for both -# sides of the join. The rel_field is the field we are joining along. -PathInfo = namedtuple('PathInfo', - 'from_field to_field from_opts to_opts join_field') - # Pairs of column clauses to select, and (possibly None) field for the clause. SelectInfo = namedtuple('SelectInfo', 'col field') diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 841452636b..e5833b2b51 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -18,9 +18,10 @@ from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import ExpressionNode from django.db.models.fields import FieldDoesNotExist from django.db.models.loading import get_model +from django.db.models.related import PathInfo from django.db.models.sql import aggregates as base_aggregates_module from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE, - ORDER_PATTERN, JoinInfo, SelectInfo, PathInfo) + ORDER_PATTERN, JoinInfo, SelectInfo) from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode, @@ -1294,7 +1295,6 @@ class Query(object): contain the same value as the final field). """ path = [] - multijoin_pos = None for pos, name in enumerate(names): if name == 'pk': name = opts.pk.name @@ -1328,92 +1328,19 @@ class Query(object): target = final_field.rel.get_related_field() opts = int_model._meta path.append(PathInfo(final_field, target, final_field.model._meta, - opts, final_field)) - # We have five different cases to solve: foreign keys, reverse - # foreign keys, m2m fields (also reverse) and non-relational - # fields. We are mostly just using the related field API to - # fetch the from and to fields. The m2m fields are handled as - # two foreign keys, first one reverse, the second one direct. - if direct and not field.rel and not m2m: + opts, final_field, False, True)) + if hasattr(field, 'get_path_info'): + pathinfos, opts, target, final_field = field.get_path_info() + path.extend(pathinfos) + else: # Local non-relational field. final_field = target = field break - elif direct and not m2m: - # Foreign Key - opts = field.rel.to._meta - target = field.rel.get_related_field() - final_field = field - from_opts = field.model._meta - path.append(PathInfo(field, target, from_opts, opts, field)) - elif not direct and not m2m: - # Revere foreign key - final_field = to_field = field.field - opts = to_field.model._meta - from_field = to_field.rel.get_related_field() - from_opts = from_field.model._meta - path.append( - PathInfo(from_field, to_field, from_opts, opts, to_field)) - if from_field.model is to_field.model: - # Recursive foreign key to self. - target = opts.get_field_by_name( - field.field.rel.field_name)[0] - else: - target = opts.pk - elif direct and m2m: - if not field.rel.through: - # Gotcha! This is just a fake m2m field - a generic relation - # field). - from_field = opts.pk - opts = field.rel.to._meta - target = opts.get_field_by_name(field.object_id_field_name)[0] - final_field = field - # Note that we are using different field for the join_field - # than from_field or to_field. This is a hack, but we need the - # GenericRelation to generate the extra SQL. - path.append(PathInfo(from_field, target, field.model._meta, opts, - field)) - else: - # m2m field. We are travelling first to the m2m table along a - # reverse relation, then from m2m table to the target table. - from_field1 = opts.get_field_by_name( - field.m2m_target_field_name())[0] - opts = field.rel.through._meta - to_field1 = opts.get_field_by_name(field.m2m_field_name())[0] - path.append( - PathInfo(from_field1, to_field1, from_field1.model._meta, - opts, to_field1)) - final_field = from_field2 = opts.get_field_by_name( - field.m2m_reverse_field_name())[0] - opts = field.rel.to._meta - target = to_field2 = opts.get_field_by_name( - field.m2m_reverse_target_field_name())[0] - path.append( - PathInfo(from_field2, to_field2, from_field2.model._meta, - opts, from_field2)) - elif not direct and m2m: - # This one is just like above, except we are travelling the - # fields in opposite direction. - field = field.field - from_field1 = opts.get_field_by_name( - field.m2m_reverse_target_field_name())[0] - int_opts = field.rel.through._meta - to_field1 = int_opts.get_field_by_name( - field.m2m_reverse_field_name())[0] - path.append( - PathInfo(from_field1, to_field1, from_field1.model._meta, - int_opts, to_field1)) - final_field = from_field2 = int_opts.get_field_by_name( - field.m2m_field_name())[0] - opts = field.opts - target = to_field2 = opts.get_field_by_name( - field.m2m_target_field_name())[0] - path.append(PathInfo(from_field2, to_field2, from_field2.model._meta, - opts, from_field2)) - - if m2m and multijoin_pos is None: - multijoin_pos = pos - if not direct and not path[-1].to_field.unique and multijoin_pos is None: - multijoin_pos = pos + multijoin_pos = None + for m2mpos, pathinfo in enumerate(path): + if pathinfo.m2m: + multijoin_pos = m2mpos + break if pos != len(names) - 1: if pos == len(names) - 2: @@ -1463,16 +1390,15 @@ class Query(object): # joins at this stage - we will need the information about join type # of the trimmed joins. for pos, join in enumerate(path): - from_field, to_field, from_opts, opts, join_field = join - direct = join_field == from_field - if direct: - nullable = self.is_nullable(from_field) + opts = join.to_opts + if join.direct: + nullable = self.is_nullable(join.from_field) else: nullable = True - connection = alias, opts.db_table, from_field.column, to_field.column - reuse = None if direct or to_field.unique else can_reuse + connection = alias, opts.db_table, join.from_field.column, join.to_field.column + reuse = can_reuse if join.m2m else None alias = self.join(connection, reuse=reuse, - nullable=nullable, join_field=join_field) + nullable=nullable, join_field=join.join_field) joins.append(alias) return final_field, target, opts, joins, path From 68985db48212c701a3d975636123a5d79bdc006f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Mon, 17 Dec 2012 14:02:41 +0200 Subject: [PATCH 076/870] Added Query.join_parent_model() This simplifies especially compiler.py a lot, where almost the same code was repeated multiple times. Refs #19385 --- django/db/models/sql/compiler.py | 62 +++----------------------------- django/db/models/sql/query.py | 36 ++++++++++++++++--- 2 files changed, 37 insertions(+), 61 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index b0fb933705..16aa031e59 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -261,27 +261,14 @@ class SQLCompiler(object): qn2 = self.connection.ops.quote_name aliases = set() only_load = self.deferred_to_columns() - + seen = self.query.included_inherited_models.copy() if start_alias: - seen = {None: start_alias} + seen[None] = start_alias for field, model in opts.get_fields_with_model(): if from_parent and model is not None and issubclass(from_parent, model): # Avoid loading data for already loaded parents. continue - if start_alias: - try: - alias = seen[model] - except KeyError: - link_field = opts.get_ancestor_link(model) - alias = self.query.join((start_alias, model._meta.db_table, - link_field.column, model._meta.pk.column), - join_field=link_field) - seen[model] = alias - else: - # If we're starting from the base model of the queryset, the - # aliases will have already been set up in pre_sql_setup(), so - # we can save time here. - alias = self.query.included_inherited_models[model] + alias = self.query.join_parent_model(opts, model, start_alias, seen) table = self.query.alias_map[alias].table_name if table in only_load and field.column not in only_load[table]: continue @@ -623,26 +610,7 @@ class SQLCompiler(object): continue table = f.rel.to._meta.db_table promote = nullable or f.null - if model: - int_opts = opts - alias = root_alias - alias_chain = [] - for int_model in opts.get_base_chain(model): - # Proxy model have elements in base chain - # with no parents, assign the new options - # object and skip to the next base in that - # case - if not int_opts.parents[int_model]: - int_opts = int_model._meta - continue - lhs_col = int_opts.parents[int_model].column - link_field = int_opts.get_ancestor_link(int_model) - int_opts = int_model._meta - alias = self.query.join((alias, int_opts.db_table, lhs_col, - int_opts.pk.column), promote=promote, join_field=link_field) - alias_chain.append(alias) - else: - alias = root_alias + alias = self.query.join_parent_model(opts, model, root_alias, {}) alias = self.query.join((alias, table, f.column, f.rel.get_related_field().column), @@ -670,28 +638,8 @@ class SQLCompiler(object): only_load.get(model), reverse=True): continue + alias = self.query.join_parent_model(opts, f.rel.to, root_alias, {}) table = model._meta.db_table - int_opts = opts - alias = root_alias - alias_chain = [] - chain = opts.get_base_chain(f.rel.to) - if chain is not None: - for int_model in chain: - # Proxy model have elements in base chain - # with no parents, assign the new options - # object and skip to the next base in that - # case - if not int_opts.parents[int_model]: - int_opts = int_model._meta - continue - lhs_col = int_opts.parents[int_model].column - link_field = int_opts.get_ancestor_link(int_model) - int_opts = int_model._meta - alias = self.query.join( - (alias, int_opts.db_table, lhs_col, int_opts.pk.column), - promote=True, join_field=link_field - ) - alias_chain.append(alias) alias = self.query.join( (alias, table, f.rel.get_related_field().column, f.column), promote=True, join_field=f diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index e5833b2b51..2546d6c889 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1004,12 +1004,40 @@ class Query(object): for field, model in opts.get_fields_with_model(): if model not in seen: - link_field = opts.get_ancestor_link(model) - seen[model] = self.join( - (root_alias, model._meta.db_table, link_field.column, - model._meta.pk.column), join_field=link_field) + self.join_parent_model(opts, model, root_alias, seen) self.included_inherited_models = seen + def join_parent_model(self, opts, model, alias, seen): + """ + Makes sure the given 'model' is joined in the query. If 'model' isn't + a parent of 'opts' or if it is None this method is a no-op. + + The 'alias' is the root alias for starting the join, 'seen' is a dict + of model -> alias of existing joins. + """ + if model in seen: + return seen[model] + int_opts = opts + chain = opts.get_base_chain(model) + if chain is None: + return alias + for int_model in chain: + if int_model in seen: + return seen[int_model] + # Proxy model have elements in base chain + # with no parents, assign the new options + # object and skip to the next base in that + # case + if not int_opts.parents[int_model]: + int_opts = int_model._meta + continue + link_field = int_opts.get_ancestor_link(int_model) + int_opts = int_model._meta + connection = (alias, int_opts.db_table, link_field.column, int_opts.pk.column) + alias = seen[int_model] = self.join(connection, nullable=False, + join_field=link_field) + return alias + def remove_inherited_models(self): """ Undoes the effects of setup_inherited_models(). Should be called From 807eff74396faba24ad420236d83e9716feffe1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Thu, 20 Dec 2012 23:54:31 +0200 Subject: [PATCH 077/870] Made use of PathInfo.direct flag in trim_joins Refs #19385 --- django/db/models/sql/query.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 2546d6c889..43ba519a68 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1445,8 +1445,7 @@ class Query(object): the join. """ for info in reversed(path): - direct = info.join_field == info.from_field - if info.to_field == target and direct: + if info.to_field == target and info.direct: target = info.from_field self.unref_alias(joins.pop()) else: From ce3c71faf1ff83494193b48a6f99256d0628b2a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Fri, 21 Dec 2012 20:07:13 +0200 Subject: [PATCH 078/870] Minor improvement to proxy model handling Refs #19385 --- django/db/models/sql/compiler.py | 2 -- django/db/models/sql/query.py | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 16aa031e59..79b5d99452 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -255,8 +255,6 @@ class SQLCompiler(object): result = [] if opts is None: opts = self.query.model._meta - # Skip all proxy to the root proxied model - opts = opts.concrete_model._meta qn = self.quote_name_unless_alias qn2 = self.connection.ops.quote_name aliases = set() diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 43ba519a68..87104f0d13 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -997,8 +997,7 @@ class Query(object): whereas column determination is a later part, and side-effect, of as_sql()). """ - # Skip all proxy models - opts = self.model._meta.concrete_model._meta + opts = self.model._meta root_alias = self.tables[0] seen = {None: root_alias} @@ -1013,7 +1012,8 @@ class Query(object): a parent of 'opts' or if it is None this method is a no-op. The 'alias' is the root alias for starting the join, 'seen' is a dict - of model -> alias of existing joins. + of model -> alias of existing joins. It must also contain a mapping + of None -> some alias. This will be returned in the no-op case. """ if model in seen: return seen[model] @@ -1036,7 +1036,7 @@ class Query(object): connection = (alias, int_opts.db_table, link_field.column, int_opts.pk.column) alias = seen[int_model] = self.join(connection, nullable=False, join_field=link_field) - return alias + return alias or seen[None] def remove_inherited_models(self): """ From f80a1934cdb465c70ecc215fdf867e555a670623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sun, 30 Dec 2012 12:10:15 +0200 Subject: [PATCH 079/870] Fixed GIS regression in get_default_columns() I changed the normal compiler's get_default_columns() but didn't change the copy-pasted code in GIS compiler's get_default_columns(). Refs #19385 --- django/contrib/gis/db/models/sql/compiler.py | 21 ++++---------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py index 81a9941c9e..fc53d08ffd 100644 --- a/django/contrib/gis/db/models/sql/compiler.py +++ b/django/contrib/gis/db/models/sql/compiler.py @@ -119,29 +119,16 @@ class GeoSQLCompiler(compiler.SQLCompiler): result = [] if opts is None: opts = self.query.model._meta - # Skip all proxy to the root proxied model - opts = opts.concrete_model._meta aliases = set() only_load = self.deferred_to_columns() - + seen = self.query.included_inherited_models.copy() if start_alias: - seen = {None: start_alias} + seen[None] = start_alias for field, model in opts.get_fields_with_model(): if from_parent and model is not None and issubclass(from_parent, model): + # Avoid loading data for already loaded parents. continue - if start_alias: - try: - alias = seen[model] - except KeyError: - link_field = opts.get_ancestor_link(model) - alias = self.query.join((start_alias, model._meta.db_table, - link_field.column, model._meta.pk.column)) - seen[model] = alias - else: - # If we're starting from the base model of the queryset, the - # aliases will have already been set up in pre_sql_setup(), so - # we can save time here. - alias = self.query.included_inherited_models[model] + alias = self.query.join_parent_model(opts, model, start_alias, seen) table = self.query.alias_map[alias].table_name if table in only_load and field.column not in only_load[table]: continue From cee40c7d79930ff42bde4730f43e68a624e47b0f Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Sun, 30 Dec 2012 21:33:21 -0800 Subject: [PATCH 080/870] Added further flexibility to ModelAdmin for controlling post-save redirections. Refs #19505. --- django/contrib/admin/options.py | 27 ++++++++++++++----- .../admin_custom_urls/models.py | 6 ++++- .../admin_custom_urls/tests.py | 23 +++++++++++++--- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index b0c2c4dcf9..8e0aaccc86 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1,6 +1,5 @@ import copy from functools import update_wrapper, partial -import warnings from django import forms from django.conf import settings @@ -824,7 +823,7 @@ class ModelAdmin(BaseModelAdmin): else: msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict self.message_user(request, msg) - return self.response_post_save(request, obj) + return self.response_post_save_add(request, obj) def response_change(self, request, obj): """ @@ -858,13 +857,27 @@ class ModelAdmin(BaseModelAdmin): else: msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict self.message_user(request, msg) - return self.response_post_save(request, obj) + return self.response_post_save_change(request, obj) - def response_post_save(self, request, obj): + def response_post_save_add(self, request, obj): """ - Figure out where to redirect after the 'Save' button has been pressed. - If the user has change permission, redirect to the change-list page for - this object. Otherwise, redirect to the admin index. + Figure out where to redirect after the 'Save' button has been pressed + when adding a new object. + """ + opts = self.model._meta + if self.has_change_permission(request, None): + post_url = reverse('admin:%s_%s_changelist' % + (opts.app_label, opts.module_name), + current_app=self.admin_site.name) + else: + post_url = reverse('admin:index', + current_app=self.admin_site.name) + return HttpResponseRedirect(post_url) + + def response_post_save_change(self, request, obj): + """ + Figure out where to redirect after the 'Save' button has been pressed + when editing an existing object. """ opts = self.model._meta if self.has_change_permission(request, None): diff --git a/tests/regressiontests/admin_custom_urls/models.py b/tests/regressiontests/admin_custom_urls/models.py index 45b6ed57a9..ef04c2aa09 100644 --- a/tests/regressiontests/admin_custom_urls/models.py +++ b/tests/regressiontests/admin_custom_urls/models.py @@ -56,10 +56,14 @@ class Person(models.Model): class PersonAdmin(admin.ModelAdmin): - def response_post_save(self, request, obj): + def response_post_save_add(self, request, obj): return HttpResponseRedirect( reverse('admin:admin_custom_urls_person_history', args=[obj.pk])) + def response_post_save_change(self, request, obj): + return HttpResponseRedirect( + reverse('admin:admin_custom_urls_person_delete', args=[obj.pk])) + class Car(models.Model): name = models.CharField(max_length=20) diff --git a/tests/regressiontests/admin_custom_urls/tests.py b/tests/regressiontests/admin_custom_urls/tests.py index a7edc77c38..31c93410f4 100644 --- a/tests/regressiontests/admin_custom_urls/tests.py +++ b/tests/regressiontests/admin_custom_urls/tests.py @@ -94,10 +94,11 @@ class CustomRedirects(TestCase): def tearDown(self): self.client.logout() - def test_post_save_redirect(self): + def test_post_save_add_redirect(self): """ - Ensures that ModelAdmin.response_post_save() controls the redirection - after the 'Save' button has been pressed. + Ensures that ModelAdmin.response_post_save_add() controls the + redirection after the 'Save' button has been pressed when adding a + new object. Refs 8001, 18310, 19505. """ post_data = { 'name': 'John Doe', } @@ -109,6 +110,22 @@ class CustomRedirects(TestCase): self.assertRedirects( response, reverse('admin:admin_custom_urls_person_history', args=[persons[0].pk])) + def test_post_save_change_redirect(self): + """ + Ensures that ModelAdmin.response_post_save_change() controls the + redirection after the 'Save' button has been pressed when editing an + existing object. + Refs 8001, 18310, 19505. + """ + Person.objects.create(name='John Doe') + self.assertEqual(Person.objects.count(), 1) + person = Person.objects.all()[0] + post_data = { 'name': 'Jack Doe', } + response = self.client.post( + reverse('admin:admin_custom_urls_person_change', args=[person.pk]), post_data) + self.assertRedirects( + response, reverse('admin:admin_custom_urls_person_delete', args=[person.pk])) + def test_post_url_continue(self): """ Ensures that the ModelAdmin.response_add()'s parameter `post_url_continue` From d11038acb2ea2f59a1d00a41b553f578b5f2c59c Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 31 Dec 2012 10:18:59 +0100 Subject: [PATCH 081/870] Fixed #19537 -- Made CheckboxInput._has_changed handle 'False' string Thanks dibrovsd@gmail.com for the report. --- django/forms/widgets.py | 3 +++ tests/regressiontests/forms/tests/widgets.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/django/forms/widgets.py b/django/forms/widgets.py index c761ea857d..4782b99117 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -533,6 +533,9 @@ class CheckboxInput(Widget): def _has_changed(self, initial, data): # Sometimes data or initial could be None or '' which should be the # same thing as False. + if initial == 'False': + # show_hidden_initial may have transformed False to 'False' + initial = False return bool(initial) != bool(data) class Select(Widget): diff --git a/tests/regressiontests/forms/tests/widgets.py b/tests/regressiontests/forms/tests/widgets.py index 31c0e65c7c..f9dc4a7ec8 100644 --- a/tests/regressiontests/forms/tests/widgets.py +++ b/tests/regressiontests/forms/tests/widgets.py @@ -240,6 +240,8 @@ class FormsWidgetTestCase(TestCase): self.assertTrue(w._has_changed(False, 'on')) self.assertFalse(w._has_changed(True, 'on')) self.assertTrue(w._has_changed(True, '')) + # Initial value may have mutated to a string due to show_hidden_initial (#19537) + self.assertTrue(w._has_changed('False', 'on')) def test_select(self): w = Select() From a53c4740268721036a6b3bc73f5e8f557c18bac0 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 30 Dec 2012 15:08:06 +0100 Subject: [PATCH 082/870] Fixed #16241 -- Ensured the WSGI iterable's close() is always called. Thanks Graham Dumpleton for the report. --- django/core/servers/basehttp.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 7387d13199..68ca0c1079 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -109,6 +109,17 @@ class ServerHandler(simple_server.ServerHandler, object): super(ServerHandler, self).error_output(environ, start_response) return ['\n'.join(traceback.format_exception(*sys.exc_info()))] + # Backport of http://hg.python.org/cpython/rev/d5af1b235dab. See #16241. + # This can be removed when support for Python <= 2.7.3 is deprecated. + def finish_response(self): + try: + if not self.result_is_file() or not self.sendfile(): + for data in self.result: + self.write(data) + self.finish_content() + finally: + self.close() + class WSGIServer(simple_server.WSGIServer, object): """BaseHTTPServer that implements the Python WSGI protocol""" From acc5396e6d0ac49ae9dc6abc08903b81e6553199 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 30 Dec 2012 15:19:22 +0100 Subject: [PATCH 083/870] Fixed #19519 -- Fired request_finished in the WSGI iterable's close(). --- django/core/handlers/wsgi.py | 4 +-- django/http/response.py | 10 ++++++- django/test/client.py | 35 ++++++++++++++--------- django/test/utils.py | 8 +++++- docs/ref/request-response.txt | 2 ++ docs/ref/signals.txt | 12 ++++++++ docs/releases/1.5.txt | 13 +++++++++ tests/regressiontests/handlers/tests.py | 38 +++++++++++++++++++++++-- tests/regressiontests/handlers/urls.py | 9 ++++++ 9 files changed, 111 insertions(+), 20 deletions(-) create mode 100644 tests/regressiontests/handlers/urls.py diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 426679ca7b..a9fa094429 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -253,8 +253,8 @@ class WSGIHandler(base.BaseHandler): response = http.HttpResponseBadRequest() else: response = self.get_response(request) - finally: - signals.request_finished.send(sender=self.__class__) + + response._handler_class = self.__class__ try: status_text = STATUS_CODE_TEXT[response.status_code] diff --git a/django/http/response.py b/django/http/response.py index d667ba6eed..48a401adcb 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -10,6 +10,7 @@ except ImportError: from urlparse import urlparse from django.conf import settings +from django.core import signals from django.core import signing from django.core.exceptions import SuspiciousOperation from django.http.cookie import SimpleCookie @@ -40,6 +41,9 @@ class HttpResponseBase(six.Iterator): self._headers = {} self._charset = settings.DEFAULT_CHARSET self._closable_objects = [] + # This parameter is set by the handler. It's necessary to preserve the + # historical behavior of request_finished. + self._handler_class = None if mimetype: warnings.warn("Using mimetype keyword argument is deprecated, use" " content_type instead", @@ -226,7 +230,11 @@ class HttpResponseBase(six.Iterator): # See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html def close(self): for closable in self._closable_objects: - closable.close() + try: + closable.close() + except Exception: + pass + signals.request_finished.send(sender=self._handler_class) def write(self, content): raise Exception("This %s instance is not writable" % self.__class__.__name__) diff --git a/django/test/client.py b/django/test/client.py index 015ee1309a..77d4de0524 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -26,7 +26,6 @@ from django.utils.http import urlencode from django.utils.importlib import import_module from django.utils.itercompat import is_iterable from django.utils import six -from django.db import close_connection from django.test.utils import ContextList __all__ = ('Client', 'RequestFactory', 'encode_file', 'encode_multipart') @@ -72,6 +71,14 @@ class FakePayload(object): self.__len += len(content) +def closing_iterator_wrapper(iterable, close): + try: + for item in iterable: + yield item + finally: + close() + + class ClientHandler(BaseHandler): """ A HTTP Handler that can be used for testing purposes. @@ -92,18 +99,20 @@ class ClientHandler(BaseHandler): self.load_middleware() signals.request_started.send(sender=self.__class__) - try: - request = WSGIRequest(environ) - # sneaky little hack so that we can easily get round - # CsrfViewMiddleware. This makes life easier, and is probably - # required for backwards compatibility with external tests against - # admin views. - request._dont_enforce_csrf_checks = not self.enforce_csrf_checks - response = self.get_response(request) - finally: - signals.request_finished.disconnect(close_connection) - signals.request_finished.send(sender=self.__class__) - signals.request_finished.connect(close_connection) + request = WSGIRequest(environ) + # sneaky little hack so that we can easily get round + # CsrfViewMiddleware. This makes life easier, and is probably + # required for backwards compatibility with external tests against + # admin views. + request._dont_enforce_csrf_checks = not self.enforce_csrf_checks + response = self.get_response(request) + # We're emulating a WSGI server; we must call the close method + # on completion. + if response.streaming: + response.streaming_content = closing_iterator_wrapper( + response.streaming_content, response.close) + else: + response.close() return response diff --git a/django/test/utils.py b/django/test/utils.py index 8114ae0e6a..a1ff826d6e 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -4,6 +4,8 @@ from xml.dom.minidom import parseString, Node from django.conf import settings, UserSettingsHolder from django.core import mail +from django.core.signals import request_finished +from django.db import close_connection from django.test.signals import template_rendered, setting_changed from django.template import Template, loader, TemplateDoesNotExist from django.template.loaders import cached @@ -68,8 +70,10 @@ def setup_test_environment(): """Perform any global pre-test setup. This involves: - Installing the instrumented test renderer - - Set the email backend to the locmem email backend. + - Setting the email backend to the locmem email backend. - Setting the active locale to match the LANGUAGE_CODE setting. + - Disconnecting the request_finished signal to avoid closing + the database connection within tests. """ Template.original_render = Template._render Template._render = instrumented_test_render @@ -81,6 +85,8 @@ def setup_test_environment(): deactivate() + request_finished.disconnect(close_connection) + def teardown_test_environment(): """Perform any global post-test teardown. This involves: diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index ae1da6cb4b..a8e0ef3f51 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -790,6 +790,8 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in :class:`~django.template.response.SimpleTemplateResponse`, and the ``render`` method must itself return a valid response object. +.. _httpresponse-streaming: + StreamingHttpResponse objects ============================= diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index b27a4f87cc..f2f1459bf0 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -448,6 +448,18 @@ request_finished Sent when Django finishes processing an HTTP request. +.. note:: + + When a view returns a :ref:`streaming response `, + this signal is sent only after the entire response is consumed by the + client (strictly speaking, by the WSGI gateway). + +.. versionchanged:: 1.5 + + Before Django 1.5, this signal was fired before sending the content to the + client. In order to accomodate streaming responses, it is now fired after + sending the content. + Arguments sent with this signal: ``sender`` diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index a449f4ab12..c5e8c61922 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -411,6 +411,19 @@ attribute. Developers wishing to access the raw POST data for these cases, should use the :attr:`request.body ` attribute instead. +:data:`~django.core.signals.request_finished` signal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django used to send the :data:`~django.core.signals.request_finished` signal +as soon as the view function returned a response. This interacted badly with +:ref:`streaming responses ` that delay content +generation. + +This signal is now sent after the content is fully consumed by the WSGI +gateway. This might be backwards incompatible if you rely on the signal being +fired before sending the response content to the client. If you do, you should +consider using a middleware instead. + OPTIONS, PUT and DELETE requests in the test client ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/handlers/tests.py b/tests/regressiontests/handlers/tests.py index 9cd5816219..3cab2aca57 100644 --- a/tests/regressiontests/handlers/tests.py +++ b/tests/regressiontests/handlers/tests.py @@ -1,10 +1,11 @@ from django.core.handlers.wsgi import WSGIHandler -from django.test import RequestFactory +from django.core import signals +from django.test import RequestFactory, TestCase from django.test.utils import override_settings from django.utils import six -from django.utils import unittest -class HandlerTests(unittest.TestCase): + +class HandlerTests(TestCase): # Mangle settings so the handler will fail @override_settings(MIDDLEWARE_CLASSES=42) @@ -27,3 +28,34 @@ class HandlerTests(unittest.TestCase): handler = WSGIHandler() response = handler(environ, lambda *a, **k: None) self.assertEqual(response.status_code, 400) + + +class SignalsTests(TestCase): + urls = 'regressiontests.handlers.urls' + + def setUp(self): + self.signals = [] + signals.request_started.connect(self.register_started) + signals.request_finished.connect(self.register_finished) + + def tearDown(self): + signals.request_started.disconnect(self.register_started) + signals.request_finished.disconnect(self.register_finished) + + def register_started(self, **kwargs): + self.signals.append('started') + + def register_finished(self, **kwargs): + self.signals.append('finished') + + def test_request_signals(self): + response = self.client.get('/regular/') + self.assertEqual(self.signals, ['started', 'finished']) + self.assertEqual(response.content, b"regular content") + + def test_request_signals_streaming_response(self): + response = self.client.get('/streaming/') + self.assertEqual(self.signals, ['started']) + # Avoid self.assertContains, because it explicitly calls response.close() + self.assertEqual(b''.join(response.streaming_content), b"streaming content") + self.assertEqual(self.signals, ['started', 'finished']) diff --git a/tests/regressiontests/handlers/urls.py b/tests/regressiontests/handlers/urls.py new file mode 100644 index 0000000000..8570f04696 --- /dev/null +++ b/tests/regressiontests/handlers/urls.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals + +from django.conf.urls import patterns, url +from django.http import HttpResponse, StreamingHttpResponse + +urlpatterns = patterns('', + url(r'^regular/$', lambda request: HttpResponse(b"regular content")), + url(r'^streaming/$', lambda request: StreamingHttpResponse([b"streaming", b" ", b"content"])), +) From 9180146d21cf2a31eec994b4adc0e50c7120f17f Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Mon, 31 Dec 2012 09:34:08 -0800 Subject: [PATCH 084/870] Fixed #19453 -- Ensured that the decorated function's arguments are obfuscated in the @sensitive_variables decorator's frame, in case the variables associated with those arguments were meant to be obfuscated from the decorated function's frame. Thanks to vzima for the report. --- django/views/debug.py | 22 ++++-- django/views/decorators/debug.py | 4 +- docs/howto/error-reporting.txt | 14 ++++ tests/regressiontests/views/tests/debug.py | 92 +++++++++++++++++----- tests/regressiontests/views/views.py | 33 ++++++++ 5 files changed, 137 insertions(+), 28 deletions(-) diff --git a/django/views/debug.py b/django/views/debug.py index aaa7e40efe..e5f4c70191 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -172,13 +172,12 @@ class SafeExceptionReporterFilter(ExceptionReporterFilter): break current_frame = current_frame.f_back - cleansed = [] + cleansed = {} if self.is_active(request) and sensitive_variables: if sensitive_variables == '__ALL__': # Cleanse all variables for name, value in tb_frame.f_locals.items(): - cleansed.append((name, CLEANSED_SUBSTITUTE)) - return cleansed + cleansed[name] = CLEANSED_SUBSTITUTE else: # Cleanse specified variables for name, value in tb_frame.f_locals.items(): @@ -187,16 +186,25 @@ class SafeExceptionReporterFilter(ExceptionReporterFilter): elif isinstance(value, HttpRequest): # Cleanse the request's POST parameters. value = self.get_request_repr(value) - cleansed.append((name, value)) - return cleansed + cleansed[name] = value else: # Potentially cleanse only the request if it's one of the frame variables. for name, value in tb_frame.f_locals.items(): if isinstance(value, HttpRequest): # Cleanse the request's POST parameters. value = self.get_request_repr(value) - cleansed.append((name, value)) - return cleansed + cleansed[name] = value + + if (tb_frame.f_code.co_name == 'sensitive_variables_wrapper' + and 'sensitive_variables_wrapper' in tb_frame.f_locals): + # For good measure, obfuscate the decorated function's arguments in + # the sensitive_variables decorator's frame, in case the variables + # associated with those arguments were meant to be obfuscated from + # the decorated function's frame. + cleansed['func_args'] = CLEANSED_SUBSTITUTE + cleansed['func_kwargs'] = CLEANSED_SUBSTITUTE + + return cleansed.items() class ExceptionReporter(object): """ diff --git a/django/views/decorators/debug.py b/django/views/decorators/debug.py index 5c222963d3..78ae6b1442 100644 --- a/django/views/decorators/debug.py +++ b/django/views/decorators/debug.py @@ -26,12 +26,12 @@ def sensitive_variables(*variables): """ def decorator(func): @functools.wraps(func) - def sensitive_variables_wrapper(*args, **kwargs): + def sensitive_variables_wrapper(*func_args, **func_kwargs): if variables: sensitive_variables_wrapper.sensitive_variables = variables else: sensitive_variables_wrapper.sensitive_variables = '__ALL__' - return func(*args, **kwargs) + return func(*func_args, **func_kwargs) return sensitive_variables_wrapper return decorator diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 35add32e4c..98b3b4e4d8 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -153,6 +153,20 @@ production environment (that is, where :setting:`DEBUG` is set to ``False``): def my_function(): ... + .. admonition:: When using mutiple decorators + + If the variable you want to hide is also a function argument (e.g. + '``user``' in the following example), and if the decorated function has + mutiple decorators, then make sure to place ``@sensible_variables`` at + the top of the decorator chain. This way it will also hide the function + argument as it gets passed through the other decorators:: + + @sensitive_variables('user', 'pw', 'cc') + @some_decorator + @another_decorator + def process_info(user): + ... + .. function:: sensitive_post_parameters(*parameters) If one of your views receives an :class:`~django.http.HttpRequest` object diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py index 4fdaad5010..0e36948b98 100644 --- a/tests/regressiontests/views/tests/debug.py +++ b/tests/regressiontests/views/tests/debug.py @@ -7,7 +7,6 @@ import inspect import os import sys -from django.conf import settings from django.core import mail from django.core.files.uploadedfile import SimpleUploadedFile from django.core.urlresolvers import reverse @@ -19,7 +18,8 @@ from django.views.debug import ExceptionReporter from .. import BrokenException, except_args from ..views import (sensitive_view, non_sensitive_view, paranoid_view, - custom_exception_reporter_filter_view, sensitive_method_view) + custom_exception_reporter_filter_view, sensitive_method_view, + sensitive_args_function_caller, sensitive_kwargs_function_caller) @override_settings(DEBUG=True, TEMPLATE_DEBUG=True) @@ -306,17 +306,28 @@ class ExceptionReportTestMixin(object): response = view(request) self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] + # Frames vars are never shown in plain text email reports. - body = force_text(email.body) - self.assertNotIn('cooked_eggs', body) - self.assertNotIn('scrambled', body) - self.assertNotIn('sauce', body) - self.assertNotIn('worcestershire', body) + body_plain = force_text(email.body) + self.assertNotIn('cooked_eggs', body_plain) + self.assertNotIn('scrambled', body_plain) + self.assertNotIn('sauce', body_plain) + self.assertNotIn('worcestershire', body_plain) + + # Frames vars are shown in html email reports. + body_html = force_text(email.alternatives[0][0]) + self.assertIn('cooked_eggs', body_html) + self.assertIn('scrambled', body_html) + self.assertIn('sauce', body_html) + self.assertIn('worcestershire', body_html) + if check_for_POST_params: for k, v in self.breakfast_data.items(): # All POST parameters are shown. - self.assertIn(k, body) - self.assertIn(v, body) + self.assertIn(k, body_plain) + self.assertIn(v, body_plain) + self.assertIn(k, body_html) + self.assertIn(v, body_html) def verify_safe_email(self, view, check_for_POST_params=True): """ @@ -328,22 +339,35 @@ class ExceptionReportTestMixin(object): response = view(request) self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] + # Frames vars are never shown in plain text email reports. - body = force_text(email.body) - self.assertNotIn('cooked_eggs', body) - self.assertNotIn('scrambled', body) - self.assertNotIn('sauce', body) - self.assertNotIn('worcestershire', body) + body_plain = force_text(email.body) + self.assertNotIn('cooked_eggs', body_plain) + self.assertNotIn('scrambled', body_plain) + self.assertNotIn('sauce', body_plain) + self.assertNotIn('worcestershire', body_plain) + + # Frames vars are shown in html email reports. + body_html = force_text(email.alternatives[0][0]) + self.assertIn('cooked_eggs', body_html) + self.assertIn('scrambled', body_html) + self.assertIn('sauce', body_html) + self.assertNotIn('worcestershire', body_html) + if check_for_POST_params: for k, v in self.breakfast_data.items(): # All POST parameters' names are shown. - self.assertIn(k, body) + self.assertIn(k, body_plain) # Non-sensitive POST parameters' values are shown. - self.assertIn('baked-beans-value', body) - self.assertIn('hash-brown-value', body) + self.assertIn('baked-beans-value', body_plain) + self.assertIn('hash-brown-value', body_plain) + self.assertIn('baked-beans-value', body_html) + self.assertIn('hash-brown-value', body_html) # Sensitive POST parameters' values are not shown. - self.assertNotIn('sausage-value', body) - self.assertNotIn('bacon-value', body) + self.assertNotIn('sausage-value', body_plain) + self.assertNotIn('bacon-value', body_plain) + self.assertNotIn('sausage-value', body_html) + self.assertNotIn('bacon-value', body_html) def verify_paranoid_email(self, view): """ @@ -445,6 +469,36 @@ class ExceptionReporterFilterTests(TestCase, ExceptionReportTestMixin): self.verify_safe_email(sensitive_method_view, check_for_POST_params=False) + def test_sensitive_function_arguments(self): + """ + Ensure that sensitive variables don't leak in the sensitive_variables + decorator's frame, when those variables are passed as arguments to the + decorated function. + Refs #19453. + """ + with self.settings(DEBUG=True): + self.verify_unsafe_response(sensitive_args_function_caller) + self.verify_unsafe_email(sensitive_args_function_caller) + + with self.settings(DEBUG=False): + self.verify_safe_response(sensitive_args_function_caller, check_for_POST_params=False) + self.verify_safe_email(sensitive_args_function_caller, check_for_POST_params=False) + + def test_sensitive_function_keyword_arguments(self): + """ + Ensure that sensitive variables don't leak in the sensitive_variables + decorator's frame, when those variables are passed as keyword arguments + to the decorated function. + Refs #19453. + """ + with self.settings(DEBUG=True): + self.verify_unsafe_response(sensitive_kwargs_function_caller) + self.verify_unsafe_email(sensitive_kwargs_function_caller) + + with self.settings(DEBUG=False): + self.verify_safe_response(sensitive_kwargs_function_caller, check_for_POST_params=False) + self.verify_safe_email(sensitive_kwargs_function_caller, check_for_POST_params=False) + class AjaxResponseExceptionReporterFilter(TestCase, ExceptionReportTestMixin): """ diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py index ed9d61144a..748f07637f 100644 --- a/tests/regressiontests/views/views.py +++ b/tests/regressiontests/views/views.py @@ -132,6 +132,7 @@ def send_log(request, exc_info): ][0] orig_filters = admin_email_handler.filters admin_email_handler.filters = [] + admin_email_handler.include_html = True logger.error('Internal Server Error: %s', request.path, exc_info=exc_info, extra={ @@ -184,6 +185,38 @@ def paranoid_view(request): send_log(request, exc_info) return technical_500_response(request, *exc_info) +def sensitive_args_function_caller(request): + try: + sensitive_args_function(''.join(['w', 'o', 'r', 'c', 'e', 's', 't', 'e', 'r', 's', 'h', 'i', 'r', 'e'])) + except Exception: + exc_info = sys.exc_info() + send_log(request, exc_info) + return technical_500_response(request, *exc_info) + +@sensitive_variables('sauce') +def sensitive_args_function(sauce): + # Do not just use plain strings for the variables' values in the code + # so that the tests don't return false positives when the function's source + # is displayed in the exception report. + cooked_eggs = ''.join(['s', 'c', 'r', 'a', 'm', 'b', 'l', 'e', 'd']) + raise Exception + +def sensitive_kwargs_function_caller(request): + try: + sensitive_kwargs_function(''.join(['w', 'o', 'r', 'c', 'e', 's', 't', 'e', 'r', 's', 'h', 'i', 'r', 'e'])) + except Exception: + exc_info = sys.exc_info() + send_log(request, exc_info) + return technical_500_response(request, *exc_info) + +@sensitive_variables('sauce') +def sensitive_kwargs_function(sauce=None): + # Do not just use plain strings for the variables' values in the code + # so that the tests don't return false positives when the function's source + # is displayed in the exception report. + cooked_eggs = ''.join(['s', 'c', 'r', 'a', 'm', 'b', 'l', 'e', 'd']) + raise Exception + class UnsafeExceptionReporterFilter(SafeExceptionReporterFilter): """ Ignores all the filtering done by its parent class. From 3570ff734e93f493e023b912c9a97101f605f7f5 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Mon, 31 Dec 2012 19:40:02 -0300 Subject: [PATCH 085/870] Fixed #17078 -- Made shell use std IPython startup. This allows for a behavior more in line with what is expected by Ipython users, e.g. the user namespace is initialized from config files, startup files. Thanks Benjamin Ragan-Kelley from the IPython dev team for the patch. --- django/core/management/commands/shell.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py index 52a8cab7e4..f883fb95d8 100644 --- a/django/core/management/commands/shell.py +++ b/django/core/management/commands/shell.py @@ -19,8 +19,10 @@ class Command(NoArgsCommand): def ipython(self): try: - from IPython import embed - embed() + from IPython.frontend.terminal.ipapp import TerminalIPythonApp + app = TerminalIPythonApp.instance() + app.initialize(argv=[]) + app.start() except ImportError: # IPython < 0.11 # Explicitly pass an empty list as arguments, because otherwise From bacb097ac31af2479c43934747b34fad7c91f55c Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 1 Jan 2013 10:12:06 +0100 Subject: [PATCH 086/870] Fixed #19519 again -- Regression in LiveServerTestCase after fd1279a4. --- django/test/client.py | 15 ++++++++++----- django/test/testcases.py | 2 -- django/test/utils.py | 8 +------- tests/regressiontests/handlers/tests.py | 1 - 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/django/test/client.py b/django/test/client.py index 77d4de0524..6bdc1cf3d3 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -16,7 +16,9 @@ from django.conf import settings from django.contrib.auth import authenticate, login from django.core.handlers.base import BaseHandler from django.core.handlers.wsgi import WSGIRequest -from django.core.signals import got_request_exception +from django.core.signals import (request_started, request_finished, + got_request_exception) +from django.db import close_connection from django.http import SimpleCookie, HttpRequest, QueryDict from django.template import TemplateDoesNotExist from django.test import signals @@ -76,7 +78,9 @@ def closing_iterator_wrapper(iterable, close): for item in iterable: yield item finally: - close() + request_finished.disconnect(close_connection) + close() # will fire request_finished + request_finished.connect(close_connection) class ClientHandler(BaseHandler): @@ -91,14 +95,13 @@ class ClientHandler(BaseHandler): def __call__(self, environ): from django.conf import settings - from django.core import signals # Set up middleware if needed. We couldn't do this earlier, because # settings weren't available. if self._request_middleware is None: self.load_middleware() - signals.request_started.send(sender=self.__class__) + request_started.send(sender=self.__class__) request = WSGIRequest(environ) # sneaky little hack so that we can easily get round # CsrfViewMiddleware. This makes life easier, and is probably @@ -112,7 +115,9 @@ class ClientHandler(BaseHandler): response.streaming_content = closing_iterator_wrapper( response.streaming_content, response.close) else: - response.close() + request_finished.disconnect(close_connection) + response.close() # will fire request_finished + request_finished.connect(close_connection) return response diff --git a/django/test/testcases.py b/django/test/testcases.py index 6f9bc0e724..c311540fc3 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -641,8 +641,6 @@ class TransactionTestCase(SimpleTestCase): else: content = response.content content = content.decode(response._charset) - # Avoid ResourceWarning about unclosed files. - response.close() if html: content = assert_and_parse_html(self, content, None, "Response's content is not valid HTML:") diff --git a/django/test/utils.py b/django/test/utils.py index a1ff826d6e..8114ae0e6a 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -4,8 +4,6 @@ from xml.dom.minidom import parseString, Node from django.conf import settings, UserSettingsHolder from django.core import mail -from django.core.signals import request_finished -from django.db import close_connection from django.test.signals import template_rendered, setting_changed from django.template import Template, loader, TemplateDoesNotExist from django.template.loaders import cached @@ -70,10 +68,8 @@ def setup_test_environment(): """Perform any global pre-test setup. This involves: - Installing the instrumented test renderer - - Setting the email backend to the locmem email backend. + - Set the email backend to the locmem email backend. - Setting the active locale to match the LANGUAGE_CODE setting. - - Disconnecting the request_finished signal to avoid closing - the database connection within tests. """ Template.original_render = Template._render Template._render = instrumented_test_render @@ -85,8 +81,6 @@ def setup_test_environment(): deactivate() - request_finished.disconnect(close_connection) - def teardown_test_environment(): """Perform any global post-test teardown. This involves: diff --git a/tests/regressiontests/handlers/tests.py b/tests/regressiontests/handlers/tests.py index 3cab2aca57..3557a63fd5 100644 --- a/tests/regressiontests/handlers/tests.py +++ b/tests/regressiontests/handlers/tests.py @@ -56,6 +56,5 @@ class SignalsTests(TestCase): def test_request_signals_streaming_response(self): response = self.client.get('/streaming/') self.assertEqual(self.signals, ['started']) - # Avoid self.assertContains, because it explicitly calls response.close() self.assertEqual(b''.join(response.streaming_content), b"streaming content") self.assertEqual(self.signals, ['started', 'finished']) From 3aa4b8165da23a2f094d0eeffacbda5484f4c1f6 Mon Sep 17 00:00:00 2001 From: Anton Baklanov Date: Thu, 13 Dec 2012 18:26:34 +0200 Subject: [PATCH 087/870] Fixed #19457 -- ImageField size detection failed for some files. This was caused by PIL raising a zlib truncated stream error since we fed the parser with chunks instead of the whole image. --- django/core/files/images.py | 14 +++++++++++++- tests/regressiontests/file_storage/magic.png | Bin 0 -> 70445 bytes tests/regressiontests/file_storage/tests.py | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tests/regressiontests/file_storage/magic.png diff --git a/django/core/files/images.py b/django/core/files/images.py index 7d7eac65db..a8d3d8018b 100644 --- a/django/core/files/images.py +++ b/django/core/files/images.py @@ -4,7 +4,11 @@ Utility functions for handling images. Requires PIL, as you might imagine. """ +import zlib +import sys + from django.core.files import File +from django.utils import six class ImageFile(File): """ @@ -55,7 +59,15 @@ def get_image_dimensions(file_or_path, close=False): data = file.read(chunk_size) if not data: break - p.feed(data) + try: + p.feed(data) + except zlib.error as e: + # ignore zlib complaining on truncated stream, just feed more + # data to parser (ticket #19457). + if e.message.startswith("Error -5"): + pass + else: + six.reraise(*sys.exc_info()) if p.image: return p.image.size chunk_size = chunk_size*2 diff --git a/tests/regressiontests/file_storage/magic.png b/tests/regressiontests/file_storage/magic.png new file mode 100644 index 0000000000000000000000000000000000000000..a06449f9e9795a3bea688f47fa79110e2762fd9c GIT binary patch literal 70445 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 literal 0 HcmV?d00001 diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py index b6d3e1ff0b..4872ce87b9 100644 --- a/tests/regressiontests/file_storage/tests.py +++ b/tests/regressiontests/file_storage/tests.py @@ -7,6 +7,7 @@ import shutil import sys import tempfile import time +import zlib from datetime import datetime, timedelta from io import BytesIO @@ -559,6 +560,20 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase): self.assertEqual(image_pil.size, size_1) self.assertEqual(size_1, size_2) + @unittest.skipUnless(Image, "PIL not installed") + def test_bug_19457(self): + """ + Regression test for #19457 + get_image_dimensions fails on some pngs, while Image.size is working good on them + """ + img_path = os.path.join(os.path.dirname(upath(__file__)), "magic.png") + try: + size = get_image_dimensions(img_path) + except zlib.error: + self.fail("Exception raised from get_image_dimensions().") + self.assertEqual(size, Image.open(img_path).size) + + class ContentFileTestCase(unittest.TestCase): def setUp(self): From cf7afeb2d18b22f857a35d5cb698e2c180aadd2f Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Tue, 1 Jan 2013 12:24:02 +0100 Subject: [PATCH 088/870] Fixed a NameError in geoip/libgeoip if the GeoIP library is not found. Thx to Bouke Haarsma for the report. --- django/contrib/gis/geoip/libgeoip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/contrib/gis/geoip/libgeoip.py b/django/contrib/gis/geoip/libgeoip.py index 613949f809..e9e7cd7fe9 100644 --- a/django/contrib/gis/geoip/libgeoip.py +++ b/django/contrib/gis/geoip/libgeoip.py @@ -19,8 +19,8 @@ else: # Getting the path to the GeoIP library. if lib_name: lib_path = find_library(lib_name) -if lib_path is None: raise GeoIPException('Could not find the GeoIP library (tried "%s"). ' - 'Try setting GEOIP_LIBRARY_PATH in your settings.' % lib_name) +if lib_path is None: raise RuntimeError('Could not find the GeoIP library (tried "%s"). ' + 'Try setting GEOIP_LIBRARY_PATH in your settings.' % lib_name) lgeoip = CDLL(lib_path) # Getting the C `free` for the platform. From a4a4b139cd1b2e5dd7a05f195c8990f4d94e882d Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Tue, 1 Jan 2013 13:13:31 +0100 Subject: [PATCH 089/870] Replaced e.message with e.args[0] in 3aa4b8165da23a2f094d0eeffacbda5484f4c1f6. --- django/core/files/images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/core/files/images.py b/django/core/files/images.py index a8d3d8018b..3baedda4cc 100644 --- a/django/core/files/images.py +++ b/django/core/files/images.py @@ -64,7 +64,7 @@ def get_image_dimensions(file_or_path, close=False): except zlib.error as e: # ignore zlib complaining on truncated stream, just feed more # data to parser (ticket #19457). - if e.message.startswith("Error -5"): + if e.args[0].startswith("Error -5"): pass else: six.reraise(*sys.exc_info()) From 7cb0cd5afffb8e38930eed7d52bd120176019e71 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Tue, 1 Jan 2013 13:20:36 +0100 Subject: [PATCH 090/870] Replaced six.reraise with a simple raise. --- django/core/files/images.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/django/core/files/images.py b/django/core/files/images.py index 3baedda4cc..0d87ae853e 100644 --- a/django/core/files/images.py +++ b/django/core/files/images.py @@ -3,12 +3,10 @@ Utility functions for handling images. Requires PIL, as you might imagine. """ - import zlib -import sys from django.core.files import File -from django.utils import six + class ImageFile(File): """ @@ -30,6 +28,7 @@ class ImageFile(File): self._dimensions_cache = get_image_dimensions(self, close=close) return self._dimensions_cache + def get_image_dimensions(file_or_path, close=False): """ Returns the (width, height) of an image, given an open file or a path. Set @@ -67,7 +66,7 @@ def get_image_dimensions(file_or_path, close=False): if e.args[0].startswith("Error -5"): pass else: - six.reraise(*sys.exc_info()) + raise if p.image: return p.image.size chunk_size = chunk_size*2 From 739724ff825fc0a37f31a28607256bd09e008f0a Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 1 Jan 2013 16:17:26 +0100 Subject: [PATCH 091/870] Added a helper script for managing django translations --- scripts/manage_translations.py | 164 +++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 scripts/manage_translations.py diff --git a/scripts/manage_translations.py b/scripts/manage_translations.py new file mode 100644 index 0000000000..b90b26e1f8 --- /dev/null +++ b/scripts/manage_translations.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +# +# This python file contains utility scripts to manage Django translations. +# It has to be run inside the django git root directory. +# +# The following commands are available: +# +# * update_catalogs: check for new strings in core and contrib catalogs, and +# output how much strings are new/changed. +# +# * lang_stats: output statistics for each catalog/language combination +# +# * fetch: fetch translations from transifex.com +# +# Each command support the --languages and --resources options to limit their +# operation to the specified language or resource. For example, to get stats +# for Spanish in contrib.admin, run: +# +# $ python scripts/manage_translations.py lang_stats --language=es --resources=admin + +import os +from optparse import OptionParser +from subprocess import call, Popen, PIPE + +from django.core.management import call_command + + +HAVE_JS = ['admin'] + +def _get_locale_dirs(include_core=True): + """ + Return a tuple (contrib name, absolute path) for all locale directories, + optionally including the django core catalog. + """ + contrib_dir = os.path.join(os.getcwd(), 'django', 'contrib') + dirs = [] + for contrib_name in os.listdir(contrib_dir): + path = os.path.join(contrib_dir, contrib_name, 'locale') + if os.path.isdir(path): + dirs.append((contrib_name, path)) + if contrib_name in HAVE_JS: + dirs.append(("%s-js" % contrib_name, path)) + if include_core: + dirs.insert(0, ('core', os.path.join(os.getcwd(), 'django', 'conf', 'locale'))) + return dirs + +def _tx_resource_for_name(name): + """ Return the Transifex resource name """ + if name == 'core': + return "django.core" + else: + return "django.contrib-%s" % name + +def _check_diff(cat_name, base_path): + """ + Output the approximate number of changed/added strings in the en catalog. + """ + po_path = '%(path)s/en/LC_MESSAGES/django%(ext)s.po' % { + 'path': base_path, 'ext': 'js' if cat_name.endswith('-js') else ''} + p = Popen("git diff -U0 %s | egrep -v '^@@|^[-+]#|^..POT-Creation' | wc -l" % po_path, + stdout=PIPE, stderr=PIPE, shell=True) + output, errors = p.communicate() + num_changes = int(output.strip()) - 4 + print("%d changed/added messages in '%s' catalog." % (num_changes, cat_name)) + + +def update_catalogs(resources=None, languages=None): + """ + Update the en/LC_MESSAGES/django.po (main and contrib) files with + new/updated translatable strings. + """ + contrib_dirs = _get_locale_dirs(include_core=False) + + os.chdir(os.path.join(os.getcwd(), 'django')) + print("Updating main en catalog") + call_command('makemessages', locale='en') + _check_diff('core', os.path.join(os.getcwd(), 'conf', 'locale')) + + # Contrib catalogs + for name, dir_ in contrib_dirs: + if resources and not name in resources: + continue + os.chdir(os.path.join(dir_, '..')) + print("Updating en catalog in %s" % dir_) + if name.endswith('-js'): + call_command('makemessages', locale='en', domain='djangojs') + else: + call_command('makemessages', locale='en') + _check_diff(name, dir_) + + +def lang_stats(resources=None, languages=None): + """ + Output language statistics of committed translation files for each + Django catalog. + If resources is provided, it should be a list of translation resource to + limit the output (e.g. ['core', 'gis']). + """ + locale_dirs = _get_locale_dirs() + + for name, dir_ in locale_dirs: + if resources and not name in resources: + continue + print("\nShowing translations stats for '%s':" % name) + langs = sorted([d for d in os.listdir(dir_) if not d.startswith('_')]) + for lang in langs: + if languages and not lang in languages: + continue + # TODO: merge first with the latest en catalog + p = Popen("msgfmt -vc -o /dev/null %(path)s/%(lang)s/LC_MESSAGES/django%(ext)s.po" % { + 'path': dir_, 'lang': lang, 'ext': 'js' if name.endswith('-js') else ''}, + stdout=PIPE, stderr=PIPE, shell=True) + output, errors = p.communicate() + if p.returncode == 0: + # msgfmt output stats on stderr + print("%s: %s" % (lang, errors.strip())) + + +def fetch(resources=None, languages=None): + """ + Fetch translations from Transifex, wrap long lines, generate mo files. + """ + locale_dirs = _get_locale_dirs() + + for name, dir_ in locale_dirs: + if resources and not name in resources: + continue + + # Transifex pull + if languages is None: + call('tx pull -r %(res)s -a -f' % {'res': _tx_resource_for_name(name)}, shell=True) + languages = sorted([d for d in os.listdir(dir_) if not d.startswith('_')]) + else: + for lang in languages: + call('tx pull -r %(res)s -f -l %(lang)s' % { + 'res': _tx_resource_for_name(name), 'lang': lang}, shell=True) + + # msgcat to wrap lines and msgfmt for compilation of .mo file + for lang in languages: + po_path = '%(path)s/%(lang)s/LC_MESSAGES/django%(ext)s.po' % { + 'path': dir_, 'lang': lang, 'ext': 'js' if name.endswith('-js') else ''} + call('msgcat -o %s %s' % (po_path, po_path), shell=True) + mo_path = '%s.mo' % po_path[:-3] + call('msgfmt -o %s %s' % (mo_path, po_path), shell=True) + + +if __name__ == "__main__": + RUNABLE_SCRIPTS = ('update_catalogs', 'lang_stats', 'fetch') + + parser = OptionParser(usage="usage: %prog [options] cmd") + parser.add_option("-r", "--resources", action='append', + help="limit operation to the specified resources") + parser.add_option("-l", "--languages", action='append', + help="limit operation to the specified languages") + options, args = parser.parse_args() + + if not args: + parser.print_usage() + exit(1) + + if args[0] in RUNABLE_SCRIPTS: + eval(args[0])(options.resources, options.languages) + else: + print("Available commands are: %s" % ", ".join(RUNABLE_SCRIPTS)) From c5ce0e8a687ecf7fbc38fa85c5311a6320a246c6 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Tue, 1 Jan 2013 15:26:34 +0100 Subject: [PATCH 092/870] Updated our six module to follow upstream changes. This includes fixes for the java/jython detection and a new license header. Thanks to Thomas Bartelmess for the report. --- django/utils/six.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/django/utils/six.py b/django/utils/six.py index 9e3823128f..73846358a1 100644 --- a/django/utils/six.py +++ b/django/utils/six.py @@ -1,5 +1,24 @@ """Utilities for writing code that runs on Python 2 and 3""" +# Copyright (c) 2010-2012 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + import operator import sys import types @@ -26,7 +45,7 @@ else: text_type = unicode binary_type = str - if sys.platform == "java": + if sys.platform.startswith("java"): # Jython always uses 32 bits. MAXSIZE = int((1 << 31) - 1) else: @@ -133,6 +152,9 @@ _moved_attributes = [ MovedModule("html_entities", "htmlentitydefs", "html.entities"), MovedModule("html_parser", "HTMLParser", "html.parser"), MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), @@ -164,7 +186,7 @@ for attr in _moved_attributes: setattr(_MovedItems, attr.name, attr) del attr -moves = sys.modules["django.utils.six.moves"] = _MovedItems("moves") +moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves") def add_move(move): From 08140aec5c565dba1d1a8158ad631889f9aab2e6 Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Tue, 1 Jan 2013 17:12:15 +0000 Subject: [PATCH 093/870] Tiny typo fixed in logging docs --- docs/topics/logging.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index 3a5a8cb489..db0c0b3d25 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -576,7 +576,7 @@ with ``ERROR`` or ``CRITICAL`` level are sent to :class:`AdminEmailHandler`, as long as the :setting:`DEBUG` setting is set to ``False``. All messages reaching the ``django`` catch-all logger when :setting:`DEBUG` is -`True` are sent ot the console. They are simply discarded (sent to +`True` are sent to the console. They are simply discarded (sent to ``NullHandler``) when :setting:`DEBUG` is `False`. .. versionchanged:: 1.5 From 884f77bd15f051224cdd8de63597db05ff3d7f43 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 1 Jan 2013 20:50:13 +0100 Subject: [PATCH 094/870] Removed unusable parameters to empty_form property --- django/forms/formsets.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 3893cc54ba..219a80edee 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -157,14 +157,12 @@ class BaseFormSet(object): return self.forms[self.initial_form_count():] @property - def empty_form(self, **kwargs): - defaults = { - 'auto_id': self.auto_id, - 'prefix': self.add_prefix('__prefix__'), - 'empty_permitted': True, - } - defaults.update(kwargs) - form = self.form(**defaults) + def empty_form(self): + form = self.form( + auto_id=self.auto_id, + prefix=self.add_prefix('__prefix__'), + empty_permitted=True, + ) self.add_fields(form, None) return form From feb41c32468b6a17ac8e0f0b5c9575ffd3f08798 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 1 Jan 2013 22:46:54 +0100 Subject: [PATCH 095/870] Modernized middleware tests. * Used override_settings consistently -- changes to DEBUG could leak. * Took advantage of assertRaisesRegexp. * Fixed indentation -- some code was indented at 2 spaces. --- tests/regressiontests/middleware/tests.py | 359 ++++++++++------------ 1 file changed, 161 insertions(+), 198 deletions(-) diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py index b8cffd9c92..e3d8350da6 100644 --- a/tests/regressiontests/middleware/tests.py +++ b/tests/regressiontests/middleware/tests.py @@ -20,17 +20,6 @@ from django.utils.six.moves import xrange class CommonMiddlewareTest(TestCase): - def setUp(self): - self.append_slash = settings.APPEND_SLASH - self.prepend_www = settings.PREPEND_WWW - self.ignorable_404_urls = settings.IGNORABLE_404_URLS - self.send_broken_email_links = settings.SEND_BROKEN_LINK_EMAILS - - def tearDown(self): - settings.APPEND_SLASH = self.append_slash - settings.PREPEND_WWW = self.prepend_www - settings.IGNORABLE_404_URLS = self.ignorable_404_urls - settings.SEND_BROKEN_LINK_EMAILS = self.send_broken_email_links def _get_request(self, path): request = HttpRequest() @@ -41,74 +30,66 @@ class CommonMiddlewareTest(TestCase): request.path = request.path_info = "/middleware/%s" % path return request + @override_settings(APPEND_SLASH=True) def test_append_slash_have_slash(self): """ Tests that URLs with slashes go unmolested. """ - settings.APPEND_SLASH = True request = self._get_request('slash/') self.assertEqual(CommonMiddleware().process_request(request), None) + @override_settings(APPEND_SLASH=True) def test_append_slash_slashless_resource(self): """ Tests that matches to explicit slashless URLs go unmolested. """ - settings.APPEND_SLASH = True request = self._get_request('noslash') self.assertEqual(CommonMiddleware().process_request(request), None) + @override_settings(APPEND_SLASH=True) def test_append_slash_slashless_unknown(self): """ Tests that APPEND_SLASH doesn't redirect to unknown resources. """ - settings.APPEND_SLASH = True request = self._get_request('unknown') self.assertEqual(CommonMiddleware().process_request(request), None) + @override_settings(APPEND_SLASH=True) def test_append_slash_redirect(self): """ Tests that APPEND_SLASH redirects slashless URLs to a valid pattern. """ - settings.APPEND_SLASH = True request = self._get_request('slash') r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) self.assertEqual(r['Location'], 'http://testserver/middleware/slash/') + @override_settings(APPEND_SLASH=True, DEBUG=True) def test_append_slash_no_redirect_on_POST_in_DEBUG(self): """ Tests that while in debug mode, an exception is raised with a warning when a failed attempt is made to POST to an URL which would normally be redirected to a slashed version. """ - settings.APPEND_SLASH = True - settings.DEBUG = True request = self._get_request('slash') request.method = 'POST' - self.assertRaises( - RuntimeError, - CommonMiddleware().process_request, - request) - try: + with six.assertRaisesRegex(self, RuntimeError, 'end in a slash'): CommonMiddleware().process_request(request) - except RuntimeError as e: - self.assertTrue('end in a slash' in str(e)) - settings.DEBUG = False + @override_settings(APPEND_SLASH=False) def test_append_slash_disabled(self): """ Tests disabling append slash functionality. """ - settings.APPEND_SLASH = False request = self._get_request('slash') self.assertEqual(CommonMiddleware().process_request(request), None) + @override_settings(APPEND_SLASH=True) def test_append_slash_quoted(self): """ Tests that URLs which require quoting are redirected to their slash version ok. """ - settings.APPEND_SLASH = True request = self._get_request('needsquoting#') r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) @@ -116,9 +97,8 @@ class CommonMiddlewareTest(TestCase): r['Location'], 'http://testserver/middleware/needsquoting%23/') + @override_settings(APPEND_SLASH=False, PREPEND_WWW=True) def test_prepend_www(self): - settings.PREPEND_WWW = True - settings.APPEND_SLASH = False request = self._get_request('path/') r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) @@ -126,18 +106,16 @@ class CommonMiddlewareTest(TestCase): r['Location'], 'http://www.testserver/middleware/path/') + @override_settings(APPEND_SLASH=True, PREPEND_WWW=True) def test_prepend_www_append_slash_have_slash(self): - settings.PREPEND_WWW = True - settings.APPEND_SLASH = True request = self._get_request('slash/') r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) self.assertEqual(r['Location'], 'http://www.testserver/middleware/slash/') + @override_settings(APPEND_SLASH=True, PREPEND_WWW=True) def test_prepend_www_append_slash_slashless(self): - settings.PREPEND_WWW = True - settings.APPEND_SLASH = True request = self._get_request('slash') r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) @@ -148,128 +126,117 @@ class CommonMiddlewareTest(TestCase): # The following tests examine expected behavior given a custom urlconf that # overrides the default one through the request object. + @override_settings(APPEND_SLASH=True) def test_append_slash_have_slash_custom_urlconf(self): - """ - Tests that URLs with slashes go unmolested. - """ - settings.APPEND_SLASH = True - request = self._get_request('customurlconf/slash/') - request.urlconf = 'regressiontests.middleware.extra_urls' - self.assertEqual(CommonMiddleware().process_request(request), None) + """ + Tests that URLs with slashes go unmolested. + """ + request = self._get_request('customurlconf/slash/') + request.urlconf = 'regressiontests.middleware.extra_urls' + self.assertEqual(CommonMiddleware().process_request(request), None) + @override_settings(APPEND_SLASH=True) def test_append_slash_slashless_resource_custom_urlconf(self): - """ - Tests that matches to explicit slashless URLs go unmolested. - """ - settings.APPEND_SLASH = True - request = self._get_request('customurlconf/noslash') - request.urlconf = 'regressiontests.middleware.extra_urls' - self.assertEqual(CommonMiddleware().process_request(request), None) + """ + Tests that matches to explicit slashless URLs go unmolested. + """ + request = self._get_request('customurlconf/noslash') + request.urlconf = 'regressiontests.middleware.extra_urls' + self.assertEqual(CommonMiddleware().process_request(request), None) + @override_settings(APPEND_SLASH=True) def test_append_slash_slashless_unknown_custom_urlconf(self): - """ - Tests that APPEND_SLASH doesn't redirect to unknown resources. - """ - settings.APPEND_SLASH = True - request = self._get_request('customurlconf/unknown') - request.urlconf = 'regressiontests.middleware.extra_urls' - self.assertEqual(CommonMiddleware().process_request(request), None) + """ + Tests that APPEND_SLASH doesn't redirect to unknown resources. + """ + request = self._get_request('customurlconf/unknown') + request.urlconf = 'regressiontests.middleware.extra_urls' + self.assertEqual(CommonMiddleware().process_request(request), None) + @override_settings(APPEND_SLASH=True) def test_append_slash_redirect_custom_urlconf(self): - """ - Tests that APPEND_SLASH redirects slashless URLs to a valid pattern. - """ - settings.APPEND_SLASH = True - request = self._get_request('customurlconf/slash') - request.urlconf = 'regressiontests.middleware.extra_urls' - r = CommonMiddleware().process_request(request) - self.assertFalse(r is None, - "CommonMiddlware failed to return APPEND_SLASH redirect using request.urlconf") - self.assertEqual(r.status_code, 301) - self.assertEqual(r['Location'], 'http://testserver/middleware/customurlconf/slash/') + """ + Tests that APPEND_SLASH redirects slashless URLs to a valid pattern. + """ + request = self._get_request('customurlconf/slash') + request.urlconf = 'regressiontests.middleware.extra_urls' + r = CommonMiddleware().process_request(request) + self.assertFalse(r is None, + "CommonMiddlware failed to return APPEND_SLASH redirect using request.urlconf") + self.assertEqual(r.status_code, 301) + self.assertEqual(r['Location'], 'http://testserver/middleware/customurlconf/slash/') + @override_settings(APPEND_SLASH=True, DEBUG=True) def test_append_slash_no_redirect_on_POST_in_DEBUG_custom_urlconf(self): - """ - Tests that while in debug mode, an exception is raised with a warning - when a failed attempt is made to POST to an URL which would normally be - redirected to a slashed version. - """ - settings.APPEND_SLASH = True - settings.DEBUG = True - request = self._get_request('customurlconf/slash') - request.urlconf = 'regressiontests.middleware.extra_urls' - request.method = 'POST' - self.assertRaises( - RuntimeError, - CommonMiddleware().process_request, - request) - try: - CommonMiddleware().process_request(request) - except RuntimeError as e: - self.assertTrue('end in a slash' in str(e)) - settings.DEBUG = False + """ + Tests that while in debug mode, an exception is raised with a warning + when a failed attempt is made to POST to an URL which would normally be + redirected to a slashed version. + """ + request = self._get_request('customurlconf/slash') + request.urlconf = 'regressiontests.middleware.extra_urls' + request.method = 'POST' + with six.assertRaisesRegex(self, RuntimeError, 'end in a slash'): + CommonMiddleware().process_request(request) + @override_settings(APPEND_SLASH=False) def test_append_slash_disabled_custom_urlconf(self): - """ - Tests disabling append slash functionality. - """ - settings.APPEND_SLASH = False - request = self._get_request('customurlconf/slash') - request.urlconf = 'regressiontests.middleware.extra_urls' - self.assertEqual(CommonMiddleware().process_request(request), None) + """ + Tests disabling append slash functionality. + """ + request = self._get_request('customurlconf/slash') + request.urlconf = 'regressiontests.middleware.extra_urls' + self.assertEqual(CommonMiddleware().process_request(request), None) + @override_settings(APPEND_SLASH=True) def test_append_slash_quoted_custom_urlconf(self): - """ - Tests that URLs which require quoting are redirected to their slash - version ok. - """ - settings.APPEND_SLASH = True - request = self._get_request('customurlconf/needsquoting#') - request.urlconf = 'regressiontests.middleware.extra_urls' - r = CommonMiddleware().process_request(request) - self.assertFalse(r is None, - "CommonMiddlware failed to return APPEND_SLASH redirect using request.urlconf") - self.assertEqual(r.status_code, 301) - self.assertEqual( - r['Location'], - 'http://testserver/middleware/customurlconf/needsquoting%23/') + """ + Tests that URLs which require quoting are redirected to their slash + version ok. + """ + request = self._get_request('customurlconf/needsquoting#') + request.urlconf = 'regressiontests.middleware.extra_urls' + r = CommonMiddleware().process_request(request) + self.assertFalse(r is None, + "CommonMiddlware failed to return APPEND_SLASH redirect using request.urlconf") + self.assertEqual(r.status_code, 301) + self.assertEqual( + r['Location'], + 'http://testserver/middleware/customurlconf/needsquoting%23/') + @override_settings(APPEND_SLASH=False, PREPEND_WWW=True) def test_prepend_www_custom_urlconf(self): - settings.PREPEND_WWW = True - settings.APPEND_SLASH = False - request = self._get_request('customurlconf/path/') - request.urlconf = 'regressiontests.middleware.extra_urls' - r = CommonMiddleware().process_request(request) - self.assertEqual(r.status_code, 301) - self.assertEqual( - r['Location'], - 'http://www.testserver/middleware/customurlconf/path/') + request = self._get_request('customurlconf/path/') + request.urlconf = 'regressiontests.middleware.extra_urls' + r = CommonMiddleware().process_request(request) + self.assertEqual(r.status_code, 301) + self.assertEqual( + r['Location'], + 'http://www.testserver/middleware/customurlconf/path/') + @override_settings(APPEND_SLASH=True, PREPEND_WWW=True) def test_prepend_www_append_slash_have_slash_custom_urlconf(self): - settings.PREPEND_WWW = True - settings.APPEND_SLASH = True - request = self._get_request('customurlconf/slash/') - request.urlconf = 'regressiontests.middleware.extra_urls' - r = CommonMiddleware().process_request(request) - self.assertEqual(r.status_code, 301) - self.assertEqual(r['Location'], - 'http://www.testserver/middleware/customurlconf/slash/') + request = self._get_request('customurlconf/slash/') + request.urlconf = 'regressiontests.middleware.extra_urls' + r = CommonMiddleware().process_request(request) + self.assertEqual(r.status_code, 301) + self.assertEqual(r['Location'], + 'http://www.testserver/middleware/customurlconf/slash/') + @override_settings(APPEND_SLASH=True, PREPEND_WWW=True) def test_prepend_www_append_slash_slashless_custom_urlconf(self): - settings.PREPEND_WWW = True - settings.APPEND_SLASH = True - request = self._get_request('customurlconf/slash') - request.urlconf = 'regressiontests.middleware.extra_urls' - r = CommonMiddleware().process_request(request) - self.assertEqual(r.status_code, 301) - self.assertEqual(r['Location'], - 'http://www.testserver/middleware/customurlconf/slash/') + request = self._get_request('customurlconf/slash') + request.urlconf = 'regressiontests.middleware.extra_urls' + r = CommonMiddleware().process_request(request) + self.assertEqual(r.status_code, 301) + self.assertEqual(r['Location'], + 'http://www.testserver/middleware/customurlconf/slash/') # Tests for the 404 error reporting via email + @override_settings(IGNORABLE_404_URLS=(re.compile(r'foo'),), + SEND_BROKEN_LINK_EMAILS = True) def test_404_error_reporting(self): - settings.IGNORABLE_404_URLS = (re.compile(r'foo'),) - settings.SEND_BROKEN_LINK_EMAILS = True request = self._get_request('regular_url/that/does/not/exist') request.META['HTTP_REFERER'] = '/another/url/' response = self.client.get(request.path) @@ -277,17 +244,17 @@ class CommonMiddlewareTest(TestCase): self.assertEqual(len(mail.outbox), 1) self.assertIn('Broken', mail.outbox[0].subject) + @override_settings(IGNORABLE_404_URLS=(re.compile(r'foo'),), + SEND_BROKEN_LINK_EMAILS = True) def test_404_error_reporting_no_referer(self): - settings.IGNORABLE_404_URLS = (re.compile(r'foo'),) - settings.SEND_BROKEN_LINK_EMAILS = True request = self._get_request('regular_url/that/does/not/exist') response = self.client.get(request.path) CommonMiddleware().process_response(request, response) self.assertEqual(len(mail.outbox), 0) + @override_settings(IGNORABLE_404_URLS=(re.compile(r'foo'),), + SEND_BROKEN_LINK_EMAILS = True) def test_404_error_reporting_ignored_url(self): - settings.IGNORABLE_404_URLS = (re.compile(r'foo'),) - settings.SEND_BROKEN_LINK_EMAILS = True request = self._get_request('foo_url/that/does/not/exist/either') request.META['HTTP_REFERER'] = '/another/url/' response = self.client.get(request.path) @@ -424,70 +391,66 @@ class XFrameOptionsMiddlewareTest(TestCase): """ Tests for the X-Frame-Options clickjacking prevention middleware. """ - def setUp(self): - self.x_frame_options = settings.X_FRAME_OPTIONS - - def tearDown(self): - settings.X_FRAME_OPTIONS = self.x_frame_options def test_same_origin(self): """ Tests that the X_FRAME_OPTIONS setting can be set to SAMEORIGIN to have the middleware use that value for the HTTP header. """ - settings.X_FRAME_OPTIONS = 'SAMEORIGIN' - r = XFrameOptionsMiddleware().process_response(HttpRequest(), - HttpResponse()) - self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') + with override_settings(X_FRAME_OPTIONS='SAMEORIGIN'): + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + HttpResponse()) + self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') - settings.X_FRAME_OPTIONS = 'sameorigin' - r = XFrameOptionsMiddleware().process_response(HttpRequest(), + with override_settings(X_FRAME_OPTIONS='sameorigin'): + r = XFrameOptionsMiddleware().process_response(HttpRequest(), HttpResponse()) - self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') + self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') def test_deny(self): """ Tests that the X_FRAME_OPTIONS setting can be set to DENY to have the middleware use that value for the HTTP header. """ - settings.X_FRAME_OPTIONS = 'DENY' - r = XFrameOptionsMiddleware().process_response(HttpRequest(), - HttpResponse()) - self.assertEqual(r['X-Frame-Options'], 'DENY') + with override_settings(X_FRAME_OPTIONS='DENY'): + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + HttpResponse()) + self.assertEqual(r['X-Frame-Options'], 'DENY') - settings.X_FRAME_OPTIONS = 'deny' - r = XFrameOptionsMiddleware().process_response(HttpRequest(), - HttpResponse()) - self.assertEqual(r['X-Frame-Options'], 'DENY') + with override_settings(X_FRAME_OPTIONS='deny'): + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + HttpResponse()) + self.assertEqual(r['X-Frame-Options'], 'DENY') def test_defaults_sameorigin(self): """ Tests that if the X_FRAME_OPTIONS setting is not set then it defaults to SAMEORIGIN. """ - del settings.X_FRAME_OPTIONS - r = XFrameOptionsMiddleware().process_response(HttpRequest(), - HttpResponse()) - self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') + with override_settings(X_FRAME_OPTIONS=None): + del settings.X_FRAME_OPTIONS # restored by override_settings + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + HttpResponse()) + self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') def test_dont_set_if_set(self): """ Tests that if the X-Frame-Options header is already set then the middleware does not attempt to override it. """ - settings.X_FRAME_OPTIONS = 'DENY' - response = HttpResponse() - response['X-Frame-Options'] = 'SAMEORIGIN' - r = XFrameOptionsMiddleware().process_response(HttpRequest(), - response) - self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') + with override_settings(X_FRAME_OPTIONS='DENY'): + response = HttpResponse() + response['X-Frame-Options'] = 'SAMEORIGIN' + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + response) + self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') - settings.X_FRAME_OPTIONS = 'SAMEORIGIN' - response = HttpResponse() - response['X-Frame-Options'] = 'DENY' - r = XFrameOptionsMiddleware().process_response(HttpRequest(), - response) - self.assertEqual(r['X-Frame-Options'], 'DENY') + with override_settings(X_FRAME_OPTIONS='SAMEORIGIN'): + response = HttpResponse() + response['X-Frame-Options'] = 'DENY' + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + response) + self.assertEqual(r['X-Frame-Options'], 'DENY') def test_response_exempt(self): """ @@ -495,18 +458,18 @@ class XFrameOptionsMiddlewareTest(TestCase): to False then it still sets the header, but if it's set to True then it does not. """ - settings.X_FRAME_OPTIONS = 'SAMEORIGIN' - response = HttpResponse() - response.xframe_options_exempt = False - r = XFrameOptionsMiddleware().process_response(HttpRequest(), - response) - self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') + with override_settings(X_FRAME_OPTIONS='SAMEORIGIN'): + response = HttpResponse() + response.xframe_options_exempt = False + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + response) + self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') - response = HttpResponse() - response.xframe_options_exempt = True - r = XFrameOptionsMiddleware().process_response(HttpRequest(), - response) - self.assertEqual(r.get('X-Frame-Options', None), None) + response = HttpResponse() + response.xframe_options_exempt = True + r = XFrameOptionsMiddleware().process_response(HttpRequest(), + response) + self.assertEqual(r.get('X-Frame-Options', None), None) def test_is_extendable(self): """ @@ -523,23 +486,23 @@ class XFrameOptionsMiddlewareTest(TestCase): return 'SAMEORIGIN' return 'DENY' - settings.X_FRAME_OPTIONS = 'DENY' - response = HttpResponse() - response.sameorigin = True - r = OtherXFrameOptionsMiddleware().process_response(HttpRequest(), - response) - self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') + with override_settings(X_FRAME_OPTIONS='DENY'): + response = HttpResponse() + response.sameorigin = True + r = OtherXFrameOptionsMiddleware().process_response(HttpRequest(), + response) + self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') - request = HttpRequest() - request.sameorigin = True - r = OtherXFrameOptionsMiddleware().process_response(request, - HttpResponse()) - self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') + request = HttpRequest() + request.sameorigin = True + r = OtherXFrameOptionsMiddleware().process_response(request, + HttpResponse()) + self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN') - settings.X_FRAME_OPTIONS = 'SAMEORIGIN' - r = OtherXFrameOptionsMiddleware().process_response(HttpRequest(), - HttpResponse()) - self.assertEqual(r['X-Frame-Options'], 'DENY') + with override_settings(X_FRAME_OPTIONS='SAMEORIGIN'): + r = OtherXFrameOptionsMiddleware().process_response(HttpRequest(), + HttpResponse()) + self.assertEqual(r['X-Frame-Options'], 'DENY') class GZipMiddlewareTest(TestCase): From 0d3f16b12ea92aff208c4bb88d342eb787c92f71 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 1 Jan 2013 18:45:57 -0500 Subject: [PATCH 096/870] Fixed #19520 - Corrected some misleading docs about template_name_suffix. Thanks jnns for the report. --- .../ref/class-based-views/generic-editing.txt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/ref/class-based-views/generic-editing.txt b/docs/ref/class-based-views/generic-editing.txt index 7ce5c1d1be..789dc2f84f 100644 --- a/docs/ref/class-based-views/generic-editing.txt +++ b/docs/ref/class-based-views/generic-editing.txt @@ -97,11 +97,11 @@ CreateView .. attribute:: template_name_suffix - The CreateView page displayed to a GET request uses a - ``template_name_suffix`` of ``'_form.html'``. For - example, changing this attribute to ``'_create_form.html'`` for a view - creating objects for the the example `Author` model would cause the the - default `template_name` to be ``'myapp/author_create_form.html'``. + The ``CreateView`` page displayed to a ``GET`` request uses a + ``template_name_suffix`` of ``'_form'``. For + example, changing this attribute to ``'_create_form'`` for a view + creating objects for the example ``Author`` model would cause the + default ``template_name`` to be ``'myapp/author_create_form.html'``. **Example views.py**:: @@ -139,11 +139,11 @@ UpdateView .. attribute:: template_name_suffix - The UpdateView page displayed to a GET request uses a - ``template_name_suffix`` of ``'_form.html'``. For - example, changing this attribute to ``'_update_form.html'`` for a view - updating objects for the the example `Author` model would cause the the - default `template_name` to be ``'myapp/author_update_form.html'``. + The ``UpdateView`` page displayed to a ``GET`` request uses a + ``template_name_suffix`` of ``'_form'``. For + example, changing this attribute to ``'_update_form'`` for a view + updating objects for the example ``Author`` model would cause the + default ``template_name`` to be ``'myapp/author_update_form.html'``. **Example views.py**:: @@ -180,11 +180,11 @@ DeleteView .. attribute:: template_name_suffix - The DeleteView page displayed to a GET request uses a - ``template_name_suffix`` of ``'_confirm_delete.html'``. For - example, changing this attribute to ``'_check_delete.html'`` for a view - deleting objects for the the example `Author` model would cause the the - default `template_name` to be ``'myapp/author_check_delete.html'``. + The ``DeleteView`` page displayed to a ``GET`` request uses a + ``template_name_suffix`` of ``'_confirm_delete'``. For + example, changing this attribute to ``'_check_delete'`` for a view + deleting objects for the example ``Author`` model would cause the + default ``template_name`` to be ``'myapp/author_check_delete.html'``. **Example views.py**:: From 695b2089e72a8ffec713b5107496b4332a4e0713 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 2 Jan 2013 15:33:18 -0500 Subject: [PATCH 097/870] Fixed #19549 - Typo in docs/topics/auth/default.txt --- docs/topics/auth/default.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 76fb7d835b..82cabadbec 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -26,8 +26,8 @@ authentication system. They typically represent the people interacting with your site and are used to enable things like restricting access, registering user profiles, associating content with creators etc. Only one class of user exists in Django's authentication framework, i.e., 'superusers' or admin -'staff' users are is just a user objects with special attributes set, not -different classes of user objects. +'staff' users are just user objects with special attributes set, not different +classes of user objects. The primary attributes of the default user are: From 07fbc6ae0e3b7742915b785c737b7e6e8a0e3503 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 2 Jan 2013 21:42:52 +0100 Subject: [PATCH 098/870] Fixed #19547 -- Caching of related instances. When &'ing or |'ing querysets, wrong values could be cached, and crashes could happen. Thanks Marc Tamlyn for figuring out the problem and writing the patch. --- django/db/models/fields/related.py | 2 +- django/db/models/query.py | 29 +++++++++---- .../fixtures/tournament.json | 11 +++++ .../known_related_objects/models.py | 4 ++ .../modeltests/known_related_objects/tests.py | 42 ++++++++++++++++++- 5 files changed, 79 insertions(+), 9 deletions(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 4b6a5b0aed..9a657d9d26 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -496,7 +496,7 @@ class ForeignRelatedObjectsDescriptor(object): except (AttributeError, KeyError): db = self._db or router.db_for_read(self.model, instance=self.instance) qs = super(RelatedManager, self).get_query_set().using(db).filter(**self.core_filters) - qs._known_related_object = (rel_field.name, self.instance) + qs._known_related_objects = {rel_field: {self.instance.pk: self.instance}} return qs def get_prefetch_query_set(self, instances): diff --git a/django/db/models/query.py b/django/db/models/query.py index ee58a77886..26b93b20bf 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -44,7 +44,7 @@ class QuerySet(object): self._for_write = False self._prefetch_related_lookups = [] self._prefetch_done = False - self._known_related_object = None # (attname, rel_obj) + self._known_related_objects = {} # {rel_field, {pk: rel_obj}} ######################## # PYTHON MAGIC METHODS # @@ -221,6 +221,7 @@ class QuerySet(object): if isinstance(other, EmptyQuerySet): return other._clone() combined = self._clone() + combined._merge_known_related_objects(other) combined.query.combine(other.query, sql.AND) return combined @@ -229,6 +230,7 @@ class QuerySet(object): combined = self._clone() if isinstance(other, EmptyQuerySet): return combined + combined._merge_known_related_objects(other) combined.query.combine(other.query, sql.OR) return combined @@ -289,10 +291,9 @@ class QuerySet(object): init_list.append(field.attname) model_cls = deferred_class_factory(self.model, skip) - # Cache db, model and known_related_object outside the loop + # Cache db and model outside the loop db = self.db model = self.model - kro_attname, kro_instance = self._known_related_object or (None, None) compiler = self.query.get_compiler(using=db) if fill_cache: klass_info = get_klass_info(model, max_depth=max_depth, @@ -323,9 +324,16 @@ class QuerySet(object): for i, aggregate in enumerate(aggregate_select): setattr(obj, aggregate, row[i + aggregate_start]) - # Add the known related object to the model, if there is one - if kro_instance: - setattr(obj, kro_attname, kro_instance) + # Add the known related objects to the model, if there are any + if self._known_related_objects: + for field, rel_objs in self._known_related_objects.items(): + pk = getattr(obj, field.get_attname()) + try: + rel_obj = rel_objs[pk] + except KeyError: + pass # may happen in qs1 | qs2 scenarios + else: + setattr(obj, field.name, rel_obj) yield obj @@ -902,7 +910,7 @@ class QuerySet(object): c = klass(model=self.model, query=query, using=self._db) c._for_write = self._for_write c._prefetch_related_lookups = self._prefetch_related_lookups[:] - c._known_related_object = self._known_related_object + c._known_related_objects = self._known_related_objects c.__dict__.update(kwargs) if setup and hasattr(c, '_setup_query'): c._setup_query() @@ -942,6 +950,13 @@ class QuerySet(object): """ pass + def _merge_known_related_objects(self, other): + """ + Keep track of all known related objects from either QuerySet instance. + """ + for field, objects in other._known_related_objects.items(): + self._known_related_objects.setdefault(field, {}).update(objects) + def _setup_aggregate_query(self, aggregates): """ Prepare the query for computing a result that contains aggregate annotations. diff --git a/tests/modeltests/known_related_objects/fixtures/tournament.json b/tests/modeltests/known_related_objects/fixtures/tournament.json index 2f2b1c5627..b8f053e152 100644 --- a/tests/modeltests/known_related_objects/fixtures/tournament.json +++ b/tests/modeltests/known_related_objects/fixtures/tournament.json @@ -13,11 +13,19 @@ "name": "Tourney 2" } }, + { + "pk": 1, + "model": "known_related_objects.organiser", + "fields": { + "name": "Organiser 1" + } + }, { "pk": 1, "model": "known_related_objects.pool", "fields": { "tournament": 1, + "organiser": 1, "name": "T1 Pool 1" } }, @@ -26,6 +34,7 @@ "model": "known_related_objects.pool", "fields": { "tournament": 1, + "organiser": 1, "name": "T1 Pool 2" } }, @@ -34,6 +43,7 @@ "model": "known_related_objects.pool", "fields": { "tournament": 2, + "organiser": 1, "name": "T2 Pool 1" } }, @@ -42,6 +52,7 @@ "model": "known_related_objects.pool", "fields": { "tournament": 2, + "organiser": 1, "name": "T2 Pool 2" } }, diff --git a/tests/modeltests/known_related_objects/models.py b/tests/modeltests/known_related_objects/models.py index 4c516dd7e8..e256cc38f2 100644 --- a/tests/modeltests/known_related_objects/models.py +++ b/tests/modeltests/known_related_objects/models.py @@ -9,9 +9,13 @@ from django.db import models class Tournament(models.Model): name = models.CharField(max_length=30) +class Organiser(models.Model): + name = models.CharField(max_length=30) + class Pool(models.Model): name = models.CharField(max_length=30) tournament = models.ForeignKey(Tournament) + organiser = models.ForeignKey(Organiser) class PoolStyle(models.Model): name = models.CharField(max_length=30) diff --git a/tests/modeltests/known_related_objects/tests.py b/tests/modeltests/known_related_objects/tests.py index 24feab2241..2371ac2e20 100644 --- a/tests/modeltests/known_related_objects/tests.py +++ b/tests/modeltests/known_related_objects/tests.py @@ -2,7 +2,7 @@ from __future__ import absolute_import from django.test import TestCase -from .models import Tournament, Pool, PoolStyle +from .models import Tournament, Organiser, Pool, PoolStyle class ExistingRelatedInstancesTests(TestCase): fixtures = ['tournament.json'] @@ -27,6 +27,46 @@ class ExistingRelatedInstancesTests(TestCase): pool2 = tournaments[1].pool_set.all()[0] self.assertIs(tournaments[1], pool2.tournament) + def test_queryset_or(self): + tournament_1 = Tournament.objects.get(pk=1) + tournament_2 = Tournament.objects.get(pk=2) + with self.assertNumQueries(1): + pools = tournament_1.pool_set.all() | tournament_2.pool_set.all() + related_objects = set(pool.tournament for pool in pools) + self.assertEqual(related_objects, set((tournament_1, tournament_2))) + + def test_queryset_or_different_cached_items(self): + tournament = Tournament.objects.get(pk=1) + organiser = Organiser.objects.get(pk=1) + with self.assertNumQueries(1): + pools = tournament.pool_set.all() | organiser.pool_set.all() + first = pools.filter(pk=1)[0] + self.assertIs(first.tournament, tournament) + self.assertIs(first.organiser, organiser) + + def test_queryset_or_only_one_with_precache(self): + tournament_1 = Tournament.objects.get(pk=1) + tournament_2 = Tournament.objects.get(pk=2) + # 2 queries here as pool id 3 has tournament 2, which is not cached + with self.assertNumQueries(2): + pools = tournament_1.pool_set.all() | Pool.objects.filter(pk=3) + related_objects = set(pool.tournament for pool in pools) + self.assertEqual(related_objects, set((tournament_1, tournament_2))) + # and the other direction + with self.assertNumQueries(2): + pools = Pool.objects.filter(pk=3) | tournament_1.pool_set.all() + related_objects = set(pool.tournament for pool in pools) + self.assertEqual(related_objects, set((tournament_1, tournament_2))) + + def test_queryset_and(self): + tournament = Tournament.objects.get(pk=1) + organiser = Organiser.objects.get(pk=1) + with self.assertNumQueries(1): + pools = tournament.pool_set.all() & organiser.pool_set.all() + first = pools.filter(pk=1)[0] + self.assertIs(first.tournament, tournament) + self.assertIs(first.organiser, organiser) + def test_one_to_one(self): with self.assertNumQueries(2): style = PoolStyle.objects.get(pk=1) From a7b7efe78d5964efbf54463316f2d88ad51c929c Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 2 Jan 2013 22:11:32 +0100 Subject: [PATCH 099/870] Minor fixes in the known_related_objects tests. * Fixed JSON indentation. * Avoided relying on implicit ordering. --- .../fixtures/tournament.json | 34 +++++++++---------- .../modeltests/known_related_objects/tests.py | 10 +++--- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/modeltests/known_related_objects/fixtures/tournament.json b/tests/modeltests/known_related_objects/fixtures/tournament.json index b8f053e152..bd4cbc17a7 100644 --- a/tests/modeltests/known_related_objects/fixtures/tournament.json +++ b/tests/modeltests/known_related_objects/fixtures/tournament.json @@ -4,22 +4,22 @@ "model": "known_related_objects.tournament", "fields": { "name": "Tourney 1" - } - }, + } + }, { "pk": 2, "model": "known_related_objects.tournament", "fields": { "name": "Tourney 2" - } - }, + } + }, { "pk": 1, "model": "known_related_objects.organiser", "fields": { "name": "Organiser 1" - } - }, + } + }, { "pk": 1, "model": "known_related_objects.pool", @@ -27,8 +27,8 @@ "tournament": 1, "organiser": 1, "name": "T1 Pool 1" - } - }, + } + }, { "pk": 2, "model": "known_related_objects.pool", @@ -36,8 +36,8 @@ "tournament": 1, "organiser": 1, "name": "T1 Pool 2" - } - }, + } + }, { "pk": 3, "model": "known_related_objects.pool", @@ -45,8 +45,8 @@ "tournament": 2, "organiser": 1, "name": "T2 Pool 1" - } - }, + } + }, { "pk": 4, "model": "known_related_objects.pool", @@ -54,23 +54,23 @@ "tournament": 2, "organiser": 1, "name": "T2 Pool 2" - } - }, + } + }, { "pk": 1, "model": "known_related_objects.poolstyle", "fields": { "name": "T1 Pool 2 Style", "pool": 2 - } - }, + } + }, { "pk": 2, "model": "known_related_objects.poolstyle", "fields": { "name": "T2 Pool 1 Style", "pool": 3 - } } + } ] diff --git a/tests/modeltests/known_related_objects/tests.py b/tests/modeltests/known_related_objects/tests.py index 2371ac2e20..d28d266557 100644 --- a/tests/modeltests/known_related_objects/tests.py +++ b/tests/modeltests/known_related_objects/tests.py @@ -21,7 +21,7 @@ class ExistingRelatedInstancesTests(TestCase): def test_foreign_key_multiple_prefetch(self): with self.assertNumQueries(2): - tournaments = list(Tournament.objects.prefetch_related('pool_set')) + tournaments = list(Tournament.objects.prefetch_related('pool_set').order_by('pk')) pool1 = tournaments[0].pool_set.all()[0] self.assertIs(tournaments[0], pool1.tournament) pool2 = tournaments[1].pool_set.all()[0] @@ -81,7 +81,7 @@ class ExistingRelatedInstancesTests(TestCase): def test_one_to_one_multi_select_related(self): with self.assertNumQueries(1): - poolstyles = list(PoolStyle.objects.select_related('pool')) + poolstyles = list(PoolStyle.objects.select_related('pool').order_by('pk')) self.assertIs(poolstyles[0], poolstyles[0].pool.poolstyle) self.assertIs(poolstyles[1], poolstyles[1].pool.poolstyle) @@ -93,7 +93,7 @@ class ExistingRelatedInstancesTests(TestCase): def test_one_to_one_multi_prefetch_related(self): with self.assertNumQueries(2): - poolstyles = list(PoolStyle.objects.prefetch_related('pool')) + poolstyles = list(PoolStyle.objects.prefetch_related('pool').order_by('pk')) self.assertIs(poolstyles[0], poolstyles[0].pool.poolstyle) self.assertIs(poolstyles[1], poolstyles[1].pool.poolstyle) @@ -117,12 +117,12 @@ class ExistingRelatedInstancesTests(TestCase): def test_reverse_one_to_one_multi_select_related(self): with self.assertNumQueries(1): - pools = list(Pool.objects.select_related('poolstyle')) + pools = list(Pool.objects.select_related('poolstyle').order_by('pk')) self.assertIs(pools[1], pools[1].poolstyle.pool) self.assertIs(pools[2], pools[2].poolstyle.pool) def test_reverse_one_to_one_multi_prefetch_related(self): with self.assertNumQueries(2): - pools = list(Pool.objects.prefetch_related('poolstyle')) + pools = list(Pool.objects.prefetch_related('poolstyle').order_by('pk')) self.assertIs(pools[1], pools[1].poolstyle.pool) self.assertIs(pools[2], pools[2].poolstyle.pool) From a051a9d9297fa0c14e000d3cb8f47606cd9ae8a8 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 2 Jan 2013 22:46:08 +0100 Subject: [PATCH 100/870] Fixed PR 478 -- Removed superfluous try/except block. --- django/db/models/query.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index 26b93b20bf..d1f519aaf8 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -209,12 +209,10 @@ class QuerySet(object): stop = None qs.query.set_limits(start, stop) return k.step and list(qs)[::k.step] or qs - try: - qs = self._clone() - qs.query.set_limits(k, k + 1) - return list(qs)[0] - except self.model.DoesNotExist as e: - raise IndexError(e.args) + + qs = self._clone() + qs.query.set_limits(k, k + 1) + return list(qs)[0] def __and__(self, other): self._merge_sanity_check(other) From 3f890f8dc707eac30a72b7f79981d79e17ba0ff4 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Thu, 3 Jan 2013 11:32:10 +1300 Subject: [PATCH 101/870] Update doc example for overriding change_form.html Slightly reworded another related paragraph for clarity, too. --- docs/ref/contrib/admin/index.txt | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index e72b2b79e9..ec63cb2dcc 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1790,31 +1790,32 @@ Because of the modular design of the admin templates, it is usually neither necessary nor advisable to replace an entire template. It is almost always better to override only the section of the template which you need to change. -To continue the example above, we want to add a new link next to the ``History`` -tool for the ``Page`` model. After looking at ``change_form.html`` we determine -that we only need to override the ``object-tools`` block. Therefore here is our -new ``change_form.html`` : +To continue the example above, we want to add a new link next to the +``History`` tool for the ``Page`` model. After looking at ``change_form.html`` +we determine that we only need to override the ``object-tools-items`` block. +Therefore here is our new ``change_form.html`` : .. code-block:: html+django {% extends "admin/change_form.html" %} - {% load i18n %} - {% block object-tools %} - {% if change %}{% if not is_popup %} - - {% endif %}{% endif %} {% endblock %} And that's it! If we placed this file in the ``templates/admin/my_app`` -directory, our link would appear on every model's change form. +directory, our link would appear on the change form for all models within +my_app. Templates which may be overridden per app or model -------------------------------------------------- From 9b5f64cc6ed5f1e904093fe4e6ff0f681b8e545f Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 1 Jan 2013 08:12:42 -0500 Subject: [PATCH 102/870] Fixed #19516 - Fixed remaining broken links. Added -n to sphinx builds to catch issues going forward. --- docs/Makefile | 2 +- docs/faq/usage.txt | 9 +- docs/howto/custom-model-fields.txt | 18 +- docs/howto/custom-template-tags.txt | 6 + .../writing-code/coding-style.txt | 6 +- docs/internals/deprecation.txt | 20 +- docs/intro/tutorial01.txt | 4 +- docs/intro/tutorial04.txt | 8 +- docs/make.bat | 2 +- docs/ref/class-based-views/base.txt | 34 ++-- .../ref/class-based-views/flattened-index.txt | 78 +++---- .../class-based-views/generic-date-based.txt | 2 +- .../ref/class-based-views/generic-display.txt | 40 ++-- .../ref/class-based-views/generic-editing.txt | 10 +- .../class-based-views/mixins-date-based.txt | 9 +- docs/ref/class-based-views/mixins-editing.txt | 43 ++-- .../mixins-multiple-object.txt | 22 +- docs/ref/class-based-views/mixins-simple.txt | 12 +- .../mixins-single-object.txt | 41 ++-- docs/ref/clickjacking.txt | 8 +- docs/ref/contrib/admin/admindocs.txt | 2 +- docs/ref/contrib/admin/index.txt | 26 ++- docs/ref/contrib/comments/custom.txt | 26 +-- docs/ref/contrib/comments/example.txt | 4 +- docs/ref/contrib/comments/moderation.txt | 9 +- docs/ref/contrib/comments/signals.txt | 4 +- docs/ref/contrib/contenttypes.txt | 8 +- docs/ref/contrib/flatpages.txt | 4 +- docs/ref/contrib/formtools/form-preview.txt | 16 +- docs/ref/contrib/formtools/form-wizard.txt | 32 ++- docs/ref/contrib/formtools/index.txt | 2 + docs/ref/contrib/gis/db-api.txt | 17 +- docs/ref/contrib/gis/feeds.txt | 10 +- docs/ref/contrib/gis/geoquerysets.txt | 4 +- docs/ref/contrib/gis/geos.txt | 14 +- docs/ref/contrib/gis/install/index.txt | 2 +- docs/ref/contrib/gis/tutorial.txt | 14 +- docs/ref/contrib/sitemaps.txt | 29 +-- docs/ref/contrib/staticfiles.txt | 10 +- docs/ref/contrib/syndication.txt | 2 +- docs/ref/databases.txt | 4 +- docs/ref/django-admin.txt | 2 + docs/ref/exceptions.txt | 15 ++ docs/ref/files/file.txt | 4 +- docs/ref/files/storage.txt | 2 +- docs/ref/forms/api.txt | 41 ++-- docs/ref/forms/widgets.txt | 12 +- docs/ref/middleware.txt | 4 +- docs/ref/models/fields.txt | 93 +++++---- docs/ref/models/options.txt | 2 +- docs/ref/request-response.txt | 2 +- docs/ref/settings.txt | 2 +- docs/ref/signals.txt | 11 +- docs/ref/template-response.txt | 24 +-- docs/ref/templates/api.txt | 22 +- docs/ref/templates/builtins.txt | 2 +- docs/ref/urls.txt | 2 - docs/ref/utils.txt | 19 +- docs/ref/validators.txt | 2 +- docs/releases/1.2-beta-1.txt | 2 +- docs/releases/1.2.txt | 10 +- docs/releases/1.3-alpha-1.txt | 2 +- docs/releases/1.3-beta-1.txt | 6 +- docs/releases/1.3.txt | 5 +- docs/releases/1.4-alpha-1.txt | 2 +- docs/releases/1.4-beta-1.txt | 2 +- docs/releases/1.4.txt | 4 +- docs/topics/auth/passwords.txt | 18 +- docs/topics/cache.txt | 8 +- .../class-based-views/generic-display.txt | 12 +- .../class-based-views/generic-editing.txt | 77 ++++--- docs/topics/class-based-views/mixins.txt | 192 +++++++++--------- docs/topics/db/sql.txt | 5 +- docs/topics/db/transactions.txt | 7 +- docs/topics/forms/formsets.txt | 2 + docs/topics/http/file-uploads.txt | 4 +- docs/topics/http/views.txt | 2 + docs/topics/i18n/timezones.txt | 4 +- docs/topics/logging.txt | 12 +- docs/topics/python3.txt | 73 ++++--- docs/topics/serialization.txt | 4 +- docs/topics/settings.txt | 3 +- docs/topics/testing/overview.txt | 8 +- 83 files changed, 727 insertions(+), 611 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index f6293a8e7f..2a8bcd7101 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -10,7 +10,7 @@ BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . diff --git a/docs/faq/usage.txt b/docs/faq/usage.txt index 151454398d..be3839e08f 100644 --- a/docs/faq/usage.txt +++ b/docs/faq/usage.txt @@ -52,10 +52,11 @@ Using a :class:`~django.db.models.FileField` or an #. All that will be stored in your database is a path to the file (relative to :setting:`MEDIA_ROOT`). You'll most likely want to use the - convenience :attr:`~django.core.files.File.url` attribute provided by - Django. For example, if your :class:`~django.db.models.ImageField` is - called ``mug_shot``, you can get the absolute path to your image in a - template with ``{{ object.mug_shot.url }}``. + convenience :attr:`~django.db.models.fields.files.FieldFile.url` attribute + provided by Django. For example, if your + :class:`~django.db.models.ImageField` is called ``mug_shot``, you can get + the absolute path to your image in a template with + ``{{ object.mug_shot.url }}``. How do I make a variable available to all my templates? ------------------------------------------------------- diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index e3dae840fc..7b5fe6349e 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -199,20 +199,20 @@ The :meth:`~django.db.models.Field.__init__` method takes the following parameters: * :attr:`~django.db.models.Field.verbose_name` -* :attr:`~django.db.models.Field.name` +* ``name`` * :attr:`~django.db.models.Field.primary_key` -* :attr:`~django.db.models.Field.max_length` +* :attr:`~django.db.models.CharField.max_length` * :attr:`~django.db.models.Field.unique` * :attr:`~django.db.models.Field.blank` * :attr:`~django.db.models.Field.null` * :attr:`~django.db.models.Field.db_index` -* :attr:`~django.db.models.Field.rel`: Used for related fields (like - :class:`ForeignKey`). For advanced use only. +* ``rel``: Used for related fields (like :class:`ForeignKey`). For advanced + use only. * :attr:`~django.db.models.Field.default` * :attr:`~django.db.models.Field.editable` -* :attr:`~django.db.models.Field.serialize`: If ``False``, the field will - not be serialized when the model is passed to Django's :doc:`serializers - `. Defaults to ``True``. +* ``serialize``: If ``False``, the field will not be serialized when the model + is passed to Django's :doc:`serializers `. Defaults to + ``True``. * :attr:`~django.db.models.Field.unique_for_date` * :attr:`~django.db.models.Field.unique_for_month` * :attr:`~django.db.models.Field.unique_for_year` @@ -222,7 +222,7 @@ parameters: * :attr:`~django.db.models.Field.db_tablespace`: Only for index creation, if the backend supports :doc:`tablespaces `. You can usually ignore this option. -* :attr:`~django.db.models.Field.auto_created`: True if the field was +* ``auto_created``: True if the field was automatically created, as for the `OneToOneField` used by model inheritance. For advanced use only. @@ -443,7 +443,7 @@ Python object type we want to store in the model's attribute. If anything is going wrong during value conversion, you should raise a :exc:`~django.core.exceptions.ValidationError` exception. -**Remember:** If your custom field needs the :meth:`to_python` method to be +**Remember:** If your custom field needs the :meth:`.to_python` method to be called when it is created, you should be using `The SubfieldBase metaclass`_ mentioned earlier. Otherwise :meth:`.to_python` won't be called automatically. diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 31fbc9e96c..0d35654a04 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -114,6 +114,8 @@ your function. Example: Registering custom filters ~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. method:: django.template.Library.filter + Once you've written your filter definition, you need to register it with your ``Library`` instance, to make it available to Django's template language: @@ -151,6 +153,8 @@ are described in :ref:`filters and auto-escaping ` and Template filters that expect strings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. method:: django.template.defaultfilters.stringfilter + If you're writing a template filter that only expects a string as the first argument, you should use the decorator ``stringfilter``. This will convert an object to its string value before being passed to your function: @@ -700,6 +704,8 @@ cannot resolve the string passed to it in the current context of the page. Simple tags ~~~~~~~~~~~ +.. method:: django.template.Library.simple_tag + Many template tags take a number of arguments -- strings or template variables -- and return a string after doing some processing based solely on the input arguments and some external information. For example, the diff --git a/docs/internals/contributing/writing-code/coding-style.txt b/docs/internals/contributing/writing-code/coding-style.txt index a699e39bd8..0d84cdac9a 100644 --- a/docs/internals/contributing/writing-code/coding-style.txt +++ b/docs/internals/contributing/writing-code/coding-style.txt @@ -177,9 +177,9 @@ That means that the ability for third parties to import the module at the top level is incompatible with the ability to configure the settings object manually, or makes it very difficult in some circumstances. -Instead of the above code, a level of laziness or indirection must be used, such -as :class:`django.utils.functional.LazyObject`, -:func:`django.utils.functional.lazy` or ``lambda``. +Instead of the above code, a level of laziness or indirection must be used, +such as ``django.utils.functional.LazyObject``, +``django.utils.functional.lazy()`` or ``lambda``. Miscellaneous ------------- diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index c976f5a880..faa6d1ff02 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -167,9 +167,8 @@ these changes. * ``django.core.context_processors.PermWrapper`` and ``django.core.context_processors.PermLookupDict`` will be removed in favor of the corresponding - :class:`django.contrib.auth.context_processors.PermWrapper` and - :class:`django.contrib.auth.context_processors.PermLookupDict`, - respectively. + ``django.contrib.auth.context_processors.PermWrapper`` and + ``django.contrib.auth.context_processors.PermLookupDict``, respectively. * The :setting:`MEDIA_URL` or :setting:`STATIC_URL` settings will be required to end with a trailing slash to ensure there is a consistent @@ -218,10 +217,10 @@ these changes. synonym for ``django.views.decorators.csrf.csrf_exempt``, which should be used to replace it. -* The :class:`~django.core.cache.backends.memcached.CacheClass` backend +* The ``django.core.cache.backends.memcached.CacheClass`` backend was split into two in Django 1.3 in order to introduce support for - PyLibMC. The historical :class:`~django.core.cache.backends.memcached.CacheClass` - will be removed in favor of :class:`~django.core.cache.backends.memcached.MemcachedCache`. + PyLibMC. The historical ``CacheClass`` will be removed in favor of + ``django.core.cache.backends.memcached.MemcachedCache``. * The UK-prefixed objects of ``django.contrib.localflavor.uk`` will only be accessible through their GB-prefixed names (GB is the correct @@ -243,8 +242,8 @@ these changes. :setting:`LOGGING` setting should include this filter explicitly if it is desired. -* The builtin truncation functions :func:`django.utils.text.truncate_words` - and :func:`django.utils.text.truncate_html_words` will be removed in +* The builtin truncation functions ``django.utils.text.truncate_words()`` + and ``django.utils.text.truncate_html_words()`` will be removed in favor of the ``django.utils.text.Truncator`` class. * The :class:`~django.contrib.gis.geoip.GeoIP` class was moved to @@ -257,9 +256,8 @@ these changes. :data:`~django.conf.urls.handler500`, are now available through :mod:`django.conf.urls` . -* The functions :func:`~django.core.management.setup_environ` and - :func:`~django.core.management.execute_manager` will be removed from - :mod:`django.core.management`. This also means that the old (pre-1.4) +* The functions ``setup_environ()`` and ``execute_manager()`` will be removed + from :mod:`django.core.management`. This also means that the old (pre-1.4) style of :file:`manage.py` file will no longer work. * Setting the ``is_safe`` and ``needs_autoescape`` flags as attributes of diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index ab6c8b999f..632f27f2d2 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -369,8 +369,8 @@ its human-readable name. Some :class:`~django.db.models.Field` classes have required elements. :class:`~django.db.models.CharField`, for example, requires that you give it a -:attr:`~django.db.models.Field.max_length`. That's used not only in the database -schema, but in validation, as we'll soon see. +:attr:`~django.db.models.CharField.max_length`. That's used not only in the +database schema, but in validation, as we'll soon see. Finally, note a relationship is defined, using :class:`~django.db.models.ForeignKey`. That tells Django each ``Choice`` is related diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 333ef9fbc3..f047067aa7 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -234,12 +234,12 @@ two views abstract the concepts of "display a list of objects" and * Each generic view needs to know what model it will be acting upon. This is provided using the ``model`` parameter. -* The :class:`~django.views.generic.list.DetailView` generic view +* The :class:`~django.views.generic.detail.DetailView` generic view expects the primary key value captured from the URL to be called ``"pk"``, so we've changed ``poll_id`` to ``pk`` for the generic views. -By default, the :class:`~django.views.generic.list.DetailView` generic +By default, the :class:`~django.views.generic.detail.DetailView` generic view uses a template called ``/_detail.html``. In our case, it'll use the template ``"polls/poll_detail.html"``. The ``template_name`` argument is used to tell Django to use a specific @@ -247,7 +247,7 @@ template name instead of the autogenerated default template name. We also specify the ``template_name`` for the ``results`` list view -- this ensures that the results view and the detail view have a different appearance when rendered, even though they're both a -:class:`~django.views.generic.list.DetailView` behind the scenes. +:class:`~django.views.generic.detail.DetailView` behind the scenes. Similarly, the :class:`~django.views.generic.list.ListView` generic view uses a default template called ``/_list.html``; we use ``template_name`` to tell In previous parts of the tutorial, the templates have been provided with a context that contains the ``poll`` and ``latest_poll_list`` -context variables. For DetailView the ``poll`` variable is provided +context variables. For ``DetailView`` the ``poll`` variable is provided automatically -- since we're using a Django model (``Poll``), Django is able to determine an appropriate name for the context variable. However, for ListView, the automatically generated context variable is diff --git a/docs/make.bat b/docs/make.bat index d7f54b2059..65602aa160 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -6,7 +6,7 @@ if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set ALLSPHINXOPTS=-n -d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index cc9aa852f1..c070ea707a 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -49,9 +49,13 @@ View **Attributes** - .. attribute:: http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] + .. attribute:: http_method_names - The default list of HTTP method names that this view will accept. + The list of HTTP method names that this view will accept. + + Default:: + + ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] **Methods** @@ -68,12 +72,11 @@ View The default implementation will inspect the HTTP method and attempt to delegate to a method that matches the HTTP method; a ``GET`` will be - delegated to :meth:`~View.get()`, a ``POST`` to :meth:`~View.post()`, - and so on. + delegated to ``get()``, a ``POST`` to ``post()``, and so on. - By default, a ``HEAD`` request will be delegated to :meth:`~View.get()`. + By default, a ``HEAD`` request will be delegated to ``get()``. If you need to handle ``HEAD`` requests in a different way than ``GET``, - you can override the :meth:`~View.head()` method. See + you can override the ``head()`` method. See :ref:`supporting-other-http-methods` for an example. The default implementation also sets ``request``, ``args`` and @@ -111,9 +114,9 @@ TemplateView **Method Flowchart** - 1. :meth:`dispatch()` - 2. :meth:`http_method_not_allowed()` - 3. :meth:`get_context_data()` + 1. :meth:`~django.views.generic.base.View.dispatch()` + 2. :meth:`~django.views.generic.base.View.http_method_not_allowed()` + 3. :meth:`~django.views.generic.base.ContextMixin.get_context_data()` **Example views.py**:: @@ -169,8 +172,8 @@ RedirectView **Method Flowchart** - 1. :meth:`dispatch()` - 2. :meth:`http_method_not_allowed()` + 1. :meth:`~django.views.generic.base.View.dispatch()` + 2. :meth:`~django.views.generic.base.View.http_method_not_allowed()` 3. :meth:`get_redirect_url()` **Example views.py**:: @@ -230,9 +233,8 @@ RedirectView Constructs the target URL for redirection. - The default implementation uses :attr:`~RedirectView.url` as a starting + The default implementation uses :attr:`url` as a starting string, performs expansion of ``%`` parameters in that string, as well - as the appending of query string if requested by - :attr:`~RedirectView.query_string`. Subclasses may implement any - behavior they wish, as long as the method returns a redirect-ready URL - string. + as the appending of query string if requested by :attr:`query_string`. + Subclasses may implement any behavior they wish, as long as the method + returns a redirect-ready URL string. diff --git a/docs/ref/class-based-views/flattened-index.txt b/docs/ref/class-based-views/flattened-index.txt index aa2f51f156..2e75363c58 100644 --- a/docs/ref/class-based-views/flattened-index.txt +++ b/docs/ref/class-based-views/flattened-index.txt @@ -23,7 +23,7 @@ View * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` TemplateView @@ -40,9 +40,9 @@ TemplateView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.base.TemplateView.get` -* :meth:`~django.views.generic.base.TemplateView.get_context_data` -* :meth:`~django.views.generic.base.View.head` +* ``get()`` +* :meth:`~django.views.generic.base.ContextMixin.get_context_data` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -60,15 +60,15 @@ RedirectView **Methods** * :meth:`~django.views.generic.base.View.as_view` -* :meth:`~django.views.generic.base.RedirectView.delete` +* ``delete()`` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.base.RedirectView.get` +* ``get()`` * :meth:`~django.views.generic.base.RedirectView.get_redirect_url` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` -* :meth:`~django.views.generic.base.RedirectView.options` -* :meth:`~django.views.generic.base.RedirectView.post` -* :meth:`~django.views.generic.base.RedirectView.put` +* ``options()`` +* ``post()`` +* ``put()`` Detail Views ------------ @@ -95,10 +95,10 @@ DetailView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.detail.BaseDetailView.get` +* ``get()`` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -130,7 +130,7 @@ ListView * :meth:`~django.views.generic.list.BaseListView.get` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -161,10 +161,10 @@ FormView * :meth:`~django.views.generic.edit.FormMixin.get_context_data` * :meth:`~django.views.generic.edit.FormMixin.get_form` * :meth:`~django.views.generic.edit.FormMixin.get_form_kwargs` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` -* :meth:`~django.views.generic.edit.ProcessFormView.post` -* :meth:`~django.views.generic.edit.ProcessFormView.put` +* ``post()`` +* ``put()`` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` CreateView @@ -199,10 +199,10 @@ CreateView * :meth:`~django.views.generic.edit.FormMixin.get_form` * :meth:`~django.views.generic.edit.FormMixin.get_form_kwargs` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.edit.ProcessFormView.post` -* :meth:`~django.views.generic.edit.ProcessFormView.put` +* ``put()`` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` UpdateView @@ -237,10 +237,10 @@ UpdateView * :meth:`~django.views.generic.edit.FormMixin.get_form` * :meth:`~django.views.generic.edit.FormMixin.get_form_kwargs` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.edit.ProcessFormView.post` -* :meth:`~django.views.generic.edit.ProcessFormView.put` +* ``put()`` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` DeleteView @@ -265,14 +265,14 @@ DeleteView **Methods** * :meth:`~django.views.generic.base.View.as_view` -* :meth:`~django.views.generic.edit.DeletionMixin.delete` +* ``delete()`` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.detail.BaseDetailView.get` +* ``get()`` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` -* :meth:`~django.views.generic.edit.DeletionMixin.post` +* ``post()`` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` Date-based views @@ -302,13 +302,13 @@ ArchiveIndexView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -324,7 +324,7 @@ YearArchiveView * :attr:`~django.views.generic.list.MultipleObjectMixin.context_object_name` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name`] * :attr:`~django.views.generic.dates.DateMixin.date_field` [:meth:`~django.views.generic.dates.DateMixin.get_date_field`] * :attr:`~django.views.generic.base.View.http_method_names` -* :attr:`~django.views.generic.dates.BaseYearArchiveView.make_object_list` [:meth:`~django.views.generic.dates.BaseYearArchiveView.get_make_object_list`] +* :attr:`~django.views.generic.dates.YearArchiveView.make_object_list` [:meth:`~django.views.generic.dates.YearArchiveView.get_make_object_list`] * :attr:`~django.views.generic.list.MultipleObjectMixin.model` * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`] @@ -340,13 +340,13 @@ YearArchiveView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -379,7 +379,7 @@ MonthArchiveView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` @@ -387,7 +387,7 @@ MonthArchiveView * :meth:`~django.views.generic.dates.MonthMixin.get_next_month` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` * :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -420,13 +420,13 @@ WeekArchiveView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -461,7 +461,7 @@ DayArchiveView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` @@ -471,7 +471,7 @@ DayArchiveView * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` * :meth:`~django.views.generic.dates.DayMixin.get_previous_day` * :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -506,7 +506,7 @@ TodayArchiveView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` @@ -516,7 +516,7 @@ TodayArchiveView * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` * :meth:`~django.views.generic.dates.DayMixin.get_previous_day` * :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -551,13 +551,13 @@ DateDetailView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.detail.BaseDetailView.get` +* ``get()`` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.DayMixin.get_next_day` * :meth:`~django.views.generic.dates.MonthMixin.get_next_month` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` * :meth:`~django.views.generic.dates.DayMixin.get_previous_day` * :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt index 0ae0bcdf42..42dbab4dd8 100644 --- a/docs/ref/class-based-views/generic-date-based.txt +++ b/docs/ref/class-based-views/generic-date-based.txt @@ -580,7 +580,7 @@ DateDetailView * :class:`django.views.generic.dates.MonthMixin` * :class:`django.views.generic.dates.DayMixin` * :class:`django.views.generic.dates.DateMixin` - * :class:`django.views.generic.detail.BaseDetailView` + * ``django.views.generic.detail.BaseDetailView`` * :class:`django.views.generic.detail.SingleObjectMixin` * :class:`django.views.generic.base.View` diff --git a/docs/ref/class-based-views/generic-display.txt b/docs/ref/class-based-views/generic-display.txt index 12603ff0df..b827c0005c 100644 --- a/docs/ref/class-based-views/generic-display.txt +++ b/docs/ref/class-based-views/generic-display.txt @@ -19,22 +19,22 @@ DetailView * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` - * :class:`django.views.generic.detail.BaseDetailView` + * ``django.views.generic.detail.BaseDetailView`` * :class:`django.views.generic.detail.SingleObjectMixin` * :class:`django.views.generic.base.View` **Method Flowchart** - 1. :meth:`dispatch()` - 2. :meth:`http_method_not_allowed()` - 3. :meth:`get_template_names()` - 4. :meth:`get_slug_field()` - 5. :meth:`get_queryset()` - 6. :meth:`get_object()` - 7. :meth:`get_context_object_name()` - 8. :meth:`get_context_data()` - 9. :meth:`get()` - 10. :meth:`render_to_response()` + 1. :meth:`~django.views.generic.base.View.dispatch()` + 2. :meth:`~django.views.generic.base.View.http_method_not_allowed()` + 3. :meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` + 4. :meth:`~django.views.generic.detail.SingleObjectMixin.get_slug_field()` + 5. :meth:`~django.views.generic.detail.SingleObjectMixin.get_queryset()` + 6. :meth:`~django.views.generic.detail.SingleObjectMixin.get_object()` + 7. :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_object_name()` + 8. :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data()` + 9. ``get()`` + 10. :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()` **Example views.py**:: @@ -86,14 +86,14 @@ ListView **Method Flowchart** - 1. :meth:`dispatch()` - 2. :meth:`http_method_not_allowed()` - 3. :meth:`get_template_names()` - 4. :meth:`get_queryset()` - 5. :meth:`get_objects()` - 6. :meth:`get_context_data()` - 7. :meth:`get()` - 8. :meth:`render_to_response()` + 1. :meth:`~django.views.generic.base.View.dispatch()` + 2. :meth:`~django.views.generic.base.View.http_method_not_allowed()` + 3. :meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` + 4. :meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset()` + 5. :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name()` + 6. :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data()` + 7. ``get()`` + 8. :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()` **Example views.py**:: @@ -140,7 +140,7 @@ ListView .. method:: get(request, *args, **kwargs) - Adds :attr:`object_list` to the context. If + Adds ``object_list`` to the context. If :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` is True then display an empty list. If :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` is diff --git a/docs/ref/class-based-views/generic-editing.txt b/docs/ref/class-based-views/generic-editing.txt index 789dc2f84f..f3679287ad 100644 --- a/docs/ref/class-based-views/generic-editing.txt +++ b/docs/ref/class-based-views/generic-editing.txt @@ -38,7 +38,7 @@ FormView * :class:`django.views.generic.edit.FormView` * :class:`django.views.generic.base.TemplateResponseMixin` - * :class:`django.views.generic.edit.BaseFormView` + * ``django.views.generic.edit.BaseFormView`` * :class:`django.views.generic.edit.FormMixin` * :class:`django.views.generic.edit.ProcessFormView` * :class:`django.views.generic.base.View` @@ -86,7 +86,7 @@ CreateView * :class:`django.views.generic.edit.CreateView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` - * :class:`django.views.generic.edit.BaseCreateView` + * ``django.views.generic.edit.BaseCreateView`` * :class:`django.views.generic.edit.ModelFormMixin` * :class:`django.views.generic.edit.FormMixin` * :class:`django.views.generic.detail.SingleObjectMixin` @@ -128,7 +128,7 @@ UpdateView * :class:`django.views.generic.edit.UpdateView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` - * :class:`django.views.generic.edit.BaseUpdateView` + * ``django.views.generic.edit.BaseUpdateView`` * :class:`django.views.generic.edit.ModelFormMixin` * :class:`django.views.generic.edit.FormMixin` * :class:`django.views.generic.detail.SingleObjectMixin` @@ -170,9 +170,9 @@ DeleteView * :class:`django.views.generic.edit.DeleteView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` - * :class:`django.views.generic.edit.BaseDeleteView` + * ``django.views.generic.edit.BaseDeleteView`` * :class:`django.views.generic.edit.DeletionMixin` - * :class:`django.views.generic.detail.BaseDetailView` + * ``django.views.generic.detail.BaseDetailView`` * :class:`django.views.generic.detail.SingleObjectMixin` * :class:`django.views.generic.base.View` diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index 561e525e70..7ff201e5a2 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -100,7 +100,7 @@ MonthMixin :attr:`~BaseDateListView.allow_empty` and :attr:`~DateMixin.allow_future`. - .. method:: get_prev_month(date) + .. method:: get_previous_month(date) Returns a date object containing the first day of the month before the date provided. This function can also return ``None`` or raise an @@ -152,7 +152,7 @@ DayMixin :attr:`~BaseDateListView.allow_empty` and :attr:`~DateMixin.allow_future`. - .. method:: get_prev_day(date) + .. method:: get_previous_day(date) Returns a date object containing the previous valid day. This function can also return ``None`` or raise an :class:`~django.http.Http404` @@ -287,8 +287,9 @@ BaseDateListView available. If this is ``True`` and no objects are available, the view will display an empty page instead of raising a 404. - This is identical to :attr:`MultipleObjectMixin.allow_empty`, except - for the default value, which is ``False``. + This is identical to + :attr:`django.views.generic.list.MultipleObjectMixin.allow_empty`, + except for the default value, which is ``False``. .. attribute:: date_list_period diff --git a/docs/ref/class-based-views/mixins-editing.txt b/docs/ref/class-based-views/mixins-editing.txt index b8b59b827f..bce3c84cb1 100644 --- a/docs/ref/class-based-views/mixins-editing.txt +++ b/docs/ref/class-based-views/mixins-editing.txt @@ -83,9 +83,8 @@ FormMixin .. note:: - Views mixing :class:`FormMixin` must provide an implementation of - :meth:`~django.views.generic.FormMixin.form_valid` and - :meth:`~django.views.generic.FormMixin.form_invalid`. + Views mixing ``FormMixin`` must provide an implementation of + :meth:`form_valid` and :meth:`form_invalid`. ModelFormMixin @@ -93,15 +92,16 @@ ModelFormMixin .. class:: django.views.generic.edit.ModelFormMixin - A form mixin that works on ModelForms, rather than a standalone form. + A form mixin that works on ``ModelForms``, rather than a standalone form. Since this is a subclass of :class:`~django.views.generic.detail.SingleObjectMixin`, instances of this - mixin have access to the :attr:`~SingleObjectMixin.model` and - :attr:`~SingleObjectMixin.queryset` attributes, describing the type of - object that the ModelForm is manipulating. The view also provides - ``self.object``, the instance being manipulated. If the instance is being - created, ``self.object`` will be ``None``. + mixin have access to the + :attr:`~django.views.generic.detail.SingleObjectMixin.model` and + :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` attributes, + describing the type of object that the ``ModelForm`` is manipulating. The + view also provides ``self.object``, the instance being manipulated. If the + instance is being created, ``self.object`` will be ``None``. **Mixins** @@ -110,6 +110,12 @@ ModelFormMixin **Methods and Attributes** + .. attribute:: model + + A model class. Can be explicitly provided, otherwise will be determined + by examining ``self.object`` or + :attr:`~django.views.generic.detail.SingleObjectMixin.queryset`. + .. attribute:: success_url The URL to redirect to when the form is successfully processed. @@ -122,22 +128,25 @@ ModelFormMixin .. method:: get_form_class() Retrieve the form class to instantiate. If - :attr:`FormMixin.form_class` is provided, that class will be used. - Otherwise, a ModelForm will be instantiated using the model associated - with the :attr:`~SingleObjectMixin.queryset`, or with the - :attr:`~SingleObjectMixin.model`, depending on which attribute is - provided. + :attr:`~django.views.generic.edit.FormMixin.form_class` is provided, + that class will be used. Otherwise, a ``ModelForm`` will be + instantiated using the model associated with the + :attr:`~django.views.generic.detail.SingleObjectMixin.queryset`, or + with the :attr:`~django.views.generic.detail.SingleObjectMixin.model`, + depending on which attribute is provided. .. method:: get_form_kwargs() Add the current instance (``self.object``) to the standard - :meth:`FormMixin.get_form_kwargs`. + :meth:`~django.views.generic.edit.FormMixin.get_form_kwargs`. .. method:: get_success_url() Determine the URL to redirect to when the form is successfully - validated. Returns :attr:`ModelFormMixin.success_url` if it is provided; - otherwise, attempts to use the ``get_absolute_url()`` of the object. + validated. Returns + :attr:`django.views.generic.edit.ModelFormMixin.success_url` if it is + provided; otherwise, attempts to use the ``get_absolute_url()`` of the + object. .. method:: form_valid(form) diff --git a/docs/ref/class-based-views/mixins-multiple-object.txt b/docs/ref/class-based-views/mixins-multiple-object.txt index c85c962bce..b28bd11a71 100644 --- a/docs/ref/class-based-views/mixins-multiple-object.txt +++ b/docs/ref/class-based-views/mixins-multiple-object.txt @@ -61,14 +61,13 @@ MultipleObjectMixin .. attribute:: queryset A ``QuerySet`` that represents the objects. If provided, the value of - :attr:`MultipleObjectMixin.queryset` supersedes the value provided for - :attr:`MultipleObjectMixin.model`. + ``queryset`` supersedes the value provided for :attr:`model`. .. attribute:: paginate_by An integer specifying how many objects should be displayed per page. If this is given, the view will paginate objects with - :attr:`MultipleObjectMixin.paginate_by` objects per page. The view will + ``paginate_by`` objects per page. The view will expect either a ``page`` query string parameter (via ``request.GET``) or a ``page`` variable specified in the URLconf. @@ -77,10 +76,9 @@ MultipleObjectMixin .. versionadded:: 1.6 An integer specifying the number of "overflow" objects the last page - can contain. This extends the :attr:`MultipleObjectMixin.paginate_by` - limit on the last page by up to - :attr:`MultipleObjectMixin.paginate_orphans`, in order to keep the last - page from having a very small number of objects. + can contain. This extends the :attr:`paginate_by` limit on the last + page by up to ``paginate_orphans``, in order to keep the last page from + having a very small number of objects. .. attribute:: page_kwarg @@ -97,7 +95,7 @@ MultipleObjectMixin :class:`django.core.paginator.Paginator` is used. If the custom paginator class doesn't have the same constructor interface as :class:`django.core.paginator.Paginator`, you will also need to - provide an implementation for :meth:`MultipleObjectMixin.get_paginator`. + provide an implementation for :meth:`get_paginator`. .. attribute:: context_object_name @@ -122,20 +120,20 @@ MultipleObjectMixin Returns the number of items to paginate by, or ``None`` for no pagination. By default this simply returns the value of - :attr:`MultipleObjectMixin.paginate_by`. + :attr:`paginate_by`. .. method:: get_paginator(queryset, per_page, orphans=0, allow_empty_first_page=True) Returns an instance of the paginator to use for this view. By default, instantiates an instance of :attr:`paginator_class`. - .. method:: get_paginate_by() + .. method:: get_paginate_orphans() .. versionadded:: 1.6 An integer specifying the number of "overflow" objects the last page can contain. By default this simply returns the value of - :attr:`MultipleObjectMixin.paginate_orphans`. + :attr:`paginate_orphans`. .. method:: get_allow_empty() @@ -149,7 +147,7 @@ MultipleObjectMixin Return the context variable name that will be used to contain the list of data that this view is manipulating. If ``object_list`` is a queryset of Django objects and - :attr:`~MultipleObjectMixin.context_object_name` is not set, + :attr:`context_object_name` is not set, the context name will be the ``object_name`` of the model that the queryset is composed from, with postfix ``'_list'`` appended. For example, the model ``Article`` would have a diff --git a/docs/ref/class-based-views/mixins-simple.txt b/docs/ref/class-based-views/mixins-simple.txt index d2f0df241e..e2e6084e8e 100644 --- a/docs/ref/class-based-views/mixins-simple.txt +++ b/docs/ref/class-based-views/mixins-simple.txt @@ -48,7 +48,7 @@ TemplateResponseMixin .. attribute:: template_name The full name of a template to use as defined by a string. Not defining - a template_name will raise a + a ``template_name`` will raise a :class:`django.core.exceptions.ImproperlyConfigured` exception. .. attribute:: response_class @@ -73,15 +73,13 @@ TemplateResponseMixin If any keyword arguments are provided, they will be passed to the constructor of the response class. - Calls :meth:`~TemplateResponseMixin.get_template_names()` to obtain the - list of template names that will be searched looking for an existent - template. + Calls :meth:`get_template_names()` to obtain the list of template names + that will be searched looking for an existent template. .. method:: get_template_names() Returns a list of template names to search for when rendering the template. - If :attr:`TemplateResponseMixin.template_name` is specified, the - default implementation will return a list containing - :attr:`TemplateResponseMixin.template_name` (if it is specified). + If :attr:`template_name` is specified, the default implementation will + return a list containing :attr:`template_name` (if it is specified). diff --git a/docs/ref/class-based-views/mixins-single-object.txt b/docs/ref/class-based-views/mixins-single-object.txt index e84ba6b8dd..299ac56ac6 100644 --- a/docs/ref/class-based-views/mixins-single-object.txt +++ b/docs/ref/class-based-views/mixins-single-object.txt @@ -21,8 +21,7 @@ SingleObjectMixin .. attribute:: queryset A ``QuerySet`` that represents the objects. If provided, the value of - :attr:`SingleObjectMixin.queryset` supersedes the value provided for - :attr:`SingleObjectMixin.model`. + ``queryset`` supersedes the value provided for :attr:`model`. .. attribute:: slug_field @@ -47,38 +46,38 @@ SingleObjectMixin Returns the single object that this view will display. If ``queryset`` is provided, that queryset will be used as the - source of objects; otherwise, - :meth:`~SingleObjectMixin.get_queryset` will be used. - ``get_object()`` looks for a - :attr:`SingleObjectMixin.pk_url_kwarg` argument in the arguments - to the view; if this argument is found, this method performs a - primary-key based lookup using that value. If this argument is not - found, it looks for a :attr:`SingleObjectMixin.slug_url_kwarg` - argument, and performs a slug lookup using the - :attr:`SingleObjectMixin.slug_field`. + source of objects; otherwise, :meth:`get_queryset` will be used. + ``get_object()`` looks for a :attr:`pk_url_kwarg` argument in the + arguments to the view; if this argument is found, this method performs + a primary-key based lookup using that value. If this argument is not + found, it looks for a :attr:`slug_url_kwarg` argument, and performs a + slug lookup using the :attr:`slug_field`. .. method:: get_queryset() Returns the queryset that will be used to retrieve the object that - this view will display. By default, - :meth:`~SingleObjectMixin.get_queryset` returns the value of the - :attr:`~SingleObjectMixin.queryset` attribute if it is set, otherwise - it constructs a :class:`QuerySet` by calling the `all()` method on the - :attr:`~SingleObjectMixin.model` attribute's default manager. + this view will display. By default, :meth:`get_queryset` returns the + value of the :attr:`queryset` attribute if it is set, otherwise + it constructs a :class:`~django.db.models.query.QuerySet` by calling + the `all()` method on the :attr:`model` attribute's default manager. .. method:: get_context_object_name(obj) Return the context variable name that will be used to contain the - data that this view is manipulating. If - :attr:`~SingleObjectMixin.context_object_name` is not set, the context - name will be constructed from the ``object_name`` of the model that - the queryset is composed from. For example, the model ``Article`` - would have context object named ``'article'``. + data that this view is manipulating. If :attr:`context_object_name` is + not set, the context name will be constructed from the ``object_name`` + of the model that the queryset is composed from. For example, the model + ``Article`` would have context object named ``'article'``. .. method:: get_context_data(**kwargs) Returns context data for displaying the list of objects. + .. method:: get_slug_field() + + Returns the name of a slug field to be used to look up by slug. By + default this simply returns the value of :attr:`slug_field`. + **Context** * ``object``: The object that this view is displaying. If diff --git a/docs/ref/clickjacking.txt b/docs/ref/clickjacking.txt index 15e85b43b7..e3d1bfc87b 100644 --- a/docs/ref/clickjacking.txt +++ b/docs/ref/clickjacking.txt @@ -111,10 +111,10 @@ Browsers that support X-Frame-Options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Internet Explorer 8+ -* Firefox 3.6.9+ -* Opera 10.5+ -* Safari 4+ -* Chrome 4.1+ +* Firefox 3.6.9+ +* Opera 10.5+ +* Safari 4+ +* Chrome 4.1+ See also ~~~~~~~~ diff --git a/docs/ref/contrib/admin/admindocs.txt b/docs/ref/contrib/admin/admindocs.txt index 4a50856f3d..b3e26eca48 100644 --- a/docs/ref/contrib/admin/admindocs.txt +++ b/docs/ref/contrib/admin/admindocs.txt @@ -24,7 +24,7 @@ the following: * Add :mod:`django.contrib.admindocs` to your :setting:`INSTALLED_APPS`. * Add ``(r'^admin/doc/', include('django.contrib.admindocs.urls'))`` to - your :data:`urlpatterns`. Make sure it's included *before* the + your ``urlpatterns``. Make sure it's included *before* the ``r'^admin/'`` entry, so that requests to ``/admin/doc/`` don't get handled by the latter entry. * Install the docutils Python module (http://docutils.sf.net/). diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index ec63cb2dcc..04a7824417 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -170,7 +170,7 @@ subclass:: ``fields`` option (for more complex layout needs see the :attr:`~ModelAdmin.fieldsets` option described in the next section). For example, you could define a simpler version of the admin form for the - ``django.contrib.flatpages.FlatPage`` model as follows:: + :class:`django.contrib.flatpages.models.FlatPage` model as follows:: class FlatPageAdmin(admin.ModelAdmin): fields = ('url', 'title', 'content') @@ -212,8 +212,8 @@ subclass:: a dictionary of information about the fieldset, including a list of fields to be displayed in it. - A full example, taken from the :class:`django.contrib.flatpages.FlatPage` - model:: + A full example, taken from the + :class:`django.contrib.flatpages.models.FlatPage` model:: class FlatPageAdmin(admin.ModelAdmin): fieldsets = ( @@ -357,7 +357,7 @@ subclass:: Note that the key in the dictionary is the actual field class, *not* a string. The value is another dictionary; these arguments will be passed to - :meth:`~django.forms.Field.__init__`. See :doc:`/ref/forms/api` for + the form field's ``__init__()`` method. See :doc:`/ref/forms/api` for details. .. warning:: @@ -584,7 +584,7 @@ subclass:: class PersonAdmin(UserAdmin): list_filter = ('company__name',) - * a class inheriting from :mod:`django.contrib.admin.SimpleListFilter`, + * a class inheriting from ``django.contrib.admin.SimpleListFilter``, which you need to provide the ``title`` and ``parameter_name`` attributes to and override the ``lookups`` and ``queryset`` methods, e.g.:: @@ -671,7 +671,7 @@ subclass:: * a tuple, where the first element is a field name and the second element is a class inheriting from - :mod:`django.contrib.admin.FieldListFilter`, for example:: + ``django.contrib.admin.FieldListFilter``, for example:: from django.contrib.admin import BooleanFieldListFilter @@ -943,10 +943,9 @@ templates used by the :class:`ModelAdmin` views: .. attribute:: ModelAdmin.delete_selected_confirmation_template - Path to a custom template, used by the :meth:`delete_selected` - action method for displaying a confirmation page when deleting one - or more objects. See the :doc:`actions - documentation`. + Path to a custom template, used by the ``delete_selected`` action method + for displaying a confirmation page when deleting one or more objects. See + the :doc:`actions documentation`. .. attribute:: ModelAdmin.object_history_template @@ -1108,9 +1107,8 @@ templates used by the :class:`ModelAdmin` views: Since this is usually not what you want, Django provides a convenience wrapper to check permissions and mark the view as non-cacheable. This - wrapper is :meth:`AdminSite.admin_view` (i.e. - ``self.admin_site.admin_view`` inside a ``ModelAdmin`` instance); use it - like so:: + wrapper is ``AdminSite.admin_view()`` (i.e. ``self.admin_site.admin_view`` + inside a ``ModelAdmin`` instance); use it like so:: class MyModelAdmin(admin.ModelAdmin): def get_urls(self): @@ -1130,7 +1128,7 @@ templates used by the :class:`ModelAdmin` views: If the page is cacheable, but you still want the permission check to be performed, you can pass a ``cacheable=True`` argument to - :meth:`AdminSite.admin_view`:: + ``AdminSite.admin_view()``:: (r'^my_view/$', self.admin_site.admin_view(self.my_view, cacheable=True)) diff --git a/docs/ref/contrib/comments/custom.txt b/docs/ref/contrib/comments/custom.txt index 0ef37a9a0b..b4ab65bc2d 100644 --- a/docs/ref/contrib/comments/custom.txt +++ b/docs/ref/contrib/comments/custom.txt @@ -66,15 +66,17 @@ In the ``models.py`` we'll define a ``CommentWithTitle`` model:: class CommentWithTitle(Comment): title = models.CharField(max_length=300) -Most custom comment models will subclass the :class:`Comment` model. However, +Most custom comment models will subclass the +:class:`~django.contrib.comments.models.Comment` model. However, if you want to substantially remove or change the fields available in the -:class:`Comment` model, but don't want to rewrite the templates, you could -try subclassing from :class:`BaseCommentAbstractModel`. +:class:`~django.contrib.comments.models.Comment` model, but don't want to +rewrite the templates, you could try subclassing from +``BaseCommentAbstractModel``. Next, we'll define a custom comment form in ``forms.py``. This is a little more tricky: we have to both create a form and override -:meth:`CommentForm.get_comment_model` and -:meth:`CommentForm.get_comment_create_data` to return deal with our custom title +``CommentForm.get_comment_model()`` and +``CommentForm.get_comment_create_data()`` to return deal with our custom title field:: from django import forms @@ -139,7 +141,7 @@ however. Return the :class:`~django.db.models.Model` class to use for comments. This model should inherit from - :class:`django.contrib.comments.models.BaseCommentAbstractModel`, which + ``django.contrib.comments.models.BaseCommentAbstractModel``, which defines necessary core fields. The default implementation returns @@ -170,33 +172,33 @@ however. attribute when rendering your comment form. The default implementation returns a reverse-resolved URL pointing - to the :func:`post_comment` view. + to the ``post_comment()`` view. .. note:: If you provide a custom comment model and/or form, but you - want to use the default :func:`post_comment` view, you will + want to use the default ``post_comment()`` view, you will need to be aware that it requires the model and form to have certain additional attributes and methods: see the - :func:`post_comment` view documentation for details. + ``django.contrib.comments.views.post_comment()`` view for details. .. function:: get_flag_url() Return the URL for the "flag this comment" view. The default implementation returns a reverse-resolved URL pointing - to the :func:`django.contrib.comments.views.moderation.flag` view. + to the ``django.contrib.comments.views.moderation.flag()`` view. .. function:: get_delete_url() Return the URL for the "delete this comment" view. The default implementation returns a reverse-resolved URL pointing - to the :func:`django.contrib.comments.views.moderation.delete` view. + to the ``django.contrib.comments.views.moderation.delete()`` view. .. function:: get_approve_url() Return the URL for the "approve this comment from moderation" view. The default implementation returns a reverse-resolved URL pointing - to the :func:`django.contrib.comments.views.moderation.approve` view. + to the ``django.contrib.comments.views.moderation.approve()`` view. diff --git a/docs/ref/contrib/comments/example.txt b/docs/ref/contrib/comments/example.txt index 2bff778c2f..4e18e37de0 100644 --- a/docs/ref/contrib/comments/example.txt +++ b/docs/ref/contrib/comments/example.txt @@ -136,7 +136,7 @@ Feeds ===== Suppose you want to export a :doc:`feed ` of the -latest comments, you can use the built-in :class:`LatestCommentFeed`. Just +latest comments, you can use the built-in ``LatestCommentFeed``. Just enable it in your project's ``urls.py``: .. code-block:: python @@ -166,7 +166,7 @@ features (all of which or only certain can be enabled): * Close comments after a particular (user-defined) number of days. * Email new comments to the site-staff. -To enable comment moderation, we subclass the :class:`CommentModerator` and +To enable comment moderation, we subclass the ``CommentModerator`` and register it with the moderation features we want. Let's suppose we want to close comments after 7 days of posting and also send out an email to the site staff. In ``blog/models.py``, we register a comment moderator in the diff --git a/docs/ref/contrib/comments/moderation.txt b/docs/ref/contrib/comments/moderation.txt index c042971d39..a7138dda53 100644 --- a/docs/ref/contrib/comments/moderation.txt +++ b/docs/ref/contrib/comments/moderation.txt @@ -185,15 +185,14 @@ via two methods: be moderated using the options defined in the ``CommentModerator`` subclass. If any of the models are already registered for moderation, the exception - :exc:`AlreadyModerated` will be raised. + ``AlreadyModerated`` will be raised. .. function:: moderator.unregister(model_or_iterable) Takes one argument: a model class or list of model classes, and removes the model or models from the set of models which are being moderated. If any of the models are not currently - being moderated, the exception - :exc:`NotModerated` will be raised. + being moderated, the exception ``NotModerated`` will be raised. Customizing the moderation system @@ -207,8 +206,8 @@ models with an instance of the subclass. .. class:: Moderator - In addition to the :meth:`Moderator.register` and - :meth:`Moderator.unregister` methods detailed above, the following methods + In addition to the :func:`moderator.register` and + :func:`moderator.unregister` methods detailed above, the following methods on :class:`Moderator` can be overridden to achieve customized behavior: .. method:: connect diff --git a/docs/ref/contrib/comments/signals.txt b/docs/ref/contrib/comments/signals.txt index 8274539ed7..ea901b6a95 100644 --- a/docs/ref/contrib/comments/signals.txt +++ b/docs/ref/contrib/comments/signals.txt @@ -81,8 +81,8 @@ Arguments sent with this signal: :meth:`~django.db.models.Model.save` again. ``flag`` - The :class:`~django.contrib.comments.models.CommentFlag` that's been - attached to the comment. + The ``django.contrib.comments.models.CommentFlag`` that's been attached to + the comment. ``created`` ``True`` if this is a new flag; ``False`` if it's a duplicate flag. diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index 8f329aa388..e9cd5e7bc0 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -453,7 +453,7 @@ Generic relations in forms and admin ------------------------------------ The :mod:`django.contrib.contenttypes.generic` module provides -:class:`~django.contrib.contenttypes.generic.BaseGenericInlineFormSet`, +``BaseGenericInlineFormSet``, :class:`~django.contrib.contenttypes.generic.GenericTabularInline` and :class:`~django.contrib.contenttypes.generic.GenericStackedInline` (the last two are subclasses of @@ -480,3 +480,9 @@ information. The name of the integer field that represents the ID of the related object. Defaults to ``object_id``. + +.. class:: GenericTabularInline +.. class:: GenericStackedInline + + Subclasses of :class:`GenericInlineModelAdmin` with stacked and tabular + layouts, respectively. diff --git a/docs/ref/contrib/flatpages.txt b/docs/ref/contrib/flatpages.txt index c360809dac..292b304acb 100644 --- a/docs/ref/contrib/flatpages.txt +++ b/docs/ref/contrib/flatpages.txt @@ -186,7 +186,7 @@ Via the Python API If you add or modify flatpages via your own code, you will likely want to check for duplicate flatpage URLs within the same site. The flatpage form used in the admin performs this validation check, and can be imported from - :class:`django.contrib.flatpages.forms.FlatPageForm` and used in your own + ``django.contrib.flatpages.forms.FlatPageForm`` and used in your own views. Flatpage templates @@ -256,7 +256,7 @@ Displaying ``registration_required`` flatpages By default, the :ttag:`get_flatpages` templatetag will only show flatpages that are marked ``registration_required = False``. If you want to display registration-protected flatpages, you need to specify -an authenticated user using a``for`` clause. +an authenticated user using a ``for`` clause. For example: diff --git a/docs/ref/contrib/formtools/form-preview.txt b/docs/ref/contrib/formtools/form-preview.txt index 784213ecba..011e72c2e0 100644 --- a/docs/ref/contrib/formtools/form-preview.txt +++ b/docs/ref/contrib/formtools/form-preview.txt @@ -25,9 +25,8 @@ application takes care of the following workflow: a. If it's valid, displays a preview page. b. If it's not valid, redisplays the form with error messages. 3. When the "confirmation" form is submitted from the preview page, calls - a hook that you define -- a - :meth:`~django.contrib.formtools.preview.FormPreview.done()` method that gets - passed the valid data. + a hook that you define -- a ``done()`` method that gets passed the valid + data. The framework enforces the required preview by passing a shared-secret hash to the preview page via hidden form fields. If somebody tweaks the form parameters @@ -51,8 +50,7 @@ How to use ``FormPreview`` directory to your :setting:`TEMPLATE_DIRS` setting. 2. Create a :class:`~django.contrib.formtools.preview.FormPreview` subclass that - overrides the :meth:`~django.contrib.formtools.preview.FormPreview.done()` - method:: + overrides the ``done()`` method:: from django.contrib.formtools.preview import FormPreview from myapp.models import SomeModel @@ -92,13 +90,15 @@ How to use ``FormPreview`` A :class:`~django.contrib.formtools.preview.FormPreview` class is a simple Python class that represents the preview workflow. :class:`~django.contrib.formtools.preview.FormPreview` classes must subclass -``django.contrib.formtools.preview.FormPreview`` and override the -:meth:`~django.contrib.formtools.preview.FormPreview.done()` method. They can live -anywhere in your codebase. +``django.contrib.formtools.preview.FormPreview`` and override the ``done()`` +method. They can live anywhere in your codebase. ``FormPreview`` templates ========================= +.. attribute:: FormPreview.form_template +.. attribute:: FormPreview.preview_template + By default, the form is rendered via the template :file:`formtools/form.html`, and the preview page is rendered via the template :file:`formtools/preview.html`. These values can be overridden for a particular form preview by setting diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index 3edc019d05..9ea65d7e5f 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -54,7 +54,8 @@ you just have to do these things: 4. Add ``django.contrib.formtools`` to your :setting:`INSTALLED_APPS` list in your settings file. -5. Point your URLconf at your :class:`WizardView` :meth:`~WizardView.as_view` method. +5. Point your URLconf at your :class:`WizardView` :meth:`~WizardView.as_view` + method. Defining ``Form`` classes ------------------------- @@ -89,6 +90,9 @@ the message itself. Here's what the :file:`forms.py` might look like:: Creating a ``WizardView`` subclass ---------------------------------- +.. class:: SessionWizardView +.. class:: CookieWizardView + The next step is to create a :class:`django.contrib.formtools.wizard.views.WizardView` subclass. You can also use the :class:`SessionWizardView` or :class:`CookieWizardView` classes @@ -225,9 +229,11 @@ Here's a full example template: Hooking the wizard into a URLconf --------------------------------- +.. method:: WizardView.as_view + Finally, we need to specify which forms to use in the wizard, and then deploy the new :class:`WizardView` object at a URL in the ``urls.py``. The -wizard's :meth:`as_view` method takes a list of your +wizard's ``as_view()`` method takes a list of your :class:`~django.forms.Form` classes as an argument during instantiation:: from django.conf.urls import patterns @@ -346,9 +352,9 @@ Advanced ``WizardView`` methods used as the form for step ``step``. Returns an :class:`~django.db.models.Model` object which will be passed as - the :attr:`~django.forms.ModelForm.instance` argument when instantiating the - ModelForm for step ``step``. If no instance object was provided while - initializing the form wizard, ``None`` will be returned. + the ``instance`` argument when instantiating the ``ModelForm`` for step + ``step``. If no instance object was provided while initializing the form + wizard, ``None`` will be returned. The default implementation:: @@ -514,10 +520,10 @@ Providing initial data for the forms .. attribute:: WizardView.initial_dict Initial data for a wizard's :class:`~django.forms.Form` objects can be - provided using the optional :attr:`~Wizard.initial_dict` keyword argument. - This argument should be a dictionary mapping the steps to dictionaries - containing the initial data for each step. The dictionary of initial data - will be passed along to the constructor of the step's + provided using the optional :attr:`~WizardView.initial_dict` keyword + argument. This argument should be a dictionary mapping the steps to + dictionaries containing the initial data for each step. The dictionary of + initial data will be passed along to the constructor of the step's :class:`~django.forms.Form`:: >>> from myapp.forms import ContactForm1, ContactForm2 @@ -542,11 +548,13 @@ Providing initial data for the forms Handling files ============== +.. attribute:: WizardView.file_storage + To handle :class:`~django.forms.FileField` within any step form of the wizard, -you have to add a :attr:`file_storage` to your :class:`WizardView` subclass. +you have to add a ``file_storage`` to your :class:`WizardView` subclass. This storage will temporarily store the uploaded files for the wizard. The -:attr:`file_storage` attribute should be a +``file_storage`` attribute should be a :class:`~django.core.files.storage.Storage` subclass. Django provides a built-in storage class (see :ref:`the built-in filesystem @@ -646,6 +654,8 @@ Usage of ``NamedUrlWizardView`` =============================== .. class:: NamedUrlWizardView +.. class:: NamedUrlSessionWizardView +.. class:: NamedUrlCookieWizardView There is a :class:`WizardView` subclass which adds named-urls support to the wizard. By doing this, you can have single urls for every step. You can also diff --git a/docs/ref/contrib/formtools/index.txt b/docs/ref/contrib/formtools/index.txt index f36470654a..e768c0e655 100644 --- a/docs/ref/contrib/formtools/index.txt +++ b/docs/ref/contrib/formtools/index.txt @@ -1,6 +1,8 @@ django.contrib.formtools ======================== +.. module:: django.contrib.formtools + A set of high-level abstractions for Django forms (:mod:`django.forms`). .. toctree:: diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 519f79f0d4..be413c9df8 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -4,20 +4,23 @@ GeoDjango Database API ====================== -.. module:: django.contrib.gis.db.models - :synopsis: GeoDjango's database API. - .. _spatial-backends: Spatial Backends ================ +.. module:: django.contrib.gis.db.backends + :synopsis: GeoDjango's spatial database backends. + GeoDjango currently provides the following spatial database backends: -* :mod:`django.contrib.gis.db.backends.postgis` -* :mod:`django.contrib.gis.db.backends.mysql` -* :mod:`django.contrib.gis.db.backends.oracle` -* :mod:`django.contrib.gis.db.backends.spatialite` +* ``django.contrib.gis.db.backends.postgis`` +* ``django.contrib.gis.db.backends.mysql`` +* ``django.contrib.gis.db.backends.oracle`` +* ``django.contrib.gis.db.backends.spatialite`` + +.. module:: django.contrib.gis.db.models + :synopsis: GeoDjango's database API. .. _mysql-spatial-limitations: diff --git a/docs/ref/contrib/gis/feeds.txt b/docs/ref/contrib/gis/feeds.txt index 7c3a2d011c..7b1b6ebccf 100644 --- a/docs/ref/contrib/gis/feeds.txt +++ b/docs/ref/contrib/gis/feeds.txt @@ -27,7 +27,7 @@ API Reference .. class:: Feed In addition to methods provided by - the :class:`django.contrib.syndication.feeds.Feed` + the :class:`django.contrib.syndication.views.Feed` base class, GeoDjango's ``Feed`` class provides the following overrides. Note that these overrides may be done in multiple ways:: @@ -71,11 +71,11 @@ API Reference can be a ``GEOSGeometry`` instance, or a tuple that represents a point coordinate or bounding box. For example:: - class ZipcodeFeed(Feed): + class ZipcodeFeed(Feed): - def item_geometry(self, obj): - # Returns the polygon. - return obj.poly + def item_geometry(self, obj): + # Returns the polygon. + return obj.poly ``SyndicationFeed`` Subclasses ------------------------------ diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index 69280dc028..66afc3d377 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -683,7 +683,7 @@ Keyword Argument Description a method name clashes with an existing ``GeoQuerySet`` method -- if you wanted to use the ``area()`` method on model with a ``PolygonField`` - named ``area``, for example. + named ``area``, for example. ===================== ===================================================== Measurement @@ -1043,7 +1043,7 @@ Keyword Argument Description ===================== ===================================================== ``relative`` If set to ``True``, the path data will be implemented in terms of relative moves. Defaults to ``False``, - meaning that absolute moves are used instead. + meaning that absolute moves are used instead. ``precision`` This keyword may be used to specify the number of significant digits for the coordinates in the SVG diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index 7d7c32781c..4d44638488 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -142,10 +142,9 @@ Geometry Objects .. class:: GEOSGeometry(geo_input[, srid=None]) - :param geo_input: Geometry input value - :type geo_input: string or buffer + :param geo_input: Geometry input value (string or buffer) :param srid: spatial reference identifier - :type srid: integer + :type srid: int This is the base class for all GEOS geometry objects. It initializes on the given ``geo_input`` argument, and then assumes the proper geometry subclass @@ -800,7 +799,7 @@ Example:: :param string: string that contains spatial data :type string: string :param srid: spatial reference identifier - :type srid: integer + :type srid: int :rtype: a :class:`GEOSGeometry` corresponding to the spatial data in the string Example:: @@ -966,3 +965,10 @@ location (e.g., ``/home/bob/lib/libgeos_c.so``). The setting must be the *full* path to the **C** shared library; in other words you want to use ``libgeos_c.so``, not ``libgeos.so``. + +Exceptions +========== + +.. exception:: GEOSException + +The base GEOS exception, indicates a GEOS-related error. diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt index 100dc2edd0..35c01c9b7e 100644 --- a/docs/ref/contrib/gis/install/index.txt +++ b/docs/ref/contrib/gis/install/index.txt @@ -530,6 +530,6 @@ Finally, :ref:`install Django ` on your system. .. rubric:: Footnotes .. [#] GeoDjango uses the :func:`~ctypes.util.find_library` routine from - :mod:`ctypes.util` to locate shared libraries. + ``ctypes.util`` to locate shared libraries. .. [#] The ``psycopg2`` Windows installers are packaged and maintained by `Jason Erickson `_. diff --git a/docs/ref/contrib/gis/tutorial.txt b/docs/ref/contrib/gis/tutorial.txt index 5000622ad4..9efa020e61 100644 --- a/docs/ref/contrib/gis/tutorial.txt +++ b/docs/ref/contrib/gis/tutorial.txt @@ -226,7 +226,7 @@ model to represent this data:: class WorldBorder(models.Model): # Regular Django fields corresponding to the attributes in the - # world borders shapefile. + # world borders shapefile. name = models.CharField(max_length=50) area = models.IntegerField() pop2005 = models.IntegerField('Population 2005') @@ -236,13 +236,13 @@ model to represent this data:: un = models.IntegerField('United Nations Code') region = models.IntegerField('Region Code') subregion = models.IntegerField('Sub-Region Code') - lon = models.FloatField() - lat = models.FloatField() + lon = models.FloatField() + lat = models.FloatField() - # GeoDjango-specific: a geometry field (MultiPolygonField), and + # GeoDjango-specific: a geometry field (MultiPolygonField), and # overriding the default manager with a GeoManager instance. - mpoly = models.MultiPolygonField() - objects = models.GeoManager() + mpoly = models.MultiPolygonField() + objects = models.GeoManager() # Returns the string representation of the model. def __unicode__(self): @@ -250,7 +250,7 @@ model to represent this data:: Please note two important things: -1. The ``models`` module is imported from :mod:`django.contrib.gis.db`. +1. The ``models`` module is imported from ``django.contrib.gis.db``. 2. You must override the model's default manager with :class:`~django.contrib.gis.db.models.GeoManager` to perform spatial queries. diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index 42c4b91bd4..1861318b95 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -49,6 +49,8 @@ loader can find the default templates.) Initialization ============== +.. function:: views.sitemap(request, sitemaps, section=None, template_name='sitemap.xml', mimetype='application/xml') + To activate sitemap generation on your Django site, add this line to your :doc:`URLconf `:: @@ -240,9 +242,9 @@ The sitemap framework provides a couple convenience classes for common cases: The :class:`django.contrib.sitemaps.GenericSitemap` class allows you to create a sitemap by passing it a dictionary which has to contain at least - a :data:`queryset` entry. This queryset will be used to generate the items - of the sitemap. It may also have a :data:`date_field` entry that - specifies a date field for objects retrieved from the :data:`queryset`. + a ``queryset`` entry. This queryset will be used to generate the items + of the sitemap. It may also have a ``date_field`` entry that + specifies a date field for objects retrieved from the ``queryset``. This will be used for the :attr:`~Sitemap.lastmod` attribute in the generated sitemap. You may also pass :attr:`~Sitemap.priority` and :attr:`~Sitemap.changefreq` keyword arguments to the @@ -281,14 +283,16 @@ Here's an example of a :doc:`URLconf ` using both:: Creating a sitemap index ======================== +.. function:: views.index(request, sitemaps, template_name='sitemap_index.xml', mimetype='application/xml', sitemap_url_name='django.contrib.sitemaps.views.sitemap') + The sitemap framework also has the ability to create a sitemap index that references individual sitemap files, one per each section defined in your -:data:`sitemaps` dictionary. The only differences in usage are: +``sitemaps`` dictionary. The only differences in usage are: * You use two views in your URLconf: :func:`django.contrib.sitemaps.views.index` and :func:`django.contrib.sitemaps.views.sitemap`. * The :func:`django.contrib.sitemaps.views.sitemap` view should take a - :data:`section` keyword argument. + ``section`` keyword argument. Here's what the relevant URLconf lines would look like for the example above:: @@ -299,7 +303,7 @@ Here's what the relevant URLconf lines would look like for the example above:: This will automatically generate a :file:`sitemap.xml` file that references both :file:`sitemap-flatpages.xml` and :file:`sitemap-blog.xml`. The -:class:`~django.contrib.sitemaps.Sitemap` classes and the :data:`sitemaps` +:class:`~django.contrib.sitemaps.Sitemap` classes and the ``sitemaps`` dict don't change at all. You should create an index file if one of your sitemaps has more than 50,000 @@ -350,19 +354,20 @@ rendering. For more details, see the :doc:`TemplateResponse documentation Context variables ------------------ -When customizing the templates for the :func:`~django.contrib.sitemaps.views.index` -and :func:`~django.contrib.sitemaps.views.sitemaps` views, you can rely on the +When customizing the templates for the +:func:`~django.contrib.sitemaps.views.index` and +:func:`~django.contrib.sitemaps.views.sitemap` views, you can rely on the following context variables. Index ----- -The variable :data:`sitemaps` is a list of absolute URLs to each of the sitemaps. +The variable ``sitemaps`` is a list of absolute URLs to each of the sitemaps. Sitemap ------- -The variable :data:`urlset` is a list of URLs that should appear in the +The variable ``urlset`` is a list of URLs that should appear in the sitemap. Each URL exposes attributes as defined in the :class:`~django.contrib.sitemaps.Sitemap` class: @@ -411,14 +416,14 @@ that: :func:`django.contrib.sitemaps.ping_google()`. .. function:: ping_google - :func:`ping_google` takes an optional argument, :data:`sitemap_url`, + :func:`ping_google` takes an optional argument, ``sitemap_url``, which should be the absolute path to your site's sitemap (e.g., :file:`'/sitemap.xml'`). If this argument isn't provided, :func:`ping_google` will attempt to figure out your sitemap by performing a reverse looking in your URLconf. :func:`ping_google` raises the exception - :exc:`django.contrib.sitemaps.SitemapNotFound` if it cannot determine your + ``django.contrib.sitemaps.SitemapNotFound`` if it cannot determine your sitemap URL. .. admonition:: Register with Google first! diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index 9c8f29a8de..a4a60f239b 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -33,7 +33,7 @@ STATICFILES_DIRS Default: ``[]`` This setting defines the additional locations the staticfiles app will traverse -if the :class:`FileSystemFinder` finder is enabled, e.g. if you use the +if the ``FileSystemFinder`` finder is enabled, e.g. if you use the :djadmin:`collectstatic` or :djadmin:`findstatic` management command or use the static file serving view. @@ -101,19 +101,19 @@ The list of finder backends that know how to find static files in various locations. The default will find files stored in the :setting:`STATICFILES_DIRS` setting -(using :class:`django.contrib.staticfiles.finders.FileSystemFinder`) and in a +(using ``django.contrib.staticfiles.finders.FileSystemFinder``) and in a ``static`` subdirectory of each app (using -:class:`django.contrib.staticfiles.finders.AppDirectoriesFinder`) +``django.contrib.staticfiles.finders.AppDirectoriesFinder``) One finder is disabled by default: -:class:`django.contrib.staticfiles.finders.DefaultStorageFinder`. If added to +``django.contrib.staticfiles.finders.DefaultStorageFinder``. If added to your :setting:`STATICFILES_FINDERS` setting, it will look for static files in the default file storage as defined by the :setting:`DEFAULT_FILE_STORAGE` setting. .. note:: - When using the :class:`AppDirectoriesFinder` finder, make sure your apps + When using the ``AppDirectoriesFinder`` finder, make sure your apps can be found by staticfiles. Simply add the app to the :setting:`INSTALLED_APPS` setting of your site. diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index 2418dba8ef..d0376e3c1b 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -334,7 +334,7 @@ And the accompanying URLconf:: Feed class reference -------------------- -.. class:: django.contrib.syndication.views.Feed +.. class:: views.Feed This example illustrates all possible attributes and methods for a :class:`~django.contrib.syndication.views.Feed` class:: diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 771085766e..e933ee350d 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -259,9 +259,9 @@ recommended solution. Should you decide to use ``utf8_bin`` collation for some of your tables with MySQLdb 1.2.1p2 or 1.2.2, you should still use ``utf8_collation_ci_swedish`` -(the default) collation for the :class:`django.contrib.sessions.models.Session` +(the default) collation for the ``django.contrib.sessions.models.Session`` table (usually called ``django_session``) and the -:class:`django.contrib.admin.models.LogEntry` table (usually called +``django.contrib.admin.models.LogEntry`` table (usually called ``django_admin_log``). Those are the two standard tables that use :class:`~django.db.models.TextField` internally. diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index e67527de23..8d612ae6a6 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -292,6 +292,8 @@ Searches for and loads the contents of the named fixture into the database. The :djadminopt:`--database` option can be used to specify the database onto which the data will be loaded. +.. django-admin-option:: --ignorenonexistent + .. versionadded:: 1.5 The :djadminopt:`--ignorenonexistent` option can be used to ignore fields that diff --git a/docs/ref/exceptions.txt b/docs/ref/exceptions.txt index e91a5dd85e..f123ae2e59 100644 --- a/docs/ref/exceptions.txt +++ b/docs/ref/exceptions.txt @@ -131,6 +131,21 @@ The Django wrappers for database exceptions behave exactly the same as the underlying database exceptions. See :pep:`249`, the Python Database API Specification v2.0, for further information. +.. exception:: models.ProtectedError + +Raised to prevent deletion of referenced objects when using +:attr:`django.db.models.PROTECT`. Subclass of :exc:`IntegrityError`. + +.. currentmodule:: django.http + +Http Exceptions +=============== + +.. exception:: UnreadablePostError + + The :exc:`UnreadablePostError` is raised when a user cancels an upload. + It is available from :mod:`django.http`. + .. currentmodule:: django.db.transaction Transaction Exceptions diff --git a/docs/ref/files/file.txt b/docs/ref/files/file.txt index ada614df45..7562f9b6bf 100644 --- a/docs/ref/files/file.txt +++ b/docs/ref/files/file.txt @@ -14,7 +14,7 @@ The ``File`` Class The :class:`File` is a thin wrapper around Python's built-in file object with some Django-specific additions. Internally, Django uses this class any time it needs to represent a file. - + :class:`File` objects have the following attributes and methods: .. attribute:: name @@ -148,7 +148,7 @@ below) will also have a couple of extra methods: Note that the ``content`` argument must be an instance of either :class:`File` or of a subclass of :class:`File`, such as - :class:`ContentFile`. + :class:`~django.core.files.base.ContentFile`. .. method:: File.delete([save=True]) diff --git a/docs/ref/files/storage.txt b/docs/ref/files/storage.txt index f9bcf9b61e..ff175d122b 100644 --- a/docs/ref/files/storage.txt +++ b/docs/ref/files/storage.txt @@ -38,7 +38,7 @@ The FileSystemStorage Class .. note:: - The :class:`FileSystemStorage.delete` method will not raise + The ``FileSystemStorage.delete()`` method will not raise raise an exception if the given file name does not exist. The Storage Class diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index ab1f4b0eea..4aacbf0a0d 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -2,9 +2,7 @@ The Forms API ============= -.. module:: django.forms.forms - -.. currentmodule:: django.forms +.. module:: django.forms .. admonition:: About this document @@ -380,6 +378,9 @@ a form object, and each rendering method returns a Unicode object. Styling required or erroneous form rows ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. attribute:: Form.error_css_class +.. attribute:: Form.required_css_class + It's pretty common to style form rows and fields that are required or have errors. For example, you might want to present required form rows in bold and highlight errors in red. @@ -587,24 +588,24 @@ lazy developers -- they're not the only way a form object can be displayed. Used to display HTML or access attributes for a single field of a :class:`Form` instance. - The :meth:`__unicode__` and :meth:`__str__` methods of this object displays + The ``__unicode__()`` and ``__str__()`` methods of this object displays the HTML for this field. To retrieve a single ``BoundField``, use dictionary lookup syntax on your form using the field's name as the key:: - >>> form = ContactForm() - >>> print(form['subject']) - + >>> form = ContactForm() + >>> print(form['subject']) + To retrieve all ``BoundField`` objects, iterate the form:: - >>> form = ContactForm() - >>> for boundfield in form: print(boundfield) - - - - + >>> form = ContactForm() + >>> for boundfield in form: print(boundfield) + + + + The field-specific output honors the form object's ``auto_id`` setting:: @@ -635,7 +636,7 @@ For a field's list of errors, access the field's ``errors`` attribute. >>> print(f['subject'].errors) >>> str(f['subject'].errors) - '' + '' .. method:: BoundField.css_classes() @@ -644,17 +645,17 @@ indicate required form fields or fields that contain errors. If you're manually rendering a form, you can access these CSS classes using the ``css_classes`` method:: - >>> f = ContactForm(data) - >>> f['message'].css_classes() - 'required' + >>> f = ContactForm(data) + >>> f['message'].css_classes() + 'required' If you want to provide some additional classes in addition to the error and required classes that may be required, you can provide those classes as an argument:: - >>> f = ContactForm(data) - >>> f['message'].css_classes('foo bar') - 'foo bar required' + >>> f = ContactForm(data) + >>> f['message'].css_classes('foo bar') + 'foo bar required' .. method:: BoundField.value() diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index d8d9c9b770..bc1270094b 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -508,9 +508,9 @@ Selector and checkbox widgets .. attribute:: Select.choices - This attribute is optional when the field does not have a - :attr:`~Field.choices` attribute. If it does, it will override anything - you set here when the attribute is updated on the :class:`Field`. + This attribute is optional when the form field does not have a + ``choices`` attribute. If it does, it will override anything you set + here when the attribute is updated on the :class:`Field`. ``NullBooleanSelect`` ~~~~~~~~~~~~~~~~~~~~~ @@ -660,9 +660,9 @@ Composite widgets .. attribute:: MultipleHiddenInput.choices - This attribute is optional when the field does not have a - :attr:`~Field.choices` attribute. If it does, it will override anything - you set here when the attribute is updated on the :class:`Field`. + This attribute is optional when the form field does not have a + ``choices`` attribute. If it does, it will override anything you set + here when the attribute is updated on the :class:`Field`. ``SplitDateTimeWidget`` ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 31cc6f24f6..2b053d80ab 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -111,7 +111,7 @@ It will NOT compress content if any of the following are true: not to be performed on certain content types. You can apply GZip compression to individual views using the -:func:`~django.views.decorators.http.gzip_page()` decorator. +:func:`~django.views.decorators.gzip.gzip_page()` decorator. Conditional GET middleware -------------------------- @@ -124,7 +124,7 @@ Conditional GET middleware Handles conditional GET operations. If the response has a ``ETag`` or ``Last-Modified`` header, and the request has ``If-None-Match`` or ``If-Modified-Since``, the response is replaced by an -:class:`~django.http.HttpNotModified`. +:class:`~django.http.HttpResponseNotModified`. Also sets the ``Date`` and ``Content-Length`` response-headers. diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index e9f85e0657..6498b6c845 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -113,7 +113,7 @@ define a suitably-named constant for each value:: default=FRESHMAN) def is_upperclass(self): - return self.year_in_school in (self.JUNIOR, self.SENIOR) + return self.year_in_school in (self.JUNIOR, self.SENIOR) Though you can define a choices list outside of a model class and then refer to it, defining the choices and names for each choice inside the @@ -509,8 +509,8 @@ Has one **required** argument: .. attribute:: FileField.upload_to A local filesystem path that will be appended to your :setting:`MEDIA_ROOT` - setting to determine the value of the :attr:`~django.core.files.File.url` - attribute. + setting to determine the value of the + :attr:`~django.db.models.fields.files.FieldFile.url` attribute. This path may contain :func:`~time.strftime` formatting, which will be replaced by the date/time of the file upload (so that uploaded files don't @@ -564,9 +564,9 @@ takes a few steps: 3. All that will be stored in your database is a path to the file (relative to :setting:`MEDIA_ROOT`). You'll most likely want to use the - convenience :attr:`~django.core.files.File.url` function provided by - Django. For example, if your :class:`ImageField` is called ``mug_shot``, - you can get the absolute path to your image in a template with + convenience :attr:`~django.db.models.fields.files.FieldFile.url` attribute + provided by Django. For example, if your :class:`ImageField` is called + ``mug_shot``, you can get the absolute path to your image in a template with ``{{ object.mug_shot.url }}``. For example, say your :setting:`MEDIA_ROOT` is set to ``'/home/media'``, and @@ -589,7 +589,7 @@ topic guide. saved. The uploaded file's relative URL can be obtained using the -:attr:`~django.db.models.FileField.url` attribute. Internally, +:attr:`~django.db.models.fields.files.FieldFile.url` attribute. Internally, this calls the :meth:`~django.core.files.storage.Storage.url` method of the underlying :class:`~django.core.files.storage.Storage` class. @@ -614,9 +614,20 @@ can change the maximum length using the :attr:`~CharField.max_length` argument. FileField and FieldFile ~~~~~~~~~~~~~~~~~~~~~~~ -When you access a :class:`FileField` on a model, you are given an instance -of :class:`FieldFile` as a proxy for accessing the underlying file. This -class has several methods that can be used to interact with file data: +.. currentmodule:: django.db.models.fields.files + +.. class:: FieldFile + +When you access a :class:`~django.db.models.FileField` on a model, you are +given an instance of :class:`FieldFile` as a proxy for accessing the underlying +file. This class has several attributes and methods that can be used to +interact with file data: + +.. attribute:: FieldFile.url + +A read-only property to access the file's relative URL by calling the +:meth:`~django.core.files.storage.Storage.url` method of the underlying +:class:`~django.core.files.storage.Storage` class. .. method:: FieldFile.open(mode='rb') @@ -632,9 +643,9 @@ associated with this instance. This method takes a filename and file contents and passes them to the storage class for the field, then associates the stored file with the model field. -If you want to manually associate file data with :class:`FileField` -instances on your model, the ``save()`` method is used to persist that file -data. +If you want to manually associate file data with +:class:`~django.db.models.FileField` instances on your model, the ``save()`` +method is used to persist that file data. Takes two required arguments: ``name`` which is the name of the file, and ``content`` which is an object containing the file's contents. The @@ -672,6 +683,8 @@ to cleanup orphaned files, you'll need to handle it yourself (for instance, with a custom management command that can be run manually or scheduled to run periodically via e.g. cron). +.. currentmodule:: django.db.models + ``FilePathField`` ----------------- @@ -759,8 +772,7 @@ Inherits all attributes and methods from :class:`FileField`, but also validates that the uploaded object is a valid image. In addition to the special attributes that are available for :class:`FileField`, -an :class:`ImageField` also has :attr:`~django.core.files.File.height` and -:attr:`~django.core.files.File.width` attributes. +an :class:`ImageField` also has ``height`` and ``width`` attributes. To facilitate querying on those attributes, :class:`ImageField` has two extra optional arguments: @@ -1047,26 +1059,36 @@ define the details of how the relation works. user = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL) - The possible values for :attr:`on_delete` are found in - :mod:`django.db.models`: +The possible values for :attr:`~ForeignKey.on_delete` are found in +:mod:`django.db.models`: - * :attr:`~django.db.models.CASCADE`: Cascade deletes; the default. +* .. attribute:: CASCADE - * :attr:`~django.db.models.PROTECT`: Prevent deletion of the referenced - object by raising :exc:`django.db.models.ProtectedError`, a subclass of - :exc:`django.db.IntegrityError`. + Cascade deletes; the default. - * :attr:`~django.db.models.SET_NULL`: Set the :class:`ForeignKey` null; - this is only possible if :attr:`null` is ``True``. +* .. attribute:: PROTECT - * :attr:`~django.db.models.SET_DEFAULT`: Set the :class:`ForeignKey` to its - default value; a default for the :class:`ForeignKey` must be set. + Prevent deletion of the referenced object by raising + :exc:`~django.db.models.ProtectedError`, a subclass of + :exc:`django.db.IntegrityError`. - * :func:`~django.db.models.SET()`: Set the :class:`ForeignKey` to the value - passed to :func:`~django.db.models.SET()`, or if a callable is passed in, - the result of calling it. In most cases, passing a callable will be - necessary to avoid executing queries at the time your models.py is - imported:: +* .. attribute:: SET_NULL + + Set the :class:`ForeignKey` null; this is only possible if + :attr:`~Field.null` is ``True``. + +* .. attribute:: SET_DEFAULT + + Set the :class:`ForeignKey` to its default value; a default for the + :class:`ForeignKey` must be set. + +* .. function:: SET() + + Set the :class:`ForeignKey` to the value passed to + :func:`~django.db.models.SET()`, or if a callable is passed in, + the result of calling it. In most cases, passing a callable will be + necessary to avoid executing queries at the time your models.py is + imported:: def get_sentinel_user(): return User.objects.get_or_create(username='deleted')[0] @@ -1074,11 +1096,12 @@ define the details of how the relation works. class MyModel(models.Model): user = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user)) - * :attr:`~django.db.models.DO_NOTHING`: Take no action. If your database - backend enforces referential integrity, this will cause an - :exc:`~django.db.IntegrityError` unless you manually add a SQL ``ON - DELETE`` constraint to the database field (perhaps using - :ref:`initial sql`). +* .. attribute:: DO_NOTHING + + Take no action. If your database backend enforces referential + integrity, this will cause an :exc:`~django.db.IntegrityError` unless + you manually add a SQL ``ON DELETE`` constraint to the database field + (perhaps using :ref:`initial sql`). .. _ref-manytomany: diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index 6fd707fdf2..b349197a5b 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -100,7 +100,7 @@ Django quotes column and table names behind the scenes. .. attribute:: Options.managed Defaults to ``True``, meaning Django will create the appropriate database - tables in :djadmin:`syncdb` and remove them as part of a :djadmin:`reset` + tables in :djadmin:`syncdb` and remove them as part of a :djadmin:`flush` management command. That is, Django *manages* the database tables' lifecycles. diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index a8e0ef3f51..2b4397a138 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -263,7 +263,7 @@ Methods .. method:: HttpRequest.get_signed_cookie(key, default=RAISE_ERROR, salt='', max_age=None) Returns a cookie value for a signed cookie, or raises a - :class:`~django.core.signing.BadSignature` exception if the signature is + ``django.core.signing.BadSignature`` exception if the signature is no longer valid. If you provide the ``default`` argument the exception will be suppressed and that default value will be returned instead. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index bfe283cc68..be21f06de7 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2159,7 +2159,7 @@ startproject ` management command will create a simple ``wsgi.py`` file with an ``application`` callable in it, and point this setting to that ``application``. -If not set, the return value of :func:`django.core.wsgi.get_wsgi_application` +If not set, the return value of ``django.core.wsgi.get_wsgi_application()`` will be used. In this case, the behavior of :djadmin:`runserver` will be identical to previous Django versions. diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index f2f1459bf0..0995789391 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -436,9 +436,8 @@ Sent when Django begins processing an HTTP request. Arguments sent with this signal: ``sender`` - The handler class -- e.g. - :class:`django.core.handlers.wsgi.WsgiHandler` -- that handled - the request. + The handler class -- e.g. ``django.core.handlers.wsgi.WsgiHandler`` -- that + handled the request. request_finished ---------------- @@ -496,7 +495,7 @@ setting_changed :module: This signal is sent when the value of a setting is changed through the -:meth:`django.test.TestCase.setting` context manager or the +``django.test.TestCase.settings()`` context manager or the :func:`django.test.utils.override_settings` decorator/context manager. It's actually sent twice: when the new value is applied ("setup") and when the @@ -558,8 +557,8 @@ Arguments sent with this signal: ``sender`` The database wrapper class -- i.e. - :class:`django.db.backends.postgresql_psycopg2.DatabaseWrapper` or - :class:`django.db.backends.mysql.DatabaseWrapper`, etc. + ``django.db.backends.postgresql_psycopg2.DatabaseWrapper`` or + ``django.db.backends.mysql.DatabaseWrapper``, etc. ``connection`` The database connection that was opened. This can be used in a diff --git a/docs/ref/template-response.txt b/docs/ref/template-response.txt index d9b7130362..3f5e772737 100644 --- a/docs/ref/template-response.txt +++ b/docs/ref/template-response.txt @@ -121,15 +121,14 @@ Methods used as the response instead of the original response object (and will be passed to the next post rendering callback etc.) -.. method:: SimpleTemplateResponse.render(): +.. method:: SimpleTemplateResponse.render() - Sets :attr:`response.content` to the result obtained by + Sets ``response.content`` to the result obtained by :attr:`SimpleTemplateResponse.rendered_content`, runs all post-rendering callbacks, and returns the resulting response object. - :meth:`~SimpleTemplateResponse.render()` will only have an effect - the first time it is called. On subsequent calls, it will return - the result obtained from the first call. + ``render()`` will only have an effect the first time it is called. On + subsequent calls, it will return the result obtained from the first call. TemplateResponse objects @@ -188,24 +187,23 @@ returned to the client, it must be rendered. The rendering process takes the intermediate representation of template and context, and turns it into the final byte stream that can be served to the client. -There are three circumstances under which a TemplateResponse will be +There are three circumstances under which a ``TemplateResponse`` will be rendered: -* When the TemplateResponse instance is explicitly rendered, using +* When the ``TemplateResponse`` instance is explicitly rendered, using the :meth:`SimpleTemplateResponse.render()` method. * When the content of the response is explicitly set by assigning - :attr:`response.content`. + ``response.content``. * After passing through template response middleware, but before passing through response middleware. -A TemplateResponse can only be rendered once. The first call to -:meth:`SimpleTemplateResponse.render` sets the content of the -response; subsequent rendering calls do not change the response -content. +A ``TemplateResponse`` can only be rendered once. The first call to +:meth:`SimpleTemplateResponse.render` sets the content of the response; +subsequent rendering calls do not change the response content. -However, when :attr:`response.content` is explicitly assigned, the +However, when ``response.content`` is explicitly assigned, the change is always applied. If you want to force the content to be re-rendered, you can re-evaluate the rendered content, and assign the content of the response manually:: diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index 7c17f0a758..0162f78eed 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -557,15 +557,17 @@ Note that these paths should use Unix-style forward slashes, even on Windows. The Python API ~~~~~~~~~~~~~~ -Django has two ways to load templates from files: +.. module:: django.template.loader -.. function:: django.template.loader.get_template(template_name) +``django.template.loader`` has two functions to load templates from files: + +.. function:: get_template(template_name) ``get_template`` returns the compiled template (a ``Template`` object) for the template with the given name. If the template doesn't exist, it raises ``django.template.TemplateDoesNotExist``. -.. function:: django.template.loader.select_template(template_name_list) +.. function:: select_template(template_name_list) ``select_template`` is just like ``get_template``, except it takes a list of template names. Of the list, it returns the first template that exists. @@ -630,11 +632,19 @@ by editing your :setting:`TEMPLATE_LOADERS` setting. :setting:`TEMPLATE_LOADERS` should be a tuple of strings, where each string represents a template loader class. Here are the template loaders that come with Django: +.. currentmodule:: django.template.loaders + ``django.template.loaders.filesystem.Loader`` + +.. class:: filesystem.Loader + Loads templates from the filesystem, according to :setting:`TEMPLATE_DIRS`. This loader is enabled by default. ``django.template.loaders.app_directories.Loader`` + +.. class:: app_directories.Loader + Loads templates from Django apps on the filesystem. For each app in :setting:`INSTALLED_APPS`, the loader looks for a ``templates`` subdirectory. If the directory exists, Django looks for templates in there. @@ -669,12 +679,18 @@ class. Here are the template loaders that come with Django: This loader is enabled by default. ``django.template.loaders.eggs.Loader`` + +.. class:: eggs.Loader + Just like ``app_directories`` above, but it loads templates from Python eggs rather than from the filesystem. This loader is disabled by default. ``django.template.loaders.cached.Loader`` + +.. class:: cached.Loader + By default, the templating system will read and compile your templates every time they need to be rendered. While the Django templating system is quite fast, the overhead from reading and compiling templates can add up. diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index aab53aed0c..cfc57cc551 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -377,7 +377,7 @@ block are output:: In the above, if ``athlete_list`` is not empty, the number of athletes will be displayed by the ``{{ athlete_list|length }}`` variable. -As you can see, the ``if`` tag may take one or several `` {% elif %}`` +As you can see, the ``if`` tag may take one or several ``{% elif %}`` clauses, as well as an ``{% else %}`` clause that will be displayed if all previous conditions fail. These clauses are optional. diff --git a/docs/ref/urls.txt b/docs/ref/urls.txt index 5a0b04f9fa..92b41b8fea 100644 --- a/docs/ref/urls.txt +++ b/docs/ref/urls.txt @@ -86,7 +86,6 @@ include() application and instance namespaces. :arg module: URLconf module (or module name) - :type module: Module or string :arg namespace: Instance namespace for the URL entries being included :type namespace: string :arg app_name: Application namespace for the URL entries being included @@ -142,4 +141,3 @@ value should suffice. See the documentation about :ref:`the 500 (HTTP Internal Server Error) view ` for more information. - diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 942cac2650..de805173d7 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -190,8 +190,7 @@ The functions defined in this module share the following properties: Like ``decorator_from_middleware``, but returns a function that accepts the arguments to be passed to the middleware_class. For example, the :func:`~django.views.decorators.cache.cache_page` - decorator is created from the - :class:`~django.middleware.cache.CacheMiddleware` like this:: + decorator is created from the ``CacheMiddleware`` like this:: cache_page = decorator_from_middleware_with_args(CacheMiddleware) @@ -282,15 +281,15 @@ The functions defined in this module share the following properties: .. function:: smart_str(s, encoding='utf-8', strings_only=False, errors='strict') Alias of :func:`smart_bytes` on Python 2 and :func:`smart_text` on Python - 3. This function returns a :class:`str` or a lazy string. + 3. This function returns a ``str`` or a lazy string. - For instance, this is suitable for writing to :attr:`sys.stdout` on + For instance, this is suitable for writing to :data:`sys.stdout` on Python 2 and 3. .. function:: force_str(s, encoding='utf-8', strings_only=False, errors='strict') Alias of :func:`force_bytes` on Python 2 and :func:`force_text` on Python - 3. This function always returns a :class:`str`. + 3. This function always returns a ``str``. .. function:: iri_to_uri(iri) @@ -624,12 +623,12 @@ escaping HTML. .. function:: base36_to_int(s) Converts a base 36 string to an integer. On Python 2 the output is - guaranteed to be an :class:`int` and not a :class:`long`. + guaranteed to be an ``int`` and not a ``long``. .. function:: int_to_base36(i) Converts a positive integer to a base 36 string. On Python 2 ``i`` must be - smaller than :attr:`sys.maxint`. + smaller than :data:`sys.maxint`. ``django.utils.safestring`` =========================== @@ -647,12 +646,12 @@ appropriate entities. .. versionadded:: 1.5 - A :class:`bytes` subclass that has been specifically marked as "safe" + A ``bytes`` subclass that has been specifically marked as "safe" (requires no further escaping) for HTML output purposes. .. class:: SafeString - A :class:`str` subclass that has been specifically marked as "safe" + A ``str`` subclass that has been specifically marked as "safe" (requires no further escaping) for HTML output purposes. This is :class:`SafeBytes` on Python 2 and :class:`SafeText` on Python 3. @@ -660,7 +659,7 @@ appropriate entities. .. versionadded:: 1.5 - A :class:`str` (in Python 3) or :class:`unicode` (in Python 2) subclass + A ``str`` (in Python 3) or ``unicode`` (in Python 2) subclass that has been specifically marked as "safe" for HTML output purposes. .. class:: SafeUnicode diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt index 0536b03d64..8da134a42d 100644 --- a/docs/ref/validators.txt +++ b/docs/ref/validators.txt @@ -118,7 +118,7 @@ to, or in lieu of custom ``field.clean()`` methods. .. data:: validate_ipv6_address - Uses :mod:`django.utils.ipv6` to check the validity of an IPv6 address. + Uses ``django.utils.ipv6`` to check the validity of an IPv6 address. ``validate_ipv46_address`` -------------------------- diff --git a/docs/releases/1.2-beta-1.txt b/docs/releases/1.2-beta-1.txt index 3549767379..abb0f3bbb9 100644 --- a/docs/releases/1.2-beta-1.txt +++ b/docs/releases/1.2-beta-1.txt @@ -47,7 +47,7 @@ should be updated to use the new :ref:`class-based runners Syndication feeds ----------------- -The :class:`django.contrib.syndication.feeds.Feed` class is being +The ``django.contrib.syndication.feeds.Feed`` class is being replaced by the :class:`django.contrib.syndication.views.Feed` class. The old ``feeds.Feed`` class is deprecated. The new class has an almost identical API, but allows instances to be used as views. diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt index 68cec91587..50c049f5da 100644 --- a/docs/releases/1.2.txt +++ b/docs/releases/1.2.txt @@ -345,10 +345,10 @@ in 1.2 is support for multiple spatial databases. As a result, the following :ref:`spatial database backends ` are now included: -* :mod:`django.contrib.gis.db.backends.postgis` -* :mod:`django.contrib.gis.db.backends.mysql` -* :mod:`django.contrib.gis.db.backends.oracle` -* :mod:`django.contrib.gis.db.backends.spatialite` +* ``django.contrib.gis.db.backends.postgis`` +* ``django.contrib.gis.db.backends.mysql`` +* ``django.contrib.gis.db.backends.oracle`` +* ``django.contrib.gis.db.backends.spatialite`` GeoDjango now supports the rich capabilities added in the `PostGIS 1.5 release `_. @@ -986,7 +986,7 @@ should be updated to use the new :ref:`class-based runners ``Feed`` in ``django.contrib.syndication.feeds`` ------------------------------------------------ -The :class:`django.contrib.syndication.feeds.Feed` class has been +The ``django.contrib.syndication.feeds.Feed`` class has been replaced by the :class:`django.contrib.syndication.views.Feed` class. The old ``feeds.Feed`` class is deprecated, and will be removed in Django 1.4. diff --git a/docs/releases/1.3-alpha-1.txt b/docs/releases/1.3-alpha-1.txt index e2c52a7264..ba8a4fc557 100644 --- a/docs/releases/1.3-alpha-1.txt +++ b/docs/releases/1.3-alpha-1.txt @@ -150,7 +150,7 @@ process has been on adding lots of smaller, long standing feature requests. These include: * Improved tools for accessing and manipulating the current Site via - :func:`django.contrib.sites.models.get_current_site`. + ``django.contrib.sites.models.get_current_site()``. * A :class:`~django.test.client.RequestFactory` for mocking requests in tests. diff --git a/docs/releases/1.3-beta-1.txt b/docs/releases/1.3-beta-1.txt index d064063fce..14897ed3b7 100644 --- a/docs/releases/1.3-beta-1.txt +++ b/docs/releases/1.3-beta-1.txt @@ -140,7 +140,7 @@ attribute. Changes to ``USStateField`` =========================== -The :mod:`django.contrib.localflavor` application contains collections +The ``django.contrib.localflavor`` application contains collections of code relevant to specific countries or cultures. One such is ``USStateField``, which provides a field for storing the two-letter postal abbreviation of a U.S. state. This field has consistently caused problems, @@ -167,13 +167,13 @@ as a pair of changes: independent nations -- the Federated States of Micronesia, the Republic of the Marshall Islands and the Republic of Palau -- which are serviced under treaty by the U.S. postal system. A new form - widget, :class:`django.contrib.localflavor.us.forms.USPSSelect`, is + widget, ``django.contrib.localflavor.us.forms.USPSSelect``, is also available and provides the same set of choices. Additionally, several finer-grained choice tuples are provided which allow mixing and matching of subsets of the U.S. states and territories, and other locations serviced by the U.S. postal -system. Consult the :mod:`django.contrib.localflavor` documentation +system. Consult the ``django.contrib.localflavor`` documentation for more details. The change to `USStateField` is technically backwards-incompatible for diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 6a056532b9..4c8dd2f81f 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -367,9 +367,8 @@ In earlier Django versions, when a model instance containing a file from the backend storage. This opened the door to several data-loss scenarios, including rolled-back transactions and fields on different models referencing the same file. In Django 1.3, when a model is deleted the -:class:`~django.db.models.FileField`'s -:func:`~django.db.models.FileField.delete` method won't be called. If you -need cleanup of orphaned files, you'll need to handle it yourself (for +:class:`~django.db.models.FileField`'s ``delete()`` method won't be called. If +you need cleanup of orphaned files, you'll need to handle it yourself (for instance, with a custom management command that can be run manually or scheduled to run periodically via e.g. cron). diff --git a/docs/releases/1.4-alpha-1.txt b/docs/releases/1.4-alpha-1.txt index 4086cfdecc..09855400eb 100644 --- a/docs/releases/1.4-alpha-1.txt +++ b/docs/releases/1.4-alpha-1.txt @@ -504,7 +504,7 @@ Django 1.4 also includes several smaller improvements worth noting: page. * The ``django.contrib.auth.models.check_password`` function has been moved - to the :mod:`django.contrib.auth.utils` module. Importing it from the old + to the ``django.contrib.auth.utils`` module. Importing it from the old location will still work, but you should update your imports. * The :djadmin:`collectstatic` management command gained a ``--clear`` option diff --git a/docs/releases/1.4-beta-1.txt b/docs/releases/1.4-beta-1.txt index a8732a9e65..8ea63742e3 100644 --- a/docs/releases/1.4-beta-1.txt +++ b/docs/releases/1.4-beta-1.txt @@ -564,7 +564,7 @@ Django 1.4 also includes several smaller improvements worth noting: page. * The ``django.contrib.auth.models.check_password`` function has been moved - to the :mod:`django.contrib.auth.utils` module. Importing it from the old + to the ``django.contrib.auth.utils`` module. Importing it from the old location will still work, but you should update your imports. * The :djadmin:`collectstatic` management command gained a ``--clear`` option diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index cf53b37f17..9459e940b4 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -888,10 +888,10 @@ object, Django raises an exception. ``MySQLdb``-specific exceptions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The MySQL backend historically has raised :class:`MySQLdb.OperationalError` +The MySQL backend historically has raised ``MySQLdb.OperationalError`` when a query triggered an exception. We've fixed this bug, and we now raise :exc:`django.db.DatabaseError` instead. If you were testing for -:class:`MySQLdb.OperationalError`, you'll need to update your ``except`` +``MySQLdb.OperationalError``, you'll need to update your ``except`` clauses. Database connection's thread-locality diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt index 0f44444416..76284ae72f 100644 --- a/docs/topics/auth/passwords.txt +++ b/docs/topics/auth/passwords.txt @@ -171,18 +171,18 @@ Manually managing a user's password .. module:: django.contrib.auth.hashers - The :mod:`django.contrib.auth.hashers` module provides a set of functions - to create and validate hashed password. You can use them independently - from the ``User`` model. +The :mod:`django.contrib.auth.hashers` module provides a set of functions +to create and validate hashed password. You can use them independently +from the ``User`` model. .. function:: check_password(password, encoded) If you'd like to manually authenticate a user by comparing a plain-text password to the hashed password in the database, use the convenience - function :func:`django.contrib.auth.hashers.check_password`. It takes two - arguments: the plain-text password to check, and the full value of a - user's ``password`` field in the database to check against, and returns - ``True`` if they match, ``False`` otherwise. + function :func:`check_password`. It takes two arguments: the plain-text + password to check, and the full value of a user's ``password`` field in the + database to check against, and returns ``True`` if they match, ``False`` + otherwise. .. function:: make_password(password[, salt, hashers]) @@ -195,9 +195,9 @@ Manually managing a user's password ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'`` if you have the ``crypt`` library installed. If the password argument is ``None``, an unusable password is returned (a one that will be never - accepted by :func:`django.contrib.auth.hashers.check_password`). + accepted by :func:`check_password`). .. function:: is_password_usable(encoded_password) Checks if the given string is a hashed password that has a chance - of being verified against :func:`django.contrib.auth.hashers.check_password`. + of being verified against :func:`check_password`. diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 9b3e41d0d4..208fa3a5e2 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -664,6 +664,8 @@ pickling.) Accessing the cache ------------------- +.. function:: django.core.cache.get_cache(backend, **kwargs) + The cache module, ``django.core.cache``, has a ``cache`` object that's automatically created from the ``'default'`` entry in the :setting:`CACHES` setting:: @@ -676,7 +678,7 @@ If you have multiple caches defined in :setting:`CACHES`, then you can use >>> from django.core.cache import get_cache >>> cache = get_cache('alternate') -If the named key does not exist, :exc:`InvalidCacheBackendError` will be raised. +If the named key does not exist, ``InvalidCacheBackendError`` will be raised. Basic usage @@ -844,7 +846,7 @@ key version to set or get. For example:: 'hello world!' The version of a specific key can be incremented and decremented using -the :func:`incr_version()` and :func:`decr_version()` methods. This +the ``incr_version()`` and ``decr_version()`` methods. This enables specific keys to be bumped to a new version, leaving other keys unaffected. Continuing our previous example:: @@ -879,7 +881,7 @@ parts), you can provide a custom key function. The :setting:`KEY_FUNCTION ` cache setting specifies a dotted-path to a function matching the prototype of -:func:`make_key()` above. If provided, this custom key function will +``make_key()`` above. If provided, this custom key function will be used instead of the default key combining function. Cache key warnings diff --git a/docs/topics/class-based-views/generic-display.txt b/docs/topics/class-based-views/generic-display.txt index 10279c0f63..dac45c8843 100644 --- a/docs/topics/class-based-views/generic-display.txt +++ b/docs/topics/class-based-views/generic-display.txt @@ -257,9 +257,9 @@ Specifying ``model = Publisher`` is really just shorthand for saying ``queryset = Publisher.objects.all()``. However, by using ``queryset`` to define a filtered list of objects you can be more specific about the objects that will be visible in the view (see :doc:`/topics/db/queries` -for more information about :class:`QuerySet` objects, and see the -:doc:`class-based views reference ` for the -complete details). +for more information about :class:`~django.db.models.query.QuerySet` objects, +and see the :doc:`class-based views reference ` +for the complete details). To pick a simple example, we might want to order a list of books by publication date, with the most recent first:: @@ -312,9 +312,9 @@ what if we wanted to write a view that displayed all the books by some arbitrary publisher? Handily, the ``ListView`` has a -:meth:`~django.views.generic.detail.ListView.get_queryset` method we can -override. Previously, it has just been returning the value of the ``queryset`` -attribute, but now we can add more logic. +:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset` method we +can override. Previously, it has just been returning the value of the +``queryset`` attribute, but now we can add more logic. The key part to making this work is that when class-based views are called, various useful things are stored on ``self``; as well as the request diff --git a/docs/topics/class-based-views/generic-editing.txt b/docs/topics/class-based-views/generic-editing.txt index 7d12184705..2f8b8b0711 100644 --- a/docs/topics/class-based-views/generic-editing.txt +++ b/docs/topics/class-based-views/generic-editing.txt @@ -7,10 +7,10 @@ Form processing generally has 3 paths: * POST with invalid data (typically redisplay form with errors) * POST with valid data (process the data and typically redirect) -Implementing this yourself often results in a lot of repeated -boilerplate code (see :ref:`Using a form in a -view`). To help avoid this, Django provides a -collection of generic class-based views for form processing. +Implementing this yourself often results in a lot of repeated boilerplate code +(see :ref:`Using a form in a view`). To help avoid +this, Django provides a collection of generic class-based views for form +processing. Basic Forms ----------- @@ -28,7 +28,7 @@ Given a simple contact form:: # send email using the self.cleaned_data dictionary pass -The view can be constructed using a FormView:: +The view can be constructed using a ``FormView``:: # views.py from myapp.forms import ContactForm @@ -50,42 +50,46 @@ Notes: * FormView inherits :class:`~django.views.generic.base.TemplateResponseMixin` so :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` - can be used here + can be used here. * The default implementation for - :meth:`~django.views.generic.edit.FormView.form_valid` simply - redirects to the :attr:`success_url` + :meth:`~django.views.generic.edit.FormMixin.form_valid` simply + redirects to the :attr:`~django.views.generic.edit.FormMixin.success_url`. Model Forms ----------- Generic views really shine when working with models. These generic -views will automatically create a :class:`ModelForm`, so long as they -can work out which model class to use: +views will automatically create a :class:`~django.forms.ModelForm`, so long as +they can work out which model class to use: -* If the :attr:`model` attribute is given, that model class will be used -* If :meth:`get_object()` returns an object, the class of that object - will be used -* If a :attr:`queryset` is given, the model for that queryset will be used +* If the :attr:`~django.views.generic.edit.ModelFormMixin.model` attribute is + given, that model class will be used. +* If :meth:`~django.views.generic.detail.SingleObjectMixin.get_object()` + returns an object, the class of that object will be used. +* If a :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` is + given, the model for that queryset will be used. -Model form views provide a :meth:`form_valid()` implementation that -saves the model automatically. You can override this if you have any +Model form views provide a +:meth:`~django.views.generic.edit.ModelFormMixin.form_valid()` implementation +that saves the model automatically. You can override this if you have any special requirements; see below for examples. -You don't even need to provide a attr:`success_url` for +You don't even need to provide a ``success_url`` for :class:`~django.views.generic.edit.CreateView` or :class:`~django.views.generic.edit.UpdateView` - they will use -:meth:`get_absolute_url()` on the model object if available. +:meth:`~django.db.models.Model.get_absolute_url()` on the model object if available. -If you want to use a custom :class:`ModelForm` (for instance to add -extra validation) simply set +If you want to use a custom :class:`~django.forms.ModelForm` (for instance to +add extra validation) simply set :attr:`~django.views.generic.edit.FormMixin.form_class` on your view. .. note:: When specifying a custom form class, you must still specify the model, - even though the :attr:`form_class` may be a :class:`ModelForm`. + even though the :attr:`~django.views.generic.edit.FormMixin.form_class` may + be a :class:`~django.forms.ModelForm`. -First we need to add :meth:`get_absolute_url()` to our :class:`Author` -class: +First we need to add :meth:`~django.db.models.Model.get_absolute_url()` to our +``Author`` class: .. code-block:: python @@ -137,8 +141,10 @@ Finally, we hook these new views into the URLconf:: .. note:: - These views inherit :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin` - which uses :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_prefix` + These views inherit + :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin` + which uses + :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` to construct the :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` based on the model. @@ -149,15 +155,17 @@ Finally, we hook these new views into the URLconf:: * :class:`DeleteView` uses ``myapp/author_confirm_delete.html`` If you wish to have separate templates for :class:`CreateView` and - :class:1UpdateView`, you can set either :attr:`template_name` or - :attr:`template_name_suffix` on your view class. + :class:`UpdateView`, you can set either + :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` or + :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` + on your view class. Models and request.user ----------------------- To track the user that created an object using a :class:`CreateView`, -you can use a custom :class:`ModelForm` to do this. First, add the -foreign key relation to the model:: +you can use a custom :class:`~django.forms.ModelForm` to do this. First, add +the foreign key relation to the model:: # models.py from django.contrib.auth import User @@ -169,7 +177,7 @@ foreign key relation to the model:: # ... -Create a custom :class:`ModelForm` in order to exclude the +Create a custom :class:`~django.forms.ModelForm` in order to exclude the ``created_by`` field and prevent the user from editing it: .. code-block:: python @@ -183,8 +191,10 @@ Create a custom :class:`ModelForm` in order to exclude the model = Author exclude = ('created_by',) -In the view, use the custom :attr:`form_class` and override -:meth:`form_valid()` to add the user:: +In the view, use the custom +:attr:`~django.views.generic.edit.FormMixin.form_class` and override +:meth:`~django.views.generic.edit.ModelFormMixin.form_valid()` to add the +user:: # views.py from django.views.generic.edit import CreateView @@ -202,7 +212,8 @@ In the view, use the custom :attr:`form_class` and override Note that you'll need to :ref:`decorate this view` using :func:`~django.contrib.auth.decorators.login_required`, or -alternatively handle unauthorised users in the :meth:`form_valid()`. +alternatively handle unauthorized users in the +:meth:`~django.views.generic.edit.ModelFormMixin.form_valid()`. AJAX example ------------ diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index 923b877cc5..4941ea9755 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -32,13 +32,14 @@ Two central mixins are provided that help in providing a consistent interface to working with templates in class-based views. :class:`~django.views.generic.base.TemplateResponseMixin` + Every built in view which returns a :class:`~django.template.response.TemplateResponse` will call the :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` - method that :class:`TemplateResponseMixin` provides. Most of the time this + method that ``TemplateResponseMixin`` provides. Most of the time this will be called for you (for instance, it is called by the ``get()`` method implemented by both :class:`~django.views.generic.base.TemplateView` and - :class:`~django.views.generic.base.DetailView`); similarly, it's unlikely + :class:`~django.views.generic.detail.DetailView`); similarly, it's unlikely that you'll need to override it, although if you want your response to return something not rendered via a Django template then you'll want to do it. For an example of this, see the :ref:`JSONResponseMixin example @@ -59,10 +60,10 @@ interface to working with templates in class-based views. :class:`~django.views.generic.base.ContextMixin` Every built in view which needs context data, such as for rendering a - template (including :class:`TemplateResponseMixin` above), should call + template (including ``TemplateResponseMixin`` above), should call :meth:`~django.views.generic.base.ContextMixin.get_context_data` passing any data they want to ensure is in there as keyword arguments. - ``get_context_data`` returns a dictionary; in :class:`ContextMixin` it + ``get_context_data`` returns a dictionary; in ``ContextMixin`` it simply returns its keyword arguments, but it is common to override this to add more members to the dictionary. @@ -106,7 +107,7 @@ URLConf, and looks the object up either from the :attr:`~django.views.generic.detail.SingleObjectMixin.model` attribute on the view, or the :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` -attribute if that's provided). :class:`SingleObjectMixin` also overrides +attribute if that's provided). ``SingleObjectMixin`` also overrides :meth:`~django.views.generic.base.ContextMixin.get_context_data`, which is used across all Django's built in class-based views to supply context data for template renders. @@ -115,10 +116,12 @@ To then make a :class:`~django.template.response.TemplateResponse`, :class:`DetailView` uses :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`, which extends :class:`~django.views.generic.base.TemplateResponseMixin`, -overriding :meth:`get_template_names()` as discussed above. It actually -provides a fairly sophisticated set of options, but the main one that most -people are going to use is ``/_detail.html``. The -``_detail`` part can be changed by setting +overriding +:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` +as discussed above. It actually provides a fairly sophisticated set of options, +but the main one that most people are going to use is +``/_detail.html``. The ``_detail`` part can be changed +by setting :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` on a subclass to something else. (For instance, the :doc:`generic edit views` use ``_form`` for create and update views, and @@ -128,9 +131,10 @@ ListView: working with many Django objects ------------------------------------------ Lists of objects follow roughly the same pattern: we need a (possibly -paginated) list of objects, typically a :class:`QuerySet`, and then we need -to make a :class:`TemplateResponse` with a suitable template using -that list of objects. +paginated) list of objects, typically a +:class:`~django.db.models.query.QuerySet`, and then we need to make a +:class:`~django.template.response.TemplateResponse` with a suitable template +using that list of objects. To get the objects, :class:`~django.views.generic.list.ListView` uses :class:`~django.views.generic.list.MultipleObjectMixin`, which @@ -138,9 +142,9 @@ provides both :meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset` and :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset`. Unlike -with :class:`SingleObjectMixin`, there's no need to key off parts of -the URL to figure out the queryset to work with, so the default just -uses the +with :class:`~django.views.generic.detail.SingleObjectMixin`, there's no need +to key off parts of the URL to figure out the queryset to work with, so the +default just uses the :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` or :attr:`~django.views.generic.list.MultipleObjectMixin.model` attribute on the view class. A common reason to override @@ -148,19 +152,19 @@ on the view class. A common reason to override here would be to dynamically vary the objects, such as depending on the current user or to exclude posts in the future for a blog. -:class:`MultipleObjectMixin` also overrides +:class:`~django.views.generic.list.MultipleObjectMixin` also overrides :meth:`~django.views.generic.base.ContextMixin.get_context_data` to include appropriate context variables for pagination (providing dummies if pagination is disabled). It relies on ``object_list`` being passed in as a keyword argument, which :class:`ListView` arranges for it. -To make a :class:`TemplateResponse`, :class:`ListView` then uses +To make a :class:`~django.template.response.TemplateResponse`, +:class:`ListView` then uses :class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`; -as with :class:`SingleObjectTemplateResponseMixin` above, this -overrides :meth:`get_template_names()` to provide :meth:`a range of -options -<~django.views.generic.list.MultipleObjectTempalteResponseMixin>`, +as with :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin` +above, this overrides ``get_template_names()`` to provide :meth:`a range of +options `, with the most commonly-used being ``/_list.html``, with the ``_list`` part again being taken from the @@ -197,13 +201,13 @@ the box. If in doubt, it's often better to back off and base your work on :class:`View` or :class:`TemplateView`, perhaps with - :class:`SimpleObjectMixin` and - :class:`MultipleObjectMixin`. Although you will probably end up - writing more code, it is more likely to be clearly understandable - to someone else coming to it later, and with fewer interactions to - worry about you will save yourself some thinking. (Of course, you - can always dip into Django's implementation of the generic class - based views for inspiration on how to tackle problems.) + :class:`~django.views.generic.detail.SingleObjectMixin` and + :class:`~django.views.generic.list.MultipleObjectMixin`. Although you + will probably end up writing more code, it is more likely to be clearly + understandable to someone else coming to it later, and with fewer + interactions to worry about you will save yourself some thinking. (Of + course, you can always dip into Django's implementation of the generic + class based views for inspiration on how to tackle problems.) .. _method resolution order: http://www.python.org/download/releases/2.3/mro/ @@ -247,9 +251,9 @@ We'll demonstrate this with the publisher modelling we used in the In practice you'd probably want to record the interest in a key-value store rather than in a relational database, so we've left that bit out. The only bit of the view that needs to worry about using -:class:`SingleObjectMixin` is where we want to look up the author -we're interested in, which it just does with a simple call to -``self.get_object()``. Everything else is taken care of for us by the +:class:`~django.views.generic.detail.SingleObjectMixin` is where we want to +look up the author we're interested in, which it just does with a simple call +to ``self.get_object()``. Everything else is taken care of for us by the mixin. We can hook this into our URLs easily enough:: @@ -265,7 +269,8 @@ We can hook this into our URLs easily enough:: Note the ``pk`` named group, which :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` uses to look up the ``Author`` instance. You could also use a slug, or -any of the other features of :class:`SingleObjectMixin`. +any of the other features of +:class:`~django.views.generic.detail.SingleObjectMixin`. Using SingleObjectMixin with ListView ------------------------------------- @@ -277,23 +282,24 @@ example, you might want to paginate through all the books by a particular publisher. One way to do this is to combine :class:`ListView` with -:class:`SingleObjectMixin`, so that the queryset for the paginated -list of books can hang off the publisher found as the single +:class:`~django.views.generic.detail.SingleObjectMixin`, so that the queryset +for the paginated list of books can hang off the publisher found as the single object. In order to do this, we need to have two different querysets: **Publisher queryset for use in get_object** - We'll set that up directly when we call :meth:`get_object()`. + We'll set that up directly when we call ``get_object()``. **Book queryset for use by ListView** - We'll figure that out ourselves in :meth:`get_queryset()` so we - can take into account the Publisher we're looking at. + We'll figure that out ourselves in ``get_queryset()`` so we + can take into account the ``Publisher`` we're looking at. .. note:: - We have to think carefully about :meth:`get_context_data()`. - Since both :class:`SingleObjectMixin` and :class:`ListView` will + We have to think carefully about ``get_context_data()``. + Since both :class:`~django.views.generic.detail.SingleObjectMixin` and + :class:`ListView` will put things in the context data under the value of - :attr:`context_object_name` if it's set, we'll instead explictly + ``context_object_name`` if it's set, we'll instead explictly ensure the Publisher is in the context data. :class:`ListView` will add in the suitable ``page_obj`` and ``paginator`` for us providing we remember to call ``super()``. @@ -316,13 +322,14 @@ Now we can write a new ``PublisherDetail``:: self.object = self.get_object(Publisher.objects.all()) return self.object.book_set.all() -Notice how we set ``self.object`` within :meth:`get_queryset` so we -can use it again later in :meth:`get_context_data`. If you don't set -:attr:`template_name`, the template will default to the normal +Notice how we set ``self.object`` within ``get_queryset()`` so we +can use it again later in ``get_context_data()``. If you don't set +``template_name``, the template will default to the normal :class:`ListView` choice, which in this case would be ``"books/book_list.html"`` because it's a list of books; -:class:`ListView` knows nothing about :class:`SingleObjectMixin`, so -it doesn't have any clue this view is anything to do with a Publisher. +:class:`ListView` knows nothing about +:class:`~django.views.generic.detail.SingleObjectMixin`, so it doesn't have +any clue this view is anything to do with a Publisher. .. highlightlang:: html+django @@ -365,7 +372,7 @@ Generally you can use :class:`~django.views.generic.base.TemplateResponseMixin` and :class:`~django.views.generic.detail.SingleObjectMixin` when you need their functionality. As shown above, with a bit of care you can even -combine :class:`SingleObjectMixin` with +combine ``SingleObjectMixin`` with :class:`~django.views.generic.list.ListView`. However things get increasingly complex as you try to do so, and a good rule of thumb is: @@ -376,48 +383,48 @@ increasingly complex as you try to do so, and a good rule of thumb is: list`, :doc:`editing` and date. For example it's fine to combine :class:`TemplateView` (built in view) with - :class:`MultipleObjectMixin` (generic list), but you're likely to - have problems combining :class:`SingleObjectMixin` (generic - detail) with :class:`MultipleObjectMixin` (generic list). + :class:`~django.views.generic.list.MultipleObjectMixin` (generic list), but + you're likely to have problems combining ``SingleObjectMixin`` (generic + detail) with ``MultipleObjectMixin`` (generic list). To show what happens when you try to get more sophisticated, we show an example that sacrifices readability and maintainability when there is a simpler solution. First, let's look at a naive attempt to combine :class:`~django.views.generic.detail.DetailView` with :class:`~django.views.generic.edit.FormMixin` to enable use to -``POST`` a Django :class:`Form` to the same URL as we're displaying an -object using :class:`DetailView`. +``POST`` a Django :class:`~django.forms.Form` to the same URL as we're +displaying an object using :class:`DetailView`. Using FormMixin with DetailView ------------------------------- Think back to our earlier example of using :class:`View` and -:class:`SingleObjectMixin` together. We were recording a user's -interest in a particular author; say now that we want to let them -leave a message saying why they like them. Again, let's assume we're +:class:`~django.views.generic.detail.SingleObjectMixin` together. We were +recording a user's interest in a particular author; say now that we want to +let them leave a message saying why they like them. Again, let's assume we're not going to store this in a relational database but instead in something more esoteric that we won't worry about here. -At this point it's natural to reach for a :class:`Form` to encapsulate -the information sent from the user's browser to Django. Say also that -we're heavily invested in `REST`_, so we want to use the same URL for +At this point it's natural to reach for a :class:`~django.forms.Form` to +encapsulate the information sent from the user's browser to Django. Say also +that we're heavily invested in `REST`_, so we want to use the same URL for displaying the author as for capturing the message from the user. Let's rewrite our ``AuthorDetailView`` to do that. .. _REST: http://en.wikipedia.org/wiki/Representational_state_transfer We'll keep the ``GET`` handling from :class:`DetailView`, although -we'll have to add a :class:`Form` into the context data so we can +we'll have to add a :class:`~django.forms.Form` into the context data so we can render it in the template. We'll also want to pull in form processing from :class:`~django.views.generic.edit.FormMixin`, and write a bit of code so that on ``POST`` the form gets called appropriately. .. note:: - We use :class:`FormMixin` and implement :meth:`post()` ourselves - rather than try to mix :class:`DetailView` with :class:`FormView` - (which provides a suitable :meth:`post()` already) because both of - the views implement :meth:`get()`, and things would get much more + We use :class:`~django.views.generic.edit.FormMixin` and implement + ``post()`` ourselves rather than try to mix :class:`DetailView` with + :class:`FormView` (which provides a suitable ``post()`` already) because + both of the views implement ``get()``, and things would get much more confusing. .. highlightlang:: python @@ -472,24 +479,24 @@ Our new ``AuthorDetail`` looks like this:: # record the interest using the message in form.cleaned_data return super(AuthorDetail, self).form_valid(form) -:meth:`get_success_url()` is just providing somewhere to redirect to, +``get_success_url()`` is just providing somewhere to redirect to, which gets used in the default implementation of -:meth:`form_valid()`. We have to provide our own :meth:`post()` as -noted earlier, and override :meth:`get_context_data()` to make the -:class:`Form` available in the context data. +``form_valid()``. We have to provide our own ``post()`` as +noted earlier, and override ``get_context_data()`` to make the +:class:`~django.forms.Form` available in the context data. A better solution ----------------- It should be obvious that the number of subtle interactions between -:class:`FormMixin` and :class:`DetailView` is already testing our -ability to manage things. It's unlikely you'd want to write this kind -of class yourself. +:class:`~django.views.generic.edit.FormMixin` and :class:`DetailView` is +already testing our ability to manage things. It's unlikely you'd want to +write this kind of class yourself. -In this case, it would be fairly easy to just write the :meth:`post()` +In this case, it would be fairly easy to just write the ``post()`` method yourself, keeping :class:`DetailView` as the only generic -functionality, although writing :class:`Form` handling code involves a -lot of duplication. +functionality, although writing :class:`~django.forms.Form` handling code +involves a lot of duplication. Alternatively, it would still be easier than the above approach to have a separate view for processing the form, which could use @@ -502,15 +509,15 @@ An alternative better solution What we're really trying to do here is to use two different class based views from the same URL. So why not do just that? We have a very clear division here: ``GET`` requests should get the -:class:`DetailView` (with the :class:`Form` added to the context +:class:`DetailView` (with the :class:`~django.forms.Form` added to the context data), and ``POST`` requests should get the :class:`FormView`. Let's set up those views first. The ``AuthorDisplay`` view is almost the same as :ref:`when we first introduced AuthorDetail`; we have to -write our own :meth:`get_context_data()` to make the +write our own ``get_context_data()`` to make the ``AuthorInterestForm`` available to the template. We'll skip the -:meth:`get_object()` override from before for clarity. +``get_object()`` override from before for clarity. .. code-block:: python @@ -533,9 +540,9 @@ write our own :meth:`get_context_data()` to make the return super(AuthorDisplay, self).get_context_data(**context) Then the ``AuthorInterest`` is a simple :class:`FormView`, but we -have to bring in :class:`SingleObjectMixin` so we can find the author -we're talking about, and we have to remember to set -:attr:`template_name` to ensure that form errors will render the same +have to bring in :class:`~django.views.generic.detail.SingleObjectMixin` so we +can find the author we're talking about, and we have to remember to set +``template_name`` to ensure that form errors will render the same template as ``AuthorDisplay`` is using on ``GET``. .. code-block:: python @@ -568,14 +575,14 @@ template as ``AuthorDisplay`` is using on ``GET``. return super(AuthorInterest, self).form_valid(form) Finally we bring this together in a new ``AuthorDetail`` view. We -already know that calling :meth:`as_view()` on a class-based view -gives us something that behaves exactly like a function based view, so -we can do that at the point we choose between the two subviews. +already know that calling :meth:`~django.views.generic.base.View.as_view()` on +a class-based view gives us something that behaves exactly like a function +based view, so we can do that at the point we choose between the two subviews. -You can of course pass through keyword arguments to :meth:`as_view()` -in the same way you would in your URLconf, such as if you wanted the -``AuthorInterest`` behaviour to also appear at another URL but -using a different template. +You can of course pass through keyword arguments to +:meth:`~django.views.generic.base.View.as_view()` in the same way you +would in your URLconf, such as if you wanted the ``AuthorInterest`` behavior +to also appear at another URL but using a different template. .. code-block:: python @@ -646,8 +653,8 @@ Now we mix this into the base TemplateView:: Equally we could use our mixin with one of the generic views. We can make our own version of :class:`~django.views.generic.detail.DetailView` by mixing -:class:`JSONResponseMixin` with the -:class:`~django.views.generic.detail.BaseDetailView` -- (the +``JSONResponseMixin`` with the +``django.views.generic.detail.BaseDetailView`` -- (the :class:`~django.views.generic.detail.DetailView` before template rendering behavior has been mixed in):: @@ -662,11 +669,12 @@ If you want to be really adventurous, you could even mix a :class:`~django.views.generic.detail.DetailView` subclass that is able to return *both* HTML and JSON content, depending on some property of the HTTP request, such as a query argument or a HTTP header. Just mix -in both the :class:`JSONResponseMixin` and a +in both the ``JSONResponseMixin`` and a :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`, -and override the implementation of :func:`render_to_response()` to defer -to the appropriate subclass depending on the type of response that the user -requested:: +and override the implementation of +:func:`~django.views.generic.base.TemplateResponseMixin.render_to_response()` +to defer to the appropriate subclass depending on the type of response that the +user requested:: class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView): def render_to_response(self, context): @@ -678,5 +686,5 @@ requested:: Because of the way that Python resolves method overloading, the local ``render_to_response()`` implementation will override the versions provided by -:class:`JSONResponseMixin` and +``JSONResponseMixin`` and :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`. diff --git a/docs/topics/db/sql.txt b/docs/topics/db/sql.txt index 310dcb5ae6..6cc174a248 100644 --- a/docs/topics/db/sql.txt +++ b/docs/topics/db/sql.txt @@ -24,9 +24,8 @@ return model instances: .. method:: Manager.raw(raw_query, params=None, translations=None) This method method takes a raw SQL query, executes it, and returns a -:class:`~django.db.models.query.RawQuerySet` instance. This -:class:`~django.db.models.query.RawQuerySet` instance can be iterated -over just like an normal QuerySet to provide object instances. +``django.db.models.query.RawQuerySet`` instance. This ``RawQuerySet`` instance +can be iterated over just like an normal QuerySet to provide object instances. This is best illustrated with an example. Suppose you've got the following model:: diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 7716c91681..11755ff5c5 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -48,10 +48,9 @@ you use the session middleware after the transaction middleware, session creation will be part of the transaction. The various cache middlewares are an exception: -:class:`~django.middleware.cache.CacheMiddleware`, -:class:`~django.middleware.cache.UpdateCacheMiddleware`, and -:class:`~django.middleware.cache.FetchFromCacheMiddleware` are never affected. -Even when using database caching, Django's cache backend uses its own +``CacheMiddleware``, :class:`~django.middleware.cache.UpdateCacheMiddleware`, +and :class:`~django.middleware.cache.FetchFromCacheMiddleware` are never +affected. Even when using database caching, Django's cache backend uses its own database cursor (which is mapped to its own database connection internally). .. note:: diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index b5b02581cd..ee1c69e031 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -3,6 +3,8 @@ Formsets ======== +.. class:: django.forms.formset.BaseFormSet + A formset is a layer of abstraction to working with multiple forms on the same page. It can be best compared to a data grid. Let's say you have the following form:: diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt index b3a830c25e..53499359e3 100644 --- a/docs/topics/http/file-uploads.txt +++ b/docs/topics/http/file-uploads.txt @@ -227,8 +227,8 @@ field in the model:: ``UploadedFile`` objects ======================== -In addition to those inherited from :class:`File`, all ``UploadedFile`` objects -define the following methods/attributes: +In addition to those inherited from :class:`~django.core.files.File`, all +``UploadedFile`` objects define the following methods/attributes: .. attribute:: UploadedFile.content_type diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index 9ef521c71d..f73ec4f5be 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -132,6 +132,8 @@ Customizing error views The 404 (page not found) view ----------------------------- +.. function:: django.views.defaults.page_not_found(request, template_name='404.html') + When you raise an ``Http404`` exception, Django loads a special view devoted to handling 404 errors. By default, it's the view ``django.views.defaults.page_not_found``, which either produces a very simple diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt index 14c81e6665..22a0edb073 100644 --- a/docs/topics/i18n/timezones.txt +++ b/docs/topics/i18n/timezones.txt @@ -310,7 +310,7 @@ time zone is unset, the default time zone applies. get_current_timezone ~~~~~~~~~~~~~~~~~~~~ -When the :func:`django.core.context_processors.tz` context processor is +When the ``django.core.context_processors.tz`` context processor is enabled -- by default, it is -- each :class:`~django.template.RequestContext` contains a ``TIME_ZONE`` variable that provides the name of the current time zone. @@ -659,7 +659,7 @@ Usage datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=) Note that ``localize`` is a pytz extension to the :class:`~datetime.tzinfo` - API. Also, you may want to catch :exc:`~pytz.InvalidTimeError`. The + API. Also, you may want to catch ``pytz.InvalidTimeError``. The documentation of pytz contains `more examples`_. You should review it before attempting to manipulate aware datetimes. diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index db0c0b3d25..3b68914c1a 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -141,7 +141,7 @@ error log record will be written. Naming loggers -------------- -The call to :meth:`logging.getLogger()` obtains (creating, if +The call to :func:`logging.getLogger()` obtains (creating, if necessary) an instance of a logger. The logger instance is identified by a name. This name is used to identify the logger for configuration purposes. @@ -242,7 +242,7 @@ An example The full documentation for `dictConfig format`_ is the best source of information about logging configuration dictionaries. However, to give you a taste of what is possible, here is an example of a fairly -complex logging setup, configured using :meth:`logging.dictConfig`:: +complex logging setup, configured using :func:`logging.config.dictConfig`:: LOGGING = { 'version': 1, @@ -317,12 +317,12 @@ This logging configuration does the following things: message, plus the time, process, thread and module that generate the log message. -* Defines one filter -- :class:`project.logging.SpecialFilter`, +* Defines one filter -- ``project.logging.SpecialFilter``, using the alias ``special``. If this filter required additional arguments at time of construction, they can be provided as additional keys in the filter configuration dictionary. In this case, the argument ``foo`` will be given a value of ``bar`` when - instantiating the :class:`SpecialFilter`. + instantiating the ``SpecialFilter``. * Defines three handlers: @@ -365,7 +365,7 @@ logger, you can specify your own configuration scheme. The :setting:`LOGGING_CONFIG` setting defines the callable that will be used to configure Django's loggers. By default, it points at -Python's :meth:`logging.dictConfig()` method. However, if you want to +Python's :func:`logging.config.dictConfig()` function. However, if you want to use a different configuration process, you can use any other callable that takes a single argument. The contents of :setting:`LOGGING` will be provided as the value of that argument when logging is configured. @@ -509,7 +509,7 @@ logging module. through the filter. Handling of that record will not proceed if the callback returns False. - For instance, to filter out :class:`~django.http.UnreadablePostError` + For instance, to filter out :exc:`~django.http.UnreadablePostError` (raised when a user cancels an upload) from the admin emails, you would create a filter function:: diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt index e1d78a10e6..b44c180d7f 100644 --- a/docs/topics/python3.txt +++ b/docs/topics/python3.txt @@ -78,8 +78,8 @@ wherever possible and avoid the ``b`` prefixes. String handling --------------- -Python 2's :class:`unicode` type was renamed :class:`str` in Python 3, -:class:`str` was renamed :class:`bytes`, and :class:`basestring` disappeared. +Python 2's :func:`unicode` type was renamed :func:`str` in Python 3, +:func:`str` was renamed ``bytes()``, and :func:`basestring` disappeared. six_ provides :ref:`tools ` to deal with these changes. @@ -131,35 +131,36 @@ and ``SafeText`` respectively. For forwards compatibility, the new names work as of Django 1.4.2. -:meth:`__str__` and :meth:`__unicode__` methods ------------------------------------------------ +:meth:`~object.__str__` and :meth:`~object.__unicode__` methods +--------------------------------------------------------------- -In Python 2, the object model specifies :meth:`__str__` and -:meth:`__unicode__` methods. If these methods exist, they must return -:class:`str` (bytes) and :class:`unicode` (text) respectively. +In Python 2, the object model specifies :meth:`~object.__str__` and +:meth:`~object.__unicode__` methods. If these methods exist, they must return +``str`` (bytes) and ``unicode`` (text) respectively. -The ``print`` statement and the :func:`str` built-in call :meth:`__str__` to -determine the human-readable representation of an object. The :func:`unicode` -built-in calls :meth:`__unicode__` if it exists, and otherwise falls back to -:meth:`__str__` and decodes the result with the system encoding. Conversely, -the :class:`~django.db.models.Model` base class automatically derives -:meth:`__str__` from :meth:`__unicode__` by encoding to UTF-8. +The ``print`` statement and the :func:`str` built-in call +:meth:`~object.__str__` to determine the human-readable representation of an +object. The :func:`unicode` built-in calls :meth:`~object.__unicode__` if it +exists, and otherwise falls back to :meth:`~object.__str__` and decodes the +result with the system encoding. Conversely, the +:class:`~django.db.models.Model` base class automatically derives +:meth:`~object.__str__` from :meth:`~object.__unicode__` by encoding to UTF-8. -In Python 3, there's simply :meth:`__str__`, which must return :class:`str` +In Python 3, there's simply :meth:`~object.__str__`, which must return ``str`` (text). -(It is also possible to define :meth:`__bytes__`, but Django application have +(It is also possible to define ``__bytes__()``, but Django application have little use for that method, because they hardly ever deal with -:class:`bytes`.) +``bytes``.) -Django provides a simple way to define :meth:`__str__` and :meth:`__unicode__` -methods that work on Python 2 and 3: you must define a :meth:`__str__` method -returning text and to apply the +Django provides a simple way to define :meth:`~object.__str__` and +:meth:`~object.__unicode__` methods that work on Python 2 and 3: you must +define a :meth:`~object.__str__` method returning text and to apply the :func:`~django.utils.encoding.python_2_unicode_compatible` decorator. On Python 3, the decorator is a no-op. On Python 2, it defines appropriate -:meth:`__unicode__` and :meth:`__str__` methods (replacing the original -:meth:`__str__` method in the process). Here's an example:: +:meth:`~object.__unicode__` and :meth:`~object.__str__` methods (replacing the +original :meth:`~object.__str__` method in the process). Here's an example:: from __future__ import unicode_literals from django.utils.encoding import python_2_unicode_compatible @@ -173,8 +174,8 @@ This technique is the best match for Django's porting philosophy. For forwards compatibility, this decorator is available as of Django 1.4.2. -Finally, note that :meth:`__repr__` must return a :class:`str` on all versions -of Python. +Finally, note that :meth:`~object.__repr__` must return a ``str`` on all +versions of Python. :class:`dict` and :class:`dict`-like classes -------------------------------------------- @@ -187,19 +188,19 @@ behave likewise in Python 3. six_ provides compatibility functions to work around this change: :func:`~six.iterkeys`, :func:`~six.iteritems`, and :func:`~six.itervalues`. Django's bundled version adds :func:`~django.utils.six.iterlists` for -:class:`~django.utils.datastructures.MultiValueDict` and its subclasses. +``django.utils.datastructures.MultiValueDict`` and its subclasses. :class:`~django.http.HttpRequest` and :class:`~django.http.HttpResponse` objects -------------------------------------------------------------------------------- According to :pep:`3333`: -- headers are always :class:`str` objects, -- input and output streams are always :class:`bytes` objects. +- headers are always ``str`` objects, +- input and output streams are always ``bytes`` objects. Specifically, :attr:`HttpResponse.content ` -contains :class:`bytes`, which may become an issue if you compare it with a -:class:`str` in your tests. The preferred solution is to rely on +contains ``bytes``, which may become an issue if you compare it with a +``str`` in your tests. The preferred solution is to rely on :meth:`~django.test.TestCase.assertContains` and :meth:`~django.test.TestCase.assertNotContains`. These methods accept a response and a unicode string as arguments. @@ -236,11 +237,10 @@ under Python 3, use the :func:`str` builtin:: str('my string') -In Python 3, there aren't any automatic conversions between :class:`str` and -:class:`bytes`, and the :mod:`codecs` module became more strict. -:meth:`str.decode` always returns :class:`bytes`, and :meth:`bytes.decode` -always returns :class:`str`. As a consequence, the following pattern is -sometimes necessary:: +In Python 3, there aren't any automatic conversions between ``str`` and +``bytes``, and the :mod:`codecs` module became more strict. :meth:`str.decode` +always returns ``bytes``, and ``bytes.decode`` always returns ``str``. As a +consequence, the following pattern is sometimes necessary:: value = value.encode('ascii', 'ignore').decode('ascii') @@ -395,11 +395,8 @@ The version of six bundled with Django includes one extra function: .. function:: iterlists(MultiValueDict) - Returns an iterator over the lists of values of a - :class:`~django.utils.datastructures.MultiValueDict`. This replaces - :meth:`~django.utils.datastructures.MultiValueDict.iterlists()` on Python - 2 and :meth:`~django.utils.datastructures.MultiValueDict.lists()` on - Python 3. + Returns an iterator over the lists of values of a ``MultiValueDict``. This + replaces ``iterlists()`` on Python 2 and ``lists()`` on Python 3. .. function:: assertRaisesRegex(testcase, *args, **kwargs) diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt index e36c7587d1..2af0584a61 100644 --- a/docs/topics/serialization.txt +++ b/docs/topics/serialization.txt @@ -26,6 +26,8 @@ to (see `Serialization formats`_) and a argument can be any iterator that yields Django model instances, but it'll almost always be a QuerySet). +.. function:: django.core.serializers.get_serializer(format) + You can also use a serializer object directly:: XMLSerializer = serializers.get_serializer("xml") @@ -43,7 +45,7 @@ This is useful if you want to serialize data directly to a file-like object Calling :func:`~django.core.serializers.get_serializer` with an unknown :ref:`format ` will raise a - :class:`~django.core.serializers.SerializerDoesNotExist` exception. + ``django.core.serializers.SerializerDoesNotExist`` exception. Subset of fields ~~~~~~~~~~~~~~~~ diff --git a/docs/topics/settings.txt b/docs/topics/settings.txt index 88fa7b6864..fa26297988 100644 --- a/docs/topics/settings.txt +++ b/docs/topics/settings.txt @@ -32,6 +32,8 @@ Because a settings file is a Python module, the following apply: Designating the settings ======================== +.. envvar:: DJANGO_SETTINGS_MODULE + When you use Django, you have to tell it which settings you're using. Do this by using an environment variable, ``DJANGO_SETTINGS_MODULE``. @@ -260,4 +262,3 @@ It boils down to this: Use exactly one of either ``configure()`` or ``DJANGO_SETTINGS_MODULE``. Not both, and not neither. .. _@login_required: ../authentication/#the-login-required-decorator - diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index e51741e549..534569efeb 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -28,7 +28,7 @@ module defines tests in class-based approach. backported for Python 2.5 compatibility. To access this library, Django provides the - :mod:`django.utils.unittest` module alias. If you are using Python + ``django.utils.unittest`` module alias. If you are using Python 2.7, or you have installed unittest2 locally, Django will map the alias to the installed version of the unittest library. Otherwise, Django will use its own bundled version of unittest2. @@ -853,7 +853,7 @@ Normal Python unit test classes extend a base class of Hierarchy of Django unit testing classes Regardless of the version of Python you're using, if you've installed -``unittest2``, :mod:`django.utils.unittest` will point to that library. +``unittest2``, ``django.utils.unittest`` will point to that library. SimpleTestCase ~~~~~~~~~~~~~~ @@ -882,7 +882,7 @@ features like: then you should use :class:`~django.test.TransactionTestCase` or :class:`~django.test.TestCase` instead. -``SimpleTestCase`` inherits from :class:`django.utils.unittest.TestCase`. +``SimpleTestCase`` inherits from ``django.utils.unittest.TestCase``. TransactionTestCase ~~~~~~~~~~~~~~~~~~~ @@ -1724,7 +1724,7 @@ test if the database doesn't support a specific named feature. The decorators use a string identifier to describe database features. This string corresponds to attributes of the database connection -features class. See :class:`~django.db.backends.BaseDatabaseFeatures` +features class. See ``django.db.backends.BaseDatabaseFeatures`` class for a full list of database features that can be used as a basis for skipping tests. From 3fc43c964ef7ab52261ec3a164d66b19f06f5cea Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Thu, 3 Jan 2013 15:13:51 +0100 Subject: [PATCH 103/870] Fixed #19545 -- Make sure media/is_multipart work with empty formsets --- django/forms/formsets.py | 7 +++++-- tests/regressiontests/forms/tests/formsets.py | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 219a80edee..ee9a2b5f63 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -333,7 +333,10 @@ class BaseFormSet(object): Returns True if the formset needs to be multipart, i.e. it has FileInput. Otherwise, False. """ - return self.forms and self.forms[0].is_multipart() + if self.forms: + return self.forms[0].is_multipart() + else: + return self.empty_form.is_multipart() @property def media(self): @@ -342,7 +345,7 @@ class BaseFormSet(object): if self.forms: return self.forms[0].media else: - return Media() + return self.empty_form.media def as_table(self): "Returns this formset rendered as HTML s -- excluding the
." diff --git a/tests/regressiontests/forms/tests/formsets.py b/tests/regressiontests/forms/tests/formsets.py index bf893c4c1d..ef6f40c3e3 100644 --- a/tests/regressiontests/forms/tests/formsets.py +++ b/tests/regressiontests/forms/tests/formsets.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.forms import Form, CharField, IntegerField, ValidationError, DateField -from django.forms.formsets import formset_factory, BaseFormSet +from django.forms import (CharField, DateField, FileField, Form, IntegerField, + ValidationError) +from django.forms.formsets import BaseFormSet, formset_factory from django.forms.util import ErrorList from django.test import TestCase @@ -974,11 +975,23 @@ class TestIsBoundBehavior(TestCase): self.assertHTMLEqual(empty_forms[0].as_p(), empty_forms[1].as_p()) class TestEmptyFormSet(TestCase): - "Test that an empty formset still calls clean()" def test_empty_formset_is_valid(self): + """Test that an empty formset still calls clean()""" EmptyFsetWontValidateFormset = formset_factory(FavoriteDrinkForm, extra=0, formset=EmptyFsetWontValidate) formset = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'0'},prefix="form") formset2 = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'1', 'form-0-name':'bah' },prefix="form") self.assertFalse(formset.is_valid()) self.assertFalse(formset2.is_valid()) + def test_empty_formset_media(self): + """Make sure media is available on empty formset, refs #19545""" + class MediaForm(Form): + class Media: + js = ('some-file.js',) + self.assertIn('some-file.js', str(formset_factory(MediaForm, extra=0)().media)) + + def test_empty_formset_is_multipart(self): + """Make sure `is_multipart()` works with empty formset, refs #19545""" + class FileForm(Form): + file = FileField() + self.assertTrue(formset_factory(FileForm, extra=0)().is_multipart()) From 84ea85fb90a3914fef9ab74b1f4f5bb919bec068 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 3 Jan 2013 17:37:40 +0100 Subject: [PATCH 104/870] Updated comment about PostGIS bug 2035 PostGIS 2.0.2 has been released on December 3rd 2012, with the fix included. --- django/contrib/gis/tests/geoapp/tests.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 8f2c22e841..a5164d39d1 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -295,10 +295,8 @@ class GeoLookupTest(TestCase): self.assertEqual(2, len(qs)) for c in qs: self.assertEqual(True, c.name in cities) - # The left/right lookup tests are known failures on PostGIS 2.0+ - # until the following bug is fixed: - # http://trac.osgeo.org/postgis/ticket/2035 - # TODO: Ensure fixed in 2.0.2, else modify upper bound for version here. + # The left/right lookup tests are known failures on PostGIS 2.0/2.0.1 + # http://trac.osgeo.org/postgis/ticket/2035 if (2, 0, 0) <= connection.ops.spatial_version <= (2, 0, 1): test_left_right_lookups = unittest.expectedFailure(test_left_right_lookups) From 1b3f832ab7291b6d92a27288bb97b3a3b712ebcb Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 3 Jan 2013 18:49:00 +0100 Subject: [PATCH 105/870] Fixed #19134 -- Allowed closing smtp backend when the server is stopped Thanks Sebastian Noack for the report and the initial patch. --- django/core/mail/backends/smtp.py | 5 +++-- tests/regressiontests/mail/tests.py | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py index b6f7f560ed..baffa8f2df 100644 --- a/django/core/mail/backends/smtp.py +++ b/django/core/mail/backends/smtp.py @@ -63,9 +63,10 @@ class EmailBackend(BaseEmailBackend): try: try: self.connection.quit() - except socket.sslerror: + except (socket.sslerror, smtplib.SMTPServerDisconnected): # This happens when calling quit() on a TLS connection - # sometimes. + # sometimes, or when the connection was already disconnected + # by the server. self.connection.close() except: if self.fail_silently: diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index a8fbf20fd6..9ab8c2c301 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -659,9 +659,9 @@ class FakeSMTPServer(smtpd.SMTPServer, threading.Thread): asyncore.close_all() def stop(self): - assert self.active - self.active = False - self.join() + if self.active: + self.active = False + self.join() class SMTPBackendTests(BaseEmailBackendTests, TestCase): @@ -715,3 +715,16 @@ class SMTPBackendTests(BaseEmailBackendTests, TestCase): backend = smtp.EmailBackend(username='', password='') self.assertEqual(backend.username, '') self.assertEqual(backend.password, '') + + def test_server_stopped(self): + """ + Test that closing the backend while the SMTP server is stopped doesn't + raise an exception. + """ + backend = smtp.EmailBackend(username='', password='') + backend.open() + self.server.stop() + try: + backend.close() + except Exception as e: + self.fail("close() unexpectedly raised an exception: %s" % e) From ffa50ca35219aa328e8e4ecda450a53c27c2e710 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 3 Jan 2013 20:41:45 +0100 Subject: [PATCH 106/870] Fixed #19382 -- Stopped smtp backend raising exception when already closed Thanks Sebastian Noack for the report and the initial patch. --- django/core/mail/backends/smtp.py | 2 ++ tests/regressiontests/mail/tests.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py index baffa8f2df..3ae08f4340 100644 --- a/django/core/mail/backends/smtp.py +++ b/django/core/mail/backends/smtp.py @@ -60,6 +60,8 @@ class EmailBackend(BaseEmailBackend): def close(self): """Closes the connection to the email server.""" + if self.connection is None: + return try: try: self.connection.quit() diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index 9ab8c2c301..059dd6d09a 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -492,6 +492,16 @@ class BaseEmailBackendTests(object): self.assertEqual(message.get('from'), "tester") self.assertEqual(message.get('to'), "django") + def test_close_connection(self): + """ + Test that connection can be closed (even when not explicitely opened) + """ + conn = mail.get_connection(username='', password='') + try: + conn.close() + except Exception as e: + self.fail("close() unexpectedly raised an exception: %s" % e) + class LocmemBackendTests(BaseEmailBackendTests, TestCase): email_backend = 'django.core.mail.backends.locmem.EmailBackend' From 6248833d9e35926d6ccd4b4d602f7ea89fea0c74 Mon Sep 17 00:00:00 2001 From: mpaolini Date: Sun, 30 Dec 2012 23:59:02 +0100 Subject: [PATCH 107/870] Added documentation for the 'db' argument of the post-syncdb signal. --- django/db/models/signals.py | 2 +- docs/ref/signals.txt | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/django/db/models/signals.py b/django/db/models/signals.py index 2ef54a7ca7..09f93d0f77 100644 --- a/django/db/models/signals.py +++ b/django/db/models/signals.py @@ -12,6 +12,6 @@ post_save = Signal(providing_args=["instance", "raw", "created", "using", "updat pre_delete = Signal(providing_args=["instance", "using"], use_caching=True) post_delete = Signal(providing_args=["instance", "using"], use_caching=True) -post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"], use_caching=True) +post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"], use_caching=True) m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True) diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 0995789391..ca472bd60e 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -406,6 +406,10 @@ Arguments sent with this signal: For example, the :mod:`django.contrib.auth` app only prompts to create a superuser when ``interactive`` is ``True``. +``db`` + The database alias used for synchronization. Defaults to the ``default`` + database. + For example, ``yourapp/management/__init__.py`` could be written like:: from django.db.models.signals import post_syncdb From 850630b4b7e79b76ad9732db1be6a2aa4257893f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 3 Jan 2013 22:12:11 +0100 Subject: [PATCH 108/870] Replaced deprecated sslerror by ssl.SSLError The exact conditions on which this exception is raised are not known, but this replacement is the best guess we can do at this point. --- django/core/mail/backends/smtp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py index 3ae08f4340..e456b7864e 100644 --- a/django/core/mail/backends/smtp.py +++ b/django/core/mail/backends/smtp.py @@ -1,6 +1,6 @@ """SMTP email backend class.""" import smtplib -import socket +import ssl import threading from django.conf import settings @@ -65,7 +65,7 @@ class EmailBackend(BaseEmailBackend): try: try: self.connection.quit() - except (socket.sslerror, smtplib.SMTPServerDisconnected): + except (ssl.SSLError, smtplib.SMTPServerDisconnected): # This happens when calling quit() on a TLS connection # sometimes, or when the connection was already disconnected # by the server. From b740da3504ea3b9c841f5a9eb14191a0b5410565 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 4 Jan 2013 13:55:20 +0100 Subject: [PATCH 109/870] Fixed #19192 -- Allowed running tests with dummy db backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Simon Charette for the initial patch, and Jan Bednařík for his work on the ticket. --- AUTHORS | 1 + django/db/backends/dummy/base.py | 6 +++++- tests/regressiontests/test_runner/tests.py | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 0f793cc5f4..4b6636314d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -126,6 +126,7 @@ answer newbie questions, and generally made Django that much better: Chris Chamberlin Amit Chakradeo ChaosKCW + Simon Charette Kowito Charoenratchatabhan Sengtha Chay ivan.chelubeev@gmail.com diff --git a/django/db/backends/dummy/base.py b/django/db/backends/dummy/base.py index 12a940d3a2..b648aae9c9 100644 --- a/django/db/backends/dummy/base.py +++ b/django/db/backends/dummy/base.py @@ -31,6 +31,10 @@ class DatabaseOperations(BaseDatabaseOperations): class DatabaseClient(BaseDatabaseClient): runshell = complain +class DatabaseCreation(BaseDatabaseCreation): + create_test_db = ignore + destroy_test_db = ignore + class DatabaseIntrospection(BaseDatabaseIntrospection): get_table_list = complain get_table_description = complain @@ -64,6 +68,6 @@ class DatabaseWrapper(BaseDatabaseWrapper): self.features = BaseDatabaseFeatures(self) self.ops = DatabaseOperations(self) self.client = DatabaseClient(self) - self.creation = BaseDatabaseCreation(self) + self.creation = DatabaseCreation(self) self.introspection = DatabaseIntrospection(self) self.validation = BaseDatabaseValidation(self) diff --git a/tests/regressiontests/test_runner/tests.py b/tests/regressiontests/test_runner/tests.py index c723f162a4..5ef4d5537d 100644 --- a/tests/regressiontests/test_runner/tests.py +++ b/tests/regressiontests/test_runner/tests.py @@ -261,6 +261,24 @@ class Sqlite3InMemoryTestDbs(unittest.TestCase): db.connections = old_db_connections +class DummyBackendTest(unittest.TestCase): + def test_setup_databases(self): + """ + Test that setup_databases() doesn't fail with dummy database backend. + """ + runner = DjangoTestSuiteRunner(verbosity=0) + old_db_connections = db.connections + try: + db.connections = db.ConnectionHandler({}) + old_config = runner.setup_databases() + runner.teardown_databases(old_config) + except Exception as e: + self.fail("setup_databases/teardown_databases unexpectedly raised " + "an error: %s" % e) + finally: + db.connections = old_db_connections + + class AutoIncrementResetTest(TransactionTestCase): """ Here we test creating the same model two times in different test methods, From c8eff0dbcb0936aac2748a7a896d08f34b54c50f Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Fri, 4 Jan 2013 17:42:25 -0800 Subject: [PATCH 110/870] Fixed #19562 -- cleaned up password storage docs --- docs/topics/auth/passwords.txt | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt index 76284ae72f..3d95b4b387 100644 --- a/docs/topics/auth/passwords.txt +++ b/docs/topics/auth/passwords.txt @@ -14,17 +14,19 @@ How Django stores passwords =========================== Django provides a flexible password storage system and uses PBKDF2 by default. -Older versions of Django used SHA1, and other algorithms couldn't be chosen. The :attr:`~django.contrib.auth.models.User.password` attribute of a :class:`~django.contrib.auth.models.User` object is a string in this format:: - algorithm$hash + $$$ -That's a storage algorithm, and hash, separated by the dollar-sign -character. The algorithm is one of a number of one way hashing or password -storage algorithms Django can use; see below. The hash is the result of the one- -way function. +Those are the components used for storing a User's password, separated by the +dollar-sign character and consist of: the hashing algorithm, the number of +algorithm iterations (work factor), the random salt, and the resulting password +hash. The algorithm is one of a number of one-way hashing or password storage +algorithms Django can use; see below. Iterations describe the number of times +the algorithm is run over the hash. Salt is the random seed used and the hash +is the result of the one-way function. By default, Django uses the PBKDF2_ algorithm with a SHA256 hash, a password stretching mechanism recommended by NIST_. This should be @@ -36,13 +38,14 @@ algorithm, or even use a custom algorithm to match your specific security situation. Again, most users shouldn't need to do this -- if you're not sure, you probably don't. If you do, please read on: -Django chooses the an algorithm by consulting the :setting:`PASSWORD_HASHERS` -setting. This is a list of hashing algorithm classes that this Django -installation supports. The first entry in this list (that is, -``settings.PASSWORD_HASHERS[0]``) will be used to store passwords, and all the -other entries are valid hashers that can be used to check existing passwords. -This means that if you want to use a different algorithm, you'll need to modify -:setting:`PASSWORD_HASHERS` to list your preferred algorithm first in the list. +Django chooses the algorithm to use by consulting the +:setting:`PASSWORD_HASHERS` setting. This is a list of hashing algorithm +classes that this Django installation supports. The first entry in this list +(that is, ``settings.PASSWORD_HASHERS[0]``) will be used to store passwords, +and all the other entries are valid hashers that can be used to check existing +passwords. This means that if you want to use a different algorithm, you'll +need to modify :setting:`PASSWORD_HASHERS` to list your preferred algorithm +first in the list. The default for :setting:`PASSWORD_HASHERS` is:: From a843539af2f557e9bdc71b9b5ef66eabe0e39e3c Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 5 Jan 2013 18:01:16 +0100 Subject: [PATCH 111/870] Fixed #12914 -- Use yaml faster C implementation when available Thanks Beuc for the report and the initial patch. --- django/core/serializers/pyyaml.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/django/core/serializers/pyyaml.py b/django/core/serializers/pyyaml.py index 4c11626bad..27239e0445 100644 --- a/django/core/serializers/pyyaml.py +++ b/django/core/serializers/pyyaml.py @@ -14,8 +14,15 @@ from django.core.serializers.python import Serializer as PythonSerializer from django.core.serializers.python import Deserializer as PythonDeserializer from django.utils import six +# Use the C (faster) implementation if possible +try: + from yaml import CSafeLoader as SafeLoader + from yaml import CSafeDumper as SafeDumper +except ImportError: + from yaml import SafeLoader, SafeDumper -class DjangoSafeDumper(yaml.SafeDumper): + +class DjangoSafeDumper(SafeDumper): def represent_decimal(self, data): return self.represent_scalar('tag:yaml.org,2002:str', str(data)) @@ -58,7 +65,7 @@ def Deserializer(stream_or_string, **options): else: stream = stream_or_string try: - for obj in PythonDeserializer(yaml.safe_load(stream), **options): + for obj in PythonDeserializer(yaml.load(stream, Loader=SafeLoader), **options): yield obj except GeneratorExit: raise From a2396a4c8f2ccd7f91adee6d8c2e9c31f13f0e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Wed, 24 Oct 2012 00:04:37 +0300 Subject: [PATCH 112/870] Fixed #19173 -- Made EmptyQuerySet a marker class only The guarantee that no queries will be made when accessing results is done by new EmptyWhere class which is used for query.where and having. Thanks to Simon Charette for reviewing and valuable suggestions. --- django/contrib/auth/models.py | 4 +- django/db/models/manager.py | 8 +- django/db/models/query.py | 159 +++----------------- django/db/models/sql/query.py | 9 +- django/db/models/sql/where.py | 8 + docs/ref/models/querysets.txt | 10 +- docs/releases/1.6.txt | 5 + tests/modeltests/basic/tests.py | 7 + tests/modeltests/get_object_or_404/tests.py | 2 +- tests/modeltests/lookup/tests.py | 2 +- tests/regressiontests/queries/tests.py | 59 ++++---- 11 files changed, 96 insertions(+), 177 deletions(-) diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 6f20981ca6..1b63833688 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -473,8 +473,8 @@ class AnonymousUser(object): is_staff = False is_active = False is_superuser = False - _groups = EmptyManager() - _user_permissions = EmptyManager() + _groups = EmptyManager(Group) + _user_permissions = EmptyManager(Permission) def __init__(self): pass diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 8da8af487c..da6523c89a 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -1,6 +1,6 @@ import copy from django.db import router -from django.db.models.query import QuerySet, EmptyQuerySet, insert_query, RawQuerySet +from django.db.models.query import QuerySet, insert_query, RawQuerySet from django.db.models import signals from django.db.models.fields import FieldDoesNotExist @@ -113,7 +113,7 @@ class Manager(object): ####################### def get_empty_query_set(self): - return EmptyQuerySet(self.model, using=self._db) + return QuerySet(self.model, using=self._db).none() def get_query_set(self): """Returns a new QuerySet object. Subclasses can override this method @@ -258,5 +258,9 @@ class SwappedManagerDescriptor(object): class EmptyManager(Manager): + def __init__(self, model): + super(EmptyManager, self).__init__() + self.model = model + def get_query_set(self): return self.get_empty_query_set() diff --git a/django/db/models/query.py b/django/db/models/query.py index d1f519aaf8..edc8cc9776 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -35,7 +35,6 @@ class QuerySet(object): """ def __init__(self, model=None, query=None, using=None): self.model = model - # EmptyQuerySet instantiates QuerySet with model as None self._db = using self.query = query or sql.Query(self.model) self._result_cache = None @@ -217,7 +216,9 @@ class QuerySet(object): def __and__(self, other): self._merge_sanity_check(other) if isinstance(other, EmptyQuerySet): - return other._clone() + return other + if isinstance(self, EmptyQuerySet): + return self combined = self._clone() combined._merge_known_related_objects(other) combined.query.combine(other.query, sql.AND) @@ -225,9 +226,11 @@ class QuerySet(object): def __or__(self, other): self._merge_sanity_check(other) - combined = self._clone() + if isinstance(self, EmptyQuerySet): + return other if isinstance(other, EmptyQuerySet): - return combined + return self + combined = self._clone() combined._merge_known_related_objects(other) combined.query.combine(other.query, sql.OR) return combined @@ -632,7 +635,9 @@ class QuerySet(object): """ Returns an empty QuerySet. """ - return self._clone(klass=EmptyQuerySet) + clone = self._clone() + clone.query.set_empty() + return clone ################################################################## # PUBLIC METHODS THAT ALTER ATTRIBUTES AND RETURN A NEW QUERYSET # @@ -981,6 +986,18 @@ class QuerySet(object): # empty" result. value_annotation = True +class InstanceCheckMeta(type): + def __instancecheck__(self, instance): + return instance.query.is_empty() + +class EmptyQuerySet(six.with_metaclass(InstanceCheckMeta), object): + """ + Marker class usable for checking if a queryset is empty by .none(): + isinstance(qs.none(), EmptyQuerySet) -> True + """ + + def __init__(self, *args, **kwargs): + raise TypeError("EmptyQuerySet can't be instantiated") class ValuesQuerySet(QuerySet): def __init__(self, *args, **kwargs): @@ -1180,138 +1197,6 @@ class DateQuerySet(QuerySet): return c -class EmptyQuerySet(QuerySet): - def __init__(self, model=None, query=None, using=None): - super(EmptyQuerySet, self).__init__(model, query, using) - self._result_cache = [] - - def __and__(self, other): - return self._clone() - - def __or__(self, other): - return other._clone() - - def count(self): - return 0 - - def delete(self): - pass - - def _clone(self, klass=None, setup=False, **kwargs): - c = super(EmptyQuerySet, self)._clone(klass, setup=setup, **kwargs) - c._result_cache = [] - return c - - def iterator(self): - # This slightly odd construction is because we need an empty generator - # (it raises StopIteration immediately). - yield next(iter([])) - - def all(self): - """ - Always returns EmptyQuerySet. - """ - return self - - def filter(self, *args, **kwargs): - """ - Always returns EmptyQuerySet. - """ - return self - - def exclude(self, *args, **kwargs): - """ - Always returns EmptyQuerySet. - """ - return self - - def complex_filter(self, filter_obj): - """ - Always returns EmptyQuerySet. - """ - return self - - def select_related(self, *fields, **kwargs): - """ - Always returns EmptyQuerySet. - """ - return self - - def annotate(self, *args, **kwargs): - """ - Always returns EmptyQuerySet. - """ - return self - - def order_by(self, *field_names): - """ - Always returns EmptyQuerySet. - """ - return self - - def distinct(self, fields=None): - """ - Always returns EmptyQuerySet. - """ - return self - - def extra(self, select=None, where=None, params=None, tables=None, - order_by=None, select_params=None): - """ - Always returns EmptyQuerySet. - """ - assert self.query.can_filter(), \ - "Cannot change a query once a slice has been taken" - return self - - def reverse(self): - """ - Always returns EmptyQuerySet. - """ - return self - - def defer(self, *fields): - """ - Always returns EmptyQuerySet. - """ - return self - - def only(self, *fields): - """ - Always returns EmptyQuerySet. - """ - return self - - def update(self, **kwargs): - """ - Don't update anything. - """ - return 0 - - def aggregate(self, *args, **kwargs): - """ - Return a dict mapping the aggregate names to None - """ - for arg in args: - kwargs[arg.default_alias] = arg - return dict([(key, None) for key in kwargs]) - - def values(self, *fields): - """ - Always returns EmptyQuerySet. - """ - return self - - def values_list(self, *fields, **kwargs): - """ - Always returns EmptyQuerySet. - """ - return self - - # EmptyQuerySet is always an empty result in where-clauses (and similar - # situations). - value_annotation = False - def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None, only_load=None, from_parent=None): """ diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 87104f0d13..f021d571e9 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -25,7 +25,7 @@ from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE, from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode, - ExtraWhere, AND, OR) + ExtraWhere, AND, OR, EmptyWhere) from django.core.exceptions import FieldError __all__ = ['Query', 'RawQuery'] @@ -1511,6 +1511,13 @@ class Query(object): self.add_filter(('%s__isnull' % trimmed_prefix, False), negate=True, can_reuse=can_reuse) + def set_empty(self): + self.where = EmptyWhere() + self.having = EmptyWhere() + + def is_empty(self): + return isinstance(self.where, EmptyWhere) or isinstance(self.having, EmptyWhere) + def set_limits(self, low=None, high=None): """ Adjusts the limits on the rows retrieved. We use low/high to set these, diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 47f4ffaba9..02847b1f54 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -272,6 +272,14 @@ class WhereNode(tree.Node): if hasattr(child[3], 'relabel_aliases'): child[3].relabel_aliases(change_map) +class EmptyWhere(WhereNode): + + def add(self, data, connector): + return + + def as_sql(self, qn=None, connection=None): + raise EmptyResultSet + class EverythingNode(object): """ A node that matches everything. diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index a8e946f8a5..2bbd895fd4 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -593,15 +593,17 @@ none .. method:: none() -Returns an ``EmptyQuerySet`` — a ``QuerySet`` subclass that always evaluates to -an empty list. This can be used in cases where you know that you should return -an empty result set and your caller is expecting a ``QuerySet`` object (instead -of returning an empty list, for example.) +Calling none() will create a queryset that never returns any objects and no +query will be executed when accessing the results. A qs.none() queryset +is an instance of ``EmptyQuerySet``. Examples:: >>> Entry.objects.none() [] + >>> from django.db.models.query import EmptyQuerySet + >>> isinstance(Entry.objects.none(), EmptyQuerySet) + True all ~~~ diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 1f57913397..e425036839 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -31,6 +31,11 @@ Minor features Backwards incompatible changes in 1.6 ===================================== +* The ``django.db.models.query.EmptyQuerySet`` can't be instantiated any more - + it is only usable as a marker class for checking if + :meth:`~django.db.models.query.QuerySet.none` has been called: + ``isinstance(qs.none(), EmptyQuerySet)`` + .. warning:: In addition to the changes outlined in this section, be sure to review the diff --git a/tests/modeltests/basic/tests.py b/tests/modeltests/basic/tests.py index 1c83b980a7..dba9a686d9 100644 --- a/tests/modeltests/basic/tests.py +++ b/tests/modeltests/basic/tests.py @@ -4,6 +4,7 @@ from datetime import datetime from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db.models.fields import Field, FieldDoesNotExist +from django.db.models.query import EmptyQuerySet from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature from django.utils import six from django.utils.translation import ugettext_lazy @@ -639,3 +640,9 @@ class ModelTest(TestCase): Article.objects.bulk_create([Article(headline=lazy, pub_date=datetime.now())]) article = Article.objects.get() self.assertEqual(article.headline, notlazy) + + def test_emptyqs(self): + # Can't be instantiated + with self.assertRaises(TypeError): + EmptyQuerySet() + self.assertTrue(isinstance(Article.objects.none(), EmptyQuerySet)) diff --git a/tests/modeltests/get_object_or_404/tests.py b/tests/modeltests/get_object_or_404/tests.py index 3b234c6cd3..38ebeb4f8c 100644 --- a/tests/modeltests/get_object_or_404/tests.py +++ b/tests/modeltests/get_object_or_404/tests.py @@ -53,7 +53,7 @@ class GetObjectOr404Tests(TestCase): get_object_or_404, Author.objects.all() ) - # Using an EmptyQuerySet raises a Http404 error. + # Using an empty QuerySet raises a Http404 error. self.assertRaises(Http404, get_object_or_404, Article.objects.none(), title__contains="Run" ) diff --git a/tests/modeltests/lookup/tests.py b/tests/modeltests/lookup/tests.py index 98358e3d10..de7105f92d 100644 --- a/tests/modeltests/lookup/tests.py +++ b/tests/modeltests/lookup/tests.py @@ -436,7 +436,7 @@ class LookupTests(TestCase): ]) def test_none(self): - # none() returns an EmptyQuerySet that behaves like any other QuerySet object + # none() returns a QuerySet that behaves like any other QuerySet object self.assertQuerysetEqual(Article.objects.none(), []) self.assertQuerysetEqual( Article.objects.none().filter(headline__startswith='Article'), []) diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py index e3e515025c..7d01c16255 100644 --- a/tests/regressiontests/queries/tests.py +++ b/tests/regressiontests/queries/tests.py @@ -9,7 +9,7 @@ from django.conf import settings from django.core.exceptions import FieldError from django.db import DatabaseError, connection, connections, DEFAULT_DB_ALIAS from django.db.models import Count, F, Q -from django.db.models.query import ITER_CHUNK_SIZE, EmptyQuerySet +from django.db.models.query import ITER_CHUNK_SIZE from django.db.models.sql.where import WhereNode, EverythingNode, NothingNode from django.db.models.sql.datastructures import EmptyResultSet from django.test import TestCase, skipUnlessDBFeature @@ -663,31 +663,32 @@ class Queries1Tests(BaseQuerysetTest): Item.objects.filter(created__in=[self.time1, self.time2]), ['', ''] ) - def test_ticket7235(self): # An EmptyQuerySet should not raise exceptions if it is filtered. - q = EmptyQuerySet() - self.assertQuerysetEqual(q.all(), []) - self.assertQuerysetEqual(q.filter(x=10), []) - self.assertQuerysetEqual(q.exclude(y=3), []) - self.assertQuerysetEqual(q.complex_filter({'pk': 1}), []) - self.assertQuerysetEqual(q.select_related('spam', 'eggs'), []) - self.assertQuerysetEqual(q.annotate(Count('eggs')), []) - self.assertQuerysetEqual(q.order_by('-pub_date', 'headline'), []) - self.assertQuerysetEqual(q.distinct(), []) - self.assertQuerysetEqual( - q.extra(select={'is_recent': "pub_date > '2006-01-01'"}), - [] - ) - q.query.low_mark = 1 - self.assertRaisesMessage( - AssertionError, - 'Cannot change a query once a slice has been taken', - q.extra, select={'is_recent': "pub_date > '2006-01-01'"} - ) - self.assertQuerysetEqual(q.reverse(), []) - self.assertQuerysetEqual(q.defer('spam', 'eggs'), []) - self.assertQuerysetEqual(q.only('spam', 'eggs'), []) + Eaten.objects.create(meal='m') + q = Eaten.objects.none() + with self.assertNumQueries(0): + self.assertQuerysetEqual(q.all(), []) + self.assertQuerysetEqual(q.filter(meal='m'), []) + self.assertQuerysetEqual(q.exclude(meal='m'), []) + self.assertQuerysetEqual(q.complex_filter({'pk': 1}), []) + self.assertQuerysetEqual(q.select_related('food'), []) + self.assertQuerysetEqual(q.annotate(Count('food')), []) + self.assertQuerysetEqual(q.order_by('meal', 'food'), []) + self.assertQuerysetEqual(q.distinct(), []) + self.assertQuerysetEqual( + q.extra(select={'foo': "1"}), + [] + ) + q.query.low_mark = 1 + self.assertRaisesMessage( + AssertionError, + 'Cannot change a query once a slice has been taken', + q.extra, select={'foo': "1"} + ) + self.assertQuerysetEqual(q.reverse(), []) + self.assertQuerysetEqual(q.defer('meal'), []) + self.assertQuerysetEqual(q.only('meal'), []) def test_ticket7791(self): # There were "issues" when ordering and distinct-ing on fields related @@ -1935,8 +1936,8 @@ class CloneTests(TestCase): class EmptyQuerySetTests(TestCase): def test_emptyqueryset_values(self): - # #14366 -- Calling .values() on an EmptyQuerySet and then cloning that - # should not cause an error" + # #14366 -- Calling .values() on an empty QuerySet and then cloning + # that should not cause an error self.assertQuerysetEqual( Number.objects.none().values('num').order_by('num'), [] ) @@ -1952,9 +1953,9 @@ class EmptyQuerySetTests(TestCase): ) def test_ticket_19151(self): - # #19151 -- Calling .values() or .values_list() on an EmptyQuerySet - # should return EmptyQuerySet and not cause an error. - q = EmptyQuerySet() + # #19151 -- Calling .values() or .values_list() on an empty QuerySet + # should return an empty QuerySet and not cause an error. + q = Author.objects.none() self.assertQuerysetEqual(q.values(), []) self.assertQuerysetEqual(q.values_list(), []) From 69a46c5ca7d4e6819096af88cd8d51174efd46df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Mon, 17 Dec 2012 23:48:23 +0200 Subject: [PATCH 113/870] Tests for various emptyqs tickets The tickets are either about different signature between qs.none() and qs or problems with subclass types (either EmptyQS overrided the custom qs class, or EmptyQS was overridden by another class - values() did this). Fixed #15959, fixed #17271, fixed #17712, fixed #19426 --- tests/modeltests/basic/tests.py | 42 ++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/modeltests/basic/tests.py b/tests/modeltests/basic/tests.py index dba9a686d9..42375ceed9 100644 --- a/tests/modeltests/basic/tests.py +++ b/tests/modeltests/basic/tests.py @@ -4,7 +4,7 @@ from datetime import datetime from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db.models.fields import Field, FieldDoesNotExist -from django.db.models.query import EmptyQuerySet +from django.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature from django.utils import six from django.utils.translation import ugettext_lazy @@ -646,3 +646,43 @@ class ModelTest(TestCase): with self.assertRaises(TypeError): EmptyQuerySet() self.assertTrue(isinstance(Article.objects.none(), EmptyQuerySet)) + + def test_emptyqs_values(self): + # test for #15959 + Article.objects.create(headline='foo', pub_date=datetime.now()) + with self.assertNumQueries(0): + qs = Article.objects.none().values_list('pk') + self.assertTrue(isinstance(qs, EmptyQuerySet)) + self.assertTrue(isinstance(qs, ValuesListQuerySet)) + self.assertEqual(len(qs), 0) + + def test_emptyqs_customqs(self): + # A hacky test for custom QuerySet subclass - refs #17271 + Article.objects.create(headline='foo', pub_date=datetime.now()) + class CustomQuerySet(QuerySet): + def do_something(self): + return 'did something' + + qs = Article.objects.all() + qs.__class__ = CustomQuerySet + qs = qs.none() + with self.assertNumQueries(0): + self.assertEqual(len(qs), 0) + self.assertTrue(isinstance(qs, EmptyQuerySet)) + self.assertEqual(qs.do_something(), 'did something') + + def test_emptyqs_values_order(self): + # Tests for ticket #17712 + Article.objects.create(headline='foo', pub_date=datetime.now()) + with self.assertNumQueries(0): + self.assertEqual(len(Article.objects.none().values_list('id').order_by('id')), 0) + with self.assertNumQueries(0): + self.assertEqual(len(Article.objects.none().filter( + id__in=Article.objects.values_list('id', flat=True))), 0) + + @skipUnlessDBFeature('can_distinct_on_fields') + def test_emptyqs_distinct(self): + # Tests for #19426 + Article.objects.create(headline='foo', pub_date=datetime.now()) + with self.assertNumQueries(0): + self.assertEqual(len(Article.objects.none().distinct('headline', 'pub_date')), 0) From a890469d3bffe267aed0260fd267e44e53b14c5e Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 6 Jan 2013 22:56:13 +0100 Subject: [PATCH 114/870] Fixed #19571 -- Updated runserver output in the tutorial --- docs/intro/tutorial01.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index 632f27f2d2..d24e19ce11 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -130,9 +130,10 @@ you haven't already, and run the command ``python manage.py runserver``. You'll see the following output on the command line:: Validating models... - 0 errors found. - Django version 1.4, using settings 'mysite.settings' + 0 errors found + January 06, 2013 - 15:50:53 + Django version 1.5, using settings 'mysite.settings' Development server is running at http://127.0.0.1:8000/ Quit the server with CONTROL-C. From c698c55966ed9179828857398d27bf69e64713a2 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 7 Jan 2013 17:54:30 +0100 Subject: [PATCH 115/870] Created special PostgreSQL text indexes when unique is True Refs #19441. --- django/db/backends/postgresql_psycopg2/creation.py | 2 +- docs/ref/models/fields.txt | 3 +++ tests/regressiontests/indexes/models.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/django/db/backends/postgresql_psycopg2/creation.py b/django/db/backends/postgresql_psycopg2/creation.py index 90304aa566..88afd5f52f 100644 --- a/django/db/backends/postgresql_psycopg2/creation.py +++ b/django/db/backends/postgresql_psycopg2/creation.py @@ -42,7 +42,7 @@ class DatabaseCreation(BaseDatabaseCreation): def sql_indexes_for_field(self, model, f, style): output = [] - if f.db_index: + if f.db_index or f.unique: qn = self.connection.ops.quote_name db_table = model._meta.db_table tablespace = f.db_tablespace or model._meta.db_tablespace diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 6498b6c845..77b838622b 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -272,6 +272,9 @@ field, a :exc:`django.db.IntegrityError` will be raised by the model's This option is valid on all field types except :class:`ManyToManyField` and :class:`FileField`. +Note that when ``unique`` is ``True``, you don't need to specify +:attr:`~Field.db_index`, because ``unique`` implies the creation of an index. + ``unique_for_date`` ------------------- diff --git a/tests/regressiontests/indexes/models.py b/tests/regressiontests/indexes/models.py index 4ab74d25bd..e38eb005db 100644 --- a/tests/regressiontests/indexes/models.py +++ b/tests/regressiontests/indexes/models.py @@ -17,4 +17,4 @@ if connection.vendor == 'postgresql': class IndexedArticle(models.Model): headline = models.CharField(max_length=100, db_index=True) body = models.TextField(db_index=True) - slug = models.CharField(max_length=40, unique=True, db_index=True) + slug = models.CharField(max_length=40, unique=True) From bb7f34d619bfe6b4e067af967ab30635c772def9 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 7 Jan 2013 20:16:46 -0700 Subject: [PATCH 116/870] Fixed typo in 1.5 release notes; thanks Jonas Obrist. --- docs/releases/1.5.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index c5e8c61922..a5ce08aed6 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -67,7 +67,7 @@ can simply remove that line under Django 1.5 Python compatibility ==================== -Django 1.5 requires Python 2.6.5 or above, though we **highly recommended** +Django 1.5 requires Python 2.6.5 or above, though we **highly recommend** Python 2.7.3 or above. Support for Python 2.5 and below has been dropped. This change should affect only a small number of Django users, as most From 23ca3a01940c63942885df4709712cebf4df79ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sat, 2 Jun 2012 04:13:36 +0300 Subject: [PATCH 117/870] Fixed #16759 -- Remove use of __deepcopy__ in qs.clone() The original problem was that queryset cloning was really expensive when filtering with F() clauses. The __deepcopy__ went too deep copying _meta attributes of the models used. To fix this the use of __deepcopy__ in qs cloning was removed. This commit results in some speed improvements across the djangobench benchmark suite. Most query_* tests are 20-30% faster, save() is 50% faster and finally complex filtering situations can see 2x to order of magnitude improvments. Thanks to Suor, Alex and lrekucki for valuable feedback. --- django/db/models/sql/aggregates.py | 6 +++++ django/db/models/sql/query.py | 14 ++++++----- django/db/models/sql/where.py | 34 +++++++++++++++++++++++++- tests/regressiontests/queries/tests.py | 34 ++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 7 deletions(-) diff --git a/django/db/models/sql/aggregates.py b/django/db/models/sql/aggregates.py index b41314a686..75a330f22a 100644 --- a/django/db/models/sql/aggregates.py +++ b/django/db/models/sql/aggregates.py @@ -1,6 +1,7 @@ """ Classes to represent the default SQL aggregate functions """ +import copy from django.db.models.fields import IntegerField, FloatField @@ -62,6 +63,11 @@ class Aggregate(object): self.field = tmp + def clone(self): + # Different aggregates have different init methods, so use copy here + # deepcopy is not needed, as self.col is only changing variable. + return copy.copy(self) + def relabel_aliases(self, change_map): if isinstance(self.col, (list, tuple)): self.col = (change_map.get(self.col[0], self.col[0]), self.col[1]) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index f021d571e9..613f4c4cfc 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -279,13 +279,13 @@ class Query(object): obj.select = self.select[:] obj.related_select_cols = [] obj.tables = self.tables[:] - obj.where = copy.deepcopy(self.where, memo=memo) + obj.where = self.where.clone() obj.where_class = self.where_class if self.group_by is None: obj.group_by = None else: obj.group_by = self.group_by[:] - obj.having = copy.deepcopy(self.having, memo=memo) + obj.having = self.having.clone() obj.order_by = self.order_by[:] obj.low_mark, obj.high_mark = self.low_mark, self.high_mark obj.distinct = self.distinct @@ -293,7 +293,9 @@ class Query(object): obj.select_for_update = self.select_for_update obj.select_for_update_nowait = self.select_for_update_nowait obj.select_related = self.select_related - obj.aggregates = copy.deepcopy(self.aggregates, memo=memo) + obj.related_select_cols = [] + obj.aggregates = SortedDict((k, v.clone()) + for k, v in self.aggregates.items()) if self.aggregate_select_mask is None: obj.aggregate_select_mask = None else: @@ -316,7 +318,7 @@ class Query(object): obj._extra_select_cache = self._extra_select_cache.copy() obj.extra_tables = self.extra_tables obj.extra_order_by = self.extra_order_by - obj.deferred_loading = copy.deepcopy(self.deferred_loading, memo=memo) + obj.deferred_loading = copy.copy(self.deferred_loading[0]), self.deferred_loading[1] if self.filter_is_sticky and self.used_aliases: obj.used_aliases = self.used_aliases.copy() else: @@ -549,7 +551,7 @@ class Query(object): # Now relabel a copy of the rhs where-clause and add it to the current # one. if rhs.where: - w = copy.deepcopy(rhs.where) + w = rhs.where.clone() w.relabel_aliases(change_map) if not self.where: # Since 'self' matches everything, add an explicit "include @@ -571,7 +573,7 @@ class Query(object): new_col = change_map.get(col[0], col[0]), col[1] self.select.append(SelectInfo(new_col, field)) else: - item = copy.deepcopy(col) + item = col.clone() item.relabel_aliases(change_map) self.select.append(SelectInfo(item, field)) diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 02847b1f54..3e4b352f10 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -10,7 +10,7 @@ from itertools import repeat from django.utils import tree from django.db.models.fields import Field -from django.db.models.sql.datastructures import EmptyResultSet +from django.db.models.sql.datastructures import EmptyResultSet, Empty from django.db.models.sql.aggregates import Aggregate from django.utils.six.moves import xrange @@ -272,6 +272,23 @@ class WhereNode(tree.Node): if hasattr(child[3], 'relabel_aliases'): child[3].relabel_aliases(change_map) + def clone(self): + """ + Creates a clone of the tree. Must only be called on root nodes (nodes + with empty subtree_parents). Childs must be either (Contraint, lookup, + value) tuples, or objects supporting .clone(). + """ + assert not self.subtree_parents + clone = self.__class__._new_instance( + children=[], connector=self.connector, negated=self.negated) + for child in self.children: + if isinstance(child, tuple): + clone.children.append( + (child[0].clone(), child[1], child[2], child[3])) + else: + clone.children.append(child.clone()) + return clone + class EmptyWhere(WhereNode): def add(self, data, connector): @@ -291,6 +308,9 @@ class EverythingNode(object): def relabel_aliases(self, change_map, node=None): return + def clone(self): + return self + class NothingNode(object): """ A node that matches nothing. @@ -301,6 +321,9 @@ class NothingNode(object): def relabel_aliases(self, change_map, node=None): return + def clone(self): + return self + class ExtraWhere(object): def __init__(self, sqls, params): self.sqls = sqls @@ -310,6 +333,9 @@ class ExtraWhere(object): sqls = ["(%s)" % sql for sql in self.sqls] return " AND ".join(sqls), tuple(self.params or ()) + def clone(self): + return self + class Constraint(object): """ An object that can be passed to WhereNode.add() and knows how to @@ -374,3 +400,9 @@ class Constraint(object): def relabel_aliases(self, change_map): if self.alias in change_map: self.alias = change_map[self.alias] + + def clone(self): + new = Empty() + new.__class__ = self.__class__ + new.alias, new.col, new.field = self.alias, self.col, self.field + return new diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py index 7d01c16255..780af5a8b7 100644 --- a/tests/regressiontests/queries/tests.py +++ b/tests/regressiontests/queries/tests.py @@ -1919,6 +1919,7 @@ class SubqueryTests(TestCase): class CloneTests(TestCase): + def test_evaluated_queryset_as_argument(self): "#13227 -- If a queryset is already evaluated, it can still be used as a query arg" n = Note(note='Test1', misc='misc') @@ -1933,6 +1934,39 @@ class CloneTests(TestCase): # that query in a way that involves cloning. self.assertEqual(ExtraInfo.objects.filter(note__in=n_list)[0].info, 'good') + def test_no_model_options_cloning(self): + """ + Test that cloning a queryset does not get out of hand. While complete + testing is impossible, this is a sanity check against invalid use of + deepcopy. refs #16759. + """ + opts_class = type(Note._meta) + note_deepcopy = getattr(opts_class, "__deepcopy__", None) + opts_class.__deepcopy__ = lambda obj, memo: self.fail("Model options shouldn't be cloned.") + try: + Note.objects.filter(pk__lte=F('pk') + 1).all() + finally: + if note_deepcopy is None: + delattr(opts_class, "__deepcopy__") + else: + opts_class.__deepcopy__ = note_deepcopy + + def test_no_fields_cloning(self): + """ + Test that cloning a queryset does not get out of hand. While complete + testing is impossible, this is a sanity check against invalid use of + deepcopy. refs #16759. + """ + opts_class = type(Note._meta.get_field_by_name("misc")[0]) + note_deepcopy = getattr(opts_class, "__deepcopy__", None) + opts_class.__deepcopy__ = lambda obj, memo: self.fail("Model fields shouldn't be cloned") + try: + Note.objects.filter(note=F('misc')).all() + finally: + if note_deepcopy is None: + delattr(opts_class, "__deepcopy__") + else: + opts_class.__deepcopy__ = note_deepcopy class EmptyQuerySetTests(TestCase): def test_emptyqueryset_values(self): From 34ee7d9875d26809b0a2c6115edee0a35be0d17d Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 8 Jan 2013 19:07:12 +0100 Subject: [PATCH 118/870] Updated deprecated test assertions --- django/contrib/auth/tests/management.py | 4 ++-- tests/modeltests/basic/tests.py | 6 +++--- tests/modeltests/str/tests.py | 2 +- tests/regressiontests/backends/tests.py | 2 +- tests/regressiontests/queries/tests.py | 12 ++++++------ .../regressiontests/select_related_onetoone/tests.py | 10 +++++----- tests/regressiontests/templates/tests.py | 4 ++-- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index 976c0c4972..02939e39dc 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -186,7 +186,7 @@ class PermissionDuplicationTestCase(TestCase): # check duplicated default permission models.Permission._meta.permissions = [ ('change_permission', 'Can edit permission (duplicate)')] - self.assertRaisesRegexp(CommandError, + six.assertRaisesRegex(self, CommandError, "The permission codename 'change_permission' clashes with a " "builtin permission for model 'auth.Permission'.", create_permissions, models, [], verbosity=0) @@ -197,7 +197,7 @@ class PermissionDuplicationTestCase(TestCase): ('other_one', 'Some other permission'), ('my_custom_permission', 'Some permission with duplicate permission code'), ] - self.assertRaisesRegexp(CommandError, + six.assertRaisesRegex(self, CommandError, "The permission codename 'my_custom_permission' is duplicated for model " "'auth.Permission'.", create_permissions, models, [], verbosity=0) diff --git a/tests/modeltests/basic/tests.py b/tests/modeltests/basic/tests.py index 42375ceed9..1ca4f20dac 100644 --- a/tests/modeltests/basic/tests.py +++ b/tests/modeltests/basic/tests.py @@ -141,21 +141,21 @@ class ModelTest(TestCase): # Django raises an Article.MultipleObjectsReturned exception if the # lookup matches more than one object - self.assertRaisesRegexp( + six.assertRaisesRegex(self, MultipleObjectsReturned, "get\(\) returned more than one Article -- it returned 2!", Article.objects.get, headline__startswith='Area', ) - self.assertRaisesRegexp( + six.assertRaisesRegex(self, MultipleObjectsReturned, "get\(\) returned more than one Article -- it returned 2!", Article.objects.get, pub_date__year=2005, ) - self.assertRaisesRegexp( + six.assertRaisesRegex(self, MultipleObjectsReturned, "get\(\) returned more than one Article -- it returned 2!", Article.objects.get, diff --git a/tests/modeltests/str/tests.py b/tests/modeltests/str/tests.py index 31869583aa..bd85c48d05 100644 --- a/tests/modeltests/str/tests.py +++ b/tests/modeltests/str/tests.py @@ -28,7 +28,7 @@ class SimpleTests(TestCase): headline='Girl wins €12.500 in lottery', pub_date=datetime.datetime(2005, 7, 28) ) - self.assertRaisesRegexp(RuntimeError, "Did you apply " + six.assertRaisesRegex(self, RuntimeError, "Did you apply " "@python_2_unicode_compatible without defining __str__\?", str, a) def test_international(self): diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index 934c37d147..791f4c1daa 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -42,7 +42,7 @@ class OracleChecks(unittest.TestCase): # Check that '%' chars are escaped for query execution. name = '"SOME%NAME"' quoted_name = connection.ops.quote_name(name) - self.assertEquals(quoted_name % (), name) + self.assertEqual(quoted_name % (), name) @unittest.skipUnless(connection.vendor == 'oracle', "No need to check Oracle cursor semantics") diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py index 780af5a8b7..4adf07657c 100644 --- a/tests/regressiontests/queries/tests.py +++ b/tests/regressiontests/queries/tests.py @@ -1293,9 +1293,9 @@ class Queries4Tests(BaseQuerysetTest): q1 = Author.objects.filter(report__name='r5') q2 = Author.objects.filter(report__name='r4').filter(report__name='r1') combined = q1|q2 - self.assertEquals(str(combined.query).count('JOIN'), 2) - self.assertEquals(len(combined), 1) - self.assertEquals(combined[0].name, 'a1') + self.assertEqual(str(combined.query).count('JOIN'), 2) + self.assertEqual(len(combined), 1) + self.assertEqual(combined[0].name, 'a1') def test_ticket7095(self): # Updates that are filtered on the model being updated are somewhat @@ -1644,8 +1644,8 @@ class NullableRelOrderingTests(TestCase): # and that join must be LEFT join. The already existing join to related # objects must be kept INNER. So, we have both a INNER and a LEFT join # in the query. - self.assertEquals(str(qs.query).count('LEFT'), 1) - self.assertEquals(str(qs.query).count('INNER'), 1) + self.assertEqual(str(qs.query).count('LEFT'), 1) + self.assertEqual(str(qs.query).count('INNER'), 1) self.assertQuerysetEqual( qs, [''] @@ -2452,7 +2452,7 @@ class ReverseJoinTrimmingTest(TestCase): t = Tag.objects.create() qs = Tag.objects.filter(annotation__tag=t.pk) self.assertIn('INNER JOIN', str(qs.query)) - self.assertEquals(list(qs), []) + self.assertEqual(list(qs), []) class JoinReuseTest(TestCase): """ diff --git a/tests/regressiontests/select_related_onetoone/tests.py b/tests/regressiontests/select_related_onetoone/tests.py index d4a1275e49..fce8fc4e73 100644 --- a/tests/regressiontests/select_related_onetoone/tests.py +++ b/tests/regressiontests/select_related_onetoone/tests.py @@ -183,11 +183,11 @@ class ReverseSelectRelatedTestCase(TestCase): p = Parent2.objects.select_related('child1').only( 'id2', 'child1__value').get(name2="n2") with self.assertNumQueries(1): - self.assertEquals(p.name2, 'n2') + self.assertEqual(p.name2, 'n2') p = Parent2.objects.select_related('child1').only( 'id2', 'child1__value').get(name2="n2") with self.assertNumQueries(1): - self.assertEquals(p.child1.name2, 'n2') + self.assertEqual(p.child1.name2, 'n2') @unittest.expectedFailure def test_inheritance_deferred2(self): @@ -202,9 +202,9 @@ class ReverseSelectRelatedTestCase(TestCase): self.assertEqual(p.child1.child4.id2, c.id2) p = qs.get(name2="n2") with self.assertNumQueries(1): - self.assertEquals(p.child1.name2, 'n2') + self.assertEqual(p.child1.name2, 'n2') p = qs.get(name2="n2") with self.assertNumQueries(1): - self.assertEquals(p.child1.name1, 'n1') + self.assertEqual(p.child1.name1, 'n1') with self.assertNumQueries(1): - self.assertEquals(p.child1.child4.name1, 'n1') + self.assertEqual(p.child1.child4.name1, 'n1') diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index d21434a12e..8d2a45b8fc 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -370,13 +370,13 @@ class Templates(TestCase): # Regression test for #19280 t = Template('{% url path.to.view %}') # not quoted = old syntax c = Context() - with self.assertRaisesRegexp(urlresolvers.NoReverseMatch, + with six.assertRaisesRegex(self, urlresolvers.NoReverseMatch, "The syntax changed in Django 1.5, see the docs."): t.render(c) def test_url_explicit_exception_for_old_syntax_at_compile_time(self): # Regression test for #19392 - with self.assertRaisesRegexp(template.TemplateSyntaxError, + with six.assertRaisesRegex(self, template.TemplateSyntaxError, "The syntax of 'url' changed in Django 1.5, see the docs."): t = Template('{% url my-view %}') # not a variable = old syntax From 55da775ce1bfba20db33b56c29957faa63917980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sun, 6 Jan 2013 18:40:56 +0200 Subject: [PATCH 119/870] Fixed #17541 -- Fixed non-saved/nullable fk querying --- django/db/models/fields/related.py | 2 ++ .../many_to_one_regress/models.py | 6 +++++ .../many_to_one_regress/tests.py | 25 ++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 9a657d9d26..ae792a30e7 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -496,6 +496,8 @@ class ForeignRelatedObjectsDescriptor(object): except (AttributeError, KeyError): db = self._db or router.db_for_read(self.model, instance=self.instance) qs = super(RelatedManager, self).get_query_set().using(db).filter(**self.core_filters) + if getattr(self.instance, attname) is None: + return qs.none() qs._known_related_objects = {rel_field: {self.instance.pk: self.instance}} return qs diff --git a/tests/regressiontests/many_to_one_regress/models.py b/tests/regressiontests/many_to_one_regress/models.py index f3727820f8..0ac0871793 100644 --- a/tests/regressiontests/many_to_one_regress/models.py +++ b/tests/regressiontests/many_to_one_regress/models.py @@ -48,3 +48,9 @@ class Relation(models.Model): def __str__(self): return "%s - %s" % (self.left.category.name, self.right.category.name) + +class Car(models.Model): + make = models.CharField(max_length=100, null=True, unique=True) + +class Driver(models.Model): + car = models.ForeignKey(Car, to_field='make', null=True, related_name='drivers') diff --git a/tests/regressiontests/many_to_one_regress/tests.py b/tests/regressiontests/many_to_one_regress/tests.py index d980d7437c..e140577a49 100644 --- a/tests/regressiontests/many_to_one_regress/tests.py +++ b/tests/regressiontests/many_to_one_regress/tests.py @@ -4,7 +4,8 @@ from django.db import models from django.test import TestCase from django.utils import six -from .models import First, Third, Parent, Child, Category, Record, Relation +from .models import ( + First, Third, Parent, Child, Category, Record, Relation, Car, Driver) class ManyToOneRegressionTests(TestCase): @@ -111,3 +112,25 @@ class ManyToOneRegressionTests(TestCase): # of a model, and interrogate its related field. cat = models.ForeignKey(Category) self.assertEqual('id', cat.rel.get_related_field().name) + + def test_relation_unsaved(self): + # Test that the _set manager does not join on Null value fields (#17541) + Third.objects.create(name='Third 1') + Third.objects.create(name='Third 2') + th = Third(name="testing") + # The object isn't saved an thus the relation field is null - we won't even + # execute a query in this case. + with self.assertNumQueries(0): + self.assertEqual(th.child_set.count(), 0) + th.save() + # Now the model is saved, so we will need to execute an query. + with self.assertNumQueries(1): + self.assertEqual(th.child_set.count(), 0) + + def test_related_null_to_field(self): + c1 = Car.objects.create() + c2 = Car.objects.create() + d1 = Driver.objects.create() + self.assertIs(d1.car, None) + with self.assertNumQueries(0): + self.assertEqual(list(c1.drivers.all()), []) From f58efd07ff2b2edb377d02567615e79e8d05248b Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Mon, 7 Jan 2013 23:41:59 -0500 Subject: [PATCH 120/870] Fixed #19576 -- Use `six.with_metaclass` uniformously accross code base. --- django/db/models/base.py | 2 +- django/db/models/query.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index bd638e2499..38afc60991 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -311,7 +311,7 @@ class ModelState(object): self.adding = True -class Model(six.with_metaclass(ModelBase, object)): +class Model(six.with_metaclass(ModelBase)): _deferred = False def __init__(self, *args, **kwargs): diff --git a/django/db/models/query.py b/django/db/models/query.py index edc8cc9776..bdb6d48adc 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -990,7 +990,7 @@ class InstanceCheckMeta(type): def __instancecheck__(self, instance): return instance.query.is_empty() -class EmptyQuerySet(six.with_metaclass(InstanceCheckMeta), object): +class EmptyQuerySet(six.with_metaclass(InstanceCheckMeta)): """ Marker class usable for checking if a queryset is empty by .none(): isinstance(qs.none(), EmptyQuerySet) -> True From 99315f709e26ea80de8ea3af4e336dbdbe467711 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 8 Jan 2013 15:43:35 -0500 Subject: [PATCH 121/870] Fixed #19555 - Removed '2012' from tutorial 1. Thanks rodrigorosa.lg and others for the report. --- docs/intro/tutorial01.txt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index d24e19ce11..cd07e081fc 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -647,8 +647,10 @@ Save these changes and start a new Python interactive shell by running >>> Poll.objects.filter(question__startswith='What') [] - # Get the poll whose year is 2012. - >>> Poll.objects.get(pub_date__year=2012) + # Get the poll that was published this year. + >>> from django.utils import timezone + >>> current_year = timezone.now().year + >>> Poll.objects.get(pub_date__year=current_year) # Request an ID that doesn't exist, this will raise an exception. @@ -699,8 +701,9 @@ Save these changes and start a new Python interactive shell by running # The API automatically follows relationships as far as you need. # Use double underscores to separate relationships. # This works as many levels deep as you want; there's no limit. - # Find all Choices for any poll whose pub_date is in 2012. - >>> Choice.objects.filter(poll__pub_date__year=2012) + # Find all Choices for any poll whose pub_date is in this year + # (reusing the 'current_year' variable we created above). + >>> Choice.objects.filter(poll__pub_date__year=current_year) [, , ] # Let's delete one of the choices. Use delete() for that. From 1884868adcc6945afaf7a96e01d35eafb623b847 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 8 Jan 2013 15:58:11 -0500 Subject: [PATCH 122/870] Added sphinx substitutions in place of hardcoded version numbers. Refs #19571 --- docs/intro/install.txt | 9 ++++----- docs/intro/tutorial01.txt | 8 +++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/intro/install.txt b/docs/intro/install.txt index f9b122e62d..3cbc8d88ab 100644 --- a/docs/intro/install.txt +++ b/docs/intro/install.txt @@ -78,11 +78,13 @@ Verifying --------- To verify that Django can be seen by Python, type ``python`` from your shell. -Then at the Python prompt, try to import Django:: +Then at the Python prompt, try to import Django: + +.. parsed-literal:: >>> import django >>> print(django.get_version()) - 1.5 + |version| You may have another version of Django installed. @@ -90,6 +92,3 @@ That's it! ---------- That's it -- you can now :doc:`move onto the tutorial `. - - - diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index cd07e081fc..fbbfea800d 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -127,13 +127,15 @@ The development server Let's verify this worked. Change into the outer :file:`mysite` directory, if you haven't already, and run the command ``python manage.py runserver``. You'll -see the following output on the command line:: +see the following output on the command line: + +.. parsed-literal:: Validating models... 0 errors found - January 06, 2013 - 15:50:53 - Django version 1.5, using settings 'mysite.settings' + |today| - 15:50:53 + Django version |version|, using settings 'mysite.settings' Development server is running at http://127.0.0.1:8000/ Quit the server with CONTROL-C. From cfa70d0c94a43d94e3ee48db87f2b88c29a862e1 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Wed, 2 Jan 2013 16:00:39 -0800 Subject: [PATCH 123/870] Fixed #19546 - ensure that deprecation warnings are shown during tests refs #18985 --- django/test/simple.py | 13 +++++++++++ tests/regressiontests/logging_tests/tests.py | 23 ++++++++++++------- .../test_runner/deprecation_app/__init__.py | 0 .../test_runner/deprecation_app/models.py | 3 +++ .../test_runner/deprecation_app/tests.py | 9 ++++++++ tests/regressiontests/test_runner/tests.py | 19 +++++++++++++++ 6 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 tests/regressiontests/test_runner/deprecation_app/__init__.py create mode 100644 tests/regressiontests/test_runner/deprecation_app/models.py create mode 100644 tests/regressiontests/test_runner/deprecation_app/tests.py diff --git a/django/test/simple.py b/django/test/simple.py index bf0219d53f..8faf1e4f93 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -1,3 +1,4 @@ +import logging import unittest as real_unittest from django.conf import settings @@ -365,7 +366,19 @@ class DjangoTestSuiteRunner(object): self.setup_test_environment() suite = self.build_suite(test_labels, extra_tests) old_config = self.setup_databases() + if self.verbosity > 0: + # ensure that deprecation warnings are displayed during testing + # the following state is assumed: + # logging.capturewarnings is true + # a "default" level warnings filter has been added for + # DeprecationWarning. See django.conf.LazySettings._configure_logging + logger = logging.getLogger('py.warnings') + handler = logging.StreamHandler() + logger.addHandler(handler) result = self.run_suite(suite) + if self.verbosity > 0: + # remove the testing-specific handler + logger.removeHandler(handler) self.teardown_databases(old_config) self.teardown_test_environment() return self.suite_result(suite, result) diff --git a/tests/regressiontests/logging_tests/tests.py b/tests/regressiontests/logging_tests/tests.py index f9838949ab..b3d9f3b352 100644 --- a/tests/regressiontests/logging_tests/tests.py +++ b/tests/regressiontests/logging_tests/tests.py @@ -93,24 +93,31 @@ class WarningLoggerTests(TestCase): and captured to the logging system """ def setUp(self): + # this convoluted setup is to avoid printing this deprecation to + # stderr during test running - as the test runner forces deprecations + # to be displayed at the global py.warnings level self.logger = logging.getLogger('py.warnings') - self.old_stream = self.logger.handlers[0].stream + self.outputs = [] + self.old_streams = [] + for handler in self.logger.handlers: + self.old_streams.append(handler.stream) + self.outputs.append(StringIO()) + handler.stream = self.outputs[-1] def tearDown(self): - self.logger.handlers[0].stream = self.old_stream + for i, handler in enumerate(self.logger.handlers): + self.logger.handlers[i].stream = self.old_streams[i] @override_settings(DEBUG=True) def test_warnings_capture(self): - output = StringIO() - self.logger.handlers[0].stream = output warnings.warn('Foo Deprecated', DeprecationWarning) - self.assertTrue('Foo Deprecated' in force_text(output.getvalue())) + output = force_text(self.outputs[0].getvalue()) + self.assertTrue('Foo Deprecated' in output) def test_warnings_capture_debug_false(self): - output = StringIO() - self.logger.handlers[0].stream = output warnings.warn('Foo Deprecated', DeprecationWarning) - self.assertFalse('Foo Deprecated' in force_text(output.getvalue())) + output = force_text(self.outputs[0].getvalue()) + self.assertFalse('Foo Deprecated' in output) class CallbackFilterTest(TestCase): diff --git a/tests/regressiontests/test_runner/deprecation_app/__init__.py b/tests/regressiontests/test_runner/deprecation_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/test_runner/deprecation_app/models.py b/tests/regressiontests/test_runner/deprecation_app/models.py new file mode 100644 index 0000000000..71a8362390 --- /dev/null +++ b/tests/regressiontests/test_runner/deprecation_app/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/tests/regressiontests/test_runner/deprecation_app/tests.py b/tests/regressiontests/test_runner/deprecation_app/tests.py new file mode 100644 index 0000000000..e676c3e3d5 --- /dev/null +++ b/tests/regressiontests/test_runner/deprecation_app/tests.py @@ -0,0 +1,9 @@ +import warnings + +from django.test import TestCase + +class DummyTest(TestCase): + def test_warn(self): + warnings.warn("warning from test", DeprecationWarning) + + diff --git a/tests/regressiontests/test_runner/tests.py b/tests/regressiontests/test_runner/tests.py index 5ef4d5537d..685e88d6ee 100644 --- a/tests/regressiontests/test_runner/tests.py +++ b/tests/regressiontests/test_runner/tests.py @@ -279,6 +279,25 @@ class DummyBackendTest(unittest.TestCase): db.connections = old_db_connections +class DeprecationDisplayTest(AdminScriptTestCase): + # tests for 19546 + def setUp(self): + settings = {'INSTALLED_APPS': '("regressiontests.test_runner.deprecation_app",)' } + self.write_settings('settings.py', sdict=settings) + + def tearDown(self): + self.remove_settings('settings.py') + + def test_runner_deprecation_verbosity_default(self): + args = ['test', '--settings=regressiontests.settings'] + out, err = self.run_django_admin(args) + self.assertTrue("DeprecationWarning: warning from test" in err) + + def test_runner_deprecation_verbosity_zero(self): + args = ['test', '--settings=regressiontests.settings', '--verbosity=0'] + out, err = self.run_django_admin(args) + self.assertFalse("DeprecationWarning: warning from test" in err) + class AutoIncrementResetTest(TransactionTestCase): """ Here we test creating the same model two times in different test methods, From efa3f71cc4d35afe7e1ca479de2eca7808215d6f Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 9 Jan 2013 11:55:58 -0700 Subject: [PATCH 124/870] Remove inaccurate comment regarding language names. --- django/conf/global_settings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index c533efc41c..4d69c6365f 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -42,8 +42,7 @@ USE_TZ = False # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en-us' -# Languages we provide translations for, out of the box. The language name -# should be the utf-8 encoded local name for the language. +# Languages we provide translations for, out of the box. LANGUAGES = ( ('ar', gettext_noop('Arabic')), ('az', gettext_noop('Azerbaijani')), From e6949373b0e4fcacde4cc269b647f1e1be8cade9 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 9 Jan 2013 19:59:24 +0100 Subject: [PATCH 125/870] Skipped deprecation warning test on Python 2.6 Refs #19546. On Python 2.6, DeprecationWarnings are visible by default. --- tests/regressiontests/test_runner/tests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/regressiontests/test_runner/tests.py b/tests/regressiontests/test_runner/tests.py index 685e88d6ee..27a1ea9ca5 100644 --- a/tests/regressiontests/test_runner/tests.py +++ b/tests/regressiontests/test_runner/tests.py @@ -3,6 +3,7 @@ Tests for django test runner """ from __future__ import absolute_import +import sys from optparse import make_option from django.core.exceptions import ImproperlyConfigured @@ -293,11 +294,14 @@ class DeprecationDisplayTest(AdminScriptTestCase): out, err = self.run_django_admin(args) self.assertTrue("DeprecationWarning: warning from test" in err) + @unittest.skipIf(sys.version_info[:2] == (2, 6), + "On Python 2.6, DeprecationWarnings are visible anyway") def test_runner_deprecation_verbosity_zero(self): args = ['test', '--settings=regressiontests.settings', '--verbosity=0'] out, err = self.run_django_admin(args) self.assertFalse("DeprecationWarning: warning from test" in err) + class AutoIncrementResetTest(TransactionTestCase): """ Here we test creating the same model two times in different test methods, From ce580dd8ea04237cfe34cd02df0b8944a5345f4f Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Wed, 9 Jan 2013 23:32:51 +0100 Subject: [PATCH 126/870] Fixed lockups in jenkins, refs #19546. --- tests/regressiontests/test_runner/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/regressiontests/test_runner/tests.py b/tests/regressiontests/test_runner/tests.py index 27a1ea9ca5..93eabf74e3 100644 --- a/tests/regressiontests/test_runner/tests.py +++ b/tests/regressiontests/test_runner/tests.py @@ -283,7 +283,8 @@ class DummyBackendTest(unittest.TestCase): class DeprecationDisplayTest(AdminScriptTestCase): # tests for 19546 def setUp(self): - settings = {'INSTALLED_APPS': '("regressiontests.test_runner.deprecation_app",)' } + settings = {'INSTALLED_APPS': '("regressiontests.test_runner.deprecation_app",)', + 'DATABASES': '{"default": {"ENGINE":"django.db.backends.sqlite3", "NAME":":memory:"}}' } self.write_settings('settings.py', sdict=settings) def tearDown(self): From 066cf2d70e30d6fae2a53b71b44137afa44ae5fa Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 9 Jan 2013 18:32:27 -0500 Subject: [PATCH 127/870] Fixed #19586 - Removed URL_VALIDATOR_USER_AGENT from setting docs. It was removed in Django 1.5, not deprecated. --- docs/ref/settings.txt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index be21f06de7..786a92c94d 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2212,12 +2212,3 @@ Default: Not defined The site-specific user profile model used by this site. See :ref:`User profiles `. - -.. setting:: URL_VALIDATOR_USER_AGENT - -URL_VALIDATOR_USER_AGENT ------------------------- - -.. deprecated:: 1.5 - This value was used as the ``User-Agent`` header when checking if a URL - exists, a feature that was removed due to security and performance issues. From 227bd3f8dbcedb4d90cf5474bc237ca4bd46d49d Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 9 Jan 2013 19:03:34 -0500 Subject: [PATCH 128/870] Addeded CSS to bold deprecation notices. Thanks Sam Lai for mentioning this on the mailing list. --- docs/_theme/djangodocs/static/djangodocs.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_theme/djangodocs/static/djangodocs.css b/docs/_theme/djangodocs/static/djangodocs.css index 4efb7e04f3..bab81cd919 100644 --- a/docs/_theme/djangodocs/static/djangodocs.css +++ b/docs/_theme/djangodocs/static/djangodocs.css @@ -115,7 +115,7 @@ div.admonition-behind-the-scenes { padding-left:65px; background:url(docicons-be /*** versoinadded/changes ***/ div.versionadded, div.versionchanged { } -div.versionadded span.title, div.versionchanged span.title { font-weight: bold; } +div.versionadded span.title, div.versionchanged span.title, div.deprecated span.title { font-weight: bold; } /*** p-links ***/ a.headerlink { color: #c60f0f; font-size: 0.8em; padding: 0 4px 0 4px; text-decoration: none; visibility: hidden; } From 62f842e2e568c87387e3da3ab76600bd0aab3e95 Mon Sep 17 00:00:00 2001 From: Loic Raucy Date: Wed, 9 Jan 2013 10:07:44 +0100 Subject: [PATCH 129/870] Fixed #19581 -- ensure unique html ids with CheckboxSelectMultiple widgets ID check is now done the same way as MultipleHiddenInput. --- django/forms/widgets.py | 8 ++++---- tests/regressiontests/forms/tests/widgets.py | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 4782b99117..d6ea56f0c8 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -754,17 +754,17 @@ class RadioSelect(Select): class CheckboxSelectMultiple(SelectMultiple): def render(self, name, value, attrs=None, choices=()): if value is None: value = [] - has_id = attrs and 'id' in attrs final_attrs = self.build_attrs(attrs, name=name) + id_ = final_attrs.get('id', None) output = ['
    '] # Normalize to strings str_values = set([force_text(v) for v in value]) for i, (option_value, option_label) in enumerate(chain(self.choices, choices)): # If an ID attribute was given, add a numeric index as a suffix, # so that the checkboxes don't all have the same ID attribute. - if has_id: - final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i)) - label_for = format_html(' for="{0}"', final_attrs['id']) + if id_: + final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) + label_for = format_html(' for="{0}_{1}"', id_, i) else: label_for = '' diff --git a/tests/regressiontests/forms/tests/widgets.py b/tests/regressiontests/forms/tests/widgets.py index f9dc4a7ec8..7a2961358a 100644 --- a/tests/regressiontests/forms/tests/widgets.py +++ b/tests/regressiontests/forms/tests/widgets.py @@ -861,6 +861,13 @@ beatle J R Ringo False""")
  • +
""") + + # Each input gets a separate ID when the ID is passed to the constructor + self.assertHTMLEqual(CheckboxSelectMultiple(attrs={'id': 'abc'}).render('letters', list('ac'), choices=zip(list('abc'), list('ABC'))), """
    +
  • +
  • +
""") def test_multi(self): From cdad0b28d49eecb13773c112410f3c126fdd1625 Mon Sep 17 00:00:00 2001 From: Nick Sandford Date: Thu, 10 Jan 2013 09:05:01 +0100 Subject: [PATCH 130/870] Fixed #19573 -- Allow override of username field label in AuthenticationForm --- django/contrib/auth/forms.py | 3 ++- django/contrib/auth/tests/forms.py | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 4e2f476cec..85291126b4 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -169,7 +169,8 @@ class AuthenticationForm(forms.Form): # Set the label for the "username" field. UserModel = get_user_model() self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD) - self.fields['username'].label = capfirst(self.username_field.verbose_name) + if not self.fields['username'].label: + self.fields['username'].label = capfirst(self.username_field.verbose_name) def clean(self): username = self.cleaned_data.get('username') diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index a9f894905a..543bb2001d 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -7,7 +7,7 @@ from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm, ReadOnlyPasswordHashWidget) from django.contrib.auth.tests.utils import skipIfCustomUser from django.core import mail -from django.forms.fields import Field, EmailField +from django.forms.fields import Field, EmailField, CharField from django.test import TestCase from django.test.utils import override_settings from django.utils.encoding import force_text @@ -138,6 +138,14 @@ class AuthenticationFormTest(TestCase): self.assertTrue(form.is_valid()) self.assertEqual(form.non_field_errors(), []) + def test_username_field_label(self): + + class CustomAuthenticationForm(AuthenticationForm): + username = CharField(label="Name", max_length=75) + + form = CustomAuthenticationForm() + self.assertEqual(form['username'].label, "Name") + @skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) From 4da5947a876a4ec2cba9de8b0ef1513832328c67 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 10 Jan 2013 15:16:25 -0500 Subject: [PATCH 131/870] Fixed #19588 - Added create_superuser to UserManager docs. Thanks minddust for the report. --- docs/ref/contrib/auth.txt | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index e35a5b3586..f871f1493f 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -218,9 +218,10 @@ Manager methods .. class:: models.UserManager The :class:`~django.contrib.auth.models.User` model has a custom manager - that has the following helper methods: + that has the following helper methods (in addition to the methods provided + by :class:`~django.contrib.auth.models.BaseUserManager`): - .. method:: create_user(username, email=None, password=None) + .. method:: create_user(username, email=None, password=None, **extra_fields) Creates, saves and returns a :class:`~django.contrib.auth.models.User`. @@ -235,18 +236,17 @@ Manager methods :meth:`~django.contrib.auth.models.User.set_unusable_password()` will be called. + The ``extra_fields`` keyword arguments are passed through to the + :class:`~django.contrib.auth.models.User`'s ``__init__`` method to + allow setting arbitrary fields on a :ref:`custom User model + `. + See :ref:`Creating users ` for example usage. - .. method:: make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789') + .. method:: create_superuser(self, username, email, password, **extra_fields) - Returns a random password with the given length and given string of - allowed characters. (Note that the default value of ``allowed_chars`` - doesn't contain letters that can cause user confusion, including: - - * ``i``, ``l``, ``I``, and ``1`` (lowercase letter i, lowercase - letter L, uppercase letter i, and the number one) - * ``o``, ``O``, and ``0`` (uppercase letter o, lowercase letter o, - and zero) + Same as :meth:`create_user`, but sets :attr:`~models.User.is_staff` and + :attr:`~models.User.is_superuser` to ``True``. Anonymous users From 71d76ec011b393990ba9f5fb63727dbe36c3c440 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 11 Jan 2013 05:59:17 -0500 Subject: [PATCH 132/870] Fixed #10239 - Added docs for modelform_factory Thanks ingenieroariel for the suggestion and slurms for the review. --- django/forms/models.py | 20 ++++++++++++++ docs/ref/forms/index.txt | 1 + docs/ref/forms/models.txt | 40 +++++++++++++++++++++++++++ docs/topics/forms/modelforms.txt | 46 +++++++++++++++++++++++++++----- 4 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 docs/ref/forms/models.txt diff --git a/django/forms/models.py b/django/forms/models.py index 27c246b668..1b6821cd5b 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -141,6 +141,11 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c ``exclude`` is an optional list of field names. If provided, the named fields will be excluded from the returned fields, even if they are listed in the ``fields`` argument. + + ``widgets`` is a dictionary of model field names mapped to a widget + + ``formfield_callback`` is a callable that takes a model field and returns + a form field. """ field_list = [] ignored = [] @@ -371,6 +376,21 @@ class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)): def modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None): + """ + Returns a ModelForm containing form fields for the given model. + + ``fields`` is an optional list of field names. If provided, only the named + fields will be included in the returned fields. + + ``exclude`` is an optional list of field names. If provided, the named + fields will be excluded from the returned fields, even if they are listed + in the ``fields`` argument. + + ``widgets`` is a dictionary of model field names mapped to a widget. + + ``formfield_callback`` is a callable that takes a model field and returns + a form field. + """ # Create the inner Meta class. FIXME: ideally, we should be able to # construct a ModelForm without creating and passing in a temporary # inner class. diff --git a/docs/ref/forms/index.txt b/docs/ref/forms/index.txt index 866afed6dc..446fdb82de 100644 --- a/docs/ref/forms/index.txt +++ b/docs/ref/forms/index.txt @@ -9,5 +9,6 @@ Detailed form API reference. For introductory material, see :doc:`/topics/forms/ api fields + models widgets validation diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt new file mode 100644 index 0000000000..1f4a0d0c3d --- /dev/null +++ b/docs/ref/forms/models.txt @@ -0,0 +1,40 @@ +==================== +Model Form Functions +==================== + +.. module:: django.forms.models + :synopsis: Django's functions for building model forms and formsets. + +.. method:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None) + + Returns a :class:`~django.forms.ModelForm` class for the given ``model``. + You can optionally pass a ``form`` argument to use as a starting point for + constructing the ``ModelForm``. + + ``fields`` is an optional list of field names. If provided, only the named + fields will be included in the returned fields. + + ``exclude`` is an optional list of field names. If provided, the named + fields will be excluded from the returned fields, even if they are listed + in the ``fields`` argument. + + ``widgets`` is a dictionary of model field names mapped to a widget. + + ``formfield_callback`` is a callable that takes a model field and returns + a form field. + + See :ref:`modelforms-factory` for example usage. + +.. method:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None) + + Returns a ``FormSet`` class for the given ``model`` class. + + Arguments ``model``, ``form``, ``fields``, ``exclude``, and + ``formfield_callback`` are all passed through to + :meth:`~django.forms.models.modelform_factory`. + + Arguments ``formset``, ``extra``, ``max_num``, ``can_order``, and + ``can_delete`` are passed through to ``formset_factory``. See + :ref:`formsets` for details. + + See :ref:`model-formsets` for example usage. diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 802150d6c3..9a33d68cf7 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -544,6 +544,33 @@ for more on how field cleaning and validation work. Also, your model's :ref:`Validating objects ` for more information on the model's ``clean()`` hook. +.. _modelforms-factory: + +ModelForm factory function +-------------------------- + +You can create forms from a given model using the standalone function +:class:`~django.forms.models.modelform_factory`, instead of using a class +definition. This may be more convenient if you do not have many customizations +to make:: + + >>> from django.forms.models import modelform_factory + >>> BookForm = modelform_factory(Book) + +This can also be used to make simple modifications to existing forms, for +example by specifying which fields should be displayed:: + + >>> Form = modelform_factory(Book, form=BookForm, fields=("author",)) + +... or which fields should be excluded:: + + >>> Form = modelform_factory(Book, form=BookForm, exclude=("title",)) + +You can also specify the widgets to be used for a given field:: + + >>> from django.forms import Textarea + >>> Form = modelform_factory(Book, form=BookForm, widgets={"title": Textarea()}) + .. _model-formsets: Model formsets @@ -574,9 +601,10 @@ with the ``Author`` model. It works just like a regular formset:: .. note:: - ``modelformset_factory`` uses ``formset_factory`` to generate formsets. - This means that a model formset is just an extension of a basic formset - that knows how to interact with a particular model. + + :func:`~django.forms.models.modelformset_factory` uses ``formset_factory`` + to generate formsets. This means that a model formset is just an extension + of a basic formset that knows how to interact with a particular model. Changing the queryset --------------------- @@ -628,8 +656,9 @@ Providing initial values As with regular formsets, it's possible to :ref:`specify initial data ` for forms in the formset by specifying an ``initial`` parameter when instantiating the model formset class returned by -``modelformset_factory``. However, with model formsets, the initial values only -apply to extra forms, those that aren't bound to an existing object instance. +:func:`~django.forms.models.modelformset_factory`. However, with model +formsets, the initial values only apply to extra forms, those that aren't bound +to an existing object instance. .. _saving-objects-in-the-formset: @@ -675,7 +704,8 @@ Limiting the number of editable objects --------------------------------------- As with regular formsets, you can use the ``max_num`` and ``extra`` parameters -to ``modelformset_factory`` to limit the number of extra forms displayed. +to :func:`~django.forms.models.modelformset_factory` to limit the number of +extra forms displayed. ``max_num`` does not prevent existing objects from being displayed:: @@ -850,7 +880,9 @@ a particular author, you could do this:: >>> formset = BookFormSet(instance=author) .. note:: - ``inlineformset_factory`` uses ``modelformset_factory`` and marks + + ``inlineformset_factory`` uses + :func:`~django.forms.models.modelformset_factory` and marks ``can_delete=True``. .. seealso:: From 9f9a7f03d77e2b6002f841be42eccf8ff287f279 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 11 Jan 2013 07:01:56 -0500 Subject: [PATCH 133/870] Fixed #19437 - Clarified pip install instructions in contributing tutorial. --- docs/intro/contributing.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/intro/contributing.txt b/docs/intro/contributing.txt index c94038bc56..f9fb451b39 100644 --- a/docs/intro/contributing.txt +++ b/docs/intro/contributing.txt @@ -96,9 +96,10 @@ Download the Django source code repository using the following command:: pip install -e /path/to/your/local/clone/django/ - to link your cloned checkout into a virtual environment. This is a great - option to isolate your development copy of Django from the rest of your - system and avoids potential package conflicts. + (where ``django`` is the directory of your clone that contains + ``setup.py``) to link your cloned checkout into a virtual environment. This + is a great option to isolate your development copy of Django from the rest + of your system and avoids potential package conflicts. __ http://www.virtualenv.org From 2e55cf580e48b02165b7aafb0d9368c714742137 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 11 Jan 2013 17:49:55 +0100 Subject: [PATCH 134/870] Adapted test assertion against yaml dump Fixes #12914 (again). --- tests/modeltests/timezones/tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/modeltests/timezones/tests.py b/tests/modeltests/timezones/tests.py index 2fdf6733a0..29a490e3fb 100644 --- a/tests/modeltests/timezones/tests.py +++ b/tests/modeltests/timezones/tests.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import datetime import os +import re import sys import time import warnings @@ -504,7 +505,9 @@ class SerializationTests(TestCase): self.assertXMLEqual(field.childNodes[0].wholeText, dt) def assert_yaml_contains_datetime(self, yaml, dt): - self.assertIn("- fields: {dt: !!timestamp '%s'}" % dt, yaml) + # Depending on the yaml dumper, '!timestamp' might be absent + self.assertRegexpMatches(yaml, + r"- fields: {dt: !(!timestamp)? '%s'}" % re.escape(dt)) def test_naive_datetime(self): dt = datetime.datetime(2011, 9, 1, 13, 20, 30) From eb6c107624930c97390185fdbf7f887c50665808 Mon Sep 17 00:00:00 2001 From: Nick Sandford Date: Fri, 11 Jan 2013 13:57:54 +0800 Subject: [PATCH 135/870] Fixed #19360 -- Raised an explicit exception for aggregates on date/time fields in sqlite3 Thanks lsaffre for the report and Chris Medrela for the initial patch. --- django/db/backends/sqlite3/base.py | 13 +++++++++++++ docs/ref/models/querysets.txt | 8 ++++++++ tests/regressiontests/backends/models.py | 11 +++++++++++ tests/regressiontests/backends/tests.py | 18 +++++++++++++++++- 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 1fcc222c80..f4fd1cc379 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -18,6 +18,8 @@ from django.db.backends.signals import connection_created from django.db.backends.sqlite3.client import DatabaseClient from django.db.backends.sqlite3.creation import DatabaseCreation from django.db.backends.sqlite3.introspection import DatabaseIntrospection +from django.db.models import fields +from django.db.models.sql import aggregates from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.functional import cached_property from django.utils.safestring import SafeBytes @@ -127,6 +129,17 @@ class DatabaseOperations(BaseDatabaseOperations): limit = 999 if len(fields) > 1 else 500 return (limit // len(fields)) if len(fields) > 0 else len(objs) + def check_aggregate_support(self, aggregate): + bad_fields = (fields.DateField, fields.DateTimeField, fields.TimeField) + bad_aggregates = (aggregates.Sum, aggregates.Avg, + aggregates.Variance, aggregates.StdDev) + if (isinstance(aggregate.source, bad_fields) and + isinstance(aggregate, bad_aggregates)): + raise NotImplementedError( + 'You cannot use Sum, Avg, StdDev and Variance aggregations ' + 'on date/time fields in sqlite3 ' + 'since date/time is saved as text.') + def date_extract_sql(self, lookup_type, field_name): # sqlite doesn't support extract, so we fake it with the user-defined # function django_extract that's registered in connect(). Note that diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 2bbd895fd4..71049703c9 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -2188,6 +2188,14 @@ Django provides the following aggregation functions in the aggregate functions, see :doc:`the topic guide on aggregation `. +.. warning:: + + SQLite can't handle aggregation on date/time fields out of the box. + This is because there are no native date/time fields in SQLite and Django + currently emulates these features using a text field. Attempts to use + aggregation on date/time fields in SQLite will raise + ``NotImplementedError``. + Avg ~~~ diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py index 344cf4c798..a92aa71e17 100644 --- a/tests/regressiontests/backends/models.py +++ b/tests/regressiontests/backends/models.py @@ -75,3 +75,14 @@ class Article(models.Model): def __str__(self): return self.headline + + +@python_2_unicode_compatible +class Item(models.Model): + name = models.CharField(max_length=30) + date = models.DateField() + time = models.TimeField() + last_modified = models.DateTimeField() + + def __str__(self): + return self.name diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index 791f4c1daa..b29384739d 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -12,6 +12,7 @@ from django.db import (backend, connection, connections, DEFAULT_DB_ALIAS, IntegrityError, transaction) from django.db.backends.signals import connection_created from django.db.backends.postgresql_psycopg2 import version as pg_version +from django.db.models import fields, Sum, Avg, Variance, StdDev from django.db.utils import ConnectionHandler, DatabaseError, load_backend from django.test import (TestCase, skipUnlessDBFeature, skipIfDBFeature, TransactionTestCase) @@ -362,6 +363,22 @@ class EscapingChecks(TestCase): self.assertTrue(int(response)) +class SqlliteAggregationTests(TestCase): + """ + #19360: Raise NotImplementedError when aggregating on date/time fields. + """ + @unittest.skipUnless(connection.vendor == 'sqlite', + "No need to check SQLite aggregation semantics") + def test_aggregation(self): + for aggregate in (Sum, Avg, Variance, StdDev): + self.assertRaises(NotImplementedError, + models.Item.objects.all().aggregate, aggregate('time')) + self.assertRaises(NotImplementedError, + models.Item.objects.all().aggregate, aggregate('date')) + self.assertRaises(NotImplementedError, + models.Item.objects.all().aggregate, aggregate('last_modified')) + + class BackendTestCase(TestCase): def create_squares_with_executemany(self, args): @@ -400,7 +417,6 @@ class BackendTestCase(TestCase): self.create_squares_with_executemany(args) self.assertEqual(models.Square.objects.count(), 9) - def test_unicode_fetches(self): #6254: fetchone, fetchmany, fetchall return strings as unicode objects qn = connection.ops.quote_name From f9a46d7bc9b321214f75c2e5fa8f416405a59ed9 Mon Sep 17 00:00:00 2001 From: Thomas Bartelmess Date: Tue, 25 Dec 2012 20:37:39 -0500 Subject: [PATCH 136/870] Made dev server autoreloader ignore filenames reported as None. Useful under Jython. Thanks Thomas Bartelmess for the report and patch. Ref #9589. --- django/utils/autoreload.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py index 617fc9da7d..cc9cc1304a 100644 --- a/django/utils/autoreload.py +++ b/django/utils/autoreload.py @@ -63,6 +63,8 @@ def code_changed(): except AttributeError: pass for filename in filenames + _error_files: + if not filename: + continue if filename.endswith(".pyc") or filename.endswith(".pyo"): filename = filename[:-1] if filename.endswith("$py.class"): From 5362134090adce86c755a6ab48831ba834b70704 Mon Sep 17 00:00:00 2001 From: Vinod Kurup Date: Thu, 10 Jan 2013 16:17:52 -0500 Subject: [PATCH 137/870] Fixed code examples in which render() calls were missing `request` parameter. --- docs/topics/http/file-uploads.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt index 53499359e3..80bd5f3c44 100644 --- a/docs/topics/http/file-uploads.txt +++ b/docs/topics/http/file-uploads.txt @@ -201,7 +201,7 @@ corresponding :class:`~django.db.models.FileField` when calling return HttpResponseRedirect('/success/url/') else: form = ModelFormWithFileField() - return render('upload.html', {'form': form}) + return render(request, 'upload.html', {'form': form}) If you are constructing an object manually, you can simply assign the file object from :attr:`request.FILES ` to the file @@ -221,7 +221,7 @@ field in the model:: return HttpResponseRedirect('/success/url/') else: form = UploadFileForm() - return render('upload.html', {'form': form}) + return render(request, 'upload.html', {'form': form}) ``UploadedFile`` objects From 1bbd36a36add9b15db43014cf5e7bdb72a86fef1 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Fri, 11 Jan 2013 16:15:17 -0300 Subject: [PATCH 138/870] Minor DEBUG setting reference formatting edit. --- docs/ref/settings.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 786a92c94d..ffe8a0fe77 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -744,13 +744,13 @@ sensitive (or offensive), such as :setting:`SECRET_KEY` or :setting:`PROFANITIES_LIST`. Specifically, it will exclude any setting whose name includes any of the following: - * API - * KEY - * PASS - * PROFANITIES_LIST - * SECRET - * SIGNATURE - * TOKEN +* ``'API'`` +* ``'KEY'`` +* ``'PASS'`` +* ``'PROFANITIES_LIST'`` +* ``'SECRET'`` +* ``'SIGNATURE'`` +* ``'TOKEN'`` Note that these are *partial* matches. ``'PASS'`` will also match PASSWORD, just as ``'TOKEN'`` will also match TOKENIZED and so on. From 4e2e8f39d19d79a59c2696b2c40cb619a54fa745 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 11 Jan 2013 20:42:33 +0100 Subject: [PATCH 139/870] Fixed #4833 -- Validate email addresses with localhost as domain --- django/core/validators.py | 65 +++++++++++++++++++--------- docs/ref/validators.txt | 2 +- tests/modeltests/validators/tests.py | 2 + 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/django/core/validators.py b/django/core/validators.py index 251b5d8856..cd9dba1ee8 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -78,30 +78,53 @@ def validate_integer(value): raise ValidationError('') -class EmailValidator(RegexValidator): +class EmailValidator(object): + message = _('Enter a valid e-mail address.') + code = 'invalid' + user_regex = re.compile( + r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$" # dot-atom + r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"$)', # quoted-string + re.IGNORECASE) + domain_regex = re.compile( + r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)' # domain + # literal form, ipv4 address (SMTP 4.1.3) + r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', + re.IGNORECASE) + domain_whitelist = ['localhost'] + + def __init__(self, message=None, code=None, whitelist=None): + if message is not None: + self.message = message + if code is not None: + self.code = code + if whitelist is not None: + self.domain_whitelist = whitelist def __call__(self, value): - try: - super(EmailValidator, self).__call__(value) - except ValidationError as e: - # Trivial case failed. Try for possible IDN domain-part - if value and '@' in value: - parts = value.split('@') - try: - parts[-1] = parts[-1].encode('idna').decode('ascii') - except UnicodeError: - raise e - super(EmailValidator, self).__call__('@'.join(parts)) - else: - raise + value = force_text(value) -email_re = re.compile( - r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom - # quoted-string, see also http://tools.ietf.org/html/rfc2822#section-3.2.5 - r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' - r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)$)' # domain - r'|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) # literal form, ipv4 address (SMTP 4.1.3) -validate_email = EmailValidator(email_re, _('Enter a valid email address.'), 'invalid') + if not value or '@' not in value: + raise ValidationError(self.message, code=self.code) + + user_part, domain_part = value.split('@', 1) + + if not self.user_regex.match(user_part): + raise ValidationError(self.message, code=self.code) + + if (not domain_part in self.domain_whitelist and + not self.domain_regex.match(domain_part)): + # Try for possible IDN domain-part + try: + domain_part = domain_part.encode('idna').decode('ascii') + if not self.domain_regex.match(domain_part): + raise ValidationError(self.message, code=self.code) + else: + return + except UnicodeError: + pass + raise ValidationError(self.message, code=self.code) + +validate_email = EmailValidator() slug_re = re.compile(r'^[-a-zA-Z0-9_]+$') validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), 'invalid') diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt index 8da134a42d..92e257ca85 100644 --- a/docs/ref/validators.txt +++ b/docs/ref/validators.txt @@ -96,7 +96,7 @@ to, or in lieu of custom ``field.clean()`` methods. ------------------ .. data:: validate_email - A :class:`RegexValidator` instance that ensures a value looks like an + An ``EmailValidator`` instance that ensures a value looks like an email address. ``validate_slug`` diff --git a/tests/modeltests/validators/tests.py b/tests/modeltests/validators/tests.py index 0174a606df..5b562a87e6 100644 --- a/tests/modeltests/validators/tests.py +++ b/tests/modeltests/validators/tests.py @@ -29,6 +29,8 @@ TEST_DATA = ( (validate_email, 'example@valid-----hyphens.com', None), (validate_email, 'example@valid-with-hyphens.com', None), (validate_email, 'test@domain.with.idn.tld.उदाहरण.परीक्षा', None), + (validate_email, 'email@localhost', None), + (EmailValidator(whitelist=['localdomain']), 'email@localdomain', None), (validate_email, None, ValidationError), (validate_email, '', ValidationError), From f08e739bc2ba5d3530a806378087227728369464 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 11 Jan 2013 21:09:33 +0100 Subject: [PATCH 140/870] Fixed #19585 -- Fixed loading cookie value as a dict This regression was introduced by the 'unicode_literals' patch. --- django/http/cookie.py | 5 ++++- tests/regressiontests/httpwrappers/tests.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/django/http/cookie.py b/django/http/cookie.py index 78adb09ce8..50ff549caf 100644 --- a/django/http/cookie.py +++ b/django/http/cookie.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, unicode_literals from django.utils.encoding import force_str +from django.utils import six from django.utils.six.moves import http_cookies @@ -48,7 +49,9 @@ else: if not _cookie_allows_colon_in_names: def load(self, rawdata): self.bad_cookies = set() - super(SimpleCookie, self).load(force_str(rawdata)) + if not six.PY3 and isinstance(rawdata, six.text_type): + rawdata = force_str(rawdata) + super(SimpleCookie, self).load(rawdata) for key in self.bad_cookies: del self[key] diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index 67172d963c..c76d8eafe3 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -588,3 +588,7 @@ class CookieTests(unittest.TestCase): c['name']['httponly'] = True self.assertTrue(c['name']['httponly']) + def test_load_dict(self): + c = SimpleCookie() + c.load({'name': 'val'}) + self.assertEqual(c['name'].value, 'val') From bcdb4898cae2f24599b39845b8e4cd7edc202424 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 11 Jan 2013 21:27:51 +0100 Subject: [PATCH 141/870] Fixed #19488 -- Made i18n_patterns redirect work with non-slash-ending paths Thanks Daniel Gerzo for the report and the initial patch. --- django/middleware/locale.py | 8 +++++--- tests/regressiontests/i18n/patterns/tests.py | 4 ++++ tests/regressiontests/i18n/patterns/urls/default.py | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/django/middleware/locale.py b/django/middleware/locale.py index 628782b181..f693e4d242 100644 --- a/django/middleware/locale.py +++ b/django/middleware/locale.py @@ -31,10 +31,12 @@ class LocaleMiddleware(object): and self.is_language_prefix_patterns_used()): urlconf = getattr(request, 'urlconf', None) language_path = '/%s%s' % (language, request.path_info) - if settings.APPEND_SLASH and not language_path.endswith('/'): - language_path = language_path + '/' + path_valid = is_valid_path(language_path, urlconf) + if (not path_valid and settings.APPEND_SLASH + and not language_path.endswith('/')): + path_valid = is_valid_path("%s/" % language_path, urlconf) - if is_valid_path(language_path, urlconf): + if path_valid: language_url = "%s://%s/%s%s" % ( request.is_secure() and 'https' or 'http', request.get_host(), language, request.get_full_path()) diff --git a/tests/regressiontests/i18n/patterns/tests.py b/tests/regressiontests/i18n/patterns/tests.py index 358cdf65db..639e03f288 100644 --- a/tests/regressiontests/i18n/patterns/tests.py +++ b/tests/regressiontests/i18n/patterns/tests.py @@ -115,6 +115,7 @@ class URLTranslationTests(URLTestCaseBase): with translation.override('nl'): self.assertEqual(reverse('users'), '/nl/gebruikers/') + self.assertEqual(reverse('prefixed_xml'), '/nl/prefixed.xml') with translation.override('pt-br'): self.assertEqual(reverse('users'), '/pt-br/usuarios/') @@ -186,6 +187,9 @@ class URLRedirectWithoutTrailingSlashTests(URLTestCaseBase): self.assertIn(('http://testserver/en/account/register/', 301), response.redirect_chain) self.assertRedirects(response, '/en/account/register/', 302) + response = self.client.get('/prefixed.xml', HTTP_ACCEPT_LANGUAGE='en', follow=True) + self.assertRedirects(response, '/en/prefixed.xml', 302) + class URLRedirectWithoutTrailingSlashSettingTests(URLTestCaseBase): """ diff --git a/tests/regressiontests/i18n/patterns/urls/default.py b/tests/regressiontests/i18n/patterns/urls/default.py index f117502753..00b90b14b7 100644 --- a/tests/regressiontests/i18n/patterns/urls/default.py +++ b/tests/regressiontests/i18n/patterns/urls/default.py @@ -14,6 +14,7 @@ urlpatterns = patterns('', urlpatterns += i18n_patterns('', url(r'^prefixed/$', view, name='prefixed'), + url(r'^prefixed\.xml$', view, name='prefixed_xml'), url(_(r'^users/$'), view, name='users'), url(_(r'^account/'), include('regressiontests.i18n.patterns.urls.namespace', namespace='account')), ) From a170c3f755351beb35f8166ec3c7e9d524d9602d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 11 Jan 2013 20:59:29 -0800 Subject: [PATCH 142/870] Removed some now dead code from deletion (thanks to Carl Meyer for noticing it). --- django/db/models/deletion.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 6dff4a2882..1c3a582fc5 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -75,7 +75,6 @@ class Collector(object): self.using = using # Initially, {model: set([instances])}, later values become lists. self.data = {} - self.batches = {} # {model: {field: set([instances])}} self.field_updates = {} # {model: {(field, value): set([instances])}} # fast_deletes is a list of queryset-likes that can be deleted without # fetching the objects into memory. @@ -115,13 +114,6 @@ class Collector(object): source._meta.concrete_model, set()).add(model._meta.concrete_model) return new_objs - def add_batch(self, model, field, objs): - """ - Schedules a batch delete. Every instance of 'model' that is related to - an instance of 'obj' through 'field' will be deleted. - """ - self.batches.setdefault(model, {}).setdefault(field, set()).update(objs) - def add_field_update(self, field, value, objs): """ Schedules a field update. 'objs' must be a homogenous iterable @@ -303,12 +295,6 @@ class Collector(object): for instances in six.itervalues(self.data): instances.reverse() - # delete batches - for model, batches in six.iteritems(self.batches): - query = sql.DeleteQuery(model) - for field, instances in six.iteritems(batches): - query.delete_batch([obj.pk for obj in instances], self.using, field) - # delete instances for model, instances in six.iteritems(self.data): query = sql.DeleteQuery(model) From 97121cb96e2f9f02f977010b5549b88f1a73610b Mon Sep 17 00:00:00 2001 From: Stephan Jaekel Date: Sat, 12 Jan 2013 12:00:31 +0100 Subject: [PATCH 143/870] Fixed #18026 -- Don't return an anonymous dict if extra_data in storage is empty. --- django/contrib/formtools/tests/wizard/storage.py | 10 ++++++++++ django/contrib/formtools/wizard/storage/base.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/django/contrib/formtools/tests/wizard/storage.py b/django/contrib/formtools/tests/wizard/storage.py index fe1d96381f..17968dfcda 100644 --- a/django/contrib/formtools/tests/wizard/storage.py +++ b/django/contrib/formtools/tests/wizard/storage.py @@ -75,3 +75,13 @@ class TestStorage(object): storage.extra_data = extra_context storage2 = self.get_storage()('wizard2', request, None) self.assertEqual(storage2.extra_data, {}) + + def test_extra_context_key_persistence(self): + request = get_request() + storage = self.get_storage()('wizard1', request, None) + + self.assertFalse('test' in storage.extra_data) + + storage.extra_data['test'] = True + + self.assertTrue('test' in storage.extra_data) diff --git a/django/contrib/formtools/wizard/storage/base.py b/django/contrib/formtools/wizard/storage/base.py index 2e59679d09..6c155e0ff0 100644 --- a/django/contrib/formtools/wizard/storage/base.py +++ b/django/contrib/formtools/wizard/storage/base.py @@ -37,7 +37,7 @@ class BaseStorage(object): current_step = lazy_property(_get_current_step, _set_current_step) def _get_extra_data(self): - return self.data[self.extra_data_key] or {} + return self.data[self.extra_data_key] def _set_extra_data(self, extra_data): self.data[self.extra_data_key] = extra_data From 17f8496fea9b866769b2d2a04326acbe25e9256f Mon Sep 17 00:00:00 2001 From: Stephan Jaekel Date: Sat, 12 Jan 2013 12:20:18 +0100 Subject: [PATCH 144/870] Fixed #19024 -- Corrected form wizard docs for get_form_prefix. --- docs/ref/contrib/formtools/form-wizard.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index 9ea65d7e5f..8cd5d4ecd3 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -318,11 +318,11 @@ Advanced ``WizardView`` methods counter as string representing the current step of the wizard. (E.g., the first form is ``'0'`` and the second form is ``'1'``) -.. method:: WizardView.get_form_prefix(step) +.. method:: WizardView.get_form_prefix(step, form) - Given the step, returns a form prefix to use. By default, this simply uses - the step itself. For more, see the :ref:`form prefix documentation - `. + Given the step and the form class which will be called with the returned + form prefix. By default, this simply uses the step itself. + For more, see the :ref:`form prefix documentation `. .. method:: WizardView.get_form_initial(step) From 223fc8eaf9d04eb2c277a30f4f46745d4403b69b Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 12 Jan 2013 21:10:08 +0100 Subject: [PATCH 145/870] Return namedtuple from get_table_description We don't make use of it currently to not break third-party db backends. --- django/db/backends/__init__.py | 7 +++++++ django/db/backends/mysql/introspection.py | 4 ++-- django/db/backends/oracle/introspection.py | 4 ++-- django/db/backends/postgresql_psycopg2/introspection.py | 4 ++-- django/db/backends/sqlite3/introspection.py | 4 ++-- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 5116f50668..78dbbc670b 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -4,6 +4,7 @@ try: from django.utils.six.moves import _thread as thread except ImportError: from django.utils.six.moves import _dummy_thread as thread +from collections import namedtuple from contextlib import contextmanager from django.conf import settings @@ -918,6 +919,12 @@ class BaseDatabaseOperations(object): """ return params + +# Structure returned by the DB-API cursor.description interface (PEP 249) +FieldInfo = namedtuple('FieldInfo', + 'name type_code display_size internal_size precision scale null_ok' +) + class BaseDatabaseIntrospection(object): """ This class encapsulates all backend-specific introspection utilities diff --git a/django/db/backends/mysql/introspection.py b/django/db/backends/mysql/introspection.py index c552263e5e..52e97baedb 100644 --- a/django/db/backends/mysql/introspection.py +++ b/django/db/backends/mysql/introspection.py @@ -1,7 +1,7 @@ import re from .base import FIELD_TYPE -from django.db.backends import BaseDatabaseIntrospection +from django.db.backends import BaseDatabaseIntrospection, FieldInfo foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)") @@ -47,7 +47,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): length_map = dict(cursor.fetchall()) cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name)) - return [line[:3] + (length_map.get(line[0], line[3]),) + line[4:] + return [FieldInfo(*(line[:3] + (length_map.get(line[0], line[3]),) + line[4:])) for line in cursor.description] def _name_to_index(self, cursor, table_name): diff --git a/django/db/backends/oracle/introspection.py b/django/db/backends/oracle/introspection.py index 7d477f6924..2a68b999bc 100644 --- a/django/db/backends/oracle/introspection.py +++ b/django/db/backends/oracle/introspection.py @@ -1,4 +1,4 @@ -from django.db.backends import BaseDatabaseIntrospection +from django.db.backends import BaseDatabaseIntrospection, FieldInfo import cx_Oracle import re @@ -47,7 +47,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): cursor.execute("SELECT * FROM %s WHERE ROWNUM < 2" % self.connection.ops.quote_name(table_name)) description = [] for desc in cursor.description: - description.append((desc[0].lower(),) + desc[1:]) + description.append(FieldInfo(*((desc[0].lower(),) + desc[1:]))) return description def table_name_converter(self, name): diff --git a/django/db/backends/postgresql_psycopg2/introspection.py b/django/db/backends/postgresql_psycopg2/introspection.py index 5d30382ddf..440fa44c4e 100644 --- a/django/db/backends/postgresql_psycopg2/introspection.py +++ b/django/db/backends/postgresql_psycopg2/introspection.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.db.backends import BaseDatabaseIntrospection +from django.db.backends import BaseDatabaseIntrospection, FieldInfo class DatabaseIntrospection(BaseDatabaseIntrospection): @@ -45,7 +45,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): WHERE table_name = %s""", [table_name]) null_map = dict(cursor.fetchall()) cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name)) - return [line[:6] + (null_map[line[0]]=='YES',) + return [FieldInfo(*(line[:6] + (null_map[line[0]]=='YES',))) for line in cursor.description] def get_relations(self, cursor, table_name): diff --git a/django/db/backends/sqlite3/introspection.py b/django/db/backends/sqlite3/introspection.py index 1df4c18c1c..dfc1d94d47 100644 --- a/django/db/backends/sqlite3/introspection.py +++ b/django/db/backends/sqlite3/introspection.py @@ -1,5 +1,5 @@ import re -from django.db.backends import BaseDatabaseIntrospection +from django.db.backends import BaseDatabaseIntrospection, FieldInfo field_size_re = re.compile(r'^\s*(?:var)?char\s*\(\s*(\d+)\s*\)\s*$') @@ -60,7 +60,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): def get_table_description(self, cursor, table_name): "Returns a description of the table, with the DB-API cursor.description interface." - return [(info['name'], info['type'], None, info['size'], None, None, + return [FieldInfo(info['name'], info['type'], None, info['size'], None, None, info['null_ok']) for info in self._table_info(cursor, table_name)] def get_relations(self, cursor, table_name): From 0171ba65dbbff377282c03b86c83036168c84b22 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 12 Jan 2013 21:46:08 +0100 Subject: [PATCH 146/870] Fixed #17574 -- Implemented missing get_key_columns in PostgreSQL backend --- .../postgresql_psycopg2/introspection.py | 17 +++++++++ tests/regressiontests/introspection/tests.py | 36 ++----------------- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/django/db/backends/postgresql_psycopg2/introspection.py b/django/db/backends/postgresql_psycopg2/introspection.py index 440fa44c4e..a71d107357 100644 --- a/django/db/backends/postgresql_psycopg2/introspection.py +++ b/django/db/backends/postgresql_psycopg2/introspection.py @@ -66,6 +66,23 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): relations[row[0][0] - 1] = (row[1][0] - 1, row[2]) return relations + def get_key_columns(self, cursor, table_name): + key_columns = [] + cursor.execute(""" + SELECT kcu.column_name, ccu.table_name AS referenced_table, ccu.column_name AS referenced_column + FROM information_schema.constraint_column_usage ccu + LEFT JOIN information_schema.key_column_usage kcu + ON ccu.constraint_catalog = kcu.constraint_catalog + AND ccu.constraint_schema = kcu.constraint_schema + AND ccu.constraint_name = kcu.constraint_name + LEFT JOIN information_schema.table_constraints tc + ON ccu.constraint_catalog = tc.constraint_catalog + AND ccu.constraint_schema = tc.constraint_schema + AND ccu.constraint_name = tc.constraint_name + WHERE kcu.table_name = %s AND tc.constraint_type = 'FOREIGN KEY'""" , [table_name]) + key_columns.extend(cursor.fetchall()) + return key_columns + def get_indexes(self, cursor, table_name): # This query retrieves each index on the given table, including the # first associated field name diff --git a/tests/regressiontests/introspection/tests.py b/tests/regressiontests/introspection/tests.py index 2df946d874..cd3e1cc8a4 100644 --- a/tests/regressiontests/introspection/tests.py +++ b/tests/regressiontests/introspection/tests.py @@ -1,10 +1,8 @@ from __future__ import absolute_import, unicode_literals -from functools import update_wrapper - from django.db import connection from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature -from django.utils import six, unittest +from django.utils import unittest from .models import Reporter, Article @@ -14,36 +12,7 @@ else: expectedFailureOnOracle = lambda f: f -# The introspection module is optional, so methods tested here might raise -# NotImplementedError. This is perfectly acceptable behavior for the backend -# in question, but the tests need to handle this without failing. Ideally we'd -# skip these tests, but until #4788 is done we'll just ignore them. -# -# The easiest way to accomplish this is to decorate every test case with a -# wrapper that ignores the exception. -# -# The metaclass is just for fun. - - -def ignore_not_implemented(func): - def _inner(*args, **kwargs): - try: - return func(*args, **kwargs) - except NotImplementedError: - return None - update_wrapper(_inner, func) - return _inner - - -class IgnoreNotimplementedError(type): - def __new__(cls, name, bases, attrs): - for k, v in attrs.items(): - if k.startswith('test'): - attrs[k] = ignore_not_implemented(v) - return type.__new__(cls, name, bases, attrs) - - -class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)): +class IntrospectionTests(TestCase): def test_table_names(self): tl = connection.introspection.table_names() self.assertEqual(tl, sorted(tl)) @@ -139,6 +108,7 @@ class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase) # That's {field_index: (field_index_other_table, other_table)} self.assertEqual(relations, {3: (0, Reporter._meta.db_table)}) + @skipUnlessDBFeature('can_introspect_foreign_keys') def test_get_key_columns(self): cursor = connection.cursor() key_columns = connection.introspection.get_key_columns(cursor, Article._meta.db_table) From ba50d3e05bc9a33aef495a5fbca239afe52237b3 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 12 Jan 2013 18:44:53 -0500 Subject: [PATCH 147/870] Fixed #14633 - Organized settings reference docs and added a topical index. Thanks Gabriel Hurley for the original idea and adamv for the draft patch. --- docs/ref/contrib/comments/index.txt | 11 +- docs/ref/contrib/comments/settings.txt | 33 - docs/ref/contrib/csrf.txt | 62 +- docs/ref/contrib/messages.txt | 94 +- docs/ref/contrib/staticfiles.txt | 106 +-- docs/ref/settings.txt | 1092 ++++++++++++++++-------- docs/topics/http/sessions.txt | 117 +-- 7 files changed, 784 insertions(+), 731 deletions(-) delete mode 100644 docs/ref/contrib/comments/settings.txt diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt index 8275092d2f..d4e967b4b2 100644 --- a/docs/ref/contrib/comments/index.txt +++ b/docs/ref/contrib/comments/index.txt @@ -34,7 +34,8 @@ To get started using the ``comments`` app, follow these steps: #. Use the `comment template tags`_ below to embed comments in your templates. -You might also want to examine :doc:`/ref/contrib/comments/settings`. +You might also want to examine :ref:`the available settings +`. Comment template tags ===================== @@ -335,6 +336,13 @@ output the CSRF token and cookie. .. _honeypot: http://en.wikipedia.org/wiki/Honeypot_(computing) + +Configuration +============= + +See :ref:`comment settings `. + + More information ================ @@ -342,7 +350,6 @@ More information :maxdepth: 1 models - settings signals custom forms diff --git a/docs/ref/contrib/comments/settings.txt b/docs/ref/contrib/comments/settings.txt deleted file mode 100644 index 1f1aecafd4..0000000000 --- a/docs/ref/contrib/comments/settings.txt +++ /dev/null @@ -1,33 +0,0 @@ -================ -Comment settings -================ - -These settings configure the behavior of the comments framework: - -.. setting:: COMMENTS_HIDE_REMOVED - -COMMENTS_HIDE_REMOVED ---------------------- - -If ``True`` (default), removed comments will be excluded from comment -lists/counts (as taken from template tags). Otherwise, the template author is -responsible for some sort of a "this comment has been removed by the site staff" -message. - -.. setting:: COMMENT_MAX_LENGTH - -COMMENT_MAX_LENGTH ------------------- - -The maximum length of the comment field, in characters. Comments longer than -this will be rejected. Defaults to 3000. - -.. setting:: COMMENTS_APP - -COMMENTS_APP ------------- - -An app which provides :doc:`customization of the comments framework -`. Use the same dotted-string notation -as in :setting:`INSTALLED_APPS`. Your custom :setting:`COMMENTS_APP` -must also be listed in :setting:`INSTALLED_APPS`. diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index 42a41c4bfc..3ad16e2f97 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -488,60 +488,10 @@ developers of other reusable apps that want the same guarantees also use the Settings ======== -A number of settings can be used to control Django's CSRF behavior. +A number of settings can be used to control Django's CSRF behavior: -CSRF_COOKIE_DOMAIN ------------------- - -Default: ``None`` - -The domain to be used when setting the CSRF cookie. This can be useful for -easily allowing cross-subdomain requests to be excluded from the normal cross -site request forgery protection. It should be set to a string such as -``".example.com"`` to allow a POST request from a form on one subdomain to be -accepted by a view served from another subdomain. - -Please note that, with or without use of this setting, this CSRF protection -mechanism is not safe against cross-subdomain attacks -- see `Limitations`_. - -CSRF_COOKIE_NAME ----------------- - -Default: ``'csrftoken'`` - -The name of the cookie to use for the CSRF authentication token. This can be -whatever you want. - -CSRF_COOKIE_PATH ----------------- - -Default: ``'/'`` - -The path set on the CSRF cookie. This should either match the URL path of your -Django installation or be a parent of that path. - -This is useful if you have multiple Django instances running under the same -hostname. They can use different cookie paths, and each instance will only see -its own CSRF cookie. - -CSRF_COOKIE_SECURE ------------------- - -Default: ``False`` - -Whether to use a secure cookie for the CSRF cookie. If this is set to ``True``, -the cookie will be marked as "secure," which means browsers may ensure that the -cookie is only sent under an HTTPS connection. - -CSRF_FAILURE_VIEW ------------------ - -Default: ``'django.views.csrf.csrf_failure'`` - -A dotted path to the view function to be used when an incoming request -is rejected by the CSRF protection. The function should have this signature:: - - def csrf_failure(request, reason="") - -where ``reason`` is a short message (intended for developers or logging, not for -end users) indicating the reason the request was rejected. +* :setting:`CSRF_COOKIE_DOMAIN` +* :setting:`CSRF_COOKIE_NAME` +* :setting:`CSRF_COOKIE_PATH` +* :setting:`CSRF_COOKIE_SECURE` +* :setting:`CSRF_FAILURE_VIEW` diff --git a/docs/ref/contrib/messages.txt b/docs/ref/contrib/messages.txt index 661d7f2103..40f7d41ceb 100644 --- a/docs/ref/contrib/messages.txt +++ b/docs/ref/contrib/messages.txt @@ -78,8 +78,8 @@ Django provides three built-in storage classes: :class:`~django.contrib.messages.storage.fallback.FallbackStorage` is the default storage class. If it isn't suitable to your needs, you can select -another storage class by setting `MESSAGE_STORAGE`_ to its full import path, -for example:: +another storage class by setting setting:`MESSAGE_STORAGE` to its full import +path, for example:: MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' @@ -87,6 +87,8 @@ To write your own storage class, subclass the ``BaseStorage`` class in ``django.contrib.messages.storage.base`` and implement the ``_get`` and ``_store`` methods. +.. _message-level: + Message levels -------------- @@ -108,7 +110,7 @@ Constant Purpose ``ERROR`` An action was **not** successful or some other failure occurred =========== ======== -The `MESSAGE_LEVEL`_ setting can be used to change the minimum recorded level +The :setting:`MESSAGE_LEVEL` setting can be used to change the minimum recorded level (or it can be `changed per request`_). Attempts to add messages of a level less than this will be ignored. @@ -136,7 +138,7 @@ Level Constant Tag ============== =========== To change the default tags for a message level (either built-in or custom), -set the `MESSAGE_TAGS`_ setting to a dictionary containing the levels +set the :setting:`MESSAGE_TAGS` setting to a dictionary containing the levels you wish to change. As this extends the default tags, you only need to provide tags for the levels you wish to override:: @@ -168,6 +170,8 @@ used tags (which are usually represented as HTML classes for the message):: messages.warning(request, 'Your account expires in three days.') messages.error(request, 'Document deleted.') +.. _message-displaying: + Displaying messages ------------------- @@ -216,7 +220,7 @@ Level Constant Value ============== ===== If you need to identify the custom levels in your HTML or CSS, you need to -provide a mapping via the `MESSAGE_TAGS`_ setting. +provide a mapping via the :setting:`MESSAGE_TAGS` setting. .. note:: If you are creating a reusable application, it is recommended to use @@ -316,80 +320,10 @@ window/tab will have its own browsing context. Settings ======== -A few :doc:`Django settings ` give you control over message +A few :ref:`settings` give you control over message behavior: -MESSAGE_LEVEL -------------- - -Default: ``messages.INFO`` - -This sets the minimum message that will be saved in the message storage. See -`Message levels`_ above for more details. - -.. admonition:: Important - - If you override ``MESSAGE_LEVEL`` in your settings file and rely on any of - the built-in constants, you must import the constants module directly to - avoid the potential for circular imports, e.g.:: - - from django.contrib.messages import constants as message_constants - MESSAGE_LEVEL = message_constants.DEBUG - - If desired, you may specify the numeric values for the constants directly - according to the values in the above :ref:`constants table - `. - -MESSAGE_STORAGE ---------------- - -Default: ``'django.contrib.messages.storage.fallback.FallbackStorage'`` - -Controls where Django stores message data. Valid values are: - -* ``'django.contrib.messages.storage.fallback.FallbackStorage'`` -* ``'django.contrib.messages.storage.session.SessionStorage'`` -* ``'django.contrib.messages.storage.cookie.CookieStorage'`` - -See `Storage backends`_ for more details. - -MESSAGE_TAGS ------------- - -Default:: - - {messages.DEBUG: 'debug', - messages.INFO: 'info', - messages.SUCCESS: 'success', - messages.WARNING: 'warning', - messages.ERROR: 'error',} - -This sets the mapping of message level to message tag, which is typically -rendered as a CSS class in HTML. If you specify a value, it will extend -the default. This means you only have to specify those values which you need -to override. See `Displaying messages`_ above for more details. - -.. admonition:: Important - - If you override ``MESSAGE_TAGS`` in your settings file and rely on any of - the built-in constants, you must import the ``constants`` module directly to - avoid the potential for circular imports, e.g.:: - - from django.contrib.messages import constants as message_constants - MESSAGE_TAGS = {message_constants.INFO: ''} - - If desired, you may specify the numeric values for the constants directly - according to the values in the above :ref:`constants table - `. - -SESSION_COOKIE_DOMAIN ---------------------- - -Default: ``None`` - -The storage backends that use cookies -- ``CookieStorage`` and -``FallbackStorage`` -- use the value of :setting:`SESSION_COOKIE_DOMAIN` in -setting their cookies. See the :doc:`settings documentation ` -for more information on how this works and why you might need to set it. - -.. _Django settings: ../settings/ +* :setting:`MESSAGE_LEVEL` +* :setting:`MESSAGE_STORAGE` +* :setting:`MESSAGE_TAGS` +* :ref:`SESSION_COOKIE_DOMAIN` diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index a4a60f239b..a7540388bc 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -19,106 +19,14 @@ can easily be served in production. Settings ======== -.. highlight:: python +See :ref:`staticfiles settings ` for details on the +following settings: -.. note:: - - The following settings control the behavior of the staticfiles app. - -.. setting:: STATICFILES_DIRS - -STATICFILES_DIRS ----------------- - -Default: ``[]`` - -This setting defines the additional locations the staticfiles app will traverse -if the ``FileSystemFinder`` finder is enabled, e.g. if you use the -:djadmin:`collectstatic` or :djadmin:`findstatic` management command or use the -static file serving view. - -This should be set to a list or tuple of strings that contain full paths to -your additional files directory(ies) e.g.:: - - STATICFILES_DIRS = ( - "/home/special.polls.com/polls/static", - "/home/polls.com/polls/static", - "/opt/webfiles/common", - ) - -Prefixes (optional) -""""""""""""""""""" - -In case you want to refer to files in one of the locations with an additional -namespace, you can **optionally** provide a prefix as ``(prefix, path)`` -tuples, e.g.:: - - STATICFILES_DIRS = ( - # ... - ("downloads", "/opt/webfiles/stats"), - ) - -Example: - -Assuming you have :setting:`STATIC_URL` set ``'/static/'``, the -:djadmin:`collectstatic` management command would collect the "stats" files -in a ``'downloads'`` subdirectory of :setting:`STATIC_ROOT`. - -This would allow you to refer to the local file -``'/opt/webfiles/stats/polls_20101022.tar.gz'`` with -``'/static/downloads/polls_20101022.tar.gz'`` in your templates, e.g.: - -.. code-block:: html+django - - - -.. setting:: STATICFILES_STORAGE - -STATICFILES_STORAGE -------------------- - -Default: ``'django.contrib.staticfiles.storage.StaticFilesStorage'`` - -The file storage engine to use when collecting static files with the -:djadmin:`collectstatic` management command. - -A ready-to-use instance of the storage backend defined in this setting -can be found at ``django.contrib.staticfiles.storage.staticfiles_storage``. - -For an example, see :ref:`staticfiles-from-cdn`. - -.. setting:: STATICFILES_FINDERS - -STATICFILES_FINDERS -------------------- - -Default:: - - ("django.contrib.staticfiles.finders.FileSystemFinder", - "django.contrib.staticfiles.finders.AppDirectoriesFinder") - -The list of finder backends that know how to find static files in -various locations. - -The default will find files stored in the :setting:`STATICFILES_DIRS` setting -(using ``django.contrib.staticfiles.finders.FileSystemFinder``) and in a -``static`` subdirectory of each app (using -``django.contrib.staticfiles.finders.AppDirectoriesFinder``) - -One finder is disabled by default: -``django.contrib.staticfiles.finders.DefaultStorageFinder``. If added to -your :setting:`STATICFILES_FINDERS` setting, it will look for static files in -the default file storage as defined by the :setting:`DEFAULT_FILE_STORAGE` -setting. - -.. note:: - - When using the ``AppDirectoriesFinder`` finder, make sure your apps - can be found by staticfiles. Simply add the app to the - :setting:`INSTALLED_APPS` setting of your site. - -Static file finders are currently considered a private interface, and this -interface is thus undocumented. +* :setting:`STATIC_ROOT` +* :setting:`STATIC_URL` +* :setting:`STATICFILES_DIRS` +* :setting:`STATICFILES_STORAGE` +* :setting:`STATICFILES_FINDERS` Management Commands =================== diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index ffe8a0fe77..110d5dbdc9 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -13,11 +13,12 @@ Settings and :setting:`TEMPLATE_CONTEXT_PROCESSORS`. Make sure you keep the components required by the features of Django you wish to use. -Available settings -================== +Core settings +============= -Here's a full list of all available settings, in alphabetical order, and their -default values. +Here's a list of settings available in Django core and their default values. +Settings provided by contrib apps are listed below, followed by a topical index +of the core settings. .. setting:: ABSOLUTE_URL_OVERRIDES @@ -38,19 +39,6 @@ a model object and return its URL. This is a way of overriding Note that the model name used in this setting should be all lower-case, regardless of the case of the actual model class name. -.. setting:: ADMIN_FOR - -ADMIN_FOR ---------- - -Default: ``()`` (Empty tuple) - -Used for admin-site settings modules, this should be a tuple of settings -modules (in the format ``'foo.bar.baz'``) for which this site is an admin. - -The admin site uses this in its automatically-introspected documentation of -models, views and template tags. - .. setting:: ADMINS ADMINS @@ -99,26 +87,6 @@ The :setting:`APPEND_SLASH` setting is only used if :class:`~django.middleware.common.CommonMiddleware` is installed (see :doc:`/topics/http/middleware`). See also :setting:`PREPEND_WWW`. -.. setting:: AUTHENTICATION_BACKENDS - -AUTHENTICATION_BACKENDS ------------------------ - -Default: ``('django.contrib.auth.backends.ModelBackend',)`` - -A tuple of authentication backend classes (as strings) to use when attempting to -authenticate a user. See the :ref:`authentication backends documentation -` for details. - -.. setting:: AUTH_USER_MODEL - -AUTH_USER_MODEL ---------------- - -Default: 'auth.User' - -The model to use to represent a User. See :ref:`auth-custom-user`. - .. setting:: CACHES CACHES @@ -179,7 +147,8 @@ implementation is equivalent to the function:: You may use any key function you want, as long as it has the same argument signature. -See the :ref:`cache documentation ` for more information. +See the :ref:`cache documentation ` for more +information. .. setting:: CACHES-KEY_PREFIX @@ -293,6 +262,8 @@ The default number of seconds to cache a page when the caching middleware or See :doc:`/topics/cache`. +.. _settings-csrf: + .. setting:: CSRF_COOKIE_DOMAIN CSRF_COOKIE_DOMAIN @@ -304,7 +275,7 @@ The domain to be used when setting the CSRF cookie. This can be useful for easily allowing cross-subdomain requests to be excluded from the normal cross site request forgery protection. It should be set to a string such as ``".example.com"`` to allow a POST request from a form on one subdomain to be -accepted by accepted by a view served from another subdomain. +accepted by a view served from another subdomain. Please note that the presence of this setting does not imply that Django's CSRF protection is safe from cross-subdomain attacks by default - please see the @@ -361,7 +332,6 @@ where ``reason`` is a short message (intended for developers or logging, not for end users) indicating the reason the request was rejected. See :doc:`/ref/contrib/csrf`. - .. setting:: DATABASES DATABASES @@ -765,6 +735,8 @@ when you're debugging, but it'll rapidly consume memory on a production server. .. _django/views/debug.py: https://github.com/django/django/blob/master/django/views/debug.py +.. setting:: DEBUG_PROPAGATE_EXCEPTIONS + DEBUG_PROPAGATE_EXCEPTIONS -------------------------- @@ -1270,54 +1242,6 @@ configuration process will be skipped. .. _dictConfig: http://docs.python.org/library/logging.config.html#configuration-dictionary-schema -.. setting:: LOGIN_REDIRECT_URL - -LOGIN_REDIRECT_URL ------------------- - -Default: ``'/accounts/profile/'`` - -The URL where requests are redirected after login when the -``contrib.auth.login`` view gets no ``next`` parameter. - -This is used by the :func:`~django.contrib.auth.decorators.login_required` -decorator, for example. - -.. versionchanged:: 1.5 - -This setting now also accepts view function names and -:ref:`named URL patterns ` which can be used to reduce -configuration duplication since you no longer have to define the URL in two -places (``settings`` and URLconf). -For backward compatibility reasons the default remains unchanged. - -.. setting:: LOGIN_URL - -LOGIN_URL ---------- - -Default: ``'/accounts/login/'`` - -The URL where requests are redirected for login, especially when using the -:func:`~django.contrib.auth.decorators.login_required` decorator. - -.. versionchanged:: 1.5 - -This setting now also accepts view function names and -:ref:`named URL patterns ` which can be used to reduce -configuration duplication since you no longer have to define the URL in two -places (``settings`` and URLconf). -For backward compatibility reasons the default remains unchanged. - -.. setting:: LOGOUT_URL - -LOGOUT_URL ----------- - -Default: ``'/accounts/logout/'`` - -LOGIN_URL counterpart. - .. setting:: MANAGERS MANAGERS @@ -1355,37 +1279,6 @@ to a non-empty value. Example: ``"http://media.example.com/"`` -MESSAGE_LEVEL -------------- - -Default: `messages.INFO` - -Sets the minimum message level that will be recorded by the messages -framework. See the :doc:`messages documentation ` for -more details. - -MESSAGE_STORAGE ---------------- - -Default: ``'django.contrib.messages.storage.fallback.FallbackStorage'`` - -Controls where Django stores message data. See the -:doc:`messages documentation ` for more details. - -MESSAGE_TAGS ------------- - -Default:: - - {messages.DEBUG: 'debug', - messages.INFO: 'info', - messages.SUCCESS: 'success', - messages.WARNING: 'warning', - messages.ERROR: 'error',} - -Sets the mapping of message levels to message tags. See the -:doc:`messages documentation ` for more details. - .. setting:: MIDDLEWARE_CLASSES MIDDLEWARE_CLASSES @@ -1441,33 +1334,6 @@ format has higher precedence and will be applied instead. See also :setting:`DECIMAL_SEPARATOR`, :setting:`THOUSAND_SEPARATOR` and :setting:`USE_THOUSAND_SEPARATOR`. -.. setting:: PASSWORD_HASHERS - -PASSWORD_HASHERS ----------------- - -See :ref:`auth_password_storage`. - -Default:: - - ('django.contrib.auth.hashers.PBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', - 'django.contrib.auth.hashers.BCryptPasswordHasher', - 'django.contrib.auth.hashers.SHA1PasswordHasher', - 'django.contrib.auth.hashers.MD5PasswordHasher', - 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', - 'django.contrib.auth.hashers.CryptPasswordHasher',) - -.. setting:: PASSWORD_RESET_TIMEOUT_DAYS - -PASSWORD_RESET_TIMEOUT_DAYS ---------------------------- - -Default: ``3`` - -The number of days a password reset link is valid for. Used by the -:mod:`django.contrib.auth` password reset mechanism. - .. setting:: PREPEND_WWW PREPEND_WWW @@ -1479,16 +1345,6 @@ Whether to prepend the "www." subdomain to URLs that don't have it. This is only used if :class:`~django.middleware.common.CommonMiddleware` is installed (see :doc:`/topics/http/middleware`). See also :setting:`APPEND_SLASH`. -.. setting:: PROFANITIES_LIST - -PROFANITIES_LIST ----------------- - -Default: ``()`` (Empty tuple) - -A tuple of profanities, as strings, that will be forbidden in comments when -``COMMENTS_ALLOW_PROFANITIES`` is ``False``. - .. setting:: ROOT_URLCONF ROOT_URLCONF @@ -1623,141 +1479,6 @@ Default: ``'root@localhost'`` The email address that error messages come from, such as those sent to :setting:`ADMINS` and :setting:`MANAGERS`. -.. setting:: SESSION_COOKIE_AGE - -SESSION_COOKIE_AGE ------------------- - -Default: ``1209600`` (2 weeks, in seconds) - -The age of session cookies, in seconds. See :doc:`/topics/http/sessions`. - -.. setting:: SESSION_COOKIE_DOMAIN - -SESSION_COOKIE_DOMAIN ---------------------- - -Default: ``None`` - -The domain to use for session cookies. Set this to a string such as -``".example.com"`` for cross-domain cookies, or use ``None`` for a standard -domain cookie. See the :doc:`/topics/http/sessions`. - -.. setting:: SESSION_COOKIE_HTTPONLY - -SESSION_COOKIE_HTTPONLY ------------------------ - -Default: ``True`` - -Whether to use HTTPOnly flag on the session cookie. If this is set to -``True``, client-side JavaScript will not to be able to access the -session cookie. - -HTTPOnly_ is a flag included in a Set-Cookie HTTP response header. It -is not part of the :rfc:`2109` standard for cookies, and it isn't honored -consistently by all browsers. However, when it is honored, it can be a -useful way to mitigate the risk of client side script accessing the -protected cookie data. - -.. _HTTPOnly: https://www.owasp.org/index.php/HTTPOnly - -.. setting:: SESSION_COOKIE_NAME - -SESSION_COOKIE_NAME -------------------- - -Default: ``'sessionid'`` - -The name of the cookie to use for sessions. This can be whatever you want (but -should be different from :setting:`LANGUAGE_COOKIE_NAME`). -See the :doc:`/topics/http/sessions`. - -.. setting:: SESSION_COOKIE_PATH - -SESSION_COOKIE_PATH -------------------- - -Default: ``'/'`` - -The path set on the session cookie. This should either match the URL path of your -Django installation or be parent of that path. - -This is useful if you have multiple Django instances running under the same -hostname. They can use different cookie paths, and each instance will only see -its own session cookie. - -.. setting:: SESSION_CACHE_ALIAS - -SESSION_CACHE_ALIAS -------------------- - -Default: ``default`` - -If you're using :ref:`cache-based session storage `, -this selects the cache to use. - -.. setting:: SESSION_COOKIE_SECURE - -SESSION_COOKIE_SECURE ---------------------- - -Default: ``False`` - -Whether to use a secure cookie for the session cookie. If this is set to -``True``, the cookie will be marked as "secure," which means browsers may -ensure that the cookie is only sent under an HTTPS connection. -See the :doc:`/topics/http/sessions`. - -.. setting:: SESSION_ENGINE - -SESSION_ENGINE --------------- - -Default: ``django.contrib.sessions.backends.db`` - -Controls where Django stores session data. Valid values are: - -* ``'django.contrib.sessions.backends.db'`` -* ``'django.contrib.sessions.backends.file'`` -* ``'django.contrib.sessions.backends.cache'`` -* ``'django.contrib.sessions.backends.cached_db'`` -* ``'django.contrib.sessions.backends.signed_cookies'`` - -See :doc:`/topics/http/sessions`. - -.. setting:: SESSION_EXPIRE_AT_BROWSER_CLOSE - -SESSION_EXPIRE_AT_BROWSER_CLOSE -------------------------------- - -Default: ``False`` - -Whether to expire the session when the user closes his or her browser. -See the :doc:`/topics/http/sessions`. - -.. setting:: SESSION_FILE_PATH - -SESSION_FILE_PATH ------------------ - -Default: ``None`` - -If you're using file-based session storage, this sets the directory in -which Django will store session data. See :doc:`/topics/http/sessions`. When -the default value (``None``) is used, Django will use the standard temporary -directory for the system. - -.. setting:: SESSION_SAVE_EVERY_REQUEST - -SESSION_SAVE_EVERY_REQUEST --------------------------- - -Default: ``False`` - -Whether to save the session data on every request. See -:doc:`/topics/http/sessions`. - .. setting:: SHORT_DATE_FORMAT SHORT_DATE_FORMAT @@ -1797,71 +1518,6 @@ The backend used for signing cookies and other data. See also the :doc:`/topics/signing` documentation. -.. setting:: SITE_ID - -SITE_ID -------- - -Default: Not defined - -The ID, as an integer, of the current site in the ``django_site`` database -table. This is used so that application data can hook into specific site(s) -and a single database can manage content for multiple sites. - -See :doc:`/ref/contrib/sites`. - -.. _site framework docs: ../sites/ - -.. setting:: STATIC_ROOT - -STATIC_ROOT ------------ - -Default: ``''`` (Empty string) - -The absolute path to the directory where :djadmin:`collectstatic` will collect -static files for deployment. - -Example: ``"/var/www/example.com/static/"`` - -If the :doc:`staticfiles` contrib app is enabled -(default) the :djadmin:`collectstatic` management command will collect static -files into this directory. See the howto on :doc:`managing static -files` for more details about usage. - -.. warning:: - - This should be an (initially empty) destination directory for collecting - your static files from their permanent locations into one directory for - ease of deployment; it is **not** a place to store your static files - permanently. You should do that in directories that will be found by - :doc:`staticfiles`'s - :setting:`finders`, which by default, are - ``'static/'`` app sub-directories and any directories you include in - :setting:`STATICFILES_DIRS`). - -See :doc:`staticfiles reference` and -:setting:`STATIC_URL`. - -.. setting:: STATIC_URL - -STATIC_URL ----------- - -Default: ``None`` - -URL to use when referring to static files located in :setting:`STATIC_ROOT`. - -Example: ``"/static/"`` or ``"http://static.example.com/"`` - -If not ``None``, this will be used as the base path for -:ref:`media definitions` and the -:doc:`staticfiles app`. - -It must end in a slash if set to a non-empty value. - -See :setting:`STATIC_ROOT`. - .. setting:: TEMPLATE_CONTEXT_PROCESSORS TEMPLATE_CONTEXT_PROCESSORS @@ -2194,8 +1850,41 @@ The default value for the X-Frame-Options header used by :class:`~django.middleware.clickjacking.XFrameOptionsMiddleware`. See the :doc:`clickjacking protection ` documentation. -Deprecated settings -=================== + +Admindocs +========= + +Settings for :mod:`django.contrib.admindocs`. + +.. setting:: ADMIN_FOR + +ADMIN_FOR +--------- + +Default: ``()`` (Empty tuple) + +Used for admin-site settings modules, this should be a tuple of settings +modules (in the format ``'foo.bar.baz'``) for which this site is an admin. + +The admin site uses this in its automatically-introspected documentation of +models, views and template tags. + + +Auth +==== + +Settings for :mod:`django.contrib.auth`. + +.. setting:: AUTHENTICATION_BACKENDS + +AUTHENTICATION_BACKENDS +----------------------- + +Default: ``('django.contrib.auth.backends.ModelBackend',)`` + +A tuple of authentication backend classes (as strings) to use when attempting to +authenticate a user. See the :ref:`authentication backends documentation +` for details. .. setting:: AUTH_PROFILE_MODULE @@ -2212,3 +1901,690 @@ Default: Not defined The site-specific user profile model used by this site. See :ref:`User profiles `. + +.. setting:: AUTH_USER_MODEL + +AUTH_USER_MODEL +--------------- + +Default: 'auth.User' + +The model to use to represent a User. See :ref:`auth-custom-user`. + +.. setting:: LOGIN_REDIRECT_URL + +LOGIN_REDIRECT_URL +------------------ + +Default: ``'/accounts/profile/'`` + +The URL where requests are redirected after login when the +``contrib.auth.login`` view gets no ``next`` parameter. + +This is used by the :func:`~django.contrib.auth.decorators.login_required` +decorator, for example. + +.. versionchanged:: 1.5 + +This setting now also accepts view function names and +:ref:`named URL patterns ` which can be used to reduce +configuration duplication since you no longer have to define the URL in two +places (``settings`` and URLconf). +For backward compatibility reasons the default remains unchanged. + +.. setting:: LOGIN_URL + +LOGIN_URL +--------- + +Default: ``'/accounts/login/'`` + +The URL where requests are redirected for login, especially when using the +:func:`~django.contrib.auth.decorators.login_required` decorator. + +.. versionchanged:: 1.5 + +This setting now also accepts view function names and +:ref:`named URL patterns ` which can be used to reduce +configuration duplication since you no longer have to define the URL in two +places (``settings`` and URLconf). +For backward compatibility reasons the default remains unchanged. + +.. setting:: LOGOUT_URL + +LOGOUT_URL +---------- + +Default: ``'/accounts/logout/'`` + +LOGIN_URL counterpart. + +.. setting:: PASSWORD_RESET_TIMEOUT_DAYS + +PASSWORD_RESET_TIMEOUT_DAYS +--------------------------- + +Default: ``3`` + +The number of days a password reset link is valid for. Used by the +:mod:`django.contrib.auth` password reset mechanism. + +.. setting:: PASSWORD_HASHERS + +PASSWORD_HASHERS +---------------- + +See :ref:`auth_password_storage`. + +Default:: + + ('django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.BCryptPasswordHasher', + 'django.contrib.auth.hashers.SHA1PasswordHasher', + 'django.contrib.auth.hashers.MD5PasswordHasher', + 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', + 'django.contrib.auth.hashers.CryptPasswordHasher',) + + +.. _settings-comments: + +Comments +======== + +Settings for :mod:`django.contrib.comments`. + +.. setting:: COMMENTS_HIDE_REMOVED + +COMMENTS_HIDE_REMOVED +--------------------- + +If ``True`` (default), removed comments will be excluded from comment +lists/counts (as taken from template tags). Otherwise, the template author is +responsible for some sort of a "this comment has been removed by the site staff" +message. + +.. setting:: COMMENT_MAX_LENGTH + +COMMENT_MAX_LENGTH +------------------ + +The maximum length of the comment field, in characters. Comments longer than +this will be rejected. Defaults to 3000. + +.. setting:: COMMENTS_APP + +COMMENTS_APP +------------ + +An app which provides :doc:`customization of the comments framework +`. Use the same dotted-string notation +as in :setting:`INSTALLED_APPS`. Your custom :setting:`COMMENTS_APP` +must also be listed in :setting:`INSTALLED_APPS`. + +.. setting:: PROFANITIES_LIST + +PROFANITIES_LIST +---------------- + +Default: ``()`` (Empty tuple) + +A tuple of profanities, as strings, that will be forbidden in comments when +``COMMENTS_ALLOW_PROFANITIES`` is ``False``. + + +.. _settings-messages: + +Messages +======== + +Settings for :mod:`django.contrib.messages`. + +.. setting:: MESSAGE_LEVEL + +MESSAGE_LEVEL +------------- + +Default: `messages.INFO` + +Sets the minimum message level that will be recorded by the messages +framework. See :ref:`message levels ` for more details. + +.. admonition:: Important + + If you override ``MESSAGE_LEVEL`` in your settings file and rely on any of + the built-in constants, you must import the constants module directly to + avoid the potential for circular imports, e.g.:: + + from django.contrib.messages import constants as message_constants + MESSAGE_LEVEL = message_constants.DEBUG + + If desired, you may specify the numeric values for the constants directly + according to the values in the above :ref:`constants table + `. + +.. setting:: MESSAGE_STORAGE + +MESSAGE_STORAGE +--------------- + +Default: ``'django.contrib.messages.storage.fallback.FallbackStorage'`` + +Controls where Django stores message data. Valid values are: + +* ``'django.contrib.messages.storage.fallback.FallbackStorage'`` +* ``'django.contrib.messages.storage.session.SessionStorage'`` +* ``'django.contrib.messages.storage.cookie.CookieStorage'`` + +See :ref:`message storage backends ` for more details. + +.. setting:: MESSAGE_TAGS + +MESSAGE_TAGS +------------ + +Default:: + + {messages.DEBUG: 'debug', + messages.INFO: 'info', + messages.SUCCESS: 'success', + messages.WARNING: 'warning', + messages.ERROR: 'error',} + +This sets the mapping of message level to message tag, which is typically +rendered as a CSS class in HTML. If you specify a value, it will extend +the default. This means you only have to specify those values which you need +to override. See :ref:`message-displaying` above for more details. + +.. admonition:: Important + + If you override ``MESSAGE_TAGS`` in your settings file and rely on any of + the built-in constants, you must import the ``constants`` module directly to + avoid the potential for circular imports, e.g.:: + + from django.contrib.messages import constants as message_constants + MESSAGE_TAGS = {message_constants.INFO: ''} + + If desired, you may specify the numeric values for the constants directly + according to the values in the above :ref:`constants table + `. + +.. _messages-session_cookie_domain: + +SESSION_COOKIE_DOMAIN +--------------------- + +Default: ``None`` + +The storage backends that use cookies -- ``CookieStorage`` and +``FallbackStorage`` -- use the value of :setting:`SESSION_COOKIE_DOMAIN` in +setting their cookies. + + +.. _settings-sessions: + +Sessions +======== + +Settings for :mod:`django.contrib.sessions`. + +.. setting:: SESSION_CACHE_ALIAS + +SESSION_CACHE_ALIAS +------------------- + +Default: ``default`` + +If you're using :ref:`cache-based session storage `, +this selects the cache to use. + +.. setting:: SESSION_COOKIE_AGE + +SESSION_COOKIE_AGE +------------------ + +Default: ``1209600`` (2 weeks, in seconds) + +The age of session cookies, in seconds. + +.. setting:: SESSION_COOKIE_DOMAIN + +SESSION_COOKIE_DOMAIN +--------------------- + +Default: ``None`` + +The domain to use for session cookies. Set this to a string such as +``".example.com"`` (note the leading dot!) for cross-domain cookies, or use +``None`` for a standard domain cookie. + +.. setting:: SESSION_COOKIE_HTTPONLY + +SESSION_COOKIE_HTTPONLY +----------------------- + +Default: ``True`` + +Whether to use HTTPOnly flag on the session cookie. If this is set to +``True``, client-side JavaScript will not to be able to access the +session cookie. + +HTTPOnly_ is a flag included in a Set-Cookie HTTP response header. It +is not part of the :rfc:`2109` standard for cookies, and it isn't honored +consistently by all browsers. However, when it is honored, it can be a +useful way to mitigate the risk of client side script accessing the +protected cookie data. + +.. _HTTPOnly: https://www.owasp.org/index.php/HTTPOnly + +.. setting:: SESSION_COOKIE_NAME + +SESSION_COOKIE_NAME +------------------- + +Default: ``'sessionid'`` + +The name of the cookie to use for sessions. This can be whatever you want (but +should be different from :setting:`LANGUAGE_COOKIE_NAME`). + +.. setting:: SESSION_COOKIE_PATH + +SESSION_COOKIE_PATH +------------------- + +Default: ``'/'`` + +The path set on the session cookie. This should either match the URL path of your +Django installation or be parent of that path. + +This is useful if you have multiple Django instances running under the same +hostname. They can use different cookie paths, and each instance will only see +its own session cookie. + +.. setting:: SESSION_COOKIE_SECURE + +SESSION_COOKIE_SECURE +--------------------- + +Default: ``False`` + +Whether to use a secure cookie for the session cookie. If this is set to +``True``, the cookie will be marked as "secure," which means browsers may +ensure that the cookie is only sent under an HTTPS connection. + +.. setting:: SESSION_ENGINE + +SESSION_ENGINE +-------------- + +Default: ``django.contrib.sessions.backends.db`` + +Controls where Django stores session data. Valid values are: + +* ``'django.contrib.sessions.backends.db'`` +* ``'django.contrib.sessions.backends.file'`` +* ``'django.contrib.sessions.backends.cache'`` +* ``'django.contrib.sessions.backends.cached_db'`` +* ``'django.contrib.sessions.backends.signed_cookies'`` + +See :ref:`configuring-sessions` for more details. + +.. setting:: SESSION_EXPIRE_AT_BROWSER_CLOSE + +SESSION_EXPIRE_AT_BROWSER_CLOSE +------------------------------- + +Default: ``False`` + +Whether to expire the session when the user closes his or her browser. See +"Browser-length sessions vs. persistent sessions" above. + +.. setting:: SESSION_FILE_PATH + +SESSION_FILE_PATH +----------------- + +Default: ``None`` + +If you're using file-based session storage, this sets the directory in +which Django will store session data. When the default value (``None``) is +used, Django will use the standard temporary directory for the system. + + +.. setting:: SESSION_SAVE_EVERY_REQUEST + +SESSION_SAVE_EVERY_REQUEST +-------------------------- + +Default: ``False`` + +Whether to save the session data on every request. If this is ``False`` +(default), then the session data will only be saved if it has been modified -- +that is, if any of its dictionary values have been assigned or deleted. + + +Sites +===== + +Settings for :mod:`django.contrib.sites`. + +.. setting:: SITE_ID + +SITE_ID +------- + +Default: Not defined + +The ID, as an integer, of the current site in the ``django_site`` database +table. This is used so that application data can hook into specific sites +and a single database can manage content for multiple sites. + + +.. _settings-staticfiles: + +Static files +============ + +Settings for :mod:`django.contrib.staticfiles`. + +.. setting:: STATIC_ROOT + +STATIC_ROOT +----------- + +Default: ``''`` (Empty string) + +The absolute path to the directory where :djadmin:`collectstatic` will collect +static files for deployment. + +Example: ``"/var/www/example.com/static/"`` + +If the :doc:`staticfiles` contrib app is enabled +(default) the :djadmin:`collectstatic` management command will collect static +files into this directory. See the howto on :doc:`managing static +files` for more details about usage. + +.. warning:: + + This should be an (initially empty) destination directory for collecting + your static files from their permanent locations into one directory for + ease of deployment; it is **not** a place to store your static files + permanently. You should do that in directories that will be found by + :doc:`staticfiles`'s + :setting:`finders`, which by default, are + ``'static/'`` app sub-directories and any directories you include in + :setting:`STATICFILES_DIRS`). + +.. setting:: STATIC_URL + +STATIC_URL +---------- + +Default: ``None`` + +URL to use when referring to static files located in :setting:`STATIC_ROOT`. + +Example: ``"/static/"`` or ``"http://static.example.com/"`` + +If not ``None``, this will be used as the base path for +:ref:`media definitions` and the +:doc:`staticfiles app`. + +It must end in a slash if set to a non-empty value. + +.. setting:: STATICFILES_DIRS + +STATICFILES_DIRS +---------------- + +Default: ``[]`` + +This setting defines the additional locations the staticfiles app will traverse +if the ``FileSystemFinder`` finder is enabled, e.g. if you use the +:djadmin:`collectstatic` or :djadmin:`findstatic` management command or use the +static file serving view. + +This should be set to a list or tuple of strings that contain full paths to +your additional files directory(ies) e.g.:: + + STATICFILES_DIRS = ( + "/home/special.polls.com/polls/static", + "/home/polls.com/polls/static", + "/opt/webfiles/common", + ) + +Prefixes (optional) +~~~~~~~~~~~~~~~~~~~ + +In case you want to refer to files in one of the locations with an additional +namespace, you can **optionally** provide a prefix as ``(prefix, path)`` +tuples, e.g.:: + + STATICFILES_DIRS = ( + # ... + ("downloads", "/opt/webfiles/stats"), + ) + +Example: + +Assuming you have :setting:`STATIC_URL` set ``'/static/'``, the +:djadmin:`collectstatic` management command would collect the "stats" files +in a ``'downloads'`` subdirectory of :setting:`STATIC_ROOT`. + +This would allow you to refer to the local file +``'/opt/webfiles/stats/polls_20101022.tar.gz'`` with +``'/static/downloads/polls_20101022.tar.gz'`` in your templates, e.g.: + +.. code-block:: html+django + + + +.. setting:: STATICFILES_STORAGE + +STATICFILES_STORAGE +------------------- + +Default: ``'django.contrib.staticfiles.storage.StaticFilesStorage'`` + +The file storage engine to use when collecting static files with the +:djadmin:`collectstatic` management command. + +A ready-to-use instance of the storage backend defined in this setting +can be found at ``django.contrib.staticfiles.storage.staticfiles_storage``. + +For an example, see :ref:`staticfiles-from-cdn`. + +.. setting:: STATICFILES_FINDERS + +STATICFILES_FINDERS +------------------- + +Default:: + + ("django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder") + +The list of finder backends that know how to find static files in +various locations. + +The default will find files stored in the :setting:`STATICFILES_DIRS` setting +(using ``django.contrib.staticfiles.finders.FileSystemFinder``) and in a +``static`` subdirectory of each app (using +``django.contrib.staticfiles.finders.AppDirectoriesFinder``) + +One finder is disabled by default: +``django.contrib.staticfiles.finders.DefaultStorageFinder``. If added to +your :setting:`STATICFILES_FINDERS` setting, it will look for static files in +the default file storage as defined by the :setting:`DEFAULT_FILE_STORAGE` +setting. + +.. note:: + + When using the ``AppDirectoriesFinder`` finder, make sure your apps + can be found by staticfiles. Simply add the app to the + :setting:`INSTALLED_APPS` setting of your site. + +Static file finders are currently considered a private interface, and this +interface is thus undocumented. + +Core Settings Topical Index +=========================== + +Cache +----- +* :setting:`CACHES` +* :setting:`CACHE_MIDDLEWARE_ALIAS` +* :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY` +* :setting:`CACHE_MIDDLEWARE_KEY_PREFIX` +* :setting:`CACHE_MIDDLEWARE_SECONDS` + +Database +-------- +* :setting:`DATABASES` +* :setting:`DATABASE_ROUTERS` +* :setting:`DEFAULT_INDEX_TABLESPACE` +* :setting:`DEFAULT_TABLESPACE` +* :setting:`TRANSACTIONS_MANAGED` + +Debugging +--------- +* :setting:`DEBUG` +* :setting:`DEBUG_PROPAGATE_EXCEPTIONS` + +Email +----- +* :setting:`ADMINS` +* :setting:`DEFAULT_CHARSET` +* :setting:`DEFAULT_FROM_EMAIL` +* :setting:`EMAIL_BACKEND` +* :setting:`EMAIL_FILE_PATH` +* :setting:`EMAIL_HOST` +* :setting:`EMAIL_HOST_PASSWORD` +* :setting:`EMAIL_HOST_USER` +* :setting:`EMAIL_PORT` +* :setting:`EMAIL_SUBJECT_PREFIX` +* :setting:`EMAIL_USE_TLS` +* :setting:`MANAGERS` +* :setting:`SEND_BROKEN_LINK_EMAILS` +* :setting:`SERVER_EMAIL` + +Error reporting +--------------- +* :setting:`DEFAULT_EXCEPTION_REPORTER_FILTER` +* :setting:`IGNORABLE_404_URLS` +* :setting:`MANAGERS` +* :setting:`SEND_BROKEN_LINK_EMAILS` + +File uploads +------------ +* :setting:`DEFAULT_FILE_STORAGE` +* :setting:`FILE_CHARSET` +* :setting:`FILE_UPLOAD_HANDLERS` +* :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE` +* :setting:`FILE_UPLOAD_PERMISSIONS` +* :setting:`FILE_UPLOAD_TEMP_DIR` +* :setting:`MEDIA_ROOT` +* :setting:`MEDIA_URL` + +Globalization (i18n/l10n) +------------------------- +* :setting:`DATE_FORMAT` +* :setting:`DATE_INPUT_FORMATS` +* :setting:`DATETIME_FORMAT` +* :setting:`DATETIME_INPUT_FORMATS` +* :setting:`DECIMAL_SEPARATOR` +* :setting:`FIRST_DAY_OF_WEEK` +* :setting:`FORMAT_MODULE_PATH` +* :setting:`LANGUAGE_CODE` +* :setting:`LANGUAGE_COOKIE_NAME` +* :setting:`LANGUAGES` +* :setting:`LOCALE_PATHS` +* :setting:`MONTH_DAY_FORMAT` +* :setting:`NUMBER_GROUPING` +* :setting:`SHORT_DATE_FORMAT` +* :setting:`SHORT_DATETIME_FORMAT` +* :setting:`THOUSAND_SEPARATOR` +* :setting:`TIME_FORMAT` +* :setting:`TIME_INPUT_FORMATS` +* :setting:`TIME_ZONE` +* :setting:`USE_I18N` +* :setting:`USE_L10N` +* :setting:`USE_THOUSAND_SEPARATOR` +* :setting:`USE_TZ` +* :setting:`YEAR_MONTH_FORMAT` + +HTTP +---- +* :setting:`DEFAULT_CHARSET` +* :setting:`DEFAULT_CONTENT_TYPE` +* :setting:`DISALLOWED_USER_AGENTS` +* :setting:`FORCE_SCRIPT_NAME` +* :setting:`INTERNAL_IPS` +* :setting:`MIDDLEWARE_CLASSES` +* :setting:`SECURE_PROXY_SSL_HEADER` +* :setting:`SIGNING_BACKEND` +* :setting:`USE_ETAGS` +* :setting:`USE_X_FORWARDED_HOST` +* :setting:`WSGI_APPLICATION` + +Logging +------- +* :setting:`LOGGING` +* :setting:`LOGGING_CONFIG` + +Models +------ +* :setting:`ABSOLUTE_URL_OVERRIDES` +* :setting:`FIXTURE_DIRS` +* :setting:`INSTALLED_APPS` + +Security +-------- +* Cross Site Request Forgery protection + + * :setting:`CSRF_COOKIE_DOMAIN` + * :setting:`CSRF_COOKIE_NAME` + * :setting:`CSRF_COOKIE_PATH` + * :setting:`CSRF_COOKIE_SECURE` + * :setting:`CSRF_FAILURE_VIEW` + +* :setting:`SECRET_KEY` +* :setting:`X_FRAME_OPTIONS` + +Serialization +------------- +* :setting:`DEFAULT_CHARSET` +* :setting:`SERIALIZATION_MODULES` + +Templates +--------- +* :setting:`ALLOWED_INCLUDE_ROOTS` +* :setting:`TEMPLATE_CONTEXT_PROCESSORS` +* :setting:`TEMPLATE_DEBUG` +* :setting:`TEMPLATE_DIRS` +* :setting:`TEMPLATE_LOADERS` +* :setting:`TEMPLATE_STRING_IF_INVALID` + +Testing +------- +* Database + + * :setting:`TEST_CHARSET` + * :setting:`TEST_COLLATION` + * :setting:`TEST_DEPENDENCIES` + * :setting:`TEST_MIRROR` + * :setting:`TEST_NAME` + * :setting:`TEST_CREATE` + * :setting:`TEST_USER` + * :setting:`TEST_USER_CREATE` + * :setting:`TEST_PASSWD` + * :setting:`TEST_TBLSPACE` + * :setting:`TEST_TBLSPACE_TMP` + +* :setting:`TEST_RUNNER` + +URLs +---- +* :setting:`APPEND_SLASH` +* :setting:`PREPEND_WWW` +* :setting:`ROOT_URLCONF` diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index 1832a55267..41ae0cafa9 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -28,6 +28,8 @@ If you don't want to use sessions, you might as well remove the ``'django.contrib.sessions'`` from your :setting:`INSTALLED_APPS`. It'll save you a small bit of overhead. +.. _configuring-sessions: + Configuring the session engine ============================== @@ -499,111 +501,20 @@ session data is stored by the users' browsers. Settings ======== -A few :doc:`Django settings ` give you control over session +A few :ref:`Django settings ` give you control over session behavior: -SESSION_ENGINE --------------- - -Default: ``django.contrib.sessions.backends.db`` - -Controls where Django stores session data. Valid values are: - -* ``'django.contrib.sessions.backends.db'`` -* ``'django.contrib.sessions.backends.file'`` -* ``'django.contrib.sessions.backends.cache'`` -* ``'django.contrib.sessions.backends.cached_db'`` -* ``'django.contrib.sessions.backends.signed_cookies'`` - -See `configuring the session engine`_ for more details. - -SESSION_FILE_PATH ------------------ - -Default: ``/tmp/`` - -If you're using file-based session storage, this sets the directory in -which Django will store session data. - -SESSION_COOKIE_AGE ------------------- - -Default: ``1209600`` (2 weeks, in seconds) - -The age of session cookies, in seconds. - -SESSION_COOKIE_DOMAIN ---------------------- - -Default: ``None`` - -The domain to use for session cookies. Set this to a string such as -``".example.com"`` (note the leading dot!) for cross-domain cookies, or use -``None`` for a standard domain cookie. - -SESSION_COOKIE_HTTPONLY ------------------------ - -Default: ``True`` - -Whether to use HTTPOnly flag on the session cookie. If this is set to -``True``, client-side JavaScript will not to be able to access the -session cookie. - -HTTPOnly_ is a flag included in a Set-Cookie HTTP response header. It -is not part of the :rfc:`2109` standard for cookies, and it isn't honored -consistently by all browsers. However, when it is honored, it can be a -useful way to mitigate the risk of client side script accessing the -protected cookie data. - -.. _HTTPOnly: https://www.owasp.org/index.php/HTTPOnly - -SESSION_COOKIE_NAME -------------------- - -Default: ``'sessionid'`` - -The name of the cookie to use for sessions. This can be whatever you want. - -SESSION_COOKIE_PATH -------------------- - -Default: ``'/'`` - -The path set on the session cookie. This should either match the URL path of -your Django installation or be parent of that path. - -This is useful if you have multiple Django instances running under the same -hostname. They can use different cookie paths, and each instance will only see -its own session cookie. - -SESSION_COOKIE_SECURE ---------------------- - -Default: ``False`` - -Whether to use a secure cookie for the session cookie. If this is set to -``True``, the cookie will be marked as "secure," which means browsers may -ensure that the cookie is only sent under an HTTPS connection. - -SESSION_EXPIRE_AT_BROWSER_CLOSE -------------------------------- - -Default: ``False`` - -Whether to expire the session when the user closes his or her browser. See -"Browser-length sessions vs. persistent sessions" above. - -SESSION_SAVE_EVERY_REQUEST --------------------------- - -Default: ``False`` - -Whether to save the session data on every request. If this is ``False`` -(default), then the session data will only be saved if it has been modified -- -that is, if any of its dictionary values have been assigned or deleted. - -.. _Django settings: ../settings/ +* :setting:`SESSION_CACHE_ALIAS` +* :setting:`SESSION_COOKIE_AGE` +* :setting:`SESSION_COOKIE_DOMAIN` +* :setting:`SESSION_COOKIE_HTTPONLY` +* :setting:`SESSION_COOKIE_NAME` +* :setting:`SESSION_COOKIE_PATH` +* :setting:`SESSION_COOKIE_SECURE` +* :setting:`SESSION_ENGINE` +* :setting:`SESSION_EXPIRE_AT_BROWSER_CLOSE` +* :setting:`SESSION_FILE_PATH` +* :setting:`SESSION_SAVE_EVERY_REQUEST` Technical details ================= From 0ca2d1e20a82d5ff0f86ac14988c5e348f9192c3 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 13 Jan 2013 19:35:59 +0100 Subject: [PATCH 148/870] Fixed typo in file storage docs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Jørgen Abrahamsen. --- docs/ref/files/storage.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/files/storage.txt b/docs/ref/files/storage.txt index ff175d122b..b9742514ea 100644 --- a/docs/ref/files/storage.txt +++ b/docs/ref/files/storage.txt @@ -66,7 +66,7 @@ The Storage Class .. method:: delete(name) Deletes the file referenced by ``name``. If deletion is not supported - on the targest storage system this will raise ``NotImplementedError`` + on the target storage system this will raise ``NotImplementedError`` instead .. method:: exists(name) From 4720117a31344483119856fb5ed803fe4c35936f Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sun, 13 Jan 2013 15:11:24 -0500 Subject: [PATCH 149/870] Added details on minified jQuery and DEBUG mode for contrib.admin. Thanks Daniele Procida. --- docs/ref/contrib/admin/index.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 04a7824417..065c9566ea 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1380,6 +1380,9 @@ The :doc:`staticfiles app ` prepends ``None``) to any media paths. The same rules apply as :ref:`regular media definitions on forms `. +jQuery +~~~~~~ + Django admin Javascript makes use of the `jQuery`_ library. To avoid conflicts with user-supplied scripts or libraries, Django's jQuery is namespaced as ``django.jQuery``. If you want to use jQuery in your own admin @@ -1390,6 +1393,15 @@ If you require the jQuery library to be in the global namespace, for example when using third-party jQuery plugins, or need a newer version of jQuery, you will have to include your own copy of jQuery. +Django provides both uncompressed and 'minified' versions of jQuery, as +``jquery.js`` and ``jquery.min.js`` respectively. + +:class:`ModelAdmin` and :class:`InlineModelAdmin` have a ``media`` property +that returns a list of ``Media`` objects which store paths to the JavaScript +files for the forms and/or formsets. If :setting:`DEBUG` is ``True`` it will +return the uncompressed versions of the various JavaScript files, including +``jquery.js``; if not, it will return the 'minified' versions. + .. _jQuery: http://jquery.com Adding custom validation to the admin From 272de9eb6baad45abec029aae92c2b7d9478c841 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 11 Jan 2013 14:12:22 -0800 Subject: [PATCH 150/870] Send post_delete signals immediately In a normal relational construct, if you're listening for an event that signals a child was deleted, you dont expect that the parent was deleted already. This change ensures that post_delete signals are fired immediately after objects are deleted in the graph. --- django/db/models/deletion.py | 22 ++++++++++++---------- tests/modeltests/delete/tests.py | 12 ++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 1c3a582fc5..e4cd89233c 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -75,17 +75,17 @@ class Collector(object): self.using = using # Initially, {model: set([instances])}, later values become lists. self.data = {} - self.field_updates = {} # {model: {(field, value): set([instances])}} + self.field_updates = {} # {model: {(field, value): set([instances])}} # fast_deletes is a list of queryset-likes that can be deleted without # fetching the objects into memory. - self.fast_deletes = [] + self.fast_deletes = [] # Tracks deletion-order dependency for databases without transactions # or ability to defer constraint checks. Only concrete model classes # should be included, as the dependencies exist only between actual # database tables; proxy models are represented here by their concrete # parent. - self.dependencies = {} # {model: set([models])} + self.dependencies = {} # {model: set([models])} def add(self, objs, source=None, nullable=False, reverse_dependency=False): """ @@ -262,6 +262,14 @@ class Collector(object): self.data = SortedDict([(model, self.data[model]) for model in sorted_models]) + def send_post_delete_signals(self, model, instances): + if model._meta.auto_created: + return + for obj in instances: + signals.post_delete.send( + sender=model, instance=obj, using=self.using + ) + @force_managed def delete(self): # sort instance collections @@ -300,13 +308,7 @@ class Collector(object): query = sql.DeleteQuery(model) pk_list = [obj.pk for obj in instances] query.delete_batch(pk_list, self.using) - - # send post_delete signals - for model, obj in self.instances_with_model(): - if not model._meta.auto_created: - signals.post_delete.send( - sender=model, instance=obj, using=self.using - ) + self.send_post_delete_signals(model, instances) # update collected instances for model, instances_for_fieldvalues in six.iteritems(self.field_updates): diff --git a/tests/modeltests/delete/tests.py b/tests/modeltests/delete/tests.py index 20b815c33d..5d7b7a0b33 100644 --- a/tests/modeltests/delete/tests.py +++ b/tests/modeltests/delete/tests.py @@ -229,6 +229,18 @@ class DeletionTests(TestCase): models.signals.post_delete.disconnect(log_post_delete) models.signals.post_delete.disconnect(log_pre_delete) + def test_relational_post_delete_signals_happen_before_parent_object(self): + def log_post_delete(instance, **kwargs): + self.assertTrue(R.objects.filter(pk=instance.r_id)) + + models.signals.post_delete.connect(log_post_delete, sender=S) + + r = R.objects.create(pk=1) + S.objects.create(pk=1, r=r) + r.delete() + + models.signals.post_delete.disconnect(log_post_delete) + @skipUnlessDBFeature("can_defer_constraint_checks") def test_can_defer_constraint_checks(self): u = User.objects.create( From d53e3b15eee6cb89e3a4651b0f89b01fd30360d7 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 11 Jan 2013 14:43:53 -0800 Subject: [PATCH 151/870] Add David Cramer to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 4b6636314d..3659d8e0df 100644 --- a/AUTHORS +++ b/AUTHORS @@ -142,6 +142,7 @@ answer newbie questions, and generally made Django that much better: crankycoder@gmail.com Paul Collier Robert Coup + David Cramer Pete Crosier Matt Croydon Jure Cuhalev From 6045efa029dcae58ec6aab20bdcb0dc325851048 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 11 Jan 2013 14:58:36 -0800 Subject: [PATCH 152/870] Move signal disconnect into finally block --- tests/modeltests/delete/tests.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/modeltests/delete/tests.py b/tests/modeltests/delete/tests.py index 5d7b7a0b33..99981df018 100644 --- a/tests/modeltests/delete/tests.py +++ b/tests/modeltests/delete/tests.py @@ -233,13 +233,15 @@ class DeletionTests(TestCase): def log_post_delete(instance, **kwargs): self.assertTrue(R.objects.filter(pk=instance.r_id)) - models.signals.post_delete.connect(log_post_delete, sender=S) - r = R.objects.create(pk=1) S.objects.create(pk=1, r=r) - r.delete() - models.signals.post_delete.disconnect(log_post_delete) + models.signals.post_delete.connect(log_post_delete, sender=S) + + try: + r.delete() + finally: + models.signals.post_delete.disconnect(log_post_delete) @skipUnlessDBFeature("can_defer_constraint_checks") def test_can_defer_constraint_checks(self): From abbb88886bb1cd6013432c70586fbc118d378e27 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Mon, 14 Jan 2013 13:17:01 -0800 Subject: [PATCH 153/870] Move logic seperation as its not longer repetitive --- django/db/models/deletion.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index e4cd89233c..81f74923c2 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -262,14 +262,6 @@ class Collector(object): self.data = SortedDict([(model, self.data[model]) for model in sorted_models]) - def send_post_delete_signals(self, model, instances): - if model._meta.auto_created: - return - for obj in instances: - signals.post_delete.send( - sender=model, instance=obj, using=self.using - ) - @force_managed def delete(self): # sort instance collections @@ -308,7 +300,12 @@ class Collector(object): query = sql.DeleteQuery(model) pk_list = [obj.pk for obj in instances] query.delete_batch(pk_list, self.using) - self.send_post_delete_signals(model, instances) + + if not model._meta.auto_created: + for obj in instances: + signals.post_delete.send( + sender=model, instance=obj, using=self.using + ) # update collected instances for model, instances_for_fieldvalues in six.iteritems(self.field_updates): From a7ed09d13d9532089bd2380edab1df5df96082a6 Mon Sep 17 00:00:00 2001 From: David Cramer Date: Mon, 14 Jan 2013 13:18:24 -0800 Subject: [PATCH 154/870] Improve test to ensure that post_delete was actually called --- tests/modeltests/delete/tests.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/modeltests/delete/tests.py b/tests/modeltests/delete/tests.py index 99981df018..0a3ddcfc2e 100644 --- a/tests/modeltests/delete/tests.py +++ b/tests/modeltests/delete/tests.py @@ -230,8 +230,12 @@ class DeletionTests(TestCase): models.signals.post_delete.disconnect(log_pre_delete) def test_relational_post_delete_signals_happen_before_parent_object(self): + deletions = [] + def log_post_delete(instance, **kwargs): self.assertTrue(R.objects.filter(pk=instance.r_id)) + self.assertEquals(type(instance), S) + deletions.append(instance.id) r = R.objects.create(pk=1) S.objects.create(pk=1, r=r) @@ -243,6 +247,9 @@ class DeletionTests(TestCase): finally: models.signals.post_delete.disconnect(log_post_delete) + self.assertEquals(len(deletions), 1) + self.assertEquals(deletions[0], 1) + @skipUnlessDBFeature("can_defer_constraint_checks") def test_can_defer_constraint_checks(self): u = User.objects.create( From 2f8ab2f1b09ae8858720af53df77f19e83bd8f66 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 15 Jan 2013 13:39:41 +0100 Subject: [PATCH 155/870] Fixed #19092 -- Completed Lithuanian date/time formats Thanks Tadas Dailyda for the report and the patch. --- django/conf/locale/lt/formats.py | 39 ++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/django/conf/locale/lt/formats.py b/django/conf/locale/lt/formats.py index 4784a1bab5..d5b01abea2 100644 --- a/django/conf/locale/lt/formats.py +++ b/django/conf/locale/lt/formats.py @@ -1,23 +1,42 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date -DATE_FORMAT = r'Y \m. F j \d.' +DATE_FORMAT = r'Y \m. E j \d.' TIME_FORMAT = 'H:i:s' -# DATETIME_FORMAT = -# YEAR_MONTH_FORMAT = -# MONTH_DAY_FORMAT = +DATETIME_FORMAT = r'Y \m. E j \d., H:i:s' +YEAR_MONTH_FORMAT = r'Y \m. F' +MONTH_DAY_FORMAT = r'E j \d.' SHORT_DATE_FORMAT = 'Y-m-d' -# SHORT_DATETIME_FORMAT = -# FIRST_DAY_OF_WEEK = +SHORT_DATETIME_FORMAT = 'Y-m-d H:i' +FIRST_DAY_OF_WEEK = 1 # Monday # The *_INPUT_FORMATS strings use the Python strftime format syntax, # see http://docs.python.org/library/datetime.html#strftime-strptime-behavior -# DATE_INPUT_FORMATS = -# TIME_INPUT_FORMATS = -# DATETIME_INPUT_FORMATS = +DATE_INPUT_FORMATS = ( + '%Y-%m-%d', '%d.%m.%Y', '%d.%m.%y', # '2006-10-25', '25.10.2006', '25.10.06' +) +TIME_INPUT_FORMATS = ( + '%H:%M:%S', # '14:30:59' + '%H:%M', # '14:30' + '%H.%M.%S', # '14.30.59' + '%H.%M', # '14.30' +) +DATETIME_INPUT_FORMATS = ( + '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' + '%Y-%m-%d %H:%M', # '2006-10-25 14:30' + '%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59' + '%d.%m.%Y %H:%M', # '25.10.2006 14:30' + '%d.%m.%Y', # '25.10.2006' + '%d.%m.%y %H:%M:%S', # '25.10.06 14:30:59' + '%d.%m.%y %H:%M', # '25.10.06 14:30' + '%d.%m.%y %H.%M.%S', # '25.10.06 14.30.59' + '%d.%m.%y %H.%M', # '25.10.06 14.30' + '%d.%m.%y', # '25.10.06' +) DECIMAL_SEPARATOR = ',' THOUSAND_SEPARATOR = '.' -# NUMBER_GROUPING = +NUMBER_GROUPING = 3 From 43f89e0ad6fa111f3d53dfa71786353e0265bf39 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 15 Jan 2013 06:29:53 -0500 Subject: [PATCH 156/870] Fixed #19605 - Removed unused url imports from doc examples. Thanks sergzach for the suggestion. --- docs/intro/overview.txt | 2 +- docs/ref/contrib/admin/index.txt | 6 +++--- docs/ref/contrib/comments/example.txt | 2 +- docs/ref/contrib/sitemaps.txt | 2 +- docs/ref/contrib/syndication.txt | 4 ++-- docs/ref/models/instances.txt | 2 +- docs/topics/class-based-views/generic-display.txt | 2 +- docs/topics/class-based-views/index.txt | 4 ++-- docs/topics/http/urls.txt | 10 +++++----- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/intro/overview.txt b/docs/intro/overview.txt index ba49e3ccf2..7cca8bf51b 100644 --- a/docs/intro/overview.txt +++ b/docs/intro/overview.txt @@ -176,7 +176,7 @@ decouple URLs from Python code. Here's what a URLconf might look like for the ``Reporter``/``Article`` example above:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns urlpatterns = patterns('', (r'^articles/(\d{4})/$', 'news.views.year_archive'), diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 065c9566ea..1dbde2a98c 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1928,7 +1928,7 @@ In this example, we register the default ``AdminSite`` instance ``django.contrib.admin.site`` at the URL ``/admin/`` :: # urls.py - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns, include from django.contrib import admin admin.autodiscover() @@ -1944,7 +1944,7 @@ In this example, we register the ``AdminSite`` instance ``myproject.admin.admin_site`` at the URL ``/myadmin/`` :: # urls.py - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns, include from myproject.admin import admin_site urlpatterns = patterns('', @@ -1968,7 +1968,7 @@ separate versions of the admin site -- using the ``AdminSite`` instances respectively:: # urls.py - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns, include from myproject.admin import basic_site, advanced_site urlpatterns = patterns('', diff --git a/docs/ref/contrib/comments/example.txt b/docs/ref/contrib/comments/example.txt index 4e18e37de0..e99c10f732 100644 --- a/docs/ref/contrib/comments/example.txt +++ b/docs/ref/contrib/comments/example.txt @@ -141,7 +141,7 @@ enable it in your project's ``urls.py``: .. code-block:: python - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns from django.contrib.comments.feeds import LatestCommentFeed urlpatterns = patterns('', diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index 1861318b95..ded7a84fbc 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -256,7 +256,7 @@ Example Here's an example of a :doc:`URLconf ` using both:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns from django.contrib.sitemaps import FlatPageSitemap, GenericSitemap from blog.models import Entry diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index d0376e3c1b..2955d7dad3 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -77,7 +77,7 @@ latest five news items:: To connect a URL to this feed, put an instance of the Feed object in your :doc:`URLconf `. For example:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns from myproject.feeds import LatestEntriesFeed urlpatterns = patterns('', @@ -321,7 +321,7 @@ Here's a full example:: And the accompanying URLconf:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed urlpatterns = patterns('', diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 4479e4b766..92071b8d3f 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -601,7 +601,7 @@ pattern, it's possible to give a name to a pattern, and then reference the name rather than the view function. A named URL pattern is defined by replacing the pattern tuple by a call to the ``url`` function):: - from django.conf.urls import patterns, url, include + from django.conf.urls import url url(r'^people/(\d+)/$', 'blog_views.generic_detail', name='people_view'), diff --git a/docs/topics/class-based-views/generic-display.txt b/docs/topics/class-based-views/generic-display.txt index dac45c8843..835ca07459 100644 --- a/docs/topics/class-based-views/generic-display.txt +++ b/docs/topics/class-based-views/generic-display.txt @@ -110,7 +110,7 @@ Now we need to define a view:: Finally hook that view into your urls:: # urls.py - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns, url from books.views import PublisherList urlpatterns = patterns('', diff --git a/docs/topics/class-based-views/index.txt b/docs/topics/class-based-views/index.txt index 54d4b0f252..302f473eea 100644 --- a/docs/topics/class-based-views/index.txt +++ b/docs/topics/class-based-views/index.txt @@ -37,7 +37,7 @@ URLconf. If you're only changing a few simple attributes on a class-based view, you can simply pass them into the :meth:`~django.views.generic.base.View.as_view` method call itself:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns from django.views.generic import TemplateView urlpatterns = patterns('', @@ -73,7 +73,7 @@ point the URL to the :meth:`~django.views.generic.base.View.as_view` class method instead, which provides a function-like entry to class-based views:: # urls.py - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns from some_app.views import AboutView urlpatterns = patterns('', diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 8a07d46f77..c5eef8bb41 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -66,7 +66,7 @@ Example Here's a sample URLconf:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns urlpatterns = patterns('', (r'^articles/2003/$', 'news.views.special_case_2003'), @@ -255,7 +255,7 @@ code duplication. Here's the example URLconf from the :doc:`Django overview `:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns urlpatterns = patterns('', (r'^articles/(\d{4})/$', 'news.views.year_archive'), @@ -270,7 +270,7 @@ each view function. With this in mind, the above example can be written more concisely as:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns urlpatterns = patterns('news.views', (r'^articles/(\d{4})/$', 'year_archive'), @@ -291,7 +291,7 @@ Just add multiple ``patterns()`` objects together, like this: Old:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns urlpatterns = patterns('', (r'^$', 'myapp.views.app_index'), @@ -301,7 +301,7 @@ Old:: New:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns urlpatterns = patterns('myapp.views', (r'^$', 'app_index'), From c9b577ead6ca9a96e2066fd739b7c340dae5ca3a Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 15 Jan 2013 06:19:49 -0500 Subject: [PATCH 157/870] Clarified WizardView.get_form_prefix doc, refs #19024 --- docs/ref/contrib/formtools/form-wizard.txt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index 8cd5d4ecd3..ee9114acf9 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -318,10 +318,15 @@ Advanced ``WizardView`` methods counter as string representing the current step of the wizard. (E.g., the first form is ``'0'`` and the second form is ``'1'``) -.. method:: WizardView.get_form_prefix(step, form) +.. method:: WizardView.get_form_prefix(step=None, form=None) + + Returns the prefix which will be used when calling the form for the given + step. ``step`` contains the step name, ``form`` the form class which will + be called with the returned prefix. + + If no ``step`` is given, it will be determined automatically. By default, + this simply uses the step itself and the ``form`` parameter is not used. - Given the step and the form class which will be called with the returned - form prefix. By default, this simply uses the step itself. For more, see the :ref:`form prefix documentation `. .. method:: WizardView.get_form_initial(step) From 74d72e21b405956bec9775b90e052e89f03a5e2e Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 15 Jan 2013 14:36:47 +0100 Subject: [PATCH 158/870] Fixed #19614 -- Missing request argument in render call. Thanks Dima Pravdin for the report. --- docs/topics/auth/default.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 82cabadbec..d1463c645b 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -372,7 +372,7 @@ login page:: def my_view(request): if not request.user.is_authenticated(): - return render('myapp/login_error.html') + return render(request, 'myapp/login_error.html') # ... .. currentmodule:: django.contrib.auth.decorators From 984e91e28f3fb1d3f87f3f1de9fd54922b91c23c Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 15 Jan 2013 07:51:33 -0800 Subject: [PATCH 159/870] Removed some uses of the deprecated assertEquals --- tests/modeltests/delete/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/modeltests/delete/tests.py b/tests/modeltests/delete/tests.py index 0a3ddcfc2e..da0430145c 100644 --- a/tests/modeltests/delete/tests.py +++ b/tests/modeltests/delete/tests.py @@ -234,7 +234,7 @@ class DeletionTests(TestCase): def log_post_delete(instance, **kwargs): self.assertTrue(R.objects.filter(pk=instance.r_id)) - self.assertEquals(type(instance), S) + self.assertIs(type(instance), S) deletions.append(instance.id) r = R.objects.create(pk=1) @@ -247,8 +247,8 @@ class DeletionTests(TestCase): finally: models.signals.post_delete.disconnect(log_post_delete) - self.assertEquals(len(deletions), 1) - self.assertEquals(deletions[0], 1) + self.assertEqual(len(deletions), 1) + self.assertEqual(deletions[0], 1) @skipUnlessDBFeature("can_defer_constraint_checks") def test_can_defer_constraint_checks(self): From 83d0cc52141dbbd977da836fd7f77e0e735e2110 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 15 Jan 2013 16:55:13 +0100 Subject: [PATCH 160/870] Fixed a typo in the error reporting docs. --- docs/howto/error-reporting.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 98b3b4e4d8..742b81b7e2 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -157,7 +157,7 @@ production environment (that is, where :setting:`DEBUG` is set to ``False``): If the variable you want to hide is also a function argument (e.g. '``user``' in the following example), and if the decorated function has - mutiple decorators, then make sure to place ``@sensible_variables`` at + mutiple decorators, then make sure to place ``@sensitive_variables`` at the top of the decorator chain. This way it will also hide the function argument as it gets passed through the other decorators:: From 50a985b09b439a0d52aad8694d377a3483cb02e1 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 1 Jan 2013 22:28:48 +0100 Subject: [PATCH 161/870] Fixed #19099 -- Split broken link emails out of common middleware. --- django/conf/global_settings.py | 4 +- django/middleware/common.py | 76 ++++++++++++++--------- docs/howto/error-reporting.txt | 19 +++--- docs/internals/deprecation.txt | 7 +++ docs/ref/middleware.txt | 8 ++- docs/ref/settings.txt | 13 +++- docs/releases/1.6.txt | 18 ++++++ tests/regressiontests/middleware/tests.py | 61 ++++++++++++++---- 8 files changed, 146 insertions(+), 60 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 4d69c6365f..740c792dcf 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -146,7 +146,7 @@ FILE_CHARSET = 'utf-8' # Email address that error messages come from. SERVER_EMAIL = 'root@localhost' -# Whether to send broken-link emails. +# Whether to send broken-link emails. Deprecated, must be removed in 1.8. SEND_BROKEN_LINK_EMAILS = False # Database connection info. If left empty, will default to the dummy backend. @@ -245,7 +245,7 @@ ALLOWED_INCLUDE_ROOTS = () ADMIN_FOR = () # List of compiled regular expression objects representing URLs that need not -# be reported when SEND_BROKEN_LINK_EMAILS is True. Here are a few examples: +# be reported by BrokenLinkEmailsMiddleware. Here are a few examples: # import re # IGNORABLE_404_URLS = ( # re.compile(r'^/apple-touch-icon.*\.png$'), diff --git a/django/middleware/common.py b/django/middleware/common.py index c6e71e0d48..92f8cb3992 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -1,13 +1,14 @@ import hashlib import logging import re +import warnings from django.conf import settings -from django import http from django.core.mail import mail_managers +from django.core import urlresolvers +from django import http from django.utils.http import urlquote from django.utils import six -from django.core import urlresolvers logger = logging.getLogger('django.request') @@ -102,25 +103,15 @@ class CommonMiddleware(object): return http.HttpResponsePermanentRedirect(newurl) def process_response(self, request, response): - "Send broken link emails and calculate the Etag, if needed." - if response.status_code == 404: - if settings.SEND_BROKEN_LINK_EMAILS and not settings.DEBUG: - # If the referrer was from an internal link or a non-search-engine site, - # send a note to the managers. - domain = request.get_host() - referer = request.META.get('HTTP_REFERER', None) - is_internal = _is_internal_request(domain, referer) - path = request.get_full_path() - if referer and not _is_ignorable_404(path) and (is_internal or '?' not in referer): - ua = request.META.get('HTTP_USER_AGENT', '') - ip = request.META.get('REMOTE_ADDR', '') - mail_managers("Broken %slink on %s" % ((is_internal and 'INTERNAL ' or ''), domain), - "Referrer: %s\nRequested URL: %s\nUser agent: %s\nIP address: %s\n" \ - % (referer, request.get_full_path(), ua, ip), - fail_silently=True) - return response + """ + Calculate the ETag, if needed. + """ + if settings.SEND_BROKEN_LINK_EMAILS: + warnings.warn("SEND_BROKEN_LINK_EMAILS is deprecated. " + "Use BrokenLinkEmailsMiddleware instead.", + PendingDeprecationWarning, stacklevel=2) + BrokenLinkEmailsMiddleware().process_response(request, response) - # Use ETags, if requested. if settings.USE_ETAGS: if response.has_header('ETag'): etag = response['ETag'] @@ -139,15 +130,38 @@ class CommonMiddleware(object): return response -def _is_ignorable_404(uri): - """ - Returns True if a 404 at the given URL *shouldn't* notify the site managers. - """ - return any(pattern.search(uri) for pattern in settings.IGNORABLE_404_URLS) -def _is_internal_request(domain, referer): - """ - Returns true if the referring URL is the same domain as the current request. - """ - # Different subdomains are treated as different domains. - return referer is not None and re.match("^https?://%s/" % re.escape(domain), referer) +class BrokenLinkEmailsMiddleware(object): + + def process_response(self, request, response): + """ + Send broken link emails for relevant 404 NOT FOUND responses. + """ + if response.status_code == 404 and not settings.DEBUG: + domain = request.get_host() + path = request.get_full_path() + referer = request.META.get('HTTP_REFERER', '') + is_internal = self.is_internal_request(domain, referer) + is_not_search_engine = '?' not in referer + is_ignorable = self.is_ignorable_404(path) + if referer and (is_internal or is_not_search_engine) and not is_ignorable: + ua = request.META.get('HTTP_USER_AGENT', '') + ip = request.META.get('REMOTE_ADDR', '') + mail_managers( + "Broken %slink on %s" % (('INTERNAL ' if is_internal else ''), domain), + "Referrer: %s\nRequested URL: %s\nUser agent: %s\nIP address: %s\n" % (referer, path, ua, ip), + fail_silently=True) + return response + + def is_internal_request(self, domain, referer): + """ + Returns True if the referring URL is the same domain as the current request. + """ + # Different subdomains are treated as different domains. + return re.match("^https?://%s/" % re.escape(domain), referer) + + def is_ignorable_404(self, uri): + """ + Returns True if a 404 at the given URL *shouldn't* notify the site managers. + """ + return any(pattern.search(uri) for pattern in settings.IGNORABLE_404_URLS) diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 742b81b7e2..7f3c68c136 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -54,18 +54,24 @@ setting. Django can also be configured to email errors about broken links (404 "page not found" errors). Django sends emails about 404 errors when: -* :setting:`DEBUG` is ``False`` +* :setting:`DEBUG` is ``False``; -* :setting:`SEND_BROKEN_LINK_EMAILS` is ``True`` - -* Your :setting:`MIDDLEWARE_CLASSES` setting includes ``CommonMiddleware`` - (which it does by default). +* Your :setting:`MIDDLEWARE_CLASSES` setting includes + :class:`django.middleware.common.BrokenLinkEmailsMiddleware`. If those conditions are met, Django will email the users listed in the :setting:`MANAGERS` setting whenever your code raises a 404 and the request has a referer. (It doesn't bother to email for 404s that don't have a referer -- those are usually just people typing in broken URLs or broken Web 'bots). +.. note:: + + :class:`~django.middleware.common.BrokenLinkEmailsMiddleware` must appear + before other middleware that intercepts 404 errors, such as + :class:`~django.middleware.locale.LocaleMiddleware` or + :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware`. + Put it towards the top of your :setting:`MIDDLEWARE_CLASSES` setting. + You can tell Django to stop reporting particular 404s by tweaking the :setting:`IGNORABLE_404_URLS` setting. It should be a tuple of compiled regular expression objects. For example:: @@ -92,9 +98,6 @@ crawlers often request:: (Note that these are regular expressions, so we put a backslash in front of periods to escape them.) -The best way to disable this behavior is to set -:setting:`SEND_BROKEN_LINK_EMAILS` to ``False``. - .. seealso:: 404 errors are logged using the logging framework. By default, these log diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index faa6d1ff02..63d65d1e4a 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -308,6 +308,13 @@ these changes. * The ``depth`` keyword argument will be removed from :meth:`~django.db.models.query.QuerySet.select_related`. +1.8 +--- + +* The ``SEND_BROKEN_LINK_EMAILS`` setting will be removed. Add the + :class:`django.middleware.common.BrokenLinkEmailsMiddleware` middleware to + your :setting:`MIDDLEWARE_CLASSES` setting instead. + 2.0 --- diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 2b053d80ab..1e6e57f720 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -61,14 +61,16 @@ Adds a few conveniences for perfectionists: indexer would treat them as separate URLs -- so it's best practice to normalize URLs. -* Sends broken link notification emails to :setting:`MANAGERS` if - :setting:`SEND_BROKEN_LINK_EMAILS` is set to ``True``. - * Handles ETags based on the :setting:`USE_ETAGS` setting. If :setting:`USE_ETAGS` is set to ``True``, Django will calculate an ETag for each request by MD5-hashing the page content, and it'll take care of sending ``Not Modified`` responses, if appropriate. +.. class:: BrokenLinkEmailsMiddleware + +* Sends broken link notification emails to :setting:`MANAGERS` (see + :doc:`/howto/error-reporting`). + View metadata middleware ------------------------ diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 110d5dbdc9..d057323c06 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1090,8 +1090,9 @@ query string, if any). Use this if your site does not provide a commonly requested file such as ``favicon.ico`` or ``robots.txt``, or if it gets hammered by script kiddies. -This is only used if :setting:`SEND_BROKEN_LINK_EMAILS` is set to ``True`` and -``CommonMiddleware`` is installed (see :doc:`/topics/http/middleware`). +This is only used if +:class:`~django.middleware.common.BrokenLinkEmailsMiddleware` is enabled (see +:doc:`/topics/http/middleware`). .. setting:: INSTALLED_APPS @@ -1250,7 +1251,8 @@ MANAGERS Default: ``()`` (Empty tuple) A tuple in the same format as :setting:`ADMINS` that specifies who should get -broken-link notifications when :setting:`SEND_BROKEN_LINK_EMAILS` is ``True``. +broken link notifications when +:class:`~django.middleware.common.BrokenLinkEmailsMiddleware` is enabled. .. setting:: MEDIA_ROOT @@ -1448,6 +1450,11 @@ available in ``request.META``.) SEND_BROKEN_LINK_EMAILS ----------------------- +.. deprecated:: 1.6 + Since :class:`~django.middleware.common.BrokenLinkEmailsMiddleware` + was split from :class:`~django.middleware.common.CommonMiddleware`, + this setting no longer serves a purpose. + Default: ``False`` Whether to send an email to the :setting:`MANAGERS` each time somebody visits diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index e425036839..dcf6f2604a 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -46,3 +46,21 @@ Backwards incompatible changes in 1.6 Features deprecated in 1.6 ========================== + +``SEND_BROKEN_LINK_EMAILS`` setting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:class:`~django.middleware.common.CommonMiddleware` used to provide basic +reporting of broken links by email when ``SEND_BROKEN_LINK_EMAILS`` is set to +``True``. + +Because of intractable ordering problems between +:class:`~django.middleware.common.CommonMiddleware` and +:class:`~django.middleware.locale.LocaleMiddleware`, this feature was split +out into a new middleware: +:class:`~django.middleware.common.BrokenLinkEmailsMiddleware`. + +If you're relying on this feature, you should add +``'django.middleware.common.BrokenLinkEmailsMiddleware'`` to your +:setting:`MIDDLEWARE_CLASSES` setting and remove ``SEND_BROKEN_LINK_EMAILS`` +from your settings. diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py index e3d8350da6..c6d42a6964 100644 --- a/tests/regressiontests/middleware/tests.py +++ b/tests/regressiontests/middleware/tests.py @@ -1,16 +1,17 @@ # -*- coding: utf-8 -*- import gzip -import re -import random from io import BytesIO +import random +import re +import warnings from django.conf import settings from django.core import mail from django.http import HttpRequest from django.http import HttpResponse, StreamingHttpResponse from django.middleware.clickjacking import XFrameOptionsMiddleware -from django.middleware.common import CommonMiddleware +from django.middleware.common import CommonMiddleware, BrokenLinkEmailsMiddleware from django.middleware.http import ConditionalGetMiddleware from django.middleware.gzip import GZipMiddleware from django.test import TestCase, RequestFactory @@ -232,33 +233,39 @@ class CommonMiddlewareTest(TestCase): self.assertEqual(r['Location'], 'http://www.testserver/middleware/customurlconf/slash/') - # Tests for the 404 error reporting via email + # Legacy tests for the 404 error reporting via email (to be removed in 1.8) @override_settings(IGNORABLE_404_URLS=(re.compile(r'foo'),), - SEND_BROKEN_LINK_EMAILS = True) + SEND_BROKEN_LINK_EMAILS=True) def test_404_error_reporting(self): request = self._get_request('regular_url/that/does/not/exist') request.META['HTTP_REFERER'] = '/another/url/' - response = self.client.get(request.path) - CommonMiddleware().process_response(request, response) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", PendingDeprecationWarning) + response = self.client.get(request.path) + CommonMiddleware().process_response(request, response) self.assertEqual(len(mail.outbox), 1) self.assertIn('Broken', mail.outbox[0].subject) @override_settings(IGNORABLE_404_URLS=(re.compile(r'foo'),), - SEND_BROKEN_LINK_EMAILS = True) + SEND_BROKEN_LINK_EMAILS=True) def test_404_error_reporting_no_referer(self): request = self._get_request('regular_url/that/does/not/exist') - response = self.client.get(request.path) - CommonMiddleware().process_response(request, response) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", PendingDeprecationWarning) + response = self.client.get(request.path) + CommonMiddleware().process_response(request, response) self.assertEqual(len(mail.outbox), 0) @override_settings(IGNORABLE_404_URLS=(re.compile(r'foo'),), - SEND_BROKEN_LINK_EMAILS = True) + SEND_BROKEN_LINK_EMAILS=True) def test_404_error_reporting_ignored_url(self): request = self._get_request('foo_url/that/does/not/exist/either') request.META['HTTP_REFERER'] = '/another/url/' - response = self.client.get(request.path) - CommonMiddleware().process_response(request, response) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", PendingDeprecationWarning) + response = self.client.get(request.path) + CommonMiddleware().process_response(request, response) self.assertEqual(len(mail.outbox), 0) # Other tests @@ -271,6 +278,34 @@ class CommonMiddlewareTest(TestCase): self.assertEqual(response.status_code, 301) +@override_settings(IGNORABLE_404_URLS=(re.compile(r'foo'),)) +class BrokenLinkEmailsMiddlewareTest(TestCase): + + def setUp(self): + self.req = HttpRequest() + self.req.META = { + 'SERVER_NAME': 'testserver', + 'SERVER_PORT': 80, + } + self.req.path = self.req.path_info = 'regular_url/that/does/not/exist' + self.resp = self.client.get(self.req.path) + + def test_404_error_reporting(self): + self.req.META['HTTP_REFERER'] = '/another/url/' + BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 1) + self.assertIn('Broken', mail.outbox[0].subject) + + def test_404_error_reporting_no_referer(self): + BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 0) + + def test_404_error_reporting_ignored_url(self): + self.req.path = self.req.path_info = 'foo_url/that/does/not/exist' + BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 0) + + class ConditionalGetMiddlewareTest(TestCase): urls = 'regressiontests.middleware.cond_get_urls' def setUp(self): From 4cf057faab0858ff73506e88dac4020e6eb96cf6 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 15 Jan 2013 18:01:18 +0100 Subject: [PATCH 162/870] Removed obsolete comment. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These features are implemented, tracked in tickets, or not necessary. Thanks Bruno Renié. --- django/forms/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/django/forms/__init__.py b/django/forms/__init__.py index ec37a475e3..2588098330 100644 --- a/django/forms/__init__.py +++ b/django/forms/__init__.py @@ -1,13 +1,5 @@ """ Django validation and HTML form handling. - -TODO: - Default value for field - Field labels - Nestable Forms - FatalValidationError -- short-circuits all other validators on a form - ValidationWarning - "This form field requires foo.js" and form.js_includes() """ from __future__ import absolute_import From 222a956ecc5b163420d524a675ed01d75622ea6b Mon Sep 17 00:00:00 2001 From: Matt Robenolt Date: Mon, 14 Jan 2013 18:23:42 -0800 Subject: [PATCH 163/870] Kill mx.TextTools with fire --- django/http/multipartparser.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py index edf98f6e49..070874f234 100644 --- a/django/http/multipartparser.py +++ b/django/http/multipartparser.py @@ -439,11 +439,6 @@ class BoundaryIter(six.Iterator): if not unused_char: raise InputStreamExhausted() self._stream.unget(unused_char) - try: - from mx.TextTools import FS - self._fs = FS(boundary).find - except ImportError: - self._fs = lambda data: data.find(boundary) def __iter__(self): return self @@ -499,7 +494,7 @@ class BoundaryIter(six.Iterator): * the end of current encapsulation * the start of the next encapsulation """ - index = self._fs(data) + index = data.find(self._boundary) if index < 0: return None else: From d406afe12edcedbaf745225713ebf3e36fa776fc Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 15 Jan 2013 15:47:31 -0500 Subject: [PATCH 164/870] Fixed #19597 - Added some notes on jQuery in admin. Thanks Daniele Procida. --- docs/ref/contrib/admin/index.txt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 1dbde2a98c..b273255c71 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1383,15 +1383,18 @@ definitions on forms `. jQuery ~~~~~~ -Django admin Javascript makes use of the `jQuery`_ library. To avoid -conflicts with user-supplied scripts or libraries, Django's jQuery is -namespaced as ``django.jQuery``. If you want to use jQuery in your own admin -JavaScript without including a second copy, you can use the ``django.jQuery`` -object on changelist and add/edit views. +Django admin Javascript makes use of the `jQuery`_ library. -If you require the jQuery library to be in the global namespace, for example -when using third-party jQuery plugins, or need a newer version of jQuery, you -will have to include your own copy of jQuery. +To avoid conflicts with user-supplied scripts or libraries, Django's jQuery +(version 1.4.2) is namespaced as ``django.jQuery``. If you want to use jQuery +in your own admin JavaScript without including a second copy, you can use the +``django.jQuery`` object on changelist and add/edit views. + +The :class:`ModelAdmin` class requires jQuery by default, so there is no need +to add jQuery to your ``ModelAdmin``'s list of media resources unless you have +a specifc need. For example, if you require the jQuery library to be in the +global namespace (for example when using third-party jQuery plugins) or if you +need a newer version of jQuery, you will have to include your own copy. Django provides both uncompressed and 'minified' versions of jQuery, as ``jquery.js`` and ``jquery.min.js`` respectively. From eee865257aaa9005947a7b4994c475c2ad59d698 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Wed, 16 Jan 2013 15:36:22 -0300 Subject: [PATCH 165/870] Fixed #17008 -- Added makemessages option to not remove .pot files. Thanks airstrike for the report and initial patch, Julien for an enhanced patch and Jannis for reviewing. --- .../core/management/commands/makemessages.py | 33 ++++++++++++------- docs/ref/django-admin.txt | 8 +++++ .../i18n/commands/extraction.py | 33 +++++++++++++++++++ tests/regressiontests/i18n/tests.py | 2 +- 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 606cbe0b85..449d3d7c5a 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -126,7 +126,7 @@ def write_pot_file(potfile, msgs, file, work_file, is_templatized): fp.write(msgs) def process_file(file, dirpath, potfile, domain, verbosity, - extensions, wrap, location, stdout=sys.stdout): + extensions, wrap, location, keep_pot, stdout=sys.stdout): """ Extract translatable literals from :param file: for :param domain: creating or updating the :param potfile: POT file. @@ -183,7 +183,7 @@ def process_file(file, dirpath, potfile, domain, verbosity, if status != STATUS_OK: if is_templatized: os.unlink(work_file) - if os.path.exists(potfile): + if not keep_pot and os.path.exists(potfile): os.unlink(potfile) raise CommandError( "errors happened while running xgettext on %s\n%s" % @@ -197,7 +197,7 @@ def process_file(file, dirpath, potfile, domain, verbosity, os.unlink(work_file) def write_po_file(pofile, potfile, domain, locale, verbosity, stdout, - copy_pforms, wrap, location, no_obsolete): + copy_pforms, wrap, location, no_obsolete, keep_pot): """ Creates of updates the :param pofile: PO file for :param domain: and :param locale:. Uses contents of the existing :param potfile:. @@ -208,7 +208,8 @@ def write_po_file(pofile, potfile, domain, locale, verbosity, stdout, (wrap, location, potfile)) if errors: if status != STATUS_OK: - os.unlink(potfile) + if not keep_pot: + os.unlink(potfile) raise CommandError( "errors happened while running msguniq\n%s" % errors) elif verbosity > 0: @@ -221,7 +222,8 @@ def write_po_file(pofile, potfile, domain, locale, verbosity, stdout, (wrap, location, pofile, potfile)) if errors: if status != STATUS_OK: - os.unlink(potfile) + if not keep_pot: + os.unlink(potfile) raise CommandError( "errors happened while running msgmerge\n%s" % errors) elif verbosity > 0: @@ -232,7 +234,8 @@ def write_po_file(pofile, potfile, domain, locale, verbosity, stdout, "#. #-#-#-#-# %s.pot (PACKAGE VERSION) #-#-#-#-#\n" % domain, "") with open(pofile, 'w') as fp: fp.write(msgs) - os.unlink(potfile) + if not keep_pot: + os.unlink(potfile) if no_obsolete: msgs, errors, status = _popen( 'msgattrib %s %s -o "%s" --no-obsolete "%s"' % @@ -246,7 +249,7 @@ def write_po_file(pofile, potfile, domain, locale, verbosity, stdout, def make_messages(locale=None, domain='django', verbosity=1, all=False, extensions=None, symlinks=False, ignore_patterns=None, no_wrap=False, - no_location=False, no_obsolete=False, stdout=sys.stdout): + no_location=False, no_obsolete=False, stdout=sys.stdout, keep_pot=False): """ Uses the ``locale/`` directory from the Django Git tree or an application/project to process all files with translatable literals for @@ -280,10 +283,12 @@ def make_messages(locale=None, domain='django', verbosity=1, all=False, "if you want to enable i18n for your project or application.") if domain not in ('django', 'djangojs'): - raise CommandError("currently makemessages only supports domains 'django' and 'djangojs'") + raise CommandError("currently makemessages only supports domains " + "'django' and 'djangojs'") if (locale is None and not all) or domain is None: - message = "Type '%s help %s' for usage information." % (os.path.basename(sys.argv[0]), sys.argv[1]) + message = "Type '%s help %s' for usage information." % ( + os.path.basename(sys.argv[0]), sys.argv[1]) raise CommandError(message) # We require gettext version 0.15 or newer. @@ -325,11 +330,11 @@ def make_messages(locale=None, domain='django', verbosity=1, all=False, for dirpath, file in find_files(".", ignore_patterns, verbosity, stdout, symlinks=symlinks): process_file(file, dirpath, potfile, domain, verbosity, extensions, - wrap, location, stdout) + wrap, location, keep_pot, stdout) if os.path.exists(potfile): write_po_file(pofile, potfile, domain, locale, verbosity, stdout, - not invoked_for_django, wrap, location, no_obsolete) + not invoked_for_django, wrap, location, no_obsolete, keep_pot) class Command(NoArgsCommand): @@ -355,6 +360,8 @@ class Command(NoArgsCommand): default=False, help="Don't write '#: filename:line' lines"), make_option('--no-obsolete', action='store_true', dest='no_obsolete', default=False, help="Remove obsolete message strings"), + make_option('--keep-pot', action='store_true', dest='keep_pot', + default=False, help="Keep .pot file after making messages. Useful when debugging."), ) help = ("Runs over the entire source tree of the current directory and " "pulls out all strings marked for translation. It creates (or updates) a message " @@ -379,6 +386,7 @@ class Command(NoArgsCommand): no_wrap = options.get('no_wrap') no_location = options.get('no_location') no_obsolete = options.get('no_obsolete') + keep_pot = options.get('keep_pot') if domain == 'djangojs': exts = extensions if extensions else ['js'] else: @@ -390,4 +398,5 @@ class Command(NoArgsCommand): % get_text_list(list(extensions), 'and')) make_messages(locale, domain, verbosity, process_all, extensions, - symlinks, ignore_patterns, no_wrap, no_location, no_obsolete, self.stdout) + symlinks, ignore_patterns, no_wrap, no_location, + no_obsolete, self.stdout, keep_pot) diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 8d612ae6a6..3c73e268e2 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -472,6 +472,14 @@ Use the ``--no-location`` option to not write '``#: filename:line``' comment lines in language files. Note that using this option makes it harder for technically skilled translators to understand each message's context. +.. django-admin-option:: --keep-pot + +.. versionadded:: 1.6 + +Use the ``--keep-pot`` option to prevent django from deleting the temporary +.pot file it generates before creating the .po file. This is useful for +debugging errors which may prevent the final language files from being created. + runfcgi [options] ----------------- diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index aa5efe1967..bd2b84a952 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -293,3 +293,36 @@ class NoLocationExtractorTests(ExtractorTests): with open(self.PO_FILE, 'r') as fp: po_contents = force_text(fp.read()) self.assertTrue('#: templates/test.html:55' in po_contents) + + +class KeepPotFileExtractorTests(ExtractorTests): + + def setUp(self): + self.POT_FILE = self.PO_FILE + 't' + super(KeepPotFileExtractorTests, self).setUp() + + def tearDown(self): + super(KeepPotFileExtractorTests, self).tearDown() + os.chdir(self.test_dir) + try: + os.unlink(self.POT_FILE) + except OSError: + pass + os.chdir(self._cwd) + + def test_keep_pot_disabled_by_default(self): + os.chdir(self.test_dir) + management.call_command('makemessages', locale=LOCALE, verbosity=0) + self.assertFalse(os.path.exists(self.POT_FILE)) + + def test_keep_pot_explicitly_disabled(self): + os.chdir(self.test_dir) + management.call_command('makemessages', locale=LOCALE, verbosity=0, + keep_pot=False) + self.assertFalse(os.path.exists(self.POT_FILE)) + + def test_keep_pot_enabled(self): + os.chdir(self.test_dir) + management.call_command('makemessages', locale=LOCALE, verbosity=0, + keep_pot=True) + self.assertTrue(os.path.exists(self.POT_FILE)) diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index 44d84f9143..5d789b4acb 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -32,7 +32,7 @@ if can_run_extraction_tests: from .commands.extraction import (ExtractorTests, BasicExtractorTests, JavascriptExtractorTests, IgnoredExtractorTests, SymlinkExtractorTests, CopyPluralFormsExtractorTests, NoWrapExtractorTests, - NoLocationExtractorTests) + NoLocationExtractorTests, KeepPotFileExtractorTests) if can_run_compilation_tests: from .commands.compilation import (PoFileTests, PoFileContentsTests, PercentRenderingTests) From 248aee16066b2a336f16c844580cf043d853874b Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Wed, 16 Jan 2013 16:21:47 -0300 Subject: [PATCH 166/870] Modified makemessages so it creates .pot files once per invocation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It creates a `locale/django.pot` file once instead of one `locale//django.pot` file for every locale involved. Thanks Michal Čihař for the report and patch. --- .../core/management/commands/makemessages.py | 24 ++++++++++--------- .../i18n/commands/extraction.py | 3 ++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 449d3d7c5a..2b2755d8d1 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -234,8 +234,6 @@ def write_po_file(pofile, potfile, domain, locale, verbosity, stdout, "#. #-#-#-#-# %s.pot (PACKAGE VERSION) #-#-#-#-#\n" % domain, "") with open(pofile, 'w') as fp: fp.write(msgs) - if not keep_pot: - os.unlink(potfile) if no_obsolete: msgs, errors, status = _popen( 'msgattrib %s %s -o "%s" --no-obsolete "%s"' % @@ -314,6 +312,16 @@ def make_messages(locale=None, domain='django', verbosity=1, all=False, wrap = '--no-wrap' if no_wrap else '' location = '--no-location' if no_location else '' + potfile = os.path.join(localedir, '%s.pot' % str(domain)) + + if os.path.exists(potfile): + os.unlink(potfile) + + for dirpath, file in find_files(".", ignore_patterns, verbosity, + stdout, symlinks=symlinks): + process_file(file, dirpath, potfile, domain, verbosity, extensions, + wrap, location, keep_pot, stdout) + for locale in locales: if verbosity > 0: stdout.write("processing language %s\n" % locale) @@ -322,20 +330,14 @@ def make_messages(locale=None, domain='django', verbosity=1, all=False, os.makedirs(basedir) pofile = os.path.join(basedir, '%s.po' % str(domain)) - potfile = os.path.join(basedir, '%s.pot' % str(domain)) - - if os.path.exists(potfile): - os.unlink(potfile) - - for dirpath, file in find_files(".", ignore_patterns, verbosity, - stdout, symlinks=symlinks): - process_file(file, dirpath, potfile, domain, verbosity, extensions, - wrap, location, keep_pot, stdout) if os.path.exists(potfile): write_po_file(pofile, potfile, domain, locale, verbosity, stdout, not invoked_for_django, wrap, location, no_obsolete, keep_pot) + if not keep_pot: + os.unlink(potfile) + class Command(NoArgsCommand): option_list = NoArgsCommand.option_list + ( diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index bd2b84a952..6a8ebc7670 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -297,8 +297,9 @@ class NoLocationExtractorTests(ExtractorTests): class KeepPotFileExtractorTests(ExtractorTests): + POT_FILE='locale/django.pot' + def setUp(self): - self.POT_FILE = self.PO_FILE + 't' super(KeepPotFileExtractorTests, self).setUp() def tearDown(self): From 295650bd01bc4d68e918b483a2366a092dae6636 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 17 Jan 2013 00:35:46 -0300 Subject: [PATCH 167/870] Simplified i18n commands tests. --- tests/regressiontests/i18n/commands/compilation.py | 6 +++--- tests/regressiontests/i18n/commands/extraction.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/regressiontests/i18n/commands/compilation.py b/tests/regressiontests/i18n/commands/compilation.py index 2944469110..492b0d22a3 100644 --- a/tests/regressiontests/i18n/commands/compilation.py +++ b/tests/regressiontests/i18n/commands/compilation.py @@ -1,16 +1,16 @@ import os from django.core.management import call_command, CommandError -from django.test import TestCase +from django.test import SimpleTestCase from django.test.utils import override_settings -from django.utils import translation, six +from django.utils import translation from django.utils._os import upath from django.utils.six import StringIO test_dir = os.path.abspath(os.path.dirname(upath(__file__))) -class MessageCompilationTests(TestCase): +class MessageCompilationTests(SimpleTestCase): def setUp(self): self._cwd = os.getcwd() diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index 6a8ebc7670..575f23cfee 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -6,7 +6,7 @@ import re import shutil from django.core import management -from django.test import TestCase +from django.test import SimpleTestCase from django.utils.encoding import force_text from django.utils._os import upath from django.utils.six import StringIO @@ -14,7 +14,7 @@ from django.utils.six import StringIO LOCALE='de' -class ExtractorTests(TestCase): +class ExtractorTests(SimpleTestCase): PO_FILE='locale/%s/LC_MESSAGES/django.po' % LOCALE From 6158c79dbef832bc8530107ab2d34f04a04471da Mon Sep 17 00:00:00 2001 From: Craig Blaszczyk Date: Thu, 7 Jun 2012 11:23:25 +0200 Subject: [PATCH 168/870] Made (make|compile)messages commands accept multiple locales at once. Thanks Craig Blaszczyk for the initial patch. Refs #17181. --- AUTHORS | 1 + .../management/commands/compilemessages.py | 16 +++-- .../core/management/commands/makemessages.py | 6 +- docs/ref/django-admin.txt | 28 +++++++- .../i18n/commands/compilation.py | 31 ++++++++ .../i18n/commands/extraction.py | 27 +++++++ .../commands/locale/hr/LC_MESSAGES/django.po | 71 +++++++++++++++++++ tests/regressiontests/i18n/tests.py | 5 +- 8 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 tests/regressiontests/i18n/commands/locale/hr/LC_MESSAGES/django.po diff --git a/AUTHORS b/AUTHORS index 3659d8e0df..16bfa574c9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -98,6 +98,7 @@ answer newbie questions, and generally made Django that much better: Mark Biggers Paul Bissex Simon Blanchard + Craig Blaszczyk David Blewett Matt Boersma Artem Gnilov diff --git a/django/core/management/commands/compilemessages.py b/django/core/management/commands/compilemessages.py index e1d8a33332..684ef3514c 100644 --- a/django/core/management/commands/compilemessages.py +++ b/django/core/management/commands/compilemessages.py @@ -28,10 +28,14 @@ def compile_messages(stderr, locale=None): for basedir in basedirs: if locale: - basedir = os.path.join(basedir, locale, 'LC_MESSAGES') - for dirpath, dirnames, filenames in os.walk(basedir): - for f in filenames: - if f.endswith('.po'): + dirs = [os.path.join(basedir, l, 'LC_MESSAGES') for l in (locale if isinstance(locale, list) else [locale])] + else: + dirs = [basedir] + for ldir in dirs: + for dirpath, dirnames, filenames in os.walk(ldir): + for f in filenames: + if not f.endswith('.po'): + continue stderr.write('processing file %s in %s\n' % (f, dirpath)) fn = os.path.join(dirpath, f) if has_bom(fn): @@ -53,8 +57,8 @@ def compile_messages(stderr, locale=None): class Command(BaseCommand): option_list = BaseCommand.option_list + ( - make_option('--locale', '-l', dest='locale', - help='The locale to process. Default is to process all.'), + make_option('--locale', '-l', dest='locale', action='append', + help='locale(s) to process (e.g. de_AT). Default is to process all. Can be used multiple times, accepts a comma-separated list of locale names.'), ) help = 'Compiles .po files to .mo files for use with builtin gettext support.' diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 2b2755d8d1..31971a9101 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -304,7 +304,7 @@ def make_messages(locale=None, domain='django', verbosity=1, all=False, locales = [] if locale is not None: - locales.append(str(locale)) + locales += locale.split(',') if not isinstance(locale, list) else locale elif all: locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir)) locales = [os.path.basename(l) for l in locale_dirs] @@ -341,8 +341,8 @@ def make_messages(locale=None, domain='django', verbosity=1, all=False, class Command(NoArgsCommand): option_list = NoArgsCommand.option_list + ( - make_option('--locale', '-l', default=None, dest='locale', - help='Creates or updates the message files for the given locale (e.g. pt_BR).'), + make_option('--locale', '-l', default=None, dest='locale', action='append', + help='Creates or updates the message files for the given locale(s) (e.g. pt_BR). Can be used multiple times, accepts a comma-separated list of locale names.'), make_option('--domain', '-d', default='django', dest='domain', help='The domain of the message files (default: "django").'), make_option('--all', '-a', action='store_true', dest='all', diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 3c73e268e2..06ec8e2031 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -107,12 +107,21 @@ compilemessages Compiles .po files created with ``makemessages`` to .mo files for use with the builtin gettext support. See :doc:`/topics/i18n/index`. -Use the :djadminopt:`--locale` option to specify the locale to process. -If not provided, all locales are processed. +Use the :djadminopt:`--locale` option (or its shorter version ``-l``) to +specify the locale(s) to process. If not provided, all locales are processed. Example usage:: django-admin.py compilemessages --locale=pt_BR + django-admin.py compilemessages --locale=pt_BR --locale=fr + django-admin.py compilemessages -l pt_BR + django-admin.py compilemessages -l pt_BR -l fr + django-admin.py compilemessages --locale=pt_BR,fr + django-admin.py compilemessages -l pt_BR,fr + +.. versionchanged:: 1.6 + +Added the ability to specify multiple locales. createcachetable ---------------- @@ -422,11 +431,24 @@ Separate multiple extensions with commas or use -e or --extension multiple times django-admin.py makemessages --locale=de --extension=html,txt --extension xml -Use the :djadminopt:`--locale` option to specify the locale to process. +Use the :djadminopt:`--locale` option (or its shorter version ``-l``) to +specify the locale(s) to process. Example usage:: django-admin.py makemessages --locale=pt_BR + django-admin.py makemessages --locale=pt_BR --locale=fr + django-admin.py makemessages -l pt_BR + django-admin.py makemessages -l pt_BR -l fr + +You can also use commas to separate multiple locales:: + + django-admin.py makemessages --locale=de,fr,pt_BR + django-admin.py makemessages -l de,fr,pt_BR + +.. versionchanged:: 1.6 + +Added the ability to specify multiple locales. .. django-admin-option:: --domain diff --git a/tests/regressiontests/i18n/commands/compilation.py b/tests/regressiontests/i18n/commands/compilation.py index 492b0d22a3..c15b95eb0e 100644 --- a/tests/regressiontests/i18n/commands/compilation.py +++ b/tests/regressiontests/i18n/commands/compilation.py @@ -68,3 +68,34 @@ class PercentRenderingTests(MessageCompilationTests): t = Template('{% load i18n %}{% trans "Completed 50%% of all the tasks" %}') rendered = t.render(Context({})) self.assertEqual(rendered, 'IT translation of Completed 50%% of all the tasks') + + +@override_settings(LOCALE_PATHS=(os.path.join(test_dir, 'locale'),)) +class MultipleLocaleCompilationTests(MessageCompilationTests): + MO_FILE_HR = None + MO_FILE_FR = None + + def setUp(self): + super(MultipleLocaleCompilationTests, self).setUp() + self.localedir = os.path.join(test_dir, 'locale') + self.MO_FILE_HR = os.path.join(self.localedir, 'hr/LC_MESSAGES/django.mo') + self.MO_FILE_FR = os.path.join(self.localedir, 'fr/LC_MESSAGES/django.mo') + self.addCleanup(self._rmfile, os.path.join(self.localedir, self.MO_FILE_HR)) + self.addCleanup(self._rmfile, os.path.join(self.localedir, self.MO_FILE_FR)) + + def _rmfile(self, filepath): + if os.path.exists(filepath): + os.remove(filepath) + + def test_one_locale(self): + os.chdir(test_dir) + call_command('compilemessages', locale='hr', stderr=StringIO()) + + self.assertTrue(os.path.exists(self.MO_FILE_HR)) + + def test_multiple_locales(self): + os.chdir(test_dir) + call_command('compilemessages', locale=['hr', 'fr'], stderr=StringIO()) + + self.assertTrue(os.path.exists(self.MO_FILE_HR)) + self.assertTrue(os.path.exists(self.MO_FILE_FR)) diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index 575f23cfee..1d6a72d725 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -327,3 +327,30 @@ class KeepPotFileExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0, keep_pot=True) self.assertTrue(os.path.exists(self.POT_FILE)) + + +class MultipleLocaleExtractionTests(ExtractorTests): + PO_FILE_PT = 'locale/pt/LC_MESSAGES/django.po' + PO_FILE_DE = 'locale/de/LC_MESSAGES/django.po' + LOCALES = ['pt', 'de', 'ch'] + + def tearDown(self): + os.chdir(self.test_dir) + for locale in self.LOCALES: + try: + self._rmrf('locale/%s' % locale) + except OSError: + pass + os.chdir(self._cwd) + + def test_multiple_locales(self): + os.chdir(self.test_dir) + management.call_command('makemessages', locale=['pt','de'], verbosity=0) + self.assertTrue(os.path.exists(self.PO_FILE_PT)) + self.assertTrue(os.path.exists(self.PO_FILE_DE)) + + def test_comma_separated_locales(self): + os.chdir(self.test_dir) + management.call_command('makemessages', locale='pt,de,ch', verbosity=0) + self.assertTrue(os.path.exists(self.PO_FILE_PT)) + self.assertTrue(os.path.exists(self.PO_FILE_DE)) diff --git a/tests/regressiontests/i18n/commands/locale/hr/LC_MESSAGES/django.po b/tests/regressiontests/i18n/commands/locale/hr/LC_MESSAGES/django.po new file mode 100644 index 0000000000..663ca0000f --- /dev/null +++ b/tests/regressiontests/i18n/commands/locale/hr/LC_MESSAGES/django.po @@ -0,0 +1,71 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-12-04 04:59-0600\n" +"PO-Revision-Date: 2013-01-16 22:53-0300\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#. Translators: Django template comment for translators +#: templates/test.html:9 +#, python-format +msgid "I think that 100%% is more that 50%% of anything." +msgstr "" + +#: templates/test.html:10 +#, python-format +msgid "I think that 100%% is more that 50%% of %(obj)s." +msgstr "" + +#: templates/test.html:70 +#, python-format +msgid "Literal with a percent symbol at the end %%" +msgstr "" + +#: templates/test.html:71 +#, python-format +msgid "Literal with a percent %% symbol in the middle" +msgstr "" + +#: templates/test.html:72 +#, python-format +msgid "Completed 50%% of all the tasks" +msgstr "" + +#: templates/test.html:73 +#, python-format +msgctxt "ctx0" +msgid "Completed 99%% of all the tasks" +msgstr "" + +#: templates/test.html:74 +#, python-format +msgid "Shouldn't double escape this sequence: %% (two percent signs)" +msgstr "" + +#: templates/test.html:75 +#, python-format +msgctxt "ctx1" +msgid "Shouldn't double escape this sequence %% either" +msgstr "" + +#: templates/test.html:76 +#, python-format +msgid "Looks like a str fmt spec %%s but shouldn't be interpreted as such" +msgstr "Translation of the above string" + +#: templates/test.html:77 +#, python-format +msgid "Looks like a str fmt spec %% o but shouldn't be interpreted as such" +msgstr "Translation contains %% for the above string" diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index 5d789b4acb..d9843c228a 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -32,10 +32,11 @@ if can_run_extraction_tests: from .commands.extraction import (ExtractorTests, BasicExtractorTests, JavascriptExtractorTests, IgnoredExtractorTests, SymlinkExtractorTests, CopyPluralFormsExtractorTests, NoWrapExtractorTests, - NoLocationExtractorTests, KeepPotFileExtractorTests) + NoLocationExtractorTests, KeepPotFileExtractorTests, + MultipleLocaleExtractionTests) if can_run_compilation_tests: from .commands.compilation import (PoFileTests, PoFileContentsTests, - PercentRenderingTests) + PercentRenderingTests, MultipleLocaleCompilationTests) from .contenttypes.tests import ContentTypeTests from .forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm from .models import Company, TestModel From 3647c0a49a2f4535b8a9aba40e662743e4d53e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Fri, 18 Jan 2013 07:09:39 +0200 Subject: [PATCH 169/870] Avoided unnecessary recreation of RelatedObjects Refs #19399. Thanks to Track alias KJ for the patch. --- django/core/management/validation.py | 5 ++--- django/db/models/options.py | 9 ++++----- django/forms/models.py | 3 --- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/django/core/management/validation.py b/django/core/management/validation.py index c0452c5aa6..f49a3c2232 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -27,7 +27,6 @@ def get_validation_errors(outfile, app=None): """ from django.db import models, connection from django.db.models.loading import get_app_errors - from django.db.models.fields.related import RelatedObject from django.db.models.deletion import SET_NULL, SET_DEFAULT e = ModelErrorCollection(outfile) @@ -154,7 +153,7 @@ def get_validation_errors(outfile, app=None): e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.rel.field_name, f.rel.to.__name__)) rel_opts = f.rel.to._meta - rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() + rel_name = f.related.get_accessor_name() rel_query_name = f.related_query_name() if not f.rel.is_hidden(): for r in rel_opts.fields: @@ -278,7 +277,7 @@ def get_validation_errors(outfile, app=None): ) rel_opts = f.rel.to._meta - rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() + rel_name = f.related.get_accessor_name() rel_query_name = f.related_query_name() # If rel_name is none, there is no reverse accessor (this only # occurs for symmetrical m2m relations to self). If this is the diff --git a/django/db/models/options.py b/django/db/models/options.py index 45b32b0ea4..952596b514 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -4,7 +4,6 @@ import re from bisect import bisect from django.conf import settings -from django.db.models.related import RelatedObject from django.db.models.fields.related import ManyToManyRel from django.db.models.fields import AutoField, FieldDoesNotExist from django.db.models.fields.proxy import OrderWrt @@ -424,10 +423,10 @@ class Options(object): for f in klass._meta.local_fields: if f.rel and not isinstance(f.rel.to, six.string_types): if self == f.rel.to._meta: - cache[RelatedObject(f.rel.to, klass, f)] = None - proxy_cache[RelatedObject(f.rel.to, klass, f)] = None + cache[f.related] = None + proxy_cache[f.related] = None elif self.concrete_model == f.rel.to._meta.concrete_model: - proxy_cache[RelatedObject(f.rel.to, klass, f)] = None + proxy_cache[f.related] = None self._related_objects_cache = cache self._related_objects_proxy_cache = proxy_cache @@ -468,7 +467,7 @@ class Options(object): if (f.rel and not isinstance(f.rel.to, six.string_types) and self == f.rel.to._meta): - cache[RelatedObject(f.rel.to, klass, f)] = None + cache[f.related] = None if app_cache_ready(): self._related_many_to_many_cache = cache return cache diff --git a/django/forms/models.py b/django/forms/models.py index 1b6821cd5b..74886d7ae0 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -702,14 +702,11 @@ class BaseInlineFormSet(BaseModelFormSet): """A formset for child objects related to a parent.""" def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None, **kwargs): - from django.db.models.fields.related import RelatedObject if instance is None: self.instance = self.fk.rel.to() else: self.instance = instance self.save_as_new = save_as_new - # is there a better way to get the object descriptor? - self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name() if queryset is None: queryset = self.model._default_manager if self.instance.pk: From 56f34f9f225026ccd8e36b2cf917d09b8f55d393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Fri, 18 Jan 2013 15:57:17 +0200 Subject: [PATCH 170/870] Removed models/related.py BoundRelatedObject The class wasn't used anywhere except in RelatedObject.bind(), which wasn't used anywhere. The class had one method defined as NotImplemented, yet the class wasn't subclassed anywhere. In short, the class was dead code. --- django/db/models/related.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/django/db/models/related.py b/django/db/models/related.py index 702853533d..26932137ad 100644 --- a/django/db/models/related.py +++ b/django/db/models/related.py @@ -10,17 +10,6 @@ PathInfo = namedtuple('PathInfo', 'from_field to_field from_opts to_opts join_field ' 'm2m direct') -class BoundRelatedObject(object): - def __init__(self, related_object, field_mapping, original): - self.relation = related_object - self.field_mappings = field_mapping[related_object.name] - - def template_name(self): - raise NotImplementedError - - def __repr__(self): - return repr(self.__dict__) - class RelatedObject(object): def __init__(self, parent_model, model, field): self.parent_model = parent_model @@ -58,9 +47,6 @@ class RelatedObject(object): def __repr__(self): return "" % (self.name, self.field.name) - def bind(self, field_mapping, original, bound_related_object_class=BoundRelatedObject): - return bound_related_object_class(self, field_mapping, original) - def get_accessor_name(self): # This method encapsulates the logic that decides what name to give an # accessor descriptor that retrieves related many-to-one or From 1dd749284325ea8fe747a3728ed92bafef4ff6a0 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 18 Jan 2013 20:50:12 +0100 Subject: [PATCH 171/870] Fixed #19632 -- Bug in code sample. Thanks grossmanandy at bfusa com and Simon Charette. --- docs/topics/auth/default.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index d1463c645b..569738569d 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -466,7 +466,7 @@ checks to make sure the user has an email in the desired domain:: from django.contrib.auth.decorators import user_passes_test def email_check(user): - return '@example.com' in request.user.email + return '@example.com' in user.email @user_passes_test(email_check) def my_view(request): From 0375244eaeae1e2c09cc58c4c62e8f9e951217d0 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 18 Jan 2013 18:38:12 -0500 Subject: [PATCH 172/870] Fixed #19628 - Noted that app for custom user model must be in INSTALLED_APPS Thanks dpravdin and Jordan Messina. --- docs/topics/auth/customizing.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 5f48e82e2b..cf031c7b84 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -404,8 +404,9 @@ the :setting:`AUTH_USER_MODEL` setting that references a custom model:: AUTH_USER_MODEL = 'myapp.MyUser' -This dotted pair describes the name of the Django app, and the name of the Django -model that you wish to use as your User model. +This dotted pair describes the name of the Django app (which must be in your +:setting:`INSTALLED_APPS`), and the name of the Django model that you wish to +use as your User model. .. admonition:: Warning From d194f290571f7e9dda7d2fd7a6f2b171120f2f14 Mon Sep 17 00:00:00 2001 From: Jani Tiainen Date: Tue, 15 Jan 2013 15:05:58 +0200 Subject: [PATCH 173/870] Fixed #19606 -- Adjusted cx_Oracle unicode detection. --- django/db/backends/oracle/base.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index c6f072df3b..17faa17270 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -58,10 +58,11 @@ from django.utils import timezone DatabaseError = Database.DatabaseError IntegrityError = Database.IntegrityError - -# Check whether cx_Oracle was compiled with the WITH_UNICODE option. This will -# also be True in Python 3.0. -if int(Database.version.split('.', 1)[0]) >= 5 and not hasattr(Database, 'UNICODE'): +# Check whether cx_Oracle was compiled with the WITH_UNICODE option if cx_Oracle is pre-5.1. This will +# also be True for cx_Oracle 5.1 and in Python 3.0. See #19606 +if int(Database.version.split('.', 1)[0]) >= 5 and \ + (int(Database.version.split('.', 2)[1]) >= 1 or + not hasattr(Database, 'UNICODE')): convert_unicode = force_text else: convert_unicode = force_bytes From 755f215590af5a9bc70917412b28cd710318ec63 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 17 Jan 2013 13:33:04 +0100 Subject: [PATCH 174/870] Refactored makemessages command --- .../core/management/commands/makemessages.py | 634 +++++++++--------- 1 file changed, 317 insertions(+), 317 deletions(-) diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 31971a9101..72128eb931 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -9,12 +9,127 @@ from subprocess import PIPE, Popen import django from django.core.management.base import CommandError, NoArgsCommand +from django.utils.functional import total_ordering from django.utils.text import get_text_list from django.utils.jslex import prepare_js_for_gettext plural_forms_re = re.compile(r'^(?P"Plural-Forms.+?\\n")\s*$', re.MULTILINE | re.DOTALL) STATUS_OK = 0 + +@total_ordering +class TranslatableFile(object): + def __init__(self, dirpath, file_name): + self.file = file_name + self.dirpath = dirpath + + def __repr__(self): + return "" % os.sep.join([self.dirpath, self.file]) + + def __eq__(self, other): + return self.dirpath == other.dirpath and self.file == other.file + + def __lt__(self, other): + if self.dirpath == other.dirpath: + return self.file < other.file + return self.dirpath < other.dirpath + + def process(self, command, potfile, domain, keep_pot=False): + """ + Extract translatable literals from self.file for :param domain: + creating or updating the :param potfile: POT file. + + Uses the xgettext GNU gettext utility. + """ + + from django.utils.translation import templatize + + if command.verbosity > 1: + command.stdout.write('processing file %s in %s\n' % (self.file, self.dirpath)) + _, file_ext = os.path.splitext(self.file) + if domain == 'djangojs' and file_ext in command.extensions: + is_templatized = True + orig_file = os.path.join(self.dirpath, self.file) + with open(orig_file) as fp: + src_data = fp.read() + src_data = prepare_js_for_gettext(src_data) + thefile = '%s.c' % self.file + work_file = os.path.join(self.dirpath, thefile) + with open(work_file, "w") as fp: + fp.write(src_data) + cmd = ( + 'xgettext -d %s -L C %s %s --keyword=gettext_noop ' + '--keyword=gettext_lazy --keyword=ngettext_lazy:1,2 ' + '--keyword=pgettext:1c,2 --keyword=npgettext:1c,2,3 ' + '--from-code UTF-8 --add-comments=Translators -o - "%s"' % + (domain, command.wrap, command.location, work_file)) + elif domain == 'django' and (file_ext == '.py' or file_ext in command.extensions): + thefile = self.file + orig_file = os.path.join(self.dirpath, self.file) + is_templatized = file_ext in command.extensions + if is_templatized: + with open(orig_file, "rU") as fp: + src_data = fp.read() + thefile = '%s.py' % self.file + content = templatize(src_data, orig_file[2:]) + with open(os.path.join(self.dirpath, thefile), "w") as fp: + fp.write(content) + work_file = os.path.join(self.dirpath, thefile) + cmd = ( + 'xgettext -d %s -L Python %s %s --keyword=gettext_noop ' + '--keyword=gettext_lazy --keyword=ngettext_lazy:1,2 ' + '--keyword=ugettext_noop --keyword=ugettext_lazy ' + '--keyword=ungettext_lazy:1,2 --keyword=pgettext:1c,2 ' + '--keyword=npgettext:1c,2,3 --keyword=pgettext_lazy:1c,2 ' + '--keyword=npgettext_lazy:1c,2,3 --from-code UTF-8 ' + '--add-comments=Translators -o - "%s"' % + (domain, command.wrap, command.location, work_file)) + else: + return + msgs, errors, status = _popen(cmd) + if errors: + if status != STATUS_OK: + if is_templatized: + os.unlink(work_file) + if not keep_pot and os.path.exists(potfile): + os.unlink(potfile) + raise CommandError( + "errors happened while running xgettext on %s\n%s" % + (self.file, errors)) + elif command.verbosity > 0: + # Print warnings + command.stdout.write(errors) + if msgs: + if is_templatized: + old = '#: ' + work_file[2:] + new = '#: ' + orig_file[2:] + msgs = msgs.replace(old, new) + write_pot_file(potfile, msgs) + if is_templatized: + os.unlink(work_file) + + +def _popen(cmd): + """ + Friendly wrapper around Popen for Windows + """ + p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, close_fds=os.name != 'nt', universal_newlines=True) + output, errors = p.communicate() + return output, errors, p.returncode + +def write_pot_file(potfile, msgs): + """ + Write the :param potfile: POT file with the :param msgs: contents, + previously making sure its format is valid. + """ + if os.path.exists(potfile): + # Strip the header + msgs = '\n'.join(dropwhile(len, msgs.split('\n'))) + else: + msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8') + with open(potfile, 'a') as fp: + fp.write(msgs) + def handle_extensions(extensions=('html',), ignored=('py',)): """ Organizes multiple extensions that are separated with commas or passed by @@ -39,310 +154,12 @@ def handle_extensions(extensions=('html',), ignored=('py',)): ext_list[i] = '.%s' % ext_list[i] return set([x for x in ext_list if x.strip('.') not in ignored]) -def _popen(cmd): - """ - Friendly wrapper around Popen for Windows - """ - p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, close_fds=os.name != 'nt', universal_newlines=True) - output, errors = p.communicate() - return output, errors, p.returncode - -def find_files(root, ignore_patterns, verbosity, stdout=sys.stdout, symlinks=False): - """ - Helper function to get all files in the given root. - """ - dir_suffix = '%s*' % os.sep - norm_patterns = [p[:-len(dir_suffix)] if p.endswith(dir_suffix) else p for p in ignore_patterns] - all_files = [] - for dirpath, dirnames, filenames in os.walk(root, topdown=True, followlinks=symlinks): - for dirname in dirnames[:]: - if is_ignored(os.path.normpath(os.path.join(dirpath, dirname)), norm_patterns): - dirnames.remove(dirname) - if verbosity > 1: - stdout.write('ignoring directory %s\n' % dirname) - for filename in filenames: - if is_ignored(os.path.normpath(os.path.join(dirpath, filename)), ignore_patterns): - if verbosity > 1: - stdout.write('ignoring file %s in %s\n' % (filename, dirpath)) - else: - all_files.extend([(dirpath, filename)]) - all_files.sort() - return all_files - -def is_ignored(path, ignore_patterns): - """ - Helper function to check if the given path should be ignored or not. - """ - for pattern in ignore_patterns: - if fnmatch.fnmatchcase(path, pattern): - return True - return False - -def copy_plural_forms(msgs, locale, domain, verbosity, stdout=sys.stdout): - """ - Copies plural forms header contents from a Django catalog of locale to - the msgs string, inserting it at the right place. msgs should be the - contents of a newly created .po file. - """ - django_dir = os.path.normpath(os.path.join(os.path.dirname(django.__file__))) - if domain == 'djangojs': - domains = ('djangojs', 'django') - else: - domains = ('django',) - for domain in domains: - django_po = os.path.join(django_dir, 'conf', 'locale', locale, 'LC_MESSAGES', '%s.po' % domain) - if os.path.exists(django_po): - with open(django_po, 'rU') as fp: - m = plural_forms_re.search(fp.read()) - if m: - if verbosity > 1: - stdout.write("copying plural forms: %s\n" % m.group('value')) - lines = [] - seen = False - for line in msgs.split('\n'): - if not line and not seen: - line = '%s\n' % m.group('value') - seen = True - lines.append(line) - msgs = '\n'.join(lines) - break - return msgs - -def write_pot_file(potfile, msgs, file, work_file, is_templatized): - """ - Write the :param potfile: POT file with the :param msgs: contents, - previously making sure its format is valid. - """ - if is_templatized: - old = '#: ' + work_file[2:] - new = '#: ' + file[2:] - msgs = msgs.replace(old, new) - if os.path.exists(potfile): - # Strip the header - msgs = '\n'.join(dropwhile(len, msgs.split('\n'))) - else: - msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8') - with open(potfile, 'a') as fp: - fp.write(msgs) - -def process_file(file, dirpath, potfile, domain, verbosity, - extensions, wrap, location, keep_pot, stdout=sys.stdout): - """ - Extract translatable literals from :param file: for :param domain: - creating or updating the :param potfile: POT file. - - Uses the xgettext GNU gettext utility. - """ - - from django.utils.translation import templatize - - if verbosity > 1: - stdout.write('processing file %s in %s\n' % (file, dirpath)) - _, file_ext = os.path.splitext(file) - if domain == 'djangojs' and file_ext in extensions: - is_templatized = True - orig_file = os.path.join(dirpath, file) - with open(orig_file) as fp: - src_data = fp.read() - src_data = prepare_js_for_gettext(src_data) - thefile = '%s.c' % file - work_file = os.path.join(dirpath, thefile) - with open(work_file, "w") as fp: - fp.write(src_data) - cmd = ( - 'xgettext -d %s -L C %s %s --keyword=gettext_noop ' - '--keyword=gettext_lazy --keyword=ngettext_lazy:1,2 ' - '--keyword=pgettext:1c,2 --keyword=npgettext:1c,2,3 ' - '--from-code UTF-8 --add-comments=Translators -o - "%s"' % - (domain, wrap, location, work_file)) - elif domain == 'django' and (file_ext == '.py' or file_ext in extensions): - thefile = file - orig_file = os.path.join(dirpath, file) - is_templatized = file_ext in extensions - if is_templatized: - with open(orig_file, "rU") as fp: - src_data = fp.read() - thefile = '%s.py' % file - content = templatize(src_data, orig_file[2:]) - with open(os.path.join(dirpath, thefile), "w") as fp: - fp.write(content) - work_file = os.path.join(dirpath, thefile) - cmd = ( - 'xgettext -d %s -L Python %s %s --keyword=gettext_noop ' - '--keyword=gettext_lazy --keyword=ngettext_lazy:1,2 ' - '--keyword=ugettext_noop --keyword=ugettext_lazy ' - '--keyword=ungettext_lazy:1,2 --keyword=pgettext:1c,2 ' - '--keyword=npgettext:1c,2,3 --keyword=pgettext_lazy:1c,2 ' - '--keyword=npgettext_lazy:1c,2,3 --from-code UTF-8 ' - '--add-comments=Translators -o - "%s"' % - (domain, wrap, location, work_file)) - else: - return - msgs, errors, status = _popen(cmd) - if errors: - if status != STATUS_OK: - if is_templatized: - os.unlink(work_file) - if not keep_pot and os.path.exists(potfile): - os.unlink(potfile) - raise CommandError( - "errors happened while running xgettext on %s\n%s" % - (file, errors)) - elif verbosity > 0: - # Print warnings - stdout.write(errors) - if msgs: - write_pot_file(potfile, msgs, orig_file, work_file, is_templatized) - if is_templatized: - os.unlink(work_file) - -def write_po_file(pofile, potfile, domain, locale, verbosity, stdout, - copy_pforms, wrap, location, no_obsolete, keep_pot): - """ - Creates of updates the :param pofile: PO file for :param domain: and :param - locale:. Uses contents of the existing :param potfile:. - - Uses mguniq, msgmerge, and msgattrib GNU gettext utilities. - """ - msgs, errors, status = _popen('msguniq %s %s --to-code=utf-8 "%s"' % - (wrap, location, potfile)) - if errors: - if status != STATUS_OK: - if not keep_pot: - os.unlink(potfile) - raise CommandError( - "errors happened while running msguniq\n%s" % errors) - elif verbosity > 0: - stdout.write(errors) - - if os.path.exists(pofile): - with open(potfile, 'w') as fp: - fp.write(msgs) - msgs, errors, status = _popen('msgmerge %s %s -q "%s" "%s"' % - (wrap, location, pofile, potfile)) - if errors: - if status != STATUS_OK: - if not keep_pot: - os.unlink(potfile) - raise CommandError( - "errors happened while running msgmerge\n%s" % errors) - elif verbosity > 0: - stdout.write(errors) - elif copy_pforms: - msgs = copy_plural_forms(msgs, locale, domain, verbosity, stdout) - msgs = msgs.replace( - "#. #-#-#-#-# %s.pot (PACKAGE VERSION) #-#-#-#-#\n" % domain, "") - with open(pofile, 'w') as fp: - fp.write(msgs) - if no_obsolete: - msgs, errors, status = _popen( - 'msgattrib %s %s -o "%s" --no-obsolete "%s"' % - (wrap, location, pofile, pofile)) - if errors: - if status != STATUS_OK: - raise CommandError( - "errors happened while running msgattrib\n%s" % errors) - elif verbosity > 0: - stdout.write(errors) - -def make_messages(locale=None, domain='django', verbosity=1, all=False, - extensions=None, symlinks=False, ignore_patterns=None, no_wrap=False, - no_location=False, no_obsolete=False, stdout=sys.stdout, keep_pot=False): - """ - Uses the ``locale/`` directory from the Django Git tree or an - application/project to process all files with translatable literals for - the :param domain: domain and :param locale: locale. - """ - # Need to ensure that the i18n framework is enabled - from django.conf import settings - if settings.configured: - settings.USE_I18N = True - else: - settings.configure(USE_I18N = True) - - if ignore_patterns is None: - ignore_patterns = [] - - invoked_for_django = False - if os.path.isdir(os.path.join('conf', 'locale')): - localedir = os.path.abspath(os.path.join('conf', 'locale')) - invoked_for_django = True - # Ignoring all contrib apps - ignore_patterns += ['contrib/*'] - elif os.path.isdir('locale'): - localedir = os.path.abspath('locale') - else: - raise CommandError("This script should be run from the Django Git " - "tree or your project or app tree. If you did indeed run it " - "from the Git checkout or your project or application, " - "maybe you are just missing the conf/locale (in the django " - "tree) or locale (for project and application) directory? It " - "is not created automatically, you have to create it by hand " - "if you want to enable i18n for your project or application.") - - if domain not in ('django', 'djangojs'): - raise CommandError("currently makemessages only supports domains " - "'django' and 'djangojs'") - - if (locale is None and not all) or domain is None: - message = "Type '%s help %s' for usage information." % ( - os.path.basename(sys.argv[0]), sys.argv[1]) - raise CommandError(message) - - # We require gettext version 0.15 or newer. - output, errors, status = _popen('xgettext --version') - if status != STATUS_OK: - raise CommandError("Error running xgettext. Note that Django " - "internationalization requires GNU gettext 0.15 or newer.") - match = re.search(r'(?P\d+)\.(?P\d+)', output) - if match: - xversion = (int(match.group('major')), int(match.group('minor'))) - if xversion < (0, 15): - raise CommandError("Django internationalization requires GNU " - "gettext 0.15 or newer. You are using version %s, please " - "upgrade your gettext toolset." % match.group()) - - locales = [] - if locale is not None: - locales += locale.split(',') if not isinstance(locale, list) else locale - elif all: - locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir)) - locales = [os.path.basename(l) for l in locale_dirs] - - wrap = '--no-wrap' if no_wrap else '' - location = '--no-location' if no_location else '' - - potfile = os.path.join(localedir, '%s.pot' % str(domain)) - - if os.path.exists(potfile): - os.unlink(potfile) - - for dirpath, file in find_files(".", ignore_patterns, verbosity, - stdout, symlinks=symlinks): - process_file(file, dirpath, potfile, domain, verbosity, extensions, - wrap, location, keep_pot, stdout) - - for locale in locales: - if verbosity > 0: - stdout.write("processing language %s\n" % locale) - basedir = os.path.join(localedir, locale, 'LC_MESSAGES') - if not os.path.isdir(basedir): - os.makedirs(basedir) - - pofile = os.path.join(basedir, '%s.po' % str(domain)) - - if os.path.exists(potfile): - write_po_file(pofile, potfile, domain, locale, verbosity, stdout, - not invoked_for_django, wrap, location, no_obsolete, keep_pot) - - if not keep_pot: - os.unlink(potfile) - class Command(NoArgsCommand): option_list = NoArgsCommand.option_list + ( make_option('--locale', '-l', default=None, dest='locale', action='append', - help='Creates or updates the message files for the given locale(s) (e.g. pt_BR). Can be used multiple times, accepts a comma-separated list of locale names.'), + help='Creates or updates the message files for the given locale(s) (e.g. pt_BR). ' + 'Can be used multiple times, accepts a comma-separated list of locale names.'), make_option('--domain', '-d', default='django', dest='domain', help='The domain of the message files (default: "django").'), make_option('--all', '-a', action='store_true', dest='all', @@ -355,7 +172,7 @@ class Command(NoArgsCommand): make_option('--ignore', '-i', action='append', dest='ignore_patterns', default=[], metavar='PATTERN', help='Ignore files or directories matching this glob-style pattern. Use multiple times to ignore more.'), make_option('--no-default-ignore', action='store_false', dest='use_default_ignore_patterns', - default=True, help="Don't ignore the common glob-style patterns 'CVS', '.*' and '*~'."), + default=True, help="Don't ignore the common glob-style patterns 'CVS', '.*', '*~' and '*.pyc'."), make_option('--no-wrap', action='store_true', dest='no_wrap', default=False, help="Don't break long message lines into several lines"), make_option('--no-location', action='store_true', dest='no_location', @@ -376,29 +193,212 @@ class Command(NoArgsCommand): def handle_noargs(self, *args, **options): locale = options.get('locale') - domain = options.get('domain') - verbosity = int(options.get('verbosity')) + self.domain = options.get('domain') + self.verbosity = int(options.get('verbosity')) process_all = options.get('all') extensions = options.get('extensions') - symlinks = options.get('symlinks') + self.symlinks = options.get('symlinks') ignore_patterns = options.get('ignore_patterns') if options.get('use_default_ignore_patterns'): - ignore_patterns += ['CVS', '.*', '*~'] - ignore_patterns = list(set(ignore_patterns)) - no_wrap = options.get('no_wrap') - no_location = options.get('no_location') - no_obsolete = options.get('no_obsolete') - keep_pot = options.get('keep_pot') - if domain == 'djangojs': + ignore_patterns += ['CVS', '.*', '*~', '*.pyc'] + self.ignore_patterns = list(set(ignore_patterns)) + self.wrap = '--no-wrap' if options.get('no_wrap') else '' + self.location = '--no-location' if options.get('no_location') else '' + self.no_obsolete = options.get('no_obsolete') + self.keep_pot = options.get('keep_pot') + + if self.domain not in ('django', 'djangojs'): + raise CommandError("currently makemessages only supports domains " + "'django' and 'djangojs'") + if self.domain == 'djangojs': exts = extensions if extensions else ['js'] else: exts = extensions if extensions else ['html', 'txt'] - extensions = handle_extensions(exts) + self.extensions = handle_extensions(exts) - if verbosity > 1: + if (locale is None and not process_all) or self.domain is None: + raise CommandError("Type '%s help %s' for usage information." % ( + os.path.basename(sys.argv[0]), sys.argv[1])) + + if self.verbosity > 1: self.stdout.write('examining files with the extensions: %s\n' - % get_text_list(list(extensions), 'and')) + % get_text_list(list(self.extensions), 'and')) - make_messages(locale, domain, verbosity, process_all, extensions, - symlinks, ignore_patterns, no_wrap, no_location, - no_obsolete, self.stdout, keep_pot) + # Need to ensure that the i18n framework is enabled + from django.conf import settings + if settings.configured: + settings.USE_I18N = True + else: + settings.configure(USE_I18N = True) + + self.invoked_for_django = False + if os.path.isdir(os.path.join('conf', 'locale')): + localedir = os.path.abspath(os.path.join('conf', 'locale')) + self.invoked_for_django = True + # Ignoring all contrib apps + self.ignore_patterns += ['contrib/*'] + elif os.path.isdir('locale'): + localedir = os.path.abspath('locale') + else: + raise CommandError("This script should be run from the Django Git " + "tree or your project or app tree. If you did indeed run it " + "from the Git checkout or your project or application, " + "maybe you are just missing the conf/locale (in the django " + "tree) or locale (for project and application) directory? It " + "is not created automatically, you have to create it by hand " + "if you want to enable i18n for your project or application.") + + # We require gettext version 0.15 or newer. + output, errors, status = _popen('xgettext --version') + if status != STATUS_OK: + raise CommandError("Error running xgettext. Note that Django " + "internationalization requires GNU gettext 0.15 or newer.") + match = re.search(r'(?P\d+)\.(?P\d+)', output) + if match: + xversion = (int(match.group('major')), int(match.group('minor'))) + if xversion < (0, 15): + raise CommandError("Django internationalization requires GNU " + "gettext 0.15 or newer. You are using version %s, please " + "upgrade your gettext toolset." % match.group()) + + potfile = self.build_pot_file(localedir) + + # Build po files for each selected locale + locales = [] + if locale is not None: + locales += locale.split(',') if not isinstance(locale, list) else locale + elif process_all: + locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir)) + locales = [os.path.basename(l) for l in locale_dirs] + + try: + for locale in locales: + if self.verbosity > 0: + self.stdout.write("processing language %s\n" % locale) + self.write_po_file(potfile, locale) + finally: + if not self.keep_pot and os.path.exists(potfile): + os.unlink(potfile) + + def build_pot_file(self, localedir): + file_list = self.find_files(".") + + potfile = os.path.join(localedir, '%s.pot' % str(self.domain)) + if os.path.exists(potfile): + # Remove a previous undeleted potfile, if any + os.unlink(potfile) + + for f in file_list: + f.process(self, potfile, self.domain, self.keep_pot) + return potfile + + def find_files(self, root): + """ + Helper method to get all files in the given root. + """ + + def is_ignored(path, ignore_patterns): + """ + Check if the given path should be ignored or not. + """ + for pattern in ignore_patterns: + if fnmatch.fnmatchcase(path, pattern): + return True + return False + + dir_suffix = '%s*' % os.sep + norm_patterns = [p[:-len(dir_suffix)] if p.endswith(dir_suffix) else p for p in self.ignore_patterns] + all_files = [] + for dirpath, dirnames, filenames in os.walk(root, topdown=True, followlinks=self.symlinks): + for dirname in dirnames[:]: + if is_ignored(os.path.normpath(os.path.join(dirpath, dirname)), norm_patterns): + dirnames.remove(dirname) + if self.verbosity > 1: + self.stdout.write('ignoring directory %s\n' % dirname) + for filename in filenames: + if is_ignored(os.path.normpath(os.path.join(dirpath, filename)), self.ignore_patterns): + if self.verbosity > 1: + self.stdout.write('ignoring file %s in %s\n' % (filename, dirpath)) + else: + all_files.append(TranslatableFile(dirpath, filename)) + return sorted(all_files) + + def write_po_file(self, potfile, locale): + """ + Creates or updates the PO file for self.domain and :param locale:. + Uses contents of the existing :param potfile:. + + Uses mguniq, msgmerge, and msgattrib GNU gettext utilities. + """ + msgs, errors, status = _popen('msguniq %s %s --to-code=utf-8 "%s"' % + (self.wrap, self.location, potfile)) + if errors: + if status != STATUS_OK: + raise CommandError( + "errors happened while running msguniq\n%s" % errors) + elif self.verbosity > 0: + self.stdout.write(errors) + + basedir = os.path.join(os.path.dirname(potfile), locale, 'LC_MESSAGES') + if not os.path.isdir(basedir): + os.makedirs(basedir) + pofile = os.path.join(basedir, '%s.po' % str(self.domain)) + + if os.path.exists(pofile): + with open(potfile, 'w') as fp: + fp.write(msgs) + msgs, errors, status = _popen('msgmerge %s %s -q "%s" "%s"' % + (self.wrap, self.location, pofile, potfile)) + if errors: + if status != STATUS_OK: + raise CommandError( + "errors happened while running msgmerge\n%s" % errors) + elif self.verbosity > 0: + self.stdout.write(errors) + elif not self.invoked_for_django: + msgs = self.copy_plural_forms(msgs, locale) + msgs = msgs.replace( + "#. #-#-#-#-# %s.pot (PACKAGE VERSION) #-#-#-#-#\n" % self.domain, "") + with open(pofile, 'w') as fp: + fp.write(msgs) + + if self.no_obsolete: + msgs, errors, status = _popen( + 'msgattrib %s %s -o "%s" --no-obsolete "%s"' % + (wrap, location, pofile, pofile)) + if errors: + if status != STATUS_OK: + raise CommandError( + "errors happened while running msgattrib\n%s" % errors) + elif self.verbosity > 0: + self.stdout.write(errors) + + def copy_plural_forms(self, msgs, locale): + """ + Copies plural forms header contents from a Django catalog of locale to + the msgs string, inserting it at the right place. msgs should be the + contents of a newly created .po file. + """ + django_dir = os.path.normpath(os.path.join(os.path.dirname(django.__file__))) + if self.domain == 'djangojs': + domains = ('djangojs', 'django') + else: + domains = ('django',) + for domain in domains: + django_po = os.path.join(django_dir, 'conf', 'locale', locale, 'LC_MESSAGES', '%s.po' % domain) + if os.path.exists(django_po): + with open(django_po, 'rU') as fp: + m = plural_forms_re.search(fp.read()) + if m: + if self.verbosity > 1: + self.stdout.write("copying plural forms: %s\n" % m.group('value')) + lines = [] + seen = False + for line in msgs.split('\n'): + if not line and not seen: + line = '%s\n' % m.group('value') + seen = True + lines.append(line) + msgs = '\n'.join(lines) + break + return msgs From 37718eb50b60f44462a64f735072e43a9d60b9a6 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 19 Jan 2013 12:06:52 -0300 Subject: [PATCH 175/870] Fix in makemessages refactoring plus UI tweaks. --- django/core/management/commands/makemessages.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 72128eb931..4550605af2 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -174,11 +174,11 @@ class Command(NoArgsCommand): make_option('--no-default-ignore', action='store_false', dest='use_default_ignore_patterns', default=True, help="Don't ignore the common glob-style patterns 'CVS', '.*', '*~' and '*.pyc'."), make_option('--no-wrap', action='store_true', dest='no_wrap', - default=False, help="Don't break long message lines into several lines"), + default=False, help="Don't break long message lines into several lines."), make_option('--no-location', action='store_true', dest='no_location', - default=False, help="Don't write '#: filename:line' lines"), + default=False, help="Don't write '#: filename:line' lines."), make_option('--no-obsolete', action='store_true', dest='no_obsolete', - default=False, help="Remove obsolete message strings"), + default=False, help="Remove obsolete message strings."), make_option('--keep-pot', action='store_true', dest='keep_pot', default=False, help="Keep .pot file after making messages. Useful when debugging."), ) @@ -274,7 +274,7 @@ class Command(NoArgsCommand): try: for locale in locales: if self.verbosity > 0: - self.stdout.write("processing language %s\n" % locale) + self.stdout.write("processing locale %s\n" % locale) self.write_po_file(potfile, locale) finally: if not self.keep_pot and os.path.exists(potfile): @@ -365,7 +365,7 @@ class Command(NoArgsCommand): if self.no_obsolete: msgs, errors, status = _popen( 'msgattrib %s %s -o "%s" --no-obsolete "%s"' % - (wrap, location, pofile, pofile)) + (self.wrap, self.location, pofile, pofile)) if errors: if status != STATUS_OK: raise CommandError( From fe54377dae1357a7f102d72614a13f0ef8b2dbdf Mon Sep 17 00:00:00 2001 From: Nick Sandford Date: Sat, 12 Jan 2013 16:37:19 +0800 Subject: [PATCH 176/870] Fixed #17813 -- Added a .earliest() method to QuerySet Thanks a lot to everybody participating in developing this feature. The patch was developed by multiple people, at least Trac aliases tonnzor, jimmysong, Fandekasp and slurms. Stylistic changes added by committer. --- django/db/models/manager.py | 3 + django/db/models/query.py | 22 +++- docs/ref/models/options.txt | 3 +- docs/ref/models/querysets.txt | 21 ++- docs/releases/1.6.txt | 3 + .../__init__.py | 0 .../models.py | 8 +- .../get_earliest_or_latest/tests.py | 123 ++++++++++++++++++ tests/modeltests/get_latest/tests.py | 58 --------- 9 files changed, 164 insertions(+), 77 deletions(-) rename tests/modeltests/{get_latest => get_earliest_or_latest}/__init__.py (100%) rename tests/modeltests/{get_latest => get_earliest_or_latest}/models.py (83%) create mode 100644 tests/modeltests/get_earliest_or_latest/tests.py delete mode 100644 tests/modeltests/get_latest/tests.py diff --git a/django/db/models/manager.py b/django/db/models/manager.py index da6523c89a..816f6194e3 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -172,6 +172,9 @@ class Manager(object): def iterator(self, *args, **kwargs): return self.get_query_set().iterator(*args, **kwargs) + def earliest(self, *args, **kwargs): + return self.get_query_set().earliest(*args, **kwargs) + def latest(self, *args, **kwargs): return self.get_query_set().latest(*args, **kwargs) diff --git a/django/db/models/query.py b/django/db/models/query.py index bdb6d48adc..1c9a68a677 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -29,6 +29,7 @@ REPR_OUTPUT_SIZE = 20 # Pull into this namespace for backwards compatibility. EmptyResultSet = sql.EmptyResultSet + class QuerySet(object): """ Represents a lazy database lookup for a set of objects. @@ -487,21 +488,28 @@ class QuerySet(object): # Re-raise the IntegrityError with its original traceback. six.reraise(*exc_info) - def latest(self, field_name=None): + def _earliest_or_latest(self, field_name=None, direction="-"): """ - Returns the latest object, according to the model's 'get_latest_by' - option or optional given field_name. + Returns the latest object, according to the model's + 'get_latest_by' option or optional given field_name. """ - latest_by = field_name or self.model._meta.get_latest_by - assert bool(latest_by), "latest() requires either a field_name parameter or 'get_latest_by' in the model" + order_by = field_name or getattr(self.model._meta, 'get_latest_by') + assert bool(order_by), "earliest() and latest() require either a "\ + "field_name parameter or 'get_latest_by' in the model" assert self.query.can_filter(), \ - "Cannot change a query once a slice has been taken." + "Cannot change a query once a slice has been taken." obj = self._clone() obj.query.set_limits(high=1) obj.query.clear_ordering() - obj.query.add_ordering('-%s' % latest_by) + obj.query.add_ordering('%s%s' % (direction, order_by)) return obj.get() + def earliest(self, field_name=None): + return self._earliest_or_latest(field_name=field_name, direction="") + + def latest(self, field_name=None): + return self._earliest_or_latest(field_name=field_name, direction="-") + def in_bulk(self, id_list): """ Returns a dictionary mapping each of the given IDs to the object with diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index b349197a5b..21265d6313 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -86,7 +86,8 @@ Django quotes column and table names behind the scenes. The name of an orderable field in the model, typically a :class:`DateField`, :class:`DateTimeField`, or :class:`IntegerField`. This specifies the default field to use in your model :class:`Manager`'s - :meth:`~django.db.models.query.QuerySet.latest` method. + :meth:`~django.db.models.query.QuerySet.latest` and + :meth:`~django.db.models.query.QuerySet.earliest` methods. Example:: diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 71049703c9..1f59ecb4f4 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1477,14 +1477,23 @@ This example returns the latest ``Entry`` in the table, according to the If your model's :ref:`Meta ` specifies :attr:`~django.db.models.Options.get_latest_by`, you can leave off the -``field_name`` argument to ``latest()``. Django will use the field specified -in :attr:`~django.db.models.Options.get_latest_by` by default. +``field_name`` argument to ``earliest()`` or ``latest()``. Django will use the +field specified in :attr:`~django.db.models.Options.get_latest_by` by default. -Like :meth:`get()`, ``latest()`` raises -:exc:`~django.core.exceptions.DoesNotExist` if there is no object with the given -parameters. +Like :meth:`get()`, ``earliest()`` and ``latest()`` raise +:exc:`~django.core.exceptions.DoesNotExist` if there is no object with the +given parameters. -Note ``latest()`` exists purely for convenience and readability. +Note that ``earliest()`` and ``latest()`` exist purely for convenience and +readability. + +earliest +~~~~~~~~ + +.. method:: earliest(field_name=None) + +Works otherwise like :meth:`~django.db.models.query.QuerySet.latest` except +the direction is changed. aggregate ~~~~~~~~~ diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index dcf6f2604a..89d7bb3c05 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -28,6 +28,9 @@ Minor features undefined if the given ``QuerySet`` isn't ordered and there are more than one ordered values to compare against. +* Added :meth:`~django.db.models.query.QuerySet.earliest` for symmetry with + :meth:`~django.db.models.query.QuerySet.latest`. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/modeltests/get_latest/__init__.py b/tests/modeltests/get_earliest_or_latest/__init__.py similarity index 100% rename from tests/modeltests/get_latest/__init__.py rename to tests/modeltests/get_earliest_or_latest/__init__.py diff --git a/tests/modeltests/get_latest/models.py b/tests/modeltests/get_earliest_or_latest/models.py similarity index 83% rename from tests/modeltests/get_latest/models.py rename to tests/modeltests/get_earliest_or_latest/models.py index fe594dd802..2453eaaccd 100644 --- a/tests/modeltests/get_latest/models.py +++ b/tests/modeltests/get_earliest_or_latest/models.py @@ -9,10 +9,8 @@ farthest into the future." """ from django.db import models -from django.utils.encoding import python_2_unicode_compatible -@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() @@ -20,15 +18,15 @@ class Article(models.Model): class Meta: get_latest_by = 'pub_date' - def __str__(self): + def __unicode__(self): return self.headline -@python_2_unicode_compatible + class Person(models.Model): name = models.CharField(max_length=30) birthday = models.DateField() # Note that this model doesn't have "get_latest_by" set. - def __str__(self): + def __unicode__(self): return self.name diff --git a/tests/modeltests/get_earliest_or_latest/tests.py b/tests/modeltests/get_earliest_or_latest/tests.py new file mode 100644 index 0000000000..6317a0974c --- /dev/null +++ b/tests/modeltests/get_earliest_or_latest/tests.py @@ -0,0 +1,123 @@ +from __future__ import absolute_import + +from datetime import datetime + +from django.test import TestCase + +from .models import Article, Person + + +class EarliestOrLatestTests(TestCase): + """Tests for the earliest() and latest() objects methods""" + + def tearDown(self): + """Makes sure Article has a get_latest_by""" + if not Article._meta.get_latest_by: + Article._meta.get_latest_by = 'pub_date' + + def test_earliest(self): + # Because no Articles exist yet, earliest() raises ArticleDoesNotExist. + self.assertRaises(Article.DoesNotExist, Article.objects.earliest) + + a1 = Article.objects.create( + headline="Article 1", pub_date=datetime(2005, 7, 26), + expire_date=datetime(2005, 9, 1) + ) + a2 = Article.objects.create( + headline="Article 2", pub_date=datetime(2005, 7, 27), + expire_date=datetime(2005, 7, 28) + ) + a3 = Article.objects.create( + headline="Article 3", pub_date=datetime(2005, 7, 28), + expire_date=datetime(2005, 8, 27) + ) + a4 = Article.objects.create( + headline="Article 4", pub_date=datetime(2005, 7, 28), + expire_date=datetime(2005, 7, 30) + ) + + # Get the earliest Article. + self.assertEqual(Article.objects.earliest(), a1) + # Get the earliest Article that matches certain filters. + self.assertEqual( + Article.objects.filter(pub_date__gt=datetime(2005, 7, 26)).earliest(), + a2 + ) + + # Pass a custom field name to earliest() to change the field that's used + # to determine the earliest object. + self.assertEqual(Article.objects.earliest('expire_date'), a2) + self.assertEqual(Article.objects.filter( + pub_date__gt=datetime(2005, 7, 26)).earliest('expire_date'), a2) + + # Ensure that earliest() overrides any other ordering specified on the + # query. Refs #11283. + self.assertEqual(Article.objects.order_by('id').earliest(), a1) + + # Ensure that error is raised if the user forgot to add a get_latest_by + # in the Model.Meta + Article.objects.model._meta.get_latest_by = None + self.assertRaisesMessage( + AssertionError, + "earliest() and latest() require either a field_name parameter or " + "'get_latest_by' in the model", + lambda: Article.objects.earliest(), + ) + + def test_latest(self): + # Because no Articles exist yet, latest() raises ArticleDoesNotExist. + self.assertRaises(Article.DoesNotExist, Article.objects.latest) + + a1 = Article.objects.create( + headline="Article 1", pub_date=datetime(2005, 7, 26), + expire_date=datetime(2005, 9, 1) + ) + a2 = Article.objects.create( + headline="Article 2", pub_date=datetime(2005, 7, 27), + expire_date=datetime(2005, 7, 28) + ) + a3 = Article.objects.create( + headline="Article 3", pub_date=datetime(2005, 7, 27), + expire_date=datetime(2005, 8, 27) + ) + a4 = Article.objects.create( + headline="Article 4", pub_date=datetime(2005, 7, 28), + expire_date=datetime(2005, 7, 30) + ) + + # Get the latest Article. + self.assertEqual(Article.objects.latest(), a4) + # Get the latest Article that matches certain filters. + self.assertEqual( + Article.objects.filter(pub_date__lt=datetime(2005, 7, 27)).latest(), + a1 + ) + + # Pass a custom field name to latest() to change the field that's used + # to determine the latest object. + self.assertEqual(Article.objects.latest('expire_date'), a1) + self.assertEqual( + Article.objects.filter(pub_date__gt=datetime(2005, 7, 26)).latest('expire_date'), + a3, + ) + + # Ensure that latest() overrides any other ordering specified on the query. Refs #11283. + self.assertEqual(Article.objects.order_by('id').latest(), a4) + + # Ensure that error is raised if the user forgot to add a get_latest_by + # in the Model.Meta + Article.objects.model._meta.get_latest_by = None + self.assertRaisesMessage( + AssertionError, + "earliest() and latest() require either a field_name parameter or " + "'get_latest_by' in the model", + lambda: Article.objects.latest(), + ) + + def test_latest_manual(self): + # You can still use latest() with a model that doesn't have + # "get_latest_by" set -- just pass in the field name manually. + p1 = Person.objects.create(name="Ralph", birthday=datetime(1950, 1, 1)) + p2 = Person.objects.create(name="Stephanie", birthday=datetime(1960, 2, 3)) + self.assertRaises(AssertionError, Person.objects.latest) + self.assertEqual(Person.objects.latest("birthday"), p2) diff --git a/tests/modeltests/get_latest/tests.py b/tests/modeltests/get_latest/tests.py deleted file mode 100644 index 948af6045a..0000000000 --- a/tests/modeltests/get_latest/tests.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import absolute_import - -from datetime import datetime - -from django.test import TestCase - -from .models import Article, Person - - -class LatestTests(TestCase): - def test_latest(self): - # Because no Articles exist yet, latest() raises ArticleDoesNotExist. - self.assertRaises(Article.DoesNotExist, Article.objects.latest) - - a1 = Article.objects.create( - headline="Article 1", pub_date=datetime(2005, 7, 26), - expire_date=datetime(2005, 9, 1) - ) - a2 = Article.objects.create( - headline="Article 2", pub_date=datetime(2005, 7, 27), - expire_date=datetime(2005, 7, 28) - ) - a3 = Article.objects.create( - headline="Article 3", pub_date=datetime(2005, 7, 27), - expire_date=datetime(2005, 8, 27) - ) - a4 = Article.objects.create( - headline="Article 4", pub_date=datetime(2005, 7, 28), - expire_date=datetime(2005, 7, 30) - ) - - # Get the latest Article. - self.assertEqual(Article.objects.latest(), a4) - # Get the latest Article that matches certain filters. - self.assertEqual( - Article.objects.filter(pub_date__lt=datetime(2005, 7, 27)).latest(), - a1 - ) - - # Pass a custom field name to latest() to change the field that's used - # to determine the latest object. - self.assertEqual(Article.objects.latest('expire_date'), a1) - self.assertEqual( - Article.objects.filter(pub_date__gt=datetime(2005, 7, 26)).latest('expire_date'), - a3, - ) - - # Ensure that latest() overrides any other ordering specified on the query. Refs #11283. - self.assertEqual(Article.objects.order_by('id').latest(), a4) - - def test_latest_manual(self): - # You can still use latest() with a model that doesn't have - # "get_latest_by" set -- just pass in the field name manually. - p1 = Person.objects.create(name="Ralph", birthday=datetime(1950, 1, 1)) - p2 = Person.objects.create(name="Stephanie", birthday=datetime(1960, 2, 3)) - self.assertRaises(AssertionError, Person.objects.latest) - - self.assertEqual(Person.objects.latest("birthday"), p2) From f96c86b02943009d4c2e01d8e4457db040723a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sun, 20 Jan 2013 06:45:00 +0200 Subject: [PATCH 177/870] Added missing versionadded 1.6 to docs of earliest() Refs #17813 --- docs/ref/models/querysets.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 1f59ecb4f4..171c2d3dcd 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1492,6 +1492,8 @@ earliest .. method:: earliest(field_name=None) +.. versionadded:: 1.6 + Works otherwise like :meth:`~django.db.models.query.QuerySet.latest` except the direction is changed. From 7aa538357c8d94df3d5811706bf6dfe5d21421ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sun, 20 Jan 2013 08:53:45 +0200 Subject: [PATCH 178/870] Cleaned up testing models.py added for earliest() The main cleanup was removal of non-necessary __unicode__ method. The tests didn't break on py3 as the string representation was never used in the tests. Refs #17813. Thanks to Simon Charette for spotting this issue. --- .../get_earliest_or_latest/models.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/tests/modeltests/get_earliest_or_latest/models.py b/tests/modeltests/get_earliest_or_latest/models.py index 2453eaaccd..cd62cf0104 100644 --- a/tests/modeltests/get_earliest_or_latest/models.py +++ b/tests/modeltests/get_earliest_or_latest/models.py @@ -1,13 +1,3 @@ -""" -8. get_latest_by - -Models can have a ``get_latest_by`` attribute, which should be set to the name -of a ``DateField`` or ``DateTimeField``. If ``get_latest_by`` exists, the -model's manager will get a ``latest()`` method, which will return the latest -object in the database according to that field. "Latest" means "having the date -farthest into the future." -""" - from django.db import models @@ -15,18 +5,12 @@ class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() expire_date = models.DateField() + class Meta: get_latest_by = 'pub_date' - def __unicode__(self): - return self.headline - class Person(models.Model): name = models.CharField(max_length=30) birthday = models.DateField() - # Note that this model doesn't have "get_latest_by" set. - - def __unicode__(self): - return self.name From c6e0dedbdb22f4db6577886a2b67e5423684fe7f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 21 Jan 2013 20:27:38 +0100 Subject: [PATCH 179/870] Fixed #19637 -- Ensured AdminEmailHandler fails silently Thanks lsaffre for the report. Refs #19325. --- django/utils/log.py | 2 +- tests/regressiontests/logging_tests/tests.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/django/utils/log.py b/django/utils/log.py index 292bd0794c..b291b86706 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -117,7 +117,7 @@ class AdminEmailHandler(logging.Handler): connection=self.connection()) def connection(self): - return get_connection(backend=self.email_backend) + return get_connection(backend=self.email_backend, fail_silently=True) def format_subject(self, subject): """ diff --git a/tests/regressiontests/logging_tests/tests.py b/tests/regressiontests/logging_tests/tests.py index b3d9f3b352..b7d06bf362 100644 --- a/tests/regressiontests/logging_tests/tests.py +++ b/tests/regressiontests/logging_tests/tests.py @@ -154,6 +154,10 @@ class AdminEmailHandlerTest(TestCase): ][0] return admin_email_handler + def test_fail_silently(self): + admin_email_handler = self.get_admin_email_handler(self.logger) + self.assertTrue(admin_email_handler.connection().fail_silently) + @override_settings( ADMINS=(('whatever admin', 'admin@example.com'),), EMAIL_SUBJECT_PREFIX='-SuperAwesomeSubject-' From 013db6ba85fb880bd1f9a5ad2e91dc5c1efe197c Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 21 Jan 2013 22:34:36 +0100 Subject: [PATCH 180/870] Fixed #18051 -- Allowed admin fieldsets to contain lists Thanks Ricardo di Virgilio for the report, Mateus Gondim for the patch and Nick Sandford for the review. --- django/contrib/admin/util.py | 3 +-- tests/regressiontests/admin_util/tests.py | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index a85045c515..07013d1d4b 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -88,8 +88,7 @@ def flatten_fieldsets(fieldsets): field_names = [] for name, opts in fieldsets: for field in opts['fields']: - # type checking feels dirty, but it seems like the best way here - if type(field) == tuple: + if isinstance(field, (list, tuple)): field_names.extend(field) else: field_names.append(field) diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py index e9e122a9f0..274103cf87 100644 --- a/tests/regressiontests/admin_util/tests.py +++ b/tests/regressiontests/admin_util/tests.py @@ -5,8 +5,8 @@ from datetime import datetime from django.conf import settings from django.contrib import admin from django.contrib.admin import helpers -from django.contrib.admin.util import (display_for_field, label_for_field, - lookup_field, NestedObjects) +from django.contrib.admin.util import (display_for_field, flatten_fieldsets, + label_for_field, lookup_field, NestedObjects) from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE from django.contrib.sites.models import Site from django.db import models, DEFAULT_DB_ALIAS @@ -300,3 +300,21 @@ class UtilTests(unittest.TestCase): '') self.assertEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(), '') + + def test_flatten_fieldsets(self): + """ + Regression test for #18051 + """ + fieldsets = ( + (None, { + 'fields': ('url', 'title', ('content', 'sites')) + }), + ) + self.assertEqual(flatten_fieldsets(fieldsets), ['url', 'title', 'content', 'sites']) + + fieldsets = ( + (None, { + 'fields': ['url', 'title', ['content', 'sites']) + }), + ) + self.assertEqual(flatten_fieldsets(fieldsets), ['url', 'title', 'content', 'sites']) From 8ce1e392fa785895be561b3164df53dc11091054 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 21 Jan 2013 22:42:47 +0100 Subject: [PATCH 181/870] Fixed error introduced when testing patch for 013db6ba85 Shame on me. --- tests/regressiontests/admin_util/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py index 274103cf87..7898f200b5 100644 --- a/tests/regressiontests/admin_util/tests.py +++ b/tests/regressiontests/admin_util/tests.py @@ -314,7 +314,7 @@ class UtilTests(unittest.TestCase): fieldsets = ( (None, { - 'fields': ['url', 'title', ['content', 'sites']) + 'fields': ('url', 'title', ['content', 'sites']) }), ) self.assertEqual(flatten_fieldsets(fieldsets), ['url', 'title', 'content', 'sites']) From 456f9b98471e2aab71c6f58e3bf40a1026a3b466 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Mon, 21 Jan 2013 23:22:18 -0300 Subject: [PATCH 182/870] Simplified a i18n test. --- tests/regressiontests/i18n/commands/extraction.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index 1d6a72d725..eeb2a9524b 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -109,14 +109,12 @@ class BasicExtractorTests(ExtractorTests): def test_extraction_error(self): os.chdir(self.test_dir) - shutil.copyfile('./templates/template_with_error.tpl', './templates/template_with_error.html') - self.assertRaises(SyntaxError, management.call_command, 'makemessages', locale=LOCALE, verbosity=0) + self.assertRaises(SyntaxError, management.call_command, 'makemessages', locale=LOCALE, extensions=['tpl'], verbosity=0) with self.assertRaises(SyntaxError) as context_manager: - management.call_command('makemessages', locale=LOCALE, verbosity=0) + management.call_command('makemessages', locale=LOCALE, extensions=['tpl'], verbosity=0) self.assertRegexpMatches(str(context_manager.exception), - r'Translation blocks must not include other block tags: blocktrans \(file templates[/\\]template_with_error\.html, line 3\)' + r'Translation blocks must not include other block tags: blocktrans \(file templates[/\\]template_with_error\.tpl, line 3\)' ) - os.remove('./templates/template_with_error.html') # Check that the temporary file was cleaned up self.assertFalse(os.path.exists('./templates/template_with_error.html.py')) From cc4de61a2b36abf418d6f4c720d9e62c405e0612 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Tue, 22 Jan 2013 12:47:34 +0100 Subject: [PATCH 183/870] Fixed #19596 -- Use `_default_manager` instead of `objects` in the auth app. This is needed to support custom user models which don't define a manager named `objects`. --- django/contrib/auth/backends.py | 4 ++-- django/contrib/auth/forms.py | 4 ++-- django/contrib/auth/handlers/modwsgi.py | 4 ++-- django/contrib/auth/management/__init__.py | 2 +- .../management/commands/changepassword.py | 2 +- .../management/commands/createsuperuser.py | 4 ++-- django/contrib/auth/tests/auth_backends.py | 22 +++++++++---------- django/contrib/auth/tests/custom_user.py | 8 +++---- django/contrib/auth/tests/handlers.py | 2 +- django/contrib/auth/tests/management.py | 6 ++--- django/contrib/auth/tests/models.py | 2 +- django/contrib/auth/views.py | 2 +- 12 files changed, 31 insertions(+), 31 deletions(-) diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index db99c94838..703fd2519d 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -13,7 +13,7 @@ class ModelBackend(object): def authenticate(self, username=None, password=None): try: UserModel = get_user_model() - user = UserModel.objects.get_by_natural_key(username) + user = UserModel._default_manager.get_by_natural_key(username) if user.check_password(password): return user except UserModel.DoesNotExist: @@ -64,7 +64,7 @@ class ModelBackend(object): def get_user(self, user_id): try: UserModel = get_user_model() - return UserModel.objects.get(pk=user_id) + return UserModel._default_manager.get(pk=user_id) except UserModel.DoesNotExist: return None diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 85291126b4..cbce8ad6e2 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -89,7 +89,7 @@ class UserCreationForm(forms.ModelForm): # but it sets a nicer error message than the ORM. See #13147. username = self.cleaned_data["username"] try: - User.objects.get(username=username) + User._default_manager.get(username=username) except User.DoesNotExist: return username raise forms.ValidationError(self.error_messages['duplicate_username']) @@ -217,7 +217,7 @@ class PasswordResetForm(forms.Form): """ UserModel = get_user_model() email = self.cleaned_data["email"] - self.users_cache = UserModel.objects.filter(email__iexact=email) + self.users_cache = UserModel._default_manager.filter(email__iexact=email) if not len(self.users_cache): raise forms.ValidationError(self.error_messages['unknown']) if not any(user.is_active for user in self.users_cache): diff --git a/django/contrib/auth/handlers/modwsgi.py b/django/contrib/auth/handlers/modwsgi.py index 5ee4d609f7..a26d6219dd 100644 --- a/django/contrib/auth/handlers/modwsgi.py +++ b/django/contrib/auth/handlers/modwsgi.py @@ -18,7 +18,7 @@ def check_password(environ, username, password): try: try: - user = UserModel.objects.get_by_natural_key(username) + user = UserModel._default_manager.get_by_natural_key(username) except UserModel.DoesNotExist: return None if not user.is_active: @@ -37,7 +37,7 @@ def groups_for_user(environ, username): try: try: - user = UserModel.objects.get_by_natural_key(username) + user = UserModel._default_manager.get_by_natural_key(username) except UserModel.DoesNotExist: return [] try: diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index ce5d57fa79..a77bba0f73 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -174,7 +174,7 @@ def get_default_username(check_db=True): # Don't return the default username if it is already taken. if check_db and default_username: try: - auth_app.User.objects.get(username=default_username) + auth_app.User._default_manager.get(username=default_username) except auth_app.User.DoesNotExist: pass else: diff --git a/django/contrib/auth/management/commands/changepassword.py b/django/contrib/auth/management/commands/changepassword.py index ff38836a95..3240b0f992 100644 --- a/django/contrib/auth/management/commands/changepassword.py +++ b/django/contrib/auth/management/commands/changepassword.py @@ -33,7 +33,7 @@ class Command(BaseCommand): UserModel = get_user_model() try: - u = UserModel.objects.using(options.get('database')).get(**{ + u = UserModel._default_manager.using(options.get('database')).get(**{ UserModel.USERNAME_FIELD: username }) except UserModel.DoesNotExist: diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index 216d56d730..7b4764abc3 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -95,7 +95,7 @@ class Command(BaseCommand): username = None continue try: - self.UserModel.objects.db_manager(database).get_by_natural_key(username) + self.UserModel._default_manager.db_manager(database).get_by_natural_key(username) except self.UserModel.DoesNotExist: pass else: @@ -134,6 +134,6 @@ class Command(BaseCommand): user_data[self.UserModel.USERNAME_FIELD] = username user_data['password'] = password - self.UserModel.objects.db_manager(database).create_superuser(**user_data) + self.UserModel._default_manager.db_manager(database).create_superuser(**user_data) if verbosity >= 1: self.stdout.write("Superuser created successfully.") diff --git a/django/contrib/auth/tests/auth_backends.py b/django/contrib/auth/tests/auth_backends.py index 71f18d32cf..074374cd5a 100644 --- a/django/contrib/auth/tests/auth_backends.py +++ b/django/contrib/auth/tests/auth_backends.py @@ -34,7 +34,7 @@ class BaseModelBackendTest(object): ContentType.objects.clear_cache() def test_has_perm(self): - user = self.UserModel.objects.get(pk=self.user.pk) + user = self.UserModel._default_manager.get(pk=self.user.pk) self.assertEqual(user.has_perm('auth.test'), False) user.is_staff = True user.save() @@ -53,14 +53,14 @@ class BaseModelBackendTest(object): self.assertEqual(user.has_perm('auth.test'), False) def test_custom_perms(self): - user = self.UserModel.objects.get(pk=self.user.pk) + user = self.UserModel._default_manager.get(pk=self.user.pk) content_type = ContentType.objects.get_for_model(Group) perm = Permission.objects.create(name='test', content_type=content_type, codename='test') user.user_permissions.add(perm) user.save() # reloading user to purge the _perm_cache - user = self.UserModel.objects.get(pk=self.user.pk) + user = self.UserModel._default_manager.get(pk=self.user.pk) self.assertEqual(user.get_all_permissions() == set(['auth.test']), True) self.assertEqual(user.get_group_permissions(), set([])) self.assertEqual(user.has_module_perms('Group'), False) @@ -71,7 +71,7 @@ class BaseModelBackendTest(object): perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3') user.user_permissions.add(perm) user.save() - user = self.UserModel.objects.get(pk=self.user.pk) + user = self.UserModel._default_manager.get(pk=self.user.pk) self.assertEqual(user.get_all_permissions(), set(['auth.test2', 'auth.test', 'auth.test3'])) self.assertEqual(user.has_perm('test'), False) self.assertEqual(user.has_perm('auth.test'), True) @@ -81,7 +81,7 @@ class BaseModelBackendTest(object): group.permissions.add(perm) group.save() user.groups.add(group) - user = self.UserModel.objects.get(pk=self.user.pk) + user = self.UserModel._default_manager.get(pk=self.user.pk) exp = set(['auth.test2', 'auth.test', 'auth.test3', 'auth.test_group']) self.assertEqual(user.get_all_permissions(), exp) self.assertEqual(user.get_group_permissions(), set(['auth.test_group'])) @@ -93,7 +93,7 @@ class BaseModelBackendTest(object): def test_has_no_object_perm(self): """Regressiontest for #12462""" - user = self.UserModel.objects.get(pk=self.user.pk) + user = self.UserModel._default_manager.get(pk=self.user.pk) content_type = ContentType.objects.get_for_model(Group) perm = Permission.objects.create(name='test', content_type=content_type, codename='test') user.user_permissions.add(perm) @@ -106,7 +106,7 @@ class BaseModelBackendTest(object): def test_get_all_superuser_permissions(self): "A superuser has all permissions. Refs #14795" - user = self.UserModel.objects.get(pk=self.superuser.pk) + user = self.UserModel._default_manager.get(pk=self.superuser.pk) self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all())) @@ -151,13 +151,13 @@ class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase): UserModel = ExtensionUser def create_users(self): - self.user = ExtensionUser.objects.create_user( + self.user = ExtensionUser._default_manager.create_user( username='test', email='test@example.com', password='test', date_of_birth=date(2006, 4, 25) ) - self.superuser = ExtensionUser.objects.create_superuser( + self.superuser = ExtensionUser._default_manager.create_superuser( username='test2', email='test2@example.com', password='test', @@ -178,12 +178,12 @@ class CustomPermissionsUserModelBackendTest(BaseModelBackendTest, TestCase): UserModel = CustomPermissionsUser def create_users(self): - self.user = CustomPermissionsUser.objects.create_user( + self.user = CustomPermissionsUser._default_manager.create_user( email='test@example.com', password='test', date_of_birth=date(2006, 4, 25) ) - self.superuser = CustomPermissionsUser.objects.create_superuser( + self.superuser = CustomPermissionsUser._default_manager.create_superuser( email='test2@example.com', password='test', date_of_birth=date(1976, 11, 8) diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py index 7e042e4895..8cc57d4caf 100644 --- a/django/contrib/auth/tests/custom_user.py +++ b/django/contrib/auth/tests/custom_user.py @@ -42,7 +42,7 @@ class CustomUser(AbstractBaseUser): is_admin = models.BooleanField(default=False) date_of_birth = models.DateField() - objects = CustomUserManager() + custom_objects = CustomUserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['date_of_birth'] @@ -88,7 +88,7 @@ class CustomUser(AbstractBaseUser): class ExtensionUser(AbstractUser): date_of_birth = models.DateField() - objects = UserManager() + custom_objects = UserManager() REQUIRED_FIELDS = AbstractUser.REQUIRED_FIELDS + ['date_of_birth'] @@ -112,7 +112,7 @@ class CustomPermissionsUser(AbstractBaseUser, PermissionsMixin): email = models.EmailField(verbose_name='email address', max_length=255, unique=True) date_of_birth = models.DateField() - objects = CustomPermissionsUserManager() + custom_objects = CustomPermissionsUserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['date_of_birth'] @@ -136,7 +136,7 @@ class IsActiveTestUser1(AbstractBaseUser): """ username = models.CharField(max_length=30, unique=True) - objects = BaseUserManager() + custom_objects = BaseUserManager() USERNAME_FIELD = 'username' diff --git a/django/contrib/auth/tests/handlers.py b/django/contrib/auth/tests/handlers.py index 04ab46f75b..41063aaf4a 100644 --- a/django/contrib/auth/tests/handlers.py +++ b/django/contrib/auth/tests/handlers.py @@ -42,7 +42,7 @@ class ModWsgiHandlerTestCase(TransactionTestCase): with custom user installed """ - CustomUser.objects.create_user('test@example.com', '1990-01-01', 'test') + CustomUser._default_manager.create_user('test@example.com', '1990-01-01', 'test') # User not in database self.assertTrue(check_password({}, 'unknown', '') is None) diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index 02939e39dc..42f14d6d5c 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -125,7 +125,7 @@ class CreatesuperuserManagementCommandTestCase(TestCase): email="joe@somewhere.org", stdout=new_io ) - u = User.objects.get(username="joe+admin@somewhere.org") + u = User._default_manager.get(username="joe+admin@somewhere.org") self.assertEqual(u.email, 'joe@somewhere.org') self.assertFalse(u.has_usable_password()) @@ -145,7 +145,7 @@ class CreatesuperuserManagementCommandTestCase(TestCase): ) command_output = new_io.getvalue().strip() self.assertEqual(command_output, 'Superuser created successfully.') - u = CustomUser.objects.get(email="joe@somewhere.org") + u = CustomUser._default_manager.get(email="joe@somewhere.org") self.assertEqual(u.date_of_birth, date(1976, 4, 1)) # created password should be unusable @@ -167,7 +167,7 @@ class CreatesuperuserManagementCommandTestCase(TestCase): skip_validation=True ) - self.assertEqual(CustomUser.objects.count(), 0) + self.assertEqual(CustomUser._default_manager.count(), 0) class PermissionDuplicationTestCase(TestCase): diff --git a/django/contrib/auth/tests/models.py b/django/contrib/auth/tests/models.py index da0e45a55e..8ac0599e6b 100644 --- a/django/contrib/auth/tests/models.py +++ b/django/contrib/auth/tests/models.py @@ -137,6 +137,6 @@ class IsActiveTestCase(TestCase): user.is_active = False # there should be no problem saving - but the attribute is not saved user.save() - user_fetched = UserModel.objects.get(pk=user.pk) + user_fetched = UserModel._default_manager.get(pk=user.pk) # the attribute is always true for newly retrieved instance self.assertEqual(user_fetched.is_active, True) diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 8514345d00..9d1534651b 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -200,7 +200,7 @@ def password_reset_confirm(request, uidb36=None, token=None, post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete') try: uid_int = base36_to_int(uidb36) - user = UserModel.objects.get(pk=uid_int) + user = UserModel._default_manager.get(pk=uid_int) except (ValueError, OverflowError, UserModel.DoesNotExist): user = None From e535da6865f0e02f0b593b52ed2e040b24a886d6 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Tue, 22 Jan 2013 13:46:42 +0100 Subject: [PATCH 184/870] Fixed #19523 -- Improved performance of Django's bash completion Previous version took about 150ms to source, even on a warm cache, primarily because it forks+execs /usr/bin/basename 44 times. This patch makes it faster by a factor of 5 (and I imagine that a little more thought would reduce the time to effectively zero). --- extras/django_bash_completion | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extras/django_bash_completion b/extras/django_bash_completion index 1c3887eba0..8f85211705 100755 --- a/extras/django_bash_completion +++ b/extras/django_bash_completion @@ -42,10 +42,10 @@ complete -F _django_completion -o default django-admin.py manage.py django-admin _python_django_completion() { if [[ ${COMP_CWORD} -ge 2 ]]; then - PYTHON_EXE=$( basename -- ${COMP_WORDS[0]} ) + PYTHON_EXE=${COMP_WORDS[0]##*/} echo $PYTHON_EXE | egrep "python([2-9]\.[0-9])?" >/dev/null 2>&1 if [[ $? == 0 ]]; then - PYTHON_SCRIPT=$( basename -- ${COMP_WORDS[1]} ) + PYTHON_SCRIPT=${COMP_WORDS[1]##*/} echo $PYTHON_SCRIPT | egrep "manage\.py|django-admin(\.py)?" >/dev/null 2>&1 if [[ $? == 0 ]]; then COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]:1}" \ @@ -61,7 +61,7 @@ unset pythons if command -v whereis &>/dev/null; then python_interpreters=$(whereis python | cut -d " " -f 2-) for python in $python_interpreters; do - pythons="${pythons} $(basename -- $python)" + pythons="${pythons} ${python##*/}" done pythons=$(echo $pythons | tr " " "\n" | sort -u | tr "\n" " ") else From 5b2d9bacd2512bcdf371c05b0b43bc713dcca080 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 22 Jan 2013 06:46:22 -0500 Subject: [PATCH 185/870] Fixed #19640 - Added inlineformset_factory to reference docs. Thanks wim@ for the suggestion. --- docs/ref/contrib/admin/index.txt | 4 ++-- docs/ref/forms/models.txt | 17 ++++++++++++++--- docs/topics/forms/modelforms.txt | 9 ++++++--- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index b273255c71..ee04d77d32 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1520,8 +1520,8 @@ The ``InlineModelAdmin`` class adds: .. attribute:: InlineModelAdmin.form The value for ``form`` defaults to ``ModelForm``. This is what is passed - through to ``inlineformset_factory`` when creating the formset for this - inline. + through to :func:`~django.forms.models.inlineformset_factory` when + creating the formset for this inline. .. attribute:: InlineModelAdmin.extra diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt index 1f4a0d0c3d..c388f402e6 100644 --- a/docs/ref/forms/models.txt +++ b/docs/ref/forms/models.txt @@ -5,7 +5,7 @@ Model Form Functions .. module:: django.forms.models :synopsis: Django's functions for building model forms and formsets. -.. method:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None) +.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None) Returns a :class:`~django.forms.ModelForm` class for the given ``model``. You can optionally pass a ``form`` argument to use as a starting point for @@ -25,16 +25,27 @@ Model Form Functions See :ref:`modelforms-factory` for example usage. -.. method:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None) +.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None) Returns a ``FormSet`` class for the given ``model`` class. Arguments ``model``, ``form``, ``fields``, ``exclude``, and ``formfield_callback`` are all passed through to - :meth:`~django.forms.models.modelform_factory`. + :func:`~django.forms.models.modelform_factory`. Arguments ``formset``, ``extra``, ``max_num``, ``can_order``, and ``can_delete`` are passed through to ``formset_factory``. See :ref:`formsets` for details. See :ref:`model-formsets` for example usage. + +.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None) + + Returns an ``InlineFormSet`` using :func:`modelformset_factory` with + defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and + ``extra=3``. + + If your model has more than one :class:`~django.db.models.ForeignKey` to + the ``parent_model``, you must specify a ``fk_name``. + + See :ref:`inline-formsets` for example usage. diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 9a33d68cf7..c091e715bb 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -550,7 +550,7 @@ ModelForm factory function -------------------------- You can create forms from a given model using the standalone function -:class:`~django.forms.models.modelform_factory`, instead of using a class +:func:`~django.forms.models.modelform_factory`, instead of using a class definition. This may be more convenient if you do not have many customizations to make:: @@ -857,6 +857,8 @@ primary key that isn't called ``id``, make sure it gets rendered.) .. highlight:: python +.. _inline-formsets: + Inline formsets =============== @@ -881,7 +883,7 @@ a particular author, you could do this:: .. note:: - ``inlineformset_factory`` uses + :func:`~django.forms.models.inlineformset_factory` uses :func:`~django.forms.models.modelformset_factory` and marks ``can_delete=True``. @@ -901,7 +903,8 @@ the following model:: to_friend = models.ForeignKey(Friend) length_in_months = models.IntegerField() -To resolve this, you can use ``fk_name`` to ``inlineformset_factory``:: +To resolve this, you can use ``fk_name`` to +:func:`~django.forms.models.inlineformset_factory`:: >>> FriendshipFormSet = inlineformset_factory(Friend, Friendship, fk_name="from_friend") From 214fb700b9e0fb7268a2c8b87595b1b9fb090867 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 22 Jan 2013 16:13:57 -0500 Subject: [PATCH 186/870] Fixed #19477 - Documented generic_inlineformset_factory Thanks epicserve for the suggestion. --- django/contrib/contenttypes/generic.py | 2 +- docs/ref/contrib/contenttypes.txt | 31 +++++++++++++++++++------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index be7a5e5a22..cda4d46fe8 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -429,7 +429,7 @@ def generic_inlineformset_factory(model, form=ModelForm, max_num=None, formfield_callback=None): """ - Returns an ``GenericInlineFormSet`` for the given kwargs. + Returns a ``GenericInlineFormSet`` for the given kwargs. You must provide ``ct_field`` and ``object_id`` if they different from the defaults ``content_type`` and ``object_id`` respectively. diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index e9cd5e7bc0..282e350a64 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -452,14 +452,18 @@ need to calculate them without using the aggregation API. Generic relations in forms and admin ------------------------------------ -The :mod:`django.contrib.contenttypes.generic` module provides -``BaseGenericInlineFormSet``, -:class:`~django.contrib.contenttypes.generic.GenericTabularInline` -and :class:`~django.contrib.contenttypes.generic.GenericStackedInline` -(the last two are subclasses of -:class:`~django.contrib.contenttypes.generic.GenericInlineModelAdmin`). -This enables the use of generic relations in forms and the admin. See the -:doc:`model formset ` and +The :mod:`django.contrib.contenttypes.generic` module provides: + +* ``BaseGenericInlineFormSet`` +* :class:`~django.contrib.contenttypes.generic.GenericTabularInline` + and :class:`~django.contrib.contenttypes.generic.GenericStackedInline` + (subclasses of + :class:`~django.contrib.contenttypes.generic.GenericInlineModelAdmin`) +* A formset factory, :func:`generic_inlineformset_factory`, for use with + :class:`GenericForeignKey` + +These classes and functions enable the use of generic relations in forms +and the admin. See the :doc:`model formset ` and :ref:`admin ` documentation for more information. @@ -486,3 +490,14 @@ information. Subclasses of :class:`GenericInlineModelAdmin` with stacked and tabular layouts, respectively. + +.. function:: generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None) + + Returns a ``GenericInlineFormSet`` using + :func:`~django.forms.models.modelformset_factory`. + + You must provide ``ct_field`` and ``object_id`` if they different from the + defaults, ``content_type`` and ``object_id`` respectively. Other parameters + are similar to those documented in + :func:`~django.forms.models.modelformset_factory` and + :func:`~django.forms.models.inlineformset_factory`. From 0db86273ae1c31ee9881fe63f210cb2120fde18a Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 22 Jan 2013 16:15:52 -0500 Subject: [PATCH 187/870] Fixed #19633 - Discouraged use of gunicorn's Django integration. --- docs/howto/deployment/wsgi/gunicorn.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/howto/deployment/wsgi/gunicorn.txt b/docs/howto/deployment/wsgi/gunicorn.txt index c4483291a3..14c80af0a0 100644 --- a/docs/howto/deployment/wsgi/gunicorn.txt +++ b/docs/howto/deployment/wsgi/gunicorn.txt @@ -48,6 +48,12 @@ ensure that is to run this command from the same directory as your Using Gunicorn's Django integration =================================== +.. note:: + + If you are using Django 1.4 or newer, it’s highly recommended to simply run + your application with the WSGI interface using the ``gunicorn`` command + as described above. + To use Gunicorn's built-in Django integration, first add ``"gunicorn"`` to :setting:`INSTALLED_APPS`. Then run ``python manage.py run_gunicorn``. From 389892aae595b86c4be28c43e3312d76a68a0173 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Wed, 23 Jan 2013 00:26:20 +0000 Subject: [PATCH 188/870] Remove dup_select_related method. This undocumented method was used in an old version of the admin, is totally untested and hails from 2008. Although it's listed in the "public methods" section, as it's not documented or used I don't think it needs a deprecation path. If we think it's useful I'll write some tests/docs for it instead... --- django/db/models/query.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index 1c9a68a677..68d7931729 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -755,13 +755,6 @@ class QuerySet(object): clone._prefetch_related_lookups.extend(lookups) return clone - def dup_select_related(self, other): - """ - Copies the related selection status from the QuerySet 'other' to the - current QuerySet. - """ - self.query.select_related = other.query.select_related - def annotate(self, *args, **kwargs): """ Return a query set in which the returned objects have been annotated From 0de2645c00c2330060c9889c71afd3a528ed7a3a Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 23 Jan 2013 04:42:34 -0500 Subject: [PATCH 189/870] Fixed #19610 - Added enctype note to forms topics doc. Thanks will@ for the suggestion. --- docs/ref/forms/api.txt | 2 ++ docs/topics/forms/index.txt | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 4aacbf0a0d..d1f877ff65 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -716,6 +716,8 @@ form data *and* file data:: Testing for multipart forms ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. method:: Form.is_multipart + If you're writing reusable views or templates, you may not know ahead of time whether your form is a multipart form or not. The ``is_multipart()`` method tells you whether the form requires multipart encoding for submission:: diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt index 9b5794a8f2..a3c17e1555 100644 --- a/docs/topics/forms/index.txt +++ b/docs/topics/forms/index.txt @@ -197,6 +197,14 @@ context variable ``form``. Here's a simple example template:: The form only outputs its own fields; it is up to you to provide the surrounding ``
`` tags and the submit button. +If your form includes uploaded files, be sure to include +``enctype="multipart/form-data"`` in the ``form`` element. If you wish to write +a generic template that will work whether or not the form has files, you can +use the :meth:`~django.forms.Form.is_multipart` attribute on the form:: + + + .. admonition:: Forms and Cross Site Request Forgery protection Django ships with an easy-to-use :doc:`protection against Cross Site Request From 71c8539570dec000ad24d1a11fa443812b56f876 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Wed, 23 Jan 2013 12:36:48 -0800 Subject: [PATCH 190/870] Fixed typo. --- docs/topics/files.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/files.txt b/docs/topics/files.txt index 66e104759a..94685f9bc7 100644 --- a/docs/topics/files.txt +++ b/docs/topics/files.txt @@ -93,7 +93,7 @@ The following approach may be used to close files automatically:: Closing files is especially important when accessing file fields in a loop over a large number of objects:: If files are not manually closed after accessing them, the risk of running out of file descriptors may arise. This -may lead to the following error: +may lead to the following error:: IOError: [Errno 24] Too many open files From 93e79b45bc5288d1ca0eb5b6eade30d3c7110b24 Mon Sep 17 00:00:00 2001 From: Nick Sandford Date: Wed, 23 Jan 2013 21:11:46 +0100 Subject: [PATCH 191/870] Fixed #17416 -- Added widgets argument to inlineformset_factory and modelformset_factory --- django/forms/models.py | 12 +++++++----- docs/ref/forms/models.txt | 16 ++++++++++++---- docs/topics/forms/modelforms.txt | 23 +++++++++++++++++++++++ tests/modeltests/model_formsets/tests.py | 24 ++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/django/forms/models.py b/django/forms/models.py index 74886d7ae0..03a14dc9ff 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -682,14 +682,15 @@ class BaseModelFormSet(BaseFormSet): super(BaseModelFormSet, self).add_fields(form, index) def modelformset_factory(model, form=ModelForm, formfield_callback=None, - formset=BaseModelFormSet, - extra=1, can_delete=False, can_order=False, - max_num=None, fields=None, exclude=None): + formset=BaseModelFormSet, extra=1, can_delete=False, + can_order=False, max_num=None, fields=None, + exclude=None, widgets=None): """ Returns a FormSet class for the given Django model class. """ form = modelform_factory(model, form=form, fields=fields, exclude=exclude, - formfield_callback=formfield_callback) + formfield_callback=formfield_callback, + widgets=widgets) FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, can_order=can_order, can_delete=can_delete) FormSet.model = model @@ -827,7 +828,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, - formfield_callback=None): + formfield_callback=None, widgets=None): """ Returns an ``InlineFormSet`` for the given kwargs. @@ -848,6 +849,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm, 'fields': fields, 'exclude': exclude, 'max_num': max_num, + 'widgets': widgets, } FormSet = modelformset_factory(model, **kwargs) FormSet.fk = fk diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt index c388f402e6..f3382d32c7 100644 --- a/docs/ref/forms/models.txt +++ b/docs/ref/forms/models.txt @@ -25,12 +25,12 @@ Model Form Functions See :ref:`modelforms-factory` for example usage. -.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None) +.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None) Returns a ``FormSet`` class for the given ``model`` class. - Arguments ``model``, ``form``, ``fields``, ``exclude``, and - ``formfield_callback`` are all passed through to + Arguments ``model``, ``form``, ``fields``, ``exclude``, + ``formfield_callback`` and ``widgets`` are all passed through to :func:`~django.forms.models.modelform_factory`. Arguments ``formset``, ``extra``, ``max_num``, ``can_order``, and @@ -39,7 +39,11 @@ Model Form Functions See :ref:`model-formsets` for example usage. -.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None) + .. versionchanged:: 1.6 + + The widgets parameter was added. + +.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None) Returns an ``InlineFormSet`` using :func:`modelformset_factory` with defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and @@ -49,3 +53,7 @@ Model Form Functions the ``parent_model``, you must specify a ``fk_name``. See :ref:`inline-formsets` for example usage. + + .. versionchanged:: 1.6 + + The widgets parameter was added. diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index c091e715bb..d9e00d86cf 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -650,6 +650,19 @@ exclude:: >>> AuthorFormSet = modelformset_factory(Author, exclude=('birth_date',)) +Specifying widgets to use in the form with ``widgets`` +------------------------------------------------------ + +.. versionadded:: 1.6 + +Using the ``widgets`` parameter, you can specify a dictionary of values to +customize the ``ModelForm``'s widget class for a particular field. This +works the same way as the ``widgets`` dictionary on the inner ``Meta`` +class of a ``ModelForm`` works:: + + >>> AuthorFormSet = modelformset_factory( + ... Author, widgets={'name': Textarea(attrs={'cols': 80, 'rows': 20}) + Providing initial values ------------------------ @@ -930,3 +943,13 @@ of a model. Here's how you can do that:: }) Notice how we pass ``instance`` in both the ``POST`` and ``GET`` cases. + +Specifying widgets to use in the inline form +-------------------------------------------- + +.. versionadded:: 1.6 + +``inlineformset_factory`` uses ``modelformset_factory`` and passes most +of its arguments to ``modelformset_factory``. This means you can use +the ``widgets`` parameter in much the same way as passing it to +``modelformset_factory``. See `Specifying widgets to use in the form with widgets`_ above. diff --git a/tests/modeltests/model_formsets/tests.py b/tests/modeltests/model_formsets/tests.py index e28560b237..a028e65143 100644 --- a/tests/modeltests/model_formsets/tests.py +++ b/tests/modeltests/model_formsets/tests.py @@ -1190,3 +1190,27 @@ class ModelFormsetTest(TestCase): self.assertFalse(formset.is_valid()) self.assertEqual(formset._non_form_errors, ['Please correct the duplicate data for subtitle which must be unique for the month in posted.']) + + +class TestModelFormsetWidgets(TestCase): + def test_modelformset_factory_widgets(self): + widgets = { + 'name': forms.TextInput(attrs={'class': 'poet'}) + } + PoetFormSet = modelformset_factory(Poet, widgets=widgets) + form = PoetFormSet.form() + self.assertHTMLEqual( + "%s" % form['name'], + '' + ) + + def test_inlineformset_factory_widgets(self): + widgets = { + 'title': forms.TextInput(attrs={'class': 'book'}) + } + BookFormSet = inlineformset_factory(Author, Book, widgets=widgets) + form = BookFormSet.form() + self.assertHTMLEqual( + "%s" % form['title'], + '' + ) From 9893fa12b735f3f47b35d4063d86dddf3145cb25 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 13 Oct 2012 16:05:34 +0200 Subject: [PATCH 192/870] Fixed #19125 -- The startproject command should validate the name earlier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Łukasz Rekucki for the report and the patch. --- django/core/management/commands/startapp.py | 3 +-- .../core/management/commands/startproject.py | 3 +-- django/core/management/templates.py | 25 +++++++++++-------- tests/regressiontests/admin_scripts/tests.py | 14 ++++++----- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/django/core/management/commands/startapp.py b/django/core/management/commands/startapp.py index 5581331b7b..692ad09a43 100644 --- a/django/core/management/commands/startapp.py +++ b/django/core/management/commands/startapp.py @@ -9,8 +9,7 @@ class Command(TemplateCommand): "directory.") def handle(self, app_name=None, target=None, **options): - if app_name is None: - raise CommandError("you must provide an app name") + self.validate_name(app_name, "app") # Check that the app_name cannot be imported. try: diff --git a/django/core/management/commands/startproject.py b/django/core/management/commands/startproject.py index a1a9b33a43..b143e6c380 100644 --- a/django/core/management/commands/startproject.py +++ b/django/core/management/commands/startproject.py @@ -10,8 +10,7 @@ class Command(TemplateCommand): "given directory.") def handle(self, project_name=None, target=None, *args, **options): - if project_name is None: - raise CommandError("you must provide a project name") + self.validate_name(project_name, "project") # Check that the project_name cannot be imported. try: diff --git a/django/core/management/templates.py b/django/core/management/templates.py index f522097b8c..927dbc13ac 100644 --- a/django/core/management/templates.py +++ b/django/core/management/templates.py @@ -67,16 +67,7 @@ class TemplateCommand(BaseCommand): self.paths_to_remove = [] self.verbosity = int(options.get('verbosity')) - # If it's not a valid directory name. - if not re.search(r'^[_a-zA-Z]\w*$', name): - # Provide a smart error message, depending on the error. - if not re.search(r'^[_a-zA-Z]', name): - message = ('make sure the name begins ' - 'with a letter or underscore') - else: - message = 'use only numbers, letters and underscores' - raise CommandError("%r is not a valid %s name. Please %s." % - (name, app_or_project, message)) + self.validate_name(name, app_or_project) # if some directory is given, make sure it's nicely expanded if target is None: @@ -211,6 +202,20 @@ class TemplateCommand(BaseCommand): raise CommandError("couldn't handle %s template %s." % (self.app_or_project, template)) + def validate_name(self, name, app_or_project): + if name is None: + raise CommandError("you must provide %s %s name" % ( + "an" if app_or_project == "app" else "a", app_or_project)) + # If it's not a valid directory name. + if not re.search(r'^[_a-zA-Z]\w*$', name): + # Provide a smart error message, depending on the error. + if not re.search(r'^[_a-zA-Z]', name): + message = 'make sure the name begins with a letter or underscore' + else: + message = 'use only numbers, letters and underscores' + raise CommandError("%r is not a valid %s name. Please %s." % + (name, app_or_project, message)) + def download(self, url): """ Downloads the given URL and returns the file name. diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index d0ca9d26df..df2326e163 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -1430,13 +1430,15 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): def test_invalid_project_name(self): "Make sure the startproject management command validates a project name" - args = ['startproject', '7testproject'] - testproject_dir = os.path.join(test_dir, '7testproject') - self.addCleanup(shutil.rmtree, testproject_dir, True) + for bad_name in ('7testproject', '../testproject'): + args = ['startproject', bad_name] + testproject_dir = os.path.join(test_dir, bad_name) + self.addCleanup(shutil.rmtree, testproject_dir, True) - out, err = self.run_django_admin(args) - self.assertOutput(err, "Error: '7testproject' is not a valid project name. Please make sure the name begins with a letter or underscore.") - self.assertFalse(os.path.exists(testproject_dir)) + out, err = self.run_django_admin(args) + self.assertOutput(err, "Error: '%s' is not a valid project name. " + "Please make sure the name begins with a letter or underscore." % bad_name) + self.assertFalse(os.path.exists(testproject_dir)) def test_simple_project_different_directory(self): "Make sure the startproject management command creates a project in a specific directory" From 57c6617c92959856e8cacd809f0e29f57df4f318 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 24 Jan 2013 11:01:12 +0100 Subject: [PATCH 193/870] Minor optimization in the static serve view. --- django/views/static.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/django/views/static.py b/django/views/static.py index f61ba28bd5..12af130796 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -58,12 +58,13 @@ def serve(request, path, document_root=None, show_indexes=False): raise Http404(_('"%(path)s" does not exist') % {'path': fullpath}) # Respect the If-Modified-Since header. statobj = os.stat(fullpath) - mimetype, encoding = mimetypes.guess_type(fullpath) - mimetype = mimetype or 'application/octet-stream' if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), statobj.st_mtime, statobj.st_size): return HttpResponseNotModified() - response = CompatibleStreamingHttpResponse(open(fullpath, 'rb'), content_type=mimetype) + content_type, encoding = mimetypes.guess_type(fullpath) + content_type = content_type or 'application/octet-stream' + response = CompatibleStreamingHttpResponse(open(fullpath, 'rb'), + content_type=content_type) response["Last-Modified"] = http_date(statobj.st_mtime) if stat.S_ISREG(statobj.st_mode): response["Content-Length"] = statobj.st_size From 57d439e2d4b1b22289cd9a16bac3324ce13a8e13 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 24 Jan 2013 07:51:14 -0300 Subject: [PATCH 194/870] More i18n makemessages tests tweaks. --- .../regressiontests/i18n/commands/extraction.py | 16 ++++++++-------- .../i18n/commands/templates/test.html | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index eeb2a9524b..ef711ec1bb 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -116,7 +116,7 @@ class BasicExtractorTests(ExtractorTests): r'Translation blocks must not include other block tags: blocktrans \(file templates[/\\]template_with_error\.tpl, line 3\)' ) # Check that the temporary file was cleaned up - self.assertFalse(os.path.exists('./templates/template_with_error.html.py')) + self.assertFalse(os.path.exists('./templates/template_with_error.tpl.py')) def test_extraction_warning(self): os.chdir(self.test_dir) @@ -139,23 +139,23 @@ class BasicExtractorTests(ExtractorTests): po_contents = force_text(fp.read()) # {% trans %} self.assertTrue('msgctxt "Special trans context #1"' in po_contents) - self.assertTrue("Translatable literal #7a" in po_contents) + self.assertMsgId("Translatable literal #7a", po_contents) self.assertTrue('msgctxt "Special trans context #2"' in po_contents) - self.assertTrue("Translatable literal #7b" in po_contents) + self.assertMsgId("Translatable literal #7b", po_contents) self.assertTrue('msgctxt "Special trans context #3"' in po_contents) - self.assertTrue("Translatable literal #7c" in po_contents) + self.assertMsgId("Translatable literal #7c", po_contents) # {% blocktrans %} self.assertTrue('msgctxt "Special blocktrans context #1"' in po_contents) - self.assertTrue("Translatable literal #8a" in po_contents) + self.assertMsgId("Translatable literal #8a", po_contents) self.assertTrue('msgctxt "Special blocktrans context #2"' in po_contents) - self.assertTrue("Translatable literal #8b-singular" in po_contents) + self.assertMsgId("Translatable literal #8b-singular", po_contents) self.assertTrue("Translatable literal #8b-plural" in po_contents) self.assertTrue('msgctxt "Special blocktrans context #3"' in po_contents) - self.assertTrue("Translatable literal #8c-singular" in po_contents) + self.assertMsgId("Translatable literal #8c-singular", po_contents) self.assertTrue("Translatable literal #8c-plural" in po_contents) self.assertTrue('msgctxt "Special blocktrans context #4"' in po_contents) - self.assertTrue("Translatable literal #8d" in po_contents) + self.assertMsgId("Translatable literal #8d %(a)s", po_contents) def test_context_in_single_quotes(self): os.chdir(self.test_dir) diff --git a/tests/regressiontests/i18n/commands/templates/test.html b/tests/regressiontests/i18n/commands/templates/test.html index e7d7f3ca53..6cb4493ef6 100644 --- a/tests/regressiontests/i18n/commands/templates/test.html +++ b/tests/regressiontests/i18n/commands/templates/test.html @@ -81,4 +81,4 @@ continued here.{% endcomment %} {% trans "Translatable literal with context wrapped in single quotes" context 'Context wrapped in single quotes' as var %} {% trans "Translatable literal with context wrapped in double quotes" context "Context wrapped in double quotes" as var %} {% blocktrans context 'Special blocktrans context wrapped in single quotes' %}Translatable literal with context wrapped in single quotes{% endblocktrans %} -{% blocktrans context "Special blocktrans context wrapped in double quotes" %}Translatable literal with context wrapped in double quotes{% endblocktrans %} \ No newline at end of file +{% blocktrans context "Special blocktrans context wrapped in double quotes" %}Translatable literal with context wrapped in double quotes{% endblocktrans %} From e2252bf9772bdcc699e4cb8ff1eb4672965bda29 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Thu, 24 Jan 2013 11:58:06 +0100 Subject: [PATCH 195/870] Fixed a typo. --- docs/ref/utils.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index de805173d7..20192ed006 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -529,7 +529,7 @@ escaping HTML. .. code-block:: python - format_html(u"%{0} {1} {2}", + format_html(u"{0} {1} {2}", mark_safe(some_html), some_text, some_other_text) This has the advantage that you don't need to apply :func:`escape` to each From eaa716a4130bb019204669d32389db8b399c0f71 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 24 Jan 2013 06:53:32 -0500 Subject: [PATCH 196/870] Fixed #19639 - Updated contributing to reflect model choices best practices. Thanks charettes. --- .../contributing/writing-code/coding-style.txt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/internals/contributing/writing-code/coding-style.txt b/docs/internals/contributing/writing-code/coding-style.txt index 0d84cdac9a..21146600b4 100644 --- a/docs/internals/contributing/writing-code/coding-style.txt +++ b/docs/internals/contributing/writing-code/coding-style.txt @@ -136,14 +136,17 @@ Model style * ``def get_absolute_url()`` * Any custom methods -* If ``choices`` is defined for a given model field, define the choices as - a tuple of tuples, with an all-uppercase name, either near the top of - the model module or just above the model class. Example:: +* If ``choices`` is defined for a given model field, define each choice as + a tuple of tuples, with an all-uppercase name as a class attribute on the + model. Example:: - DIRECTION_CHOICES = ( - ('U', 'Up'), - ('D', 'Down'), - ) + class MyModel(models.Model): + DIRECTION_UP = 'U' + DIRECTION_DOWN = 'D' + DIRECTION_CHOICES = ( + (DIRECTION_UP, 'Up'), + (DIRECTION_DOWN, 'Down'), + ) Use of ``django.conf.settings`` ------------------------------- From 1f6b2e7a658594e6ae9507c5f98eb429d19c0c9d Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 24 Jan 2013 21:21:26 -0300 Subject: [PATCH 197/870] Fixed #6682 -- Made shell's REPL actually execute $PYTHONSTARTUP and `~/.pythonrc.py`. Also: * Added a ``--no-startup`` option to disable this behavior. Previous logic to try to execute the code in charge of this funcionality was flawed (it only tried to do so if the user asked for ipython/bpython and they weren't found) * Expand ``~`` in PYTHONSTARTUP value. Thanks hekevintran at gmail dot com for the report and initial patch. Refs #3381. --- django/core/management/commands/shell.py | 24 +++++++++++++++--------- docs/ref/django-admin.txt | 12 ++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py index f883fb95d8..851d4e3cfb 100644 --- a/django/core/management/commands/shell.py +++ b/django/core/management/commands/shell.py @@ -9,6 +9,8 @@ class Command(NoArgsCommand): option_list = NoArgsCommand.option_list + ( make_option('--plain', action='store_true', dest='plain', help='Tells Django to use plain Python, not IPython or bpython.'), + make_option('--no-startup', action='store_true', dest='no_startup', + help='When using plain Python, ignore the PYTHONSTARTUP environment variable and ~/.pythonrc.py script.'), make_option('-i', '--interface', action='store', type='choice', choices=shells, dest='interface', help='Specify an interactive interpreter interface. Available options: "ipython" and "bpython"'), @@ -56,6 +58,7 @@ class Command(NoArgsCommand): get_models() use_plain = options.get('plain', False) + no_startup = options.get('no_startup', False) interface = options.get('interface', None) try: @@ -83,13 +86,16 @@ class Command(NoArgsCommand): # We want to honor both $PYTHONSTARTUP and .pythonrc.py, so follow system # conventions and get $PYTHONSTARTUP first then .pythonrc.py. - if not use_plain: - for pythonrc in (os.environ.get("PYTHONSTARTUP"), - os.path.expanduser('~/.pythonrc.py')): - if pythonrc and os.path.isfile(pythonrc): - try: - with open(pythonrc) as handle: - exec(compile(handle.read(), pythonrc, 'exec')) - except NameError: - pass + if not no_startup: + for pythonrc in (os.environ.get("PYTHONSTARTUP"), '~/.pythonrc.py'): + if not pythonrc: + continue + pythonrc = os.path.expanduser(pythonrc) + if not os.path.isfile(pythonrc): + continue + try: + with open(pythonrc) as handle: + exec(compile(handle.read(), pythonrc, 'exec'), imported_objects) + except NameError: + pass code.interact(local=imported_objects) diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 06ec8e2031..8f6664edb7 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -779,6 +779,18 @@ bpython:: .. _IPython: http://ipython.scipy.org/ .. _bpython: http://bpython-interpreter.org/ +When the "plain" Python interactive interpreter starts (be it because +``--plain`` was specified or because no other interactive interface is +available) it reads the script pointed to by the :envvar:`PYTHONSTARTUP` +environment variable and the ``~/.pythonrc.py`` script. If you don't wish this +behavior you can use the ``--no-startup`` option. e.g.:: + + django-admin.py shell --plain --no-startup + +.. versionadded:: 1.6 + +The ``--no-startup`` option was added in Django 1.6. + sql ------------------------- From eafc0364764ba12babd76194d8e1f78b876471ec Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 25 Jan 2013 06:53:40 -0500 Subject: [PATCH 198/870] Fixed #19577 - Added HTML escaping to admin examples. Thanks foo@ for the report and Florian Apolloner for the review. --- django/utils/html.py | 4 ++-- docs/ref/contrib/admin/index.txt | 33 ++++++++++++++++++++++++++------ docs/ref/utils.txt | 13 +++++++++++++ 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/django/utils/html.py b/django/utils/html.py index 25605bea04..ec7b28d330 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -87,8 +87,8 @@ def format_html(format_string, *args, **kwargs): def format_html_join(sep, format_string, args_generator): """ - A wrapper format_html, for the common case of a group of arguments that need - to be formatted using the same format string, and then joined using + A wrapper of format_html, for the common case of a group of arguments that + need to be formatted using the same format string, and then joined using 'sep'. 'sep' is also passed through conditional_escape. 'args_generator' should be an iterator that returns the sequence of 'args' diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index ee04d77d32..a862d55875 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -449,17 +449,25 @@ subclass:: * If the string given is a method of the model, ``ModelAdmin`` or a callable, Django will HTML-escape the output by default. If you'd rather not escape the output of the method, give the method an - ``allow_tags`` attribute whose value is ``True``. + ``allow_tags`` attribute whose value is ``True``. However, to avoid an + XSS vulnerability, you should use :func:`~django.utils.html.format_html` + to escape user-provided inputs. Here's a full example model:: + from django.utils.html import format_html + class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) color_code = models.CharField(max_length=6) def colored_name(self): - return '%s %s' % (self.color_code, self.first_name, self.last_name) + return format_html('{1} {2}', + self.color_code, + self.first_name, + self.last_name) + colored_name.allow_tags = True class PersonAdmin(admin.ModelAdmin): @@ -500,12 +508,17 @@ subclass:: For example:: + from django.utils.html import format_html + class Person(models.Model): first_name = models.CharField(max_length=50) color_code = models.CharField(max_length=6) def colored_first_name(self): - return '%s' % (self.color_code, self.first_name) + return format_html('{1}', + self.color_code, + self.first_name) + colored_first_name.allow_tags = True colored_first_name.admin_order_field = 'first_name' @@ -817,19 +830,27 @@ subclass:: the admin interface to provide feedback on the status of the objects being edited, for example:: + from django.utils.html import format_html_join + from django.utils.safestring import mark_safe + class PersonAdmin(ModelAdmin): readonly_fields = ('address_report',) def address_report(self, instance): - return ", ".join(instance.get_full_address()) or \ - "I can't determine this address." + # assuming get_full_address() returns a list of strings + # for each line of the address and you want to separate each + # line by a linebreak + return format_html_join( + mark_safe('
'), + '{0}', + ((line,) for line in instance.get_full_address()), + ) or "I can't determine this address." # short_description functions like a model field's verbose_name address_report.short_description = "Address" # in this example, we have used HTML tags in the output address_report.allow_tags = True - .. attribute:: ModelAdmin.save_as Set ``save_as`` to enable a "save as" feature on admin change forms. diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 20192ed006..e9b29602ac 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -541,6 +541,19 @@ escaping HTML. through :func:`conditional_escape` which (ultimately) calls :func:`~django.utils.encoding.force_text` on the values. +.. function:: format_html_join(sep, format_string, args_generator) + + A wrapper of :func:`format_html`, for the common case of a group of + arguments that need to be formatted using the same format string, and then + joined using ``sep``. ``sep`` is also passed through + :func:`conditional_escape`. + + ``args_generator`` should be an iterator that returns the sequence of + ``args`` that will be passed to :func:`format_html`. For example:: + + format_html_join('\n', "
  • {0} {1}
  • ", ((u.first_name, u.last_name) + for u in users)) + .. function:: strip_tags(value) Removes anything that looks like an html tag from the string, that is From b9c8bbf3726a1956be1db70ffd3bef04a2e5311a Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 25 Jan 2013 14:52:24 +0100 Subject: [PATCH 199/870] Fixed #19665 -- Ensured proper stderr output for Command.run_from_argv Thanks Stefan Koegl for the report and Simon Charette for the review. --- django/core/management/base.py | 7 +++-- tests/regressiontests/admin_scripts/tests.py | 27 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/django/core/management/base.py b/django/core/management/base.py index 895753ea12..7b9001ed14 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -221,9 +221,12 @@ class BaseCommand(object): try: self.execute(*args, **options.__dict__) except Exception as e: + # self.stderr is not guaranteed to be set here + stderr = getattr(self, 'stderr', OutputWrapper(sys.stderr, self.style.ERROR)) if options.traceback: - self.stderr.write(traceback.format_exc()) - self.stderr.write('%s: %s' % (e.__class__.__name__, e)) + stderr.write(traceback.format_exc()) + else: + stderr.write('%s: %s' % (e.__class__.__name__, e)) sys.exit(1) def execute(self, *args, **options): diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index df2326e163..921108600e 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -16,11 +16,13 @@ import codecs from django import conf, bin, get_version from django.conf import settings +from django.core.management import BaseCommand from django.db import connection from django.test.simple import DjangoTestSuiteRunner from django.utils import unittest from django.utils.encoding import force_str, force_text from django.utils._os import upath +from django.utils.six import StringIO from django.test import LiveServerTestCase test_dir = os.path.dirname(os.path.dirname(upath(__file__))) @@ -1279,6 +1281,31 @@ class CommandTypes(AdminScriptTestCase): self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + def test_base_run_from_argv(self): + """ + Test run_from_argv properly terminates even with custom execute() (#19665) + Also test proper traceback display. + """ + command = BaseCommand() + command.execute = lambda args: args # This will trigger TypeError + + old_stderr = sys.stderr + sys.stderr = err = StringIO() + try: + with self.assertRaises(SystemExit): + command.run_from_argv(['', '']) + err_message = err.getvalue() + self.assertNotIn("Traceback", err_message) + self.assertIn("TypeError", err_message) + + with self.assertRaises(SystemExit): + command.run_from_argv(['', '', '--traceback']) + err_message = err.getvalue() + self.assertIn("Traceback (most recent call last)", err_message) + self.assertIn("TypeError", err_message) + finally: + sys.stderr = old_stderr + def test_noargs(self): "NoArg Commands can be executed" args = ['noargs_command'] From 2babab0bb351ff7a13fd23795f5e926a9bf95d22 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Fri, 25 Jan 2013 13:23:33 -0300 Subject: [PATCH 200/870] Patch by Claude for #16084. --- .../core/management/commands/makemessages.py | 141 ++++++++++++------ docs/man/django-admin.1 | 3 +- docs/ref/django-admin.txt | 4 +- docs/topics/i18n/translation.txt | 17 +-- .../i18n/commands/extraction.py | 44 ++++++ .../i18n/commands/project_dir/__init__.py | 3 + .../project_dir/app_no_locale/models.py | 4 + .../project_dir/app_with_locale/models.py | 4 + tests/regressiontests/i18n/tests.py | 2 +- 9 files changed, 153 insertions(+), 69 deletions(-) create mode 100644 tests/regressiontests/i18n/commands/project_dir/__init__.py create mode 100644 tests/regressiontests/i18n/commands/project_dir/app_no_locale/models.py create mode 100644 tests/regressiontests/i18n/commands/project_dir/app_with_locale/models.py diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 4550605af2..b086e5f2dd 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -19,25 +19,28 @@ STATUS_OK = 0 @total_ordering class TranslatableFile(object): - def __init__(self, dirpath, file_name): + def __init__(self, dirpath, file_name, locale_dir): self.file = file_name self.dirpath = dirpath + self.locale_dir = locale_dir def __repr__(self): return "" % os.sep.join([self.dirpath, self.file]) def __eq__(self, other): - return self.dirpath == other.dirpath and self.file == other.file + return self.path == other.path def __lt__(self, other): - if self.dirpath == other.dirpath: - return self.file < other.file - return self.dirpath < other.dirpath + return self.path < other.path - def process(self, command, potfile, domain, keep_pot=False): + @property + def path(self): + return os.path.join(self.dirpath, self.file) + + def process(self, command, domain): """ - Extract translatable literals from self.file for :param domain: - creating or updating the :param potfile: POT file. + Extract translatable literals from self.file for :param domain:, + creating or updating the POT file. Uses the xgettext GNU gettext utility. """ @@ -91,8 +94,6 @@ class TranslatableFile(object): if status != STATUS_OK: if is_templatized: os.unlink(work_file) - if not keep_pot and os.path.exists(potfile): - os.unlink(potfile) raise CommandError( "errors happened while running xgettext on %s\n%s" % (self.file, errors)) @@ -100,11 +101,14 @@ class TranslatableFile(object): # Print warnings command.stdout.write(errors) if msgs: + # Write/append messages to pot file + potfile = os.path.join(self.locale_dir, '%s.pot' % str(domain)) if is_templatized: old = '#: ' + work_file[2:] new = '#: ' + orig_file[2:] msgs = msgs.replace(old, new) write_pot_file(potfile, msgs) + if is_templatized: os.unlink(work_file) @@ -232,21 +236,21 @@ class Command(NoArgsCommand): settings.configure(USE_I18N = True) self.invoked_for_django = False + self.locale_paths = [] + self.default_locale_path = None if os.path.isdir(os.path.join('conf', 'locale')): - localedir = os.path.abspath(os.path.join('conf', 'locale')) + self.locale_paths = [os.path.abspath(os.path.join('conf', 'locale'))] + self.default_locale_path = self.locale_paths[0] self.invoked_for_django = True # Ignoring all contrib apps self.ignore_patterns += ['contrib/*'] - elif os.path.isdir('locale'): - localedir = os.path.abspath('locale') else: - raise CommandError("This script should be run from the Django Git " - "tree or your project or app tree. If you did indeed run it " - "from the Git checkout or your project or application, " - "maybe you are just missing the conf/locale (in the django " - "tree) or locale (for project and application) directory? It " - "is not created automatically, you have to create it by hand " - "if you want to enable i18n for your project or application.") + self.locale_paths.extend(list(settings.LOCALE_PATHS)) + # Allow to run makemessages inside an app dir + if os.path.isdir('locale'): + self.locale_paths.append(os.path.abspath('locale')) + if self.locale_paths: + self.default_locale_path = self.locale_paths[0] # We require gettext version 0.15 or newer. output, errors, status = _popen('xgettext --version') @@ -261,24 +265,25 @@ class Command(NoArgsCommand): "gettext 0.15 or newer. You are using version %s, please " "upgrade your gettext toolset." % match.group()) - potfile = self.build_pot_file(localedir) - - # Build po files for each selected locale - locales = [] - if locale is not None: - locales += locale.split(',') if not isinstance(locale, list) else locale - elif process_all: - locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir)) - locales = [os.path.basename(l) for l in locale_dirs] - try: + potfiles = self.build_potfiles() + + # Build po files for each selected locale + locales = [] + if locale is not None: + locales = locale.split(',') if not isinstance(locale, list) else locale + elif process_all: + locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path)) + locales = [os.path.basename(l) for l in locale_dirs] + for locale in locales: if self.verbosity > 0: self.stdout.write("processing locale %s\n" % locale) - self.write_po_file(potfile, locale) + for potfile in potfiles: + self.write_po_file(potfile, locale) finally: - if not self.keep_pot and os.path.exists(potfile): - os.unlink(potfile) + if not self.keep_pot: + self.remove_potfiles() def build_pot_file(self, localedir): file_list = self.find_files(".") @@ -292,9 +297,41 @@ class Command(NoArgsCommand): f.process(self, potfile, self.domain, self.keep_pot) return potfile + def build_potfiles(self): + """Build pot files and apply msguniq to them""" + file_list = self.find_files(".") + self.remove_potfiles() + for f in file_list: + f.process(self, self.domain) + + potfiles = [] + for path in self.locale_paths: + potfile = os.path.join(path, '%s.pot' % str(self.domain)) + if not os.path.exists(potfile): + continue + msgs, errors, status = _popen('msguniq %s %s --to-code=utf-8 "%s"' % + (self.wrap, self.location, potfile)) + if errors: + if status != STATUS_OK: + raise CommandError( + "errors happened while running msguniq\n%s" % errors) + elif self.verbosity > 0: + self.stdout.write(errors) + with open(potfile, 'w') as fp: + fp.write(msgs) + potfiles.append(potfile) + return potfiles + + def remove_potfiles(self): + for path in self.locale_paths: + pot_path = os.path.join(path, '%s.pot' % str(self.domain)) + if os.path.exists(pot_path): + os.unlink(pot_path) + def find_files(self, root): """ - Helper method to get all files in the given root. + Helper function to get all files in the given root. Also check that there + is a matching locale dir for each file. """ def is_ignored(path, ignore_patterns): @@ -315,12 +352,26 @@ class Command(NoArgsCommand): dirnames.remove(dirname) if self.verbosity > 1: self.stdout.write('ignoring directory %s\n' % dirname) + elif dirname == 'locale': + dirnames.remove(dirname) + self.locale_paths.insert(0, os.path.join(os.path.abspath(dirpath), dirname)) for filename in filenames: - if is_ignored(os.path.normpath(os.path.join(dirpath, filename)), self.ignore_patterns): + file_path = os.path.normpath(os.path.join(dirpath, filename)) + if is_ignored(file_path, self.ignore_patterns): if self.verbosity > 1: self.stdout.write('ignoring file %s in %s\n' % (filename, dirpath)) else: - all_files.append(TranslatableFile(dirpath, filename)) + locale_dir = None + for path in self.locale_paths: + if os.path.abspath(dirpath).startswith(os.path.dirname(path)): + locale_dir = path + break + if not locale_dir: + locale_dir = self.default_locale_path + if not locale_dir: + raise CommandError( + "Unable to find a locale path to store translations for file %s" % file_path) + all_files.append(TranslatableFile(dirpath, filename, locale_dir)) return sorted(all_files) def write_po_file(self, potfile, locale): @@ -328,16 +379,8 @@ class Command(NoArgsCommand): Creates or updates the PO file for self.domain and :param locale:. Uses contents of the existing :param potfile:. - Uses mguniq, msgmerge, and msgattrib GNU gettext utilities. + Uses msgmerge, and msgattrib GNU gettext utilities. """ - msgs, errors, status = _popen('msguniq %s %s --to-code=utf-8 "%s"' % - (self.wrap, self.location, potfile)) - if errors: - if status != STATUS_OK: - raise CommandError( - "errors happened while running msguniq\n%s" % errors) - elif self.verbosity > 0: - self.stdout.write(errors) basedir = os.path.join(os.path.dirname(potfile), locale, 'LC_MESSAGES') if not os.path.isdir(basedir): @@ -345,8 +388,6 @@ class Command(NoArgsCommand): pofile = os.path.join(basedir, '%s.po' % str(self.domain)) if os.path.exists(pofile): - with open(potfile, 'w') as fp: - fp.write(msgs) msgs, errors, status = _popen('msgmerge %s %s -q "%s" "%s"' % (self.wrap, self.location, pofile, potfile)) if errors: @@ -355,8 +396,10 @@ class Command(NoArgsCommand): "errors happened while running msgmerge\n%s" % errors) elif self.verbosity > 0: self.stdout.write(errors) - elif not self.invoked_for_django: - msgs = self.copy_plural_forms(msgs, locale) + else: + msgs = open(potfile, 'r').read() + if not self.invoked_for_django: + msgs = self.copy_plural_forms(msgs, locale) msgs = msgs.replace( "#. #-#-#-#-# %s.pot (PACKAGE VERSION) #-#-#-#-#\n" % self.domain, "") with open(pofile, 'w') as fp: diff --git a/docs/man/django-admin.1 b/docs/man/django-admin.1 index 4d937b488b..c9c8d19869 100644 --- a/docs/man/django-admin.1 +++ b/docs/man/django-admin.1 @@ -193,7 +193,8 @@ Ignore files or directories matching this glob-style pattern. Use multiple times to ignore more (makemessages command). .TP .I \-\-no\-default\-ignore -Don't ignore the common private glob-style patterns 'CVS', '.*' and '*~' (makemessages command). +Don't ignore the common private glob-style patterns 'CVS', '.*', '*~' and '*.pyc' +(makemessages command). .TP .I \-\-no\-wrap Don't break long message lines into several lines (makemessages command). diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 8f6664edb7..f7b91bbdab 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -472,7 +472,7 @@ Example usage:: Use the ``--ignore`` or ``-i`` option to ignore files or directories matching the given :mod:`glob`-style pattern. Use multiple times to ignore more. -These patterns are used by default: ``'CVS'``, ``'.*'``, ``'*~'`` +These patterns are used by default: ``'CVS'``, ``'.*'``, ``'*~'``, ``'*.pyc'`` Example usage:: @@ -499,7 +499,7 @@ for technically skilled translators to understand each message's context. .. versionadded:: 1.6 Use the ``--keep-pot`` option to prevent django from deleting the temporary -.pot file it generates before creating the .po file. This is useful for +.pot files it generates before creating the .po file. This is useful for debugging errors which may prevent the final language files from being created. runfcgi [options] diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 01f168bc10..8ef51e4052 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -1543,24 +1543,9 @@ All message file repositories are structured the same way. They are: * ``$PYTHONPATH/django/conf/locale//LC_MESSAGES/django.(po|mo)`` To create message files, you use the :djadmin:`django-admin.py makemessages ` -tool. You only need to be in the same directory where the ``locale/`` directory -is located. And you use :djadmin:`django-admin.py compilemessages ` +tool. And you use :djadmin:`django-admin.py compilemessages ` to produce the binary ``.mo`` files that are used by ``gettext``. You can also run :djadmin:`django-admin.py compilemessages --settings=path.to.settings ` to make the compiler process all the directories in your :setting:`LOCALE_PATHS` setting. - -Finally, you should give some thought to the structure of your translation -files. If your applications need to be delivered to other users and will be used -in other projects, you might want to use app-specific translations. But using -app-specific translations and project-specific translations could produce weird -problems with :djadmin:`makemessages`: it will traverse all directories below -the current path and so might put message IDs into a unified, common message -file for the current project that are already in application message files. - -The easiest way out is to store applications that are not part of the project -(and so carry their own translations) outside the project tree. That way, -:djadmin:`django-admin.py makemessages `, when ran on a project -level will only extract strings that are connected to your explicit project and -not strings that are distributed independently. diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index ef711ec1bb..8b2941c4d4 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -5,10 +5,13 @@ import os import re import shutil +from django.conf import settings from django.core import management from django.test import SimpleTestCase +from django.test.utils import override_settings from django.utils.encoding import force_text from django.utils._os import upath +from django.utils import six from django.utils.six import StringIO @@ -352,3 +355,44 @@ class MultipleLocaleExtractionTests(ExtractorTests): management.call_command('makemessages', locale='pt,de,ch', verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE_PT)) self.assertTrue(os.path.exists(self.PO_FILE_DE)) + + +class CustomLayoutExtractionTests(ExtractorTests): + def setUp(self): + self._cwd = os.getcwd() + self.test_dir = os.path.join(os.path.dirname(upath(__file__)), 'project_dir') + + def test_no_locale_raises(self): + os.chdir(self.test_dir) + with six.assertRaisesRegex(self, management.CommandError, + "Unable to find a locale path to store translations for file"): + management.call_command('makemessages', locale=LOCALE, verbosity=0) + + @override_settings( + LOCALE_PATHS=(os.path.join(os.path.dirname(upath(__file__)), 'project_dir/project_locale'),) + ) + def test_project_locale_paths(self): + """ + Test that: + * translations for app containing locale folder are stored in that folder + * translations outside of that app are in LOCALE_PATHS[0] + """ + os.chdir(self.test_dir) + self.addCleanup(shutil.rmtree, os.path.join(settings.LOCALE_PATHS[0], LOCALE)) + self.addCleanup(shutil.rmtree, os.path.join(self.test_dir, 'app_with_locale/locale', LOCALE)) + + management.call_command('makemessages', locale=LOCALE, verbosity=0) + project_de_locale = os.path.join( + self.test_dir, 'project_locale/de/LC_MESSAGES/django.po',) + app_de_locale = os.path.join( + self.test_dir, 'app_with_locale/locale/de/LC_MESSAGES/django.po',) + self.assertTrue(os.path.exists(project_de_locale)) + self.assertTrue(os.path.exists(app_de_locale)) + + with open(project_de_locale, 'r') as fp: + po_contents = force_text(fp.read()) + self.assertMsgId('This app has no locale directory', po_contents) + self.assertMsgId('This is a project-level string', po_contents) + with open(app_de_locale, 'r') as fp: + po_contents = force_text(fp.read()) + self.assertMsgId('This app has a locale directory', po_contents) diff --git a/tests/regressiontests/i18n/commands/project_dir/__init__.py b/tests/regressiontests/i18n/commands/project_dir/__init__.py new file mode 100644 index 0000000000..9c6768e4ab --- /dev/null +++ b/tests/regressiontests/i18n/commands/project_dir/__init__.py @@ -0,0 +1,3 @@ +from django.utils.translation import ugettext as _ + +string = _("This is a project-level string") diff --git a/tests/regressiontests/i18n/commands/project_dir/app_no_locale/models.py b/tests/regressiontests/i18n/commands/project_dir/app_no_locale/models.py new file mode 100644 index 0000000000..adcb2ef173 --- /dev/null +++ b/tests/regressiontests/i18n/commands/project_dir/app_no_locale/models.py @@ -0,0 +1,4 @@ +from django.utils.translation import ugettext as _ + +string = _("This app has no locale directory") + diff --git a/tests/regressiontests/i18n/commands/project_dir/app_with_locale/models.py b/tests/regressiontests/i18n/commands/project_dir/app_with_locale/models.py new file mode 100644 index 0000000000..44037157a0 --- /dev/null +++ b/tests/regressiontests/i18n/commands/project_dir/app_with_locale/models.py @@ -0,0 +1,4 @@ +from django.utils.translation import ugettext as _ + +string = _("This app has a locale directory") + diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index d9843c228a..9f6e73dcd2 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -33,7 +33,7 @@ if can_run_extraction_tests: JavascriptExtractorTests, IgnoredExtractorTests, SymlinkExtractorTests, CopyPluralFormsExtractorTests, NoWrapExtractorTests, NoLocationExtractorTests, KeepPotFileExtractorTests, - MultipleLocaleExtractionTests) + MultipleLocaleExtractionTests, CustomLayoutExtractionTests) if can_run_compilation_tests: from .commands.compilation import (PoFileTests, PoFileContentsTests, PercentRenderingTests, MultipleLocaleCompilationTests) From 5b99d5a330fc412ce56b9e5f9cf0b971654da90c Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Fri, 25 Jan 2013 13:50:37 -0300 Subject: [PATCH 201/870] Added more shortcuts to i18n docs in index page. --- docs/index.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/index.txt b/docs/index.txt index d047abafd4..73b378de4d 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -221,8 +221,11 @@ Django offers a robust internationalization and localization framework to assist you in the development of applications for multiple languages and world regions: -* :doc:`Internationalization ` +* :doc:`Overview ` | + :doc:`Internationalization ` | + :ref:`Localization ` * :doc:`"Local flavor" ` +* :doc:`Time zones ` Python compatibility ==================== From ce27fb198dcce5dad47de83fc81119d3bb6567ce Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Fri, 25 Jan 2013 13:58:37 -0300 Subject: [PATCH 202/870] Revert "Patch by Claude for #16084." This reverts commit 2babab0bb351ff7a13fd23795f5e926a9bf95d22. --- .../core/management/commands/makemessages.py | 141 ++++++------------ docs/man/django-admin.1 | 3 +- docs/ref/django-admin.txt | 4 +- docs/topics/i18n/translation.txt | 17 ++- .../i18n/commands/extraction.py | 44 ------ .../i18n/commands/project_dir/__init__.py | 3 - .../project_dir/app_no_locale/models.py | 4 - .../project_dir/app_with_locale/models.py | 4 - tests/regressiontests/i18n/tests.py | 2 +- 9 files changed, 69 insertions(+), 153 deletions(-) delete mode 100644 tests/regressiontests/i18n/commands/project_dir/__init__.py delete mode 100644 tests/regressiontests/i18n/commands/project_dir/app_no_locale/models.py delete mode 100644 tests/regressiontests/i18n/commands/project_dir/app_with_locale/models.py diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index b086e5f2dd..4550605af2 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -19,28 +19,25 @@ STATUS_OK = 0 @total_ordering class TranslatableFile(object): - def __init__(self, dirpath, file_name, locale_dir): + def __init__(self, dirpath, file_name): self.file = file_name self.dirpath = dirpath - self.locale_dir = locale_dir def __repr__(self): return "" % os.sep.join([self.dirpath, self.file]) def __eq__(self, other): - return self.path == other.path + return self.dirpath == other.dirpath and self.file == other.file def __lt__(self, other): - return self.path < other.path + if self.dirpath == other.dirpath: + return self.file < other.file + return self.dirpath < other.dirpath - @property - def path(self): - return os.path.join(self.dirpath, self.file) - - def process(self, command, domain): + def process(self, command, potfile, domain, keep_pot=False): """ - Extract translatable literals from self.file for :param domain:, - creating or updating the POT file. + Extract translatable literals from self.file for :param domain: + creating or updating the :param potfile: POT file. Uses the xgettext GNU gettext utility. """ @@ -94,6 +91,8 @@ class TranslatableFile(object): if status != STATUS_OK: if is_templatized: os.unlink(work_file) + if not keep_pot and os.path.exists(potfile): + os.unlink(potfile) raise CommandError( "errors happened while running xgettext on %s\n%s" % (self.file, errors)) @@ -101,14 +100,11 @@ class TranslatableFile(object): # Print warnings command.stdout.write(errors) if msgs: - # Write/append messages to pot file - potfile = os.path.join(self.locale_dir, '%s.pot' % str(domain)) if is_templatized: old = '#: ' + work_file[2:] new = '#: ' + orig_file[2:] msgs = msgs.replace(old, new) write_pot_file(potfile, msgs) - if is_templatized: os.unlink(work_file) @@ -236,21 +232,21 @@ class Command(NoArgsCommand): settings.configure(USE_I18N = True) self.invoked_for_django = False - self.locale_paths = [] - self.default_locale_path = None if os.path.isdir(os.path.join('conf', 'locale')): - self.locale_paths = [os.path.abspath(os.path.join('conf', 'locale'))] - self.default_locale_path = self.locale_paths[0] + localedir = os.path.abspath(os.path.join('conf', 'locale')) self.invoked_for_django = True # Ignoring all contrib apps self.ignore_patterns += ['contrib/*'] + elif os.path.isdir('locale'): + localedir = os.path.abspath('locale') else: - self.locale_paths.extend(list(settings.LOCALE_PATHS)) - # Allow to run makemessages inside an app dir - if os.path.isdir('locale'): - self.locale_paths.append(os.path.abspath('locale')) - if self.locale_paths: - self.default_locale_path = self.locale_paths[0] + raise CommandError("This script should be run from the Django Git " + "tree or your project or app tree. If you did indeed run it " + "from the Git checkout or your project or application, " + "maybe you are just missing the conf/locale (in the django " + "tree) or locale (for project and application) directory? It " + "is not created automatically, you have to create it by hand " + "if you want to enable i18n for your project or application.") # We require gettext version 0.15 or newer. output, errors, status = _popen('xgettext --version') @@ -265,25 +261,24 @@ class Command(NoArgsCommand): "gettext 0.15 or newer. You are using version %s, please " "upgrade your gettext toolset." % match.group()) + potfile = self.build_pot_file(localedir) + + # Build po files for each selected locale + locales = [] + if locale is not None: + locales += locale.split(',') if not isinstance(locale, list) else locale + elif process_all: + locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir)) + locales = [os.path.basename(l) for l in locale_dirs] + try: - potfiles = self.build_potfiles() - - # Build po files for each selected locale - locales = [] - if locale is not None: - locales = locale.split(',') if not isinstance(locale, list) else locale - elif process_all: - locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path)) - locales = [os.path.basename(l) for l in locale_dirs] - for locale in locales: if self.verbosity > 0: self.stdout.write("processing locale %s\n" % locale) - for potfile in potfiles: - self.write_po_file(potfile, locale) + self.write_po_file(potfile, locale) finally: - if not self.keep_pot: - self.remove_potfiles() + if not self.keep_pot and os.path.exists(potfile): + os.unlink(potfile) def build_pot_file(self, localedir): file_list = self.find_files(".") @@ -297,41 +292,9 @@ class Command(NoArgsCommand): f.process(self, potfile, self.domain, self.keep_pot) return potfile - def build_potfiles(self): - """Build pot files and apply msguniq to them""" - file_list = self.find_files(".") - self.remove_potfiles() - for f in file_list: - f.process(self, self.domain) - - potfiles = [] - for path in self.locale_paths: - potfile = os.path.join(path, '%s.pot' % str(self.domain)) - if not os.path.exists(potfile): - continue - msgs, errors, status = _popen('msguniq %s %s --to-code=utf-8 "%s"' % - (self.wrap, self.location, potfile)) - if errors: - if status != STATUS_OK: - raise CommandError( - "errors happened while running msguniq\n%s" % errors) - elif self.verbosity > 0: - self.stdout.write(errors) - with open(potfile, 'w') as fp: - fp.write(msgs) - potfiles.append(potfile) - return potfiles - - def remove_potfiles(self): - for path in self.locale_paths: - pot_path = os.path.join(path, '%s.pot' % str(self.domain)) - if os.path.exists(pot_path): - os.unlink(pot_path) - def find_files(self, root): """ - Helper function to get all files in the given root. Also check that there - is a matching locale dir for each file. + Helper method to get all files in the given root. """ def is_ignored(path, ignore_patterns): @@ -352,26 +315,12 @@ class Command(NoArgsCommand): dirnames.remove(dirname) if self.verbosity > 1: self.stdout.write('ignoring directory %s\n' % dirname) - elif dirname == 'locale': - dirnames.remove(dirname) - self.locale_paths.insert(0, os.path.join(os.path.abspath(dirpath), dirname)) for filename in filenames: - file_path = os.path.normpath(os.path.join(dirpath, filename)) - if is_ignored(file_path, self.ignore_patterns): + if is_ignored(os.path.normpath(os.path.join(dirpath, filename)), self.ignore_patterns): if self.verbosity > 1: self.stdout.write('ignoring file %s in %s\n' % (filename, dirpath)) else: - locale_dir = None - for path in self.locale_paths: - if os.path.abspath(dirpath).startswith(os.path.dirname(path)): - locale_dir = path - break - if not locale_dir: - locale_dir = self.default_locale_path - if not locale_dir: - raise CommandError( - "Unable to find a locale path to store translations for file %s" % file_path) - all_files.append(TranslatableFile(dirpath, filename, locale_dir)) + all_files.append(TranslatableFile(dirpath, filename)) return sorted(all_files) def write_po_file(self, potfile, locale): @@ -379,8 +328,16 @@ class Command(NoArgsCommand): Creates or updates the PO file for self.domain and :param locale:. Uses contents of the existing :param potfile:. - Uses msgmerge, and msgattrib GNU gettext utilities. + Uses mguniq, msgmerge, and msgattrib GNU gettext utilities. """ + msgs, errors, status = _popen('msguniq %s %s --to-code=utf-8 "%s"' % + (self.wrap, self.location, potfile)) + if errors: + if status != STATUS_OK: + raise CommandError( + "errors happened while running msguniq\n%s" % errors) + elif self.verbosity > 0: + self.stdout.write(errors) basedir = os.path.join(os.path.dirname(potfile), locale, 'LC_MESSAGES') if not os.path.isdir(basedir): @@ -388,6 +345,8 @@ class Command(NoArgsCommand): pofile = os.path.join(basedir, '%s.po' % str(self.domain)) if os.path.exists(pofile): + with open(potfile, 'w') as fp: + fp.write(msgs) msgs, errors, status = _popen('msgmerge %s %s -q "%s" "%s"' % (self.wrap, self.location, pofile, potfile)) if errors: @@ -396,10 +355,8 @@ class Command(NoArgsCommand): "errors happened while running msgmerge\n%s" % errors) elif self.verbosity > 0: self.stdout.write(errors) - else: - msgs = open(potfile, 'r').read() - if not self.invoked_for_django: - msgs = self.copy_plural_forms(msgs, locale) + elif not self.invoked_for_django: + msgs = self.copy_plural_forms(msgs, locale) msgs = msgs.replace( "#. #-#-#-#-# %s.pot (PACKAGE VERSION) #-#-#-#-#\n" % self.domain, "") with open(pofile, 'w') as fp: diff --git a/docs/man/django-admin.1 b/docs/man/django-admin.1 index c9c8d19869..4d937b488b 100644 --- a/docs/man/django-admin.1 +++ b/docs/man/django-admin.1 @@ -193,8 +193,7 @@ Ignore files or directories matching this glob-style pattern. Use multiple times to ignore more (makemessages command). .TP .I \-\-no\-default\-ignore -Don't ignore the common private glob-style patterns 'CVS', '.*', '*~' and '*.pyc' -(makemessages command). +Don't ignore the common private glob-style patterns 'CVS', '.*' and '*~' (makemessages command). .TP .I \-\-no\-wrap Don't break long message lines into several lines (makemessages command). diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index f7b91bbdab..8f6664edb7 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -472,7 +472,7 @@ Example usage:: Use the ``--ignore`` or ``-i`` option to ignore files or directories matching the given :mod:`glob`-style pattern. Use multiple times to ignore more. -These patterns are used by default: ``'CVS'``, ``'.*'``, ``'*~'``, ``'*.pyc'`` +These patterns are used by default: ``'CVS'``, ``'.*'``, ``'*~'`` Example usage:: @@ -499,7 +499,7 @@ for technically skilled translators to understand each message's context. .. versionadded:: 1.6 Use the ``--keep-pot`` option to prevent django from deleting the temporary -.pot files it generates before creating the .po file. This is useful for +.pot file it generates before creating the .po file. This is useful for debugging errors which may prevent the final language files from being created. runfcgi [options] diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 8ef51e4052..01f168bc10 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -1543,9 +1543,24 @@ All message file repositories are structured the same way. They are: * ``$PYTHONPATH/django/conf/locale//LC_MESSAGES/django.(po|mo)`` To create message files, you use the :djadmin:`django-admin.py makemessages ` -tool. And you use :djadmin:`django-admin.py compilemessages ` +tool. You only need to be in the same directory where the ``locale/`` directory +is located. And you use :djadmin:`django-admin.py compilemessages ` to produce the binary ``.mo`` files that are used by ``gettext``. You can also run :djadmin:`django-admin.py compilemessages --settings=path.to.settings ` to make the compiler process all the directories in your :setting:`LOCALE_PATHS` setting. + +Finally, you should give some thought to the structure of your translation +files. If your applications need to be delivered to other users and will be used +in other projects, you might want to use app-specific translations. But using +app-specific translations and project-specific translations could produce weird +problems with :djadmin:`makemessages`: it will traverse all directories below +the current path and so might put message IDs into a unified, common message +file for the current project that are already in application message files. + +The easiest way out is to store applications that are not part of the project +(and so carry their own translations) outside the project tree. That way, +:djadmin:`django-admin.py makemessages `, when ran on a project +level will only extract strings that are connected to your explicit project and +not strings that are distributed independently. diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index 8b2941c4d4..ef711ec1bb 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -5,13 +5,10 @@ import os import re import shutil -from django.conf import settings from django.core import management from django.test import SimpleTestCase -from django.test.utils import override_settings from django.utils.encoding import force_text from django.utils._os import upath -from django.utils import six from django.utils.six import StringIO @@ -355,44 +352,3 @@ class MultipleLocaleExtractionTests(ExtractorTests): management.call_command('makemessages', locale='pt,de,ch', verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE_PT)) self.assertTrue(os.path.exists(self.PO_FILE_DE)) - - -class CustomLayoutExtractionTests(ExtractorTests): - def setUp(self): - self._cwd = os.getcwd() - self.test_dir = os.path.join(os.path.dirname(upath(__file__)), 'project_dir') - - def test_no_locale_raises(self): - os.chdir(self.test_dir) - with six.assertRaisesRegex(self, management.CommandError, - "Unable to find a locale path to store translations for file"): - management.call_command('makemessages', locale=LOCALE, verbosity=0) - - @override_settings( - LOCALE_PATHS=(os.path.join(os.path.dirname(upath(__file__)), 'project_dir/project_locale'),) - ) - def test_project_locale_paths(self): - """ - Test that: - * translations for app containing locale folder are stored in that folder - * translations outside of that app are in LOCALE_PATHS[0] - """ - os.chdir(self.test_dir) - self.addCleanup(shutil.rmtree, os.path.join(settings.LOCALE_PATHS[0], LOCALE)) - self.addCleanup(shutil.rmtree, os.path.join(self.test_dir, 'app_with_locale/locale', LOCALE)) - - management.call_command('makemessages', locale=LOCALE, verbosity=0) - project_de_locale = os.path.join( - self.test_dir, 'project_locale/de/LC_MESSAGES/django.po',) - app_de_locale = os.path.join( - self.test_dir, 'app_with_locale/locale/de/LC_MESSAGES/django.po',) - self.assertTrue(os.path.exists(project_de_locale)) - self.assertTrue(os.path.exists(app_de_locale)) - - with open(project_de_locale, 'r') as fp: - po_contents = force_text(fp.read()) - self.assertMsgId('This app has no locale directory', po_contents) - self.assertMsgId('This is a project-level string', po_contents) - with open(app_de_locale, 'r') as fp: - po_contents = force_text(fp.read()) - self.assertMsgId('This app has a locale directory', po_contents) diff --git a/tests/regressiontests/i18n/commands/project_dir/__init__.py b/tests/regressiontests/i18n/commands/project_dir/__init__.py deleted file mode 100644 index 9c6768e4ab..0000000000 --- a/tests/regressiontests/i18n/commands/project_dir/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.utils.translation import ugettext as _ - -string = _("This is a project-level string") diff --git a/tests/regressiontests/i18n/commands/project_dir/app_no_locale/models.py b/tests/regressiontests/i18n/commands/project_dir/app_no_locale/models.py deleted file mode 100644 index adcb2ef173..0000000000 --- a/tests/regressiontests/i18n/commands/project_dir/app_no_locale/models.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.utils.translation import ugettext as _ - -string = _("This app has no locale directory") - diff --git a/tests/regressiontests/i18n/commands/project_dir/app_with_locale/models.py b/tests/regressiontests/i18n/commands/project_dir/app_with_locale/models.py deleted file mode 100644 index 44037157a0..0000000000 --- a/tests/regressiontests/i18n/commands/project_dir/app_with_locale/models.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.utils.translation import ugettext as _ - -string = _("This app has a locale directory") - diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index 9f6e73dcd2..d9843c228a 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -33,7 +33,7 @@ if can_run_extraction_tests: JavascriptExtractorTests, IgnoredExtractorTests, SymlinkExtractorTests, CopyPluralFormsExtractorTests, NoWrapExtractorTests, NoLocationExtractorTests, KeepPotFileExtractorTests, - MultipleLocaleExtractionTests, CustomLayoutExtractionTests) + MultipleLocaleExtractionTests) if can_run_compilation_tests: from .commands.compilation import (PoFileTests, PoFileContentsTests, PercentRenderingTests, MultipleLocaleCompilationTests) From ebb504db692cac496f4f45762d1d14644c9fa6fa Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 25 Jan 2013 20:50:46 +0100 Subject: [PATCH 203/870] Moved has_changed logic from widget to form field Refs #16612. Thanks Aymeric Augustin for the suggestion. --- django/contrib/admin/widgets.py | 14 --- django/contrib/gis/admin/widgets.py | 24 +---- django/contrib/gis/forms/fields.py | 26 +++++- django/contrib/gis/tests/geoadmin/tests.py | 2 +- django/forms/extras/widgets.py | 8 -- django/forms/fields.py | 77 ++++++++++++++++ django/forms/forms.py | 8 +- django/forms/models.py | 9 +- django/forms/widgets.py | 92 ------------------- docs/releases/1.6.txt | 6 ++ tests/regressiontests/admin_widgets/tests.py | 7 -- tests/regressiontests/forms/tests/extra.py | 21 ++++- tests/regressiontests/forms/tests/fields.py | 90 ++++++++++++++++++ tests/regressiontests/forms/tests/widgets.py | 97 -------------------- 14 files changed, 230 insertions(+), 251 deletions(-) diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 1e6277fb87..a3887740d8 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -213,17 +213,6 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): if value: return value.split(',') - def _has_changed(self, initial, data): - if initial is None: - initial = [] - if data is None: - data = [] - if len(initial) != len(data): - return True - for pk1, pk2 in zip(initial, data): - if force_text(pk1) != force_text(pk2): - return True - return False class RelatedFieldWidgetWrapper(forms.Widget): """ @@ -279,9 +268,6 @@ class RelatedFieldWidgetWrapper(forms.Widget): def value_from_datadict(self, data, files, name): return self.widget.value_from_datadict(data, files, name) - def _has_changed(self, initial, data): - return self.widget._has_changed(initial, data) - def id_for_label(self, id_): return self.widget.id_for_label(id_) diff --git a/django/contrib/gis/admin/widgets.py b/django/contrib/gis/admin/widgets.py index f4379be7f3..a06933660f 100644 --- a/django/contrib/gis/admin/widgets.py +++ b/django/contrib/gis/admin/widgets.py @@ -7,7 +7,7 @@ from django.utils import six from django.utils import translation from django.contrib.gis.gdal import OGRException -from django.contrib.gis.geos import GEOSGeometry, GEOSException, fromstr +from django.contrib.gis.geos import GEOSGeometry, GEOSException # Creating a template context that contains Django settings # values needed by admin map templates. @@ -117,25 +117,3 @@ class OpenLayersWidget(Textarea): raise TypeError map_options[js_name] = value return map_options - - def _has_changed(self, initial, data): - """ Compare geographic value of data with its initial value. """ - - # Ensure we are dealing with a geographic object - if isinstance(initial, six.string_types): - try: - initial = GEOSGeometry(initial) - except (GEOSException, ValueError): - initial = None - - # Only do a geographic comparison if both values are available - if initial and data: - data = fromstr(data) - data.transform(initial.srid) - # If the initial value was not added by the browser, the geometry - # provided may be slightly different, the first time it is saved. - # The comparison is done with a very low tolerance. - return not initial.equals_exact(data, tolerance=0.000001) - else: - # Check for change of state of existence - return bool(initial) != bool(data) diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py index cefb6830ba..ab2e37f1e1 100644 --- a/django/contrib/gis/forms/fields.py +++ b/django/contrib/gis/forms/fields.py @@ -1,11 +1,13 @@ from __future__ import unicode_literals from django import forms +from django.utils import six from django.utils.translation import ugettext_lazy as _ # While this couples the geographic forms to the GEOS library, # it decouples from database (by not importing SpatialBackend). -from django.contrib.gis.geos import GEOSException, GEOSGeometry +from django.contrib.gis.geos import GEOSException, GEOSGeometry, fromstr + class GeometryField(forms.Field): """ @@ -73,3 +75,25 @@ class GeometryField(forms.Field): raise forms.ValidationError(self.error_messages['transform_error']) return geom + + def _has_changed(self, initial, data): + """ Compare geographic value of data with its initial value. """ + + # Ensure we are dealing with a geographic object + if isinstance(initial, six.string_types): + try: + initial = GEOSGeometry(initial) + except (GEOSException, ValueError): + initial = None + + # Only do a geographic comparison if both values are available + if initial and data: + data = fromstr(data) + data.transform(initial.srid) + # If the initial value was not added by the browser, the geometry + # provided may be slightly different, the first time it is saved. + # The comparison is done with a very low tolerance. + return not initial.equals_exact(data, tolerance=0.000001) + else: + # Check for change of state of existence + return bool(initial) != bool(data) diff --git a/django/contrib/gis/tests/geoadmin/tests.py b/django/contrib/gis/tests/geoadmin/tests.py index 6fadebdb9a..669914bdea 100644 --- a/django/contrib/gis/tests/geoadmin/tests.py +++ b/django/contrib/gis/tests/geoadmin/tests.py @@ -38,7 +38,7 @@ class GeoAdminTest(TestCase): """ Check that changes are accurately noticed by OpenLayersWidget. """ geoadmin = admin.site._registry[City] form = geoadmin.get_changelist_form(None)() - has_changed = form.fields['point'].widget._has_changed + has_changed = form.fields['point']._has_changed initial = Point(13.4197458572965953, 52.5194108501149799, srid=4326) data_same = "SRID=3857;POINT(1493879.2754093995 6894592.019687599)" diff --git a/django/forms/extras/widgets.py b/django/forms/extras/widgets.py index c5ca1424c8..e939a8f665 100644 --- a/django/forms/extras/widgets.py +++ b/django/forms/extras/widgets.py @@ -135,11 +135,3 @@ class SelectDateWidget(Widget): s = Select(choices=choices) select_html = s.render(field % name, val, local_attrs) return select_html - - def _has_changed(self, initial, data): - try: - input_format = get_format('DATE_INPUT_FORMATS')[0] - data = datetime_safe.datetime.strptime(data, input_format).date() - except (TypeError, ValueError): - pass - return super(SelectDateWidget, self)._has_changed(initial, data) diff --git a/django/forms/fields.py b/django/forms/fields.py index 4438812a37..1e9cbcb4d9 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -175,6 +175,25 @@ class Field(object): """ return {} + def _has_changed(self, initial, data): + """ + Return True if data differs from initial. + """ + # For purposes of seeing whether something has changed, None is + # the same as an empty string, if the data or inital value we get + # is None, replace it w/ ''. + if data is None: + data_value = '' + else: + data_value = data + if initial is None: + initial_value = '' + else: + initial_value = initial + if force_text(initial_value) != force_text(data_value): + return True + return False + def __deepcopy__(self, memo): result = copy.copy(self) memo[id(self)] = result @@ -348,6 +367,13 @@ class BaseTemporalField(Field): def strptime(self, value, format): raise NotImplementedError('Subclasses must define this method.') + def _has_changed(self, initial, data): + try: + data = self.to_python(data) + except ValidationError: + return True + return self.to_python(initial) != data + class DateField(BaseTemporalField): widget = DateInput input_formats = formats.get_format_lazy('DATE_INPUT_FORMATS') @@ -371,6 +397,7 @@ class DateField(BaseTemporalField): def strptime(self, value, format): return datetime.datetime.strptime(value, format).date() + class TimeField(BaseTemporalField): widget = TimeInput input_formats = formats.get_format_lazy('TIME_INPUT_FORMATS') @@ -529,6 +556,12 @@ class FileField(Field): return initial return data + def _has_changed(self, initial, data): + if data is None: + return False + return True + + class ImageField(FileField): default_error_messages = { 'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."), @@ -618,6 +651,7 @@ class URLField(CharField): value = urlunsplit(url_fields) return value + class BooleanField(Field): widget = CheckboxInput @@ -636,6 +670,15 @@ class BooleanField(Field): raise ValidationError(self.error_messages['required']) return value + def _has_changed(self, initial, data): + # Sometimes data or initial could be None or '' which should be the + # same thing as False. + if initial == 'False': + # show_hidden_initial may have transformed False to 'False' + initial = False + return bool(initial) != bool(data) + + class NullBooleanField(BooleanField): """ A field whose valid values are None, True and False. Invalid values are @@ -660,6 +703,15 @@ class NullBooleanField(BooleanField): def validate(self, value): pass + def _has_changed(self, initial, data): + # None (unknown) and False (No) are not the same + if initial is not None: + initial = bool(initial) + if data is not None: + data = bool(data) + return initial != data + + class ChoiceField(Field): widget = Select default_error_messages = { @@ -739,6 +791,7 @@ class TypedChoiceField(ChoiceField): def validate(self, value): pass + class MultipleChoiceField(ChoiceField): hidden_widget = MultipleHiddenInput widget = SelectMultiple @@ -765,6 +818,18 @@ class MultipleChoiceField(ChoiceField): if not self.valid_value(val): raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) + def _has_changed(self, initial, data): + if initial is None: + initial = [] + if data is None: + data = [] + if len(initial) != len(data): + return True + initial_set = set([force_text(value) for value in initial]) + data_set = set([force_text(value) for value in data]) + return data_set != initial_set + + class TypedMultipleChoiceField(MultipleChoiceField): def __init__(self, *args, **kwargs): self.coerce = kwargs.pop('coerce', lambda val: val) @@ -899,6 +964,18 @@ class MultiValueField(Field): """ raise NotImplementedError('Subclasses must implement this method.') + def _has_changed(self, initial, data): + if initial is None: + initial = ['' for x in range(0, len(data))] + else: + if not isinstance(initial, list): + initial = self.widget.decompress(initial) + for field, initial, data in zip(self.fields, initial, data): + if field._has_changed(initial, data): + return True + return False + + class FilePathField(ChoiceField): def __init__(self, path, match=None, recursive=False, allow_files=True, allow_folders=False, required=True, widget=None, label=None, diff --git a/django/forms/forms.py b/django/forms/forms.py index 3299c2becc..f532391296 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -341,7 +341,13 @@ class BaseForm(object): hidden_widget = field.hidden_widget() initial_value = hidden_widget.value_from_datadict( self.data, self.files, initial_prefixed_name) - if field.widget._has_changed(initial_value, data_value): + if hasattr(field.widget, '_has_changed'): + warnings.warn("The _has_changed method on widgets is deprecated," + " define it at field level instead.", + PendingDeprecationWarning, stacklevel=2) + if field.widget._has_changed(initial_value, data_value): + self._changed_data.append(name) + elif field._has_changed(initial_value, data_value): self._changed_data.append(name) return self._changed_data changed_data = property(_get_changed_data) diff --git a/django/forms/models.py b/django/forms/models.py index 03a14dc9ff..837da74814 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -858,15 +858,12 @@ def inlineformset_factory(parent_model, model, form=ModelForm, # Fields ##################################################################### -class InlineForeignKeyHiddenInput(HiddenInput): - def _has_changed(self, initial, data): - return False - class InlineForeignKeyField(Field): """ A basic integer field that deals with validating the given value to a given parent instance in an inline. """ + widget = HiddenInput default_error_messages = { 'invalid_choice': _('The inline foreign key did not match the parent instance primary key.'), } @@ -881,7 +878,6 @@ class InlineForeignKeyField(Field): else: kwargs["initial"] = self.parent_instance.pk kwargs["required"] = False - kwargs["widget"] = InlineForeignKeyHiddenInput super(InlineForeignKeyField, self).__init__(*args, **kwargs) def clean(self, value): @@ -899,6 +895,9 @@ class InlineForeignKeyField(Field): raise ValidationError(self.error_messages['invalid_choice']) return self.parent_instance + def _has_changed(self, initial, data): + return False + class ModelChoiceIterator(object): def __init__(self, field): self.field = field diff --git a/django/forms/widgets.py b/django/forms/widgets.py index d6ea56f0c8..303844d44b 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -208,25 +208,6 @@ class Widget(six.with_metaclass(MediaDefiningClass)): """ return data.get(name, None) - def _has_changed(self, initial, data): - """ - Return True if data differs from initial. - """ - # For purposes of seeing whether something has changed, None is - # the same as an empty string, if the data or inital value we get - # is None, replace it w/ ''. - if data is None: - data_value = '' - else: - data_value = data - if initial is None: - initial_value = '' - else: - initial_value = initial - if force_text(initial_value) != force_text(data_value): - return True - return False - def id_for_label(self, id_): """ Returns the HTML ID attribute of this Widget for use by a
    Lookup' % dict(admin_static_prefix(), m1pk=m1.pk) ) - self.assertEqual(w._has_changed(None, None), False) - self.assertEqual(w._has_changed([], None), False) - self.assertEqual(w._has_changed(None, ['1']), True) - self.assertEqual(w._has_changed([1, 2], ['1', '2']), False) - self.assertEqual(w._has_changed([1, 2], ['1']), True) - self.assertEqual(w._has_changed([1, 2], ['1', '3']), True) - def test_m2m_related_model_not_in_admin(self): # M2M relationship with model not registered with admin site. Raw ID # widget should have no magnifying glass link. See #16542 diff --git a/tests/regressiontests/forms/tests/extra.py b/tests/regressiontests/forms/tests/extra.py index 07acb29741..762b774a93 100644 --- a/tests/regressiontests/forms/tests/extra.py +++ b/tests/regressiontests/forms/tests/extra.py @@ -428,6 +428,23 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): # If insufficient data is provided, None is substituted self.assertFormErrors(['This field is required.'], f.clean, ['some text',['JP']]) + # test with no initial data + self.assertTrue(f._has_changed(None, ['some text', ['J','P'], ['2007-04-25','6:24:00']])) + + # test when the data is the same as initial + self.assertFalse(f._has_changed('some text,JP,2007-04-25 06:24:00', + ['some text', ['J','P'], ['2007-04-25','6:24:00']])) + + # test when the first widget's data has changed + self.assertTrue(f._has_changed('some text,JP,2007-04-25 06:24:00', + ['other text', ['J','P'], ['2007-04-25','6:24:00']])) + + # test when the last widget's data has changed. this ensures that it is not + # short circuiting while testing the widgets. + self.assertTrue(f._has_changed('some text,JP,2007-04-25 06:24:00', + ['some text', ['J','P'], ['2009-04-25','11:44:00']])) + + class ComplexFieldForm(Form): field1 = ComplexField(widget=w) @@ -725,8 +742,8 @@ class FormsExtraL10NTestCase(TestCase): def test_l10n_date_changed(self): """ - Ensure that SelectDateWidget._has_changed() works correctly with a - localized date format. + Ensure that DateField._has_changed() with SelectDateWidget works + correctly with a localized date format. Refs #17165. """ # With Field.show_hidden_initial=False ----------------------- diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index e17d976fcf..7deb345a33 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -35,6 +35,7 @@ from decimal import Decimal from django.core.files.uploadedfile import SimpleUploadedFile from django.forms import * from django.test import SimpleTestCase +from django.utils import formats from django.utils import six from django.utils._os import upath @@ -362,6 +363,13 @@ class FieldsTests(SimpleTestCase): f = DateField() self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, 'a\x00b') + def test_datefield_changed(self): + format = '%d/%m/%Y' + f = DateField(input_formats=[format]) + d = datetime.date(2007, 9, 17) + self.assertFalse(f._has_changed(d, '17/09/2007')) + self.assertFalse(f._has_changed(d.strftime(format), '17/09/2007')) + # TimeField ################################################################### def test_timefield_1(self): @@ -388,6 +396,18 @@ class FieldsTests(SimpleTestCase): self.assertEqual(datetime.time(14, 25, 59), f.clean(' 14:25:59 ')) self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ' ') + def test_timefield_changed(self): + t1 = datetime.time(12, 51, 34, 482548) + t2 = datetime.time(12, 51) + format = '%H:%M' + f = TimeField(input_formats=[format]) + self.assertTrue(f._has_changed(t1, '12:51')) + self.assertFalse(f._has_changed(t2, '12:51')) + + format = '%I:%M %p' + f = TimeField(input_formats=[format]) + self.assertFalse(f._has_changed(t2.strftime(format), '12:51 PM')) + # DateTimeField ############################################################### def test_datetimefield_1(self): @@ -446,6 +466,15 @@ class FieldsTests(SimpleTestCase): def test_datetimefield_5(self): f = DateTimeField(input_formats=['%Y.%m.%d %H:%M:%S.%f']) self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 45, 200), f.clean('2006.10.25 14:30:45.0002')) + + def test_datetimefield_changed(self): + format = '%Y %m %d %I:%M %p' + f = DateTimeField(input_formats=[format]) + d = datetime.datetime(2006, 9, 17, 14, 30, 0) + self.assertFalse(f._has_changed(d, '2006 09 17 2:30 PM')) + # Initial value may be a string from a hidden input + self.assertFalse(f._has_changed(d.strftime(format), '2006 09 17 2:30 PM')) + # RegexField ################################################################## def test_regexfield_1(self): @@ -566,6 +595,29 @@ class FieldsTests(SimpleTestCase): self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', b'')))) + def test_filefield_changed(self): + ''' + Test for the behavior of _has_changed for FileField. The value of data will + more than likely come from request.FILES. The value of initial data will + likely be a filename stored in the database. Since its value is of no use to + a FileField it is ignored. + ''' + f = FileField() + + # No file was uploaded and no initial data. + self.assertFalse(f._has_changed('', None)) + + # A file was uploaded and no initial data. + self.assertTrue(f._has_changed('', {'filename': 'resume.txt', 'content': 'My resume'})) + + # A file was not uploaded, but there is initial data + self.assertFalse(f._has_changed('resume.txt', None)) + + # A file was uploaded and there is initial data (file identity is not dealt + # with here) + self.assertTrue(f._has_changed('resume.txt', {'filename': 'resume.txt', 'content': 'My resume'})) + + # URLField ################################################################## def test_urlfield_1(self): @@ -709,6 +761,18 @@ class FieldsTests(SimpleTestCase): def test_boolean_picklable(self): self.assertIsInstance(pickle.loads(pickle.dumps(BooleanField())), BooleanField) + def test_booleanfield_changed(self): + f = BooleanField() + self.assertFalse(f._has_changed(None, None)) + self.assertFalse(f._has_changed(None, '')) + self.assertFalse(f._has_changed('', None)) + self.assertFalse(f._has_changed('', '')) + self.assertTrue(f._has_changed(False, 'on')) + self.assertFalse(f._has_changed(True, 'on')) + self.assertTrue(f._has_changed(True, '')) + # Initial value may have mutated to a string due to show_hidden_initial (#19537) + self.assertTrue(f._has_changed('False', 'on')) + # ChoiceField ################################################################# def test_choicefield_1(self): @@ -825,6 +889,16 @@ class FieldsTests(SimpleTestCase): self.assertEqual(False, f.cleaned_data['nullbool1']) self.assertEqual(None, f.cleaned_data['nullbool2']) + def test_nullbooleanfield_changed(self): + f = NullBooleanField() + self.assertTrue(f._has_changed(False, None)) + self.assertTrue(f._has_changed(None, False)) + self.assertFalse(f._has_changed(None, None)) + self.assertFalse(f._has_changed(False, False)) + self.assertTrue(f._has_changed(True, False)) + self.assertTrue(f._has_changed(True, None)) + self.assertTrue(f._has_changed(True, False)) + # MultipleChoiceField ######################################################### def test_multiplechoicefield_1(self): @@ -866,6 +940,16 @@ class FieldsTests(SimpleTestCase): self.assertRaisesMessage(ValidationError, "'Select a valid choice. 6 is not one of the available choices.'", f.clean, ['6']) self.assertRaisesMessage(ValidationError, "'Select a valid choice. 6 is not one of the available choices.'", f.clean, ['1','6']) + def test_multiplechoicefield_changed(self): + f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two'), ('3', 'Three')]) + self.assertFalse(f._has_changed(None, None)) + self.assertFalse(f._has_changed([], None)) + self.assertTrue(f._has_changed(None, ['1'])) + self.assertFalse(f._has_changed([1, 2], ['1', '2'])) + self.assertFalse(f._has_changed([2, 1], ['1', '2'])) + self.assertTrue(f._has_changed([1, 2], ['1'])) + self.assertTrue(f._has_changed([1, 2], ['1', '3'])) + # TypedMultipleChoiceField ############################################################ # TypedMultipleChoiceField is just like MultipleChoiceField, except that coerced types # will be returned: @@ -1048,3 +1132,9 @@ class FieldsTests(SimpleTestCase): self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ['2006-01-10', '']) self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ['2006-01-10']) self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, ['', '07:30']) + + def test_splitdatetimefield_changed(self): + f = SplitDateTimeField(input_date_formats=['%d/%m/%Y']) + self.assertTrue(f._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['2008-05-06', '12:40:00'])) + self.assertFalse(f._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:40'])) + self.assertTrue(f._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:41'])) diff --git a/tests/regressiontests/forms/tests/widgets.py b/tests/regressiontests/forms/tests/widgets.py index 7a2961358a..6aa56ec8b0 100644 --- a/tests/regressiontests/forms/tests/widgets.py +++ b/tests/regressiontests/forms/tests/widgets.py @@ -148,25 +148,6 @@ class FormsWidgetTestCase(TestCase): self.assertHTMLEqual(w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), '') - # Test for the behavior of _has_changed for FileInput. The value of data will - # more than likely come from request.FILES. The value of initial data will - # likely be a filename stored in the database. Since its value is of no use to - # a FileInput it is ignored. - w = FileInput() - - # No file was uploaded and no initial data. - self.assertFalse(w._has_changed('', None)) - - # A file was uploaded and no initial data. - self.assertTrue(w._has_changed('', {'filename': 'resume.txt', 'content': 'My resume'})) - - # A file was not uploaded, but there is initial data - self.assertFalse(w._has_changed('resume.txt', None)) - - # A file was uploaded and there is initial data (file identity is not dealt - # with here) - self.assertTrue(w._has_changed('resume.txt', {'filename': 'resume.txt', 'content': 'My resume'})) - def test_textarea(self): w = Textarea() self.assertHTMLEqual(w.render('msg', ''), '') @@ -233,16 +214,6 @@ class FormsWidgetTestCase(TestCase): self.assertIsInstance(value, bool) self.assertTrue(value) - self.assertFalse(w._has_changed(None, None)) - self.assertFalse(w._has_changed(None, '')) - self.assertFalse(w._has_changed('', None)) - self.assertFalse(w._has_changed('', '')) - self.assertTrue(w._has_changed(False, 'on')) - self.assertFalse(w._has_changed(True, 'on')) - self.assertTrue(w._has_changed(True, '')) - # Initial value may have mutated to a string due to show_hidden_initial (#19537) - self.assertTrue(w._has_changed('False', 'on')) - def test_select(self): w = Select() self.assertHTMLEqual(w.render('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """""") - self.assertTrue(w._has_changed(False, None)) - self.assertTrue(w._has_changed(None, False)) - self.assertFalse(w._has_changed(None, None)) - self.assertFalse(w._has_changed(False, False)) - self.assertTrue(w._has_changed(True, False)) - self.assertTrue(w._has_changed(True, None)) - self.assertTrue(w._has_changed(True, False)) def test_selectmultiple(self): w = SelectMultiple() @@ -535,14 +499,6 @@ class FormsWidgetTestCase(TestCase): # Unicode choices are correctly rendered as HTML self.assertHTMLEqual(w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]), '') - # Test the usage of _has_changed - self.assertFalse(w._has_changed(None, None)) - self.assertFalse(w._has_changed([], None)) - self.assertTrue(w._has_changed(None, ['1'])) - self.assertFalse(w._has_changed([1, 2], ['1', '2'])) - self.assertTrue(w._has_changed([1, 2], ['1'])) - self.assertTrue(w._has_changed([1, 2], ['1', '3'])) - # Choices can be nested one level in order to create HTML optgroups: w.choices = (('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))) self.assertHTMLEqual(w.render('nestchoice', None), """ you > me """) - # Test the usage of _has_changed - self.assertFalse(w._has_changed(None, None)) - self.assertFalse(w._has_changed([], None)) - self.assertTrue(w._has_changed(None, ['1'])) - self.assertFalse(w._has_changed([1, 2], ['1', '2'])) - self.assertTrue(w._has_changed([1, 2], ['1'])) - self.assertTrue(w._has_changed([1, 2], ['1', '3'])) - self.assertFalse(w._has_changed([2, 1], ['1', '2'])) - # Unicode choices are correctly rendered as HTML self.assertHTMLEqual(w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]), '
      \n
    • \n
    • \n
    • \n
    • \n
    • \n
    ') @@ -886,21 +833,6 @@ beatle J R Ringo False""") w = MyMultiWidget(widgets=(TextInput(attrs={'class': 'big'}), TextInput(attrs={'class': 'small'})), attrs={'id': 'bar'}) self.assertHTMLEqual(w.render('name', ['john', 'lennon']), '
    ') - w = MyMultiWidget(widgets=(TextInput(), TextInput())) - - # test with no initial data - self.assertTrue(w._has_changed(None, ['john', 'lennon'])) - - # test when the data is the same as initial - self.assertFalse(w._has_changed('john__lennon', ['john', 'lennon'])) - - # test when the first widget's data has changed - self.assertTrue(w._has_changed('john__lennon', ['alfred', 'lennon'])) - - # test when the last widget's data has changed. this ensures that it is not - # short circuiting while testing the widgets. - self.assertTrue(w._has_changed('john__lennon', ['john', 'denver'])) - def test_splitdatetime(self): w = SplitDateTimeWidget() self.assertHTMLEqual(w.render('date', ''), '') @@ -916,10 +848,6 @@ beatle J R Ringo False""") w = SplitDateTimeWidget(date_format='%d/%m/%Y', time_format='%H:%M') self.assertHTMLEqual(w.render('date', datetime.datetime(2006, 1, 10, 7, 30)), '') - self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['2008-05-06', '12:40:00'])) - self.assertFalse(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:40'])) - self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:41'])) - def test_datetimeinput(self): w = DateTimeInput() self.assertHTMLEqual(w.render('date', None), '') @@ -934,13 +862,6 @@ beatle J R Ringo False""") # Use 'format' to change the way a value is displayed. w = DateTimeInput(format='%d/%m/%Y %H:%M', attrs={'type': 'datetime'}) self.assertHTMLEqual(w.render('date', d), '') - self.assertFalse(w._has_changed(d, '17/09/2007 12:51')) - - # Make sure a custom format works with _has_changed. The hidden input will use - data = datetime.datetime(2010, 3, 6, 12, 0, 0) - custom_format = '%d.%m.%Y %H:%M' - w = DateTimeInput(format=custom_format) - self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format))) def test_dateinput(self): w = DateInput() @@ -957,13 +878,6 @@ beatle J R Ringo False""") # Use 'format' to change the way a value is displayed. w = DateInput(format='%d/%m/%Y', attrs={'type': 'date'}) self.assertHTMLEqual(w.render('date', d), '') - self.assertFalse(w._has_changed(d, '17/09/2007')) - - # Make sure a custom format works with _has_changed. The hidden input will use - data = datetime.date(2010, 3, 6) - custom_format = '%d.%m.%Y' - w = DateInput(format=custom_format) - self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format))) def test_timeinput(self): w = TimeInput() @@ -982,13 +896,6 @@ beatle J R Ringo False""") # Use 'format' to change the way a value is displayed. w = TimeInput(format='%H:%M', attrs={'type': 'time'}) self.assertHTMLEqual(w.render('time', t), '') - self.assertFalse(w._has_changed(t, '12:51')) - - # Make sure a custom format works with _has_changed. The hidden input will use - data = datetime.time(13, 0) - custom_format = '%I:%M %p' - w = TimeInput(format=custom_format) - self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format))) def test_splithiddendatetime(self): from django.forms.widgets import SplitHiddenDateTimeWidget @@ -1016,10 +923,6 @@ class FormsI18NWidgetsTestCase(TestCase): deactivate() super(FormsI18NWidgetsTestCase, self).tearDown() - def test_splitdatetime(self): - w = SplitDateTimeWidget(date_format='%d/%m/%Y', time_format='%H:%M') - self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06.05.2008', '12:41'])) - def test_datetimeinput(self): w = DateTimeInput() d = datetime.datetime(2007, 9, 17, 12, 51, 34, 482548) From 1686e0d184aaf704e5131a8651a070c4a0e58b03 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 25 Jan 2013 21:27:49 +0100 Subject: [PATCH 204/870] Fixed #18460 -- Fixed change detection of ReadOnlyPasswordHashField Thanks jose.sanchez et ezeep.com for the report and Vladimir Ulupov for the initial patch. --- django/contrib/auth/forms.py | 3 +++ django/contrib/auth/tests/forms.py | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index cbce8ad6e2..ee4fb482ce 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -57,6 +57,9 @@ class ReadOnlyPasswordHashField(forms.Field): # render an input field. return initial + def _has_changed(self, initial, data): + return False + class UserCreationForm(forms.ModelForm): """ diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index 543bb2001d..0c0973d543 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -4,7 +4,7 @@ import os from django.contrib.auth.models import User from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm, PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm, - ReadOnlyPasswordHashWidget) + ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget) from django.contrib.auth.tests.utils import skipIfCustomUser from django.core import mail from django.forms.fields import Field, EmailField, CharField @@ -384,7 +384,7 @@ class PasswordResetFormTest(TestCase): [_("The user account associated with this email address cannot reset the password.")]) -class ReadOnlyPasswordHashWidgetTest(TestCase): +class ReadOnlyPasswordHashTest(TestCase): def test_bug_19349_render_with_none_value(self): # Rendering the widget with value set to None @@ -392,3 +392,7 @@ class ReadOnlyPasswordHashWidgetTest(TestCase): widget = ReadOnlyPasswordHashWidget() html = widget.render(name='password', value=None, attrs={}) self.assertIn(_("No password set."), html) + + def test_readonly_field_has_changed(self): + field = ReadOnlyPasswordHashField() + self.assertFalse(field._has_changed('aaa', 'bbb')) From 58062a6302a2bf1013d100deb053ccae2298bb84 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 25 Jan 2013 22:41:45 +0100 Subject: [PATCH 205/870] Used property decorators in django/forms.py --- django/forms/forms.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/django/forms/forms.py b/django/forms/forms.py index f532391296..a33bf8a648 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -111,12 +111,12 @@ class BaseForm(object): raise KeyError('Key %r not found in Form' % name) return BoundField(self, field, name) - def _get_errors(self): + @property + def errors(self): "Returns an ErrorDict for the data provided for the form" if self._errors is None: self.full_clean() return self._errors - errors = property(_get_errors) def is_valid(self): """ @@ -322,7 +322,8 @@ class BaseForm(object): """ return bool(self.changed_data) - def _get_changed_data(self): + @property + def changed_data(self): if self._changed_data is None: self._changed_data = [] # XXX: For now we're asking the individual widgets whether or not the @@ -350,9 +351,9 @@ class BaseForm(object): elif field._has_changed(initial_value, data_value): self._changed_data.append(name) return self._changed_data - changed_data = property(_get_changed_data) - def _get_media(self): + @property + def media(self): """ Provide a description of all media required to render the widgets on this form """ @@ -360,7 +361,6 @@ class BaseForm(object): for field in self.fields.values(): media = media + field.widget.media return media - media = property(_get_media) def is_multipart(self): """ @@ -432,13 +432,13 @@ class BoundField(object): def __getitem__(self, idx): return list(self.__iter__())[idx] - def _errors(self): + @property + def errors(self): """ Returns an ErrorList for this field. Returns an empty ErrorList if there are none. """ return self.form.errors.get(self.name, self.form.error_class()) - errors = property(_errors) def as_widget(self, widget=None, attrs=None, only_initial=False): """ @@ -479,12 +479,12 @@ class BoundField(object): """ return self.as_widget(self.field.hidden_widget(), attrs, **kwargs) - def _data(self): + @property + def data(self): """ Returns the data for this BoundField, or None if it wasn't given. """ return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name) - data = property(_data) def value(self): """ @@ -532,12 +532,13 @@ class BoundField(object): extra_classes.add(self.form.required_css_class) return ' '.join(extra_classes) - def _is_hidden(self): + @property + def is_hidden(self): "Returns True if this BoundField's widget is hidden." return self.field.widget.is_hidden - is_hidden = property(_is_hidden) - def _auto_id(self): + @property + def auto_id(self): """ Calculates and returns the ID attribute for this BoundField, if the associated Form has specified auto_id. Returns an empty string otherwise. @@ -548,9 +549,9 @@ class BoundField(object): elif auto_id: return self.html_name return '' - auto_id = property(_auto_id) - def _id_for_label(self): + @property + def id_for_label(self): """ Wrapper around the field widget's `id_for_label` method. Useful, for example, for focusing on this field regardless of whether @@ -559,4 +560,3 @@ class BoundField(object): widget = self.field.widget id_ = widget.attrs.get('id') or self.auto_id return widget.id_for_label(id_) - id_for_label = property(_id_for_label) From 424eb67867162032d92e0bfe3403f051765de805 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 26 Jan 2013 12:19:31 +0100 Subject: [PATCH 206/870] Fixed validation of email addresses when the local part contains an @. See also BaseUserManager.normalize_email -- it uses rsplit. Refs #4833. --- django/core/validators.py | 2 +- tests/modeltests/validators/tests.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/django/core/validators.py b/django/core/validators.py index cd9dba1ee8..f094b7bf07 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -106,7 +106,7 @@ class EmailValidator(object): if not value or '@' not in value: raise ValidationError(self.message, code=self.code) - user_part, domain_part = value.split('@', 1) + user_part, domain_part = value.rsplit('@', 1) if not self.user_regex.match(user_part): raise ValidationError(self.message, code=self.code) diff --git a/tests/modeltests/validators/tests.py b/tests/modeltests/validators/tests.py index 5b562a87e6..6b46c53cc3 100644 --- a/tests/modeltests/validators/tests.py +++ b/tests/modeltests/validators/tests.py @@ -31,6 +31,7 @@ TEST_DATA = ( (validate_email, 'test@domain.with.idn.tld.उदाहरण.परीक्षा', None), (validate_email, 'email@localhost', None), (EmailValidator(whitelist=['localdomain']), 'email@localdomain', None), + (validate_email, '"test@test"@example.com', None), (validate_email, None, ValidationError), (validate_email, '', ValidationError), From cebbec9b6122579a32a019a0449d4995dcf2191d Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 26 Jan 2013 13:28:44 +0100 Subject: [PATCH 207/870] Fixed #19540 -- Stopped using deprecated os.stat_float_times. --- .../staticfiles/management/commands/collectstatic.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index edf8c62dd2..6116f31efc 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -60,9 +60,6 @@ class Command(NoArgsCommand): self.local = False else: self.local = True - # Use ints for file times (ticket #14665), if supported - if hasattr(os, 'stat_float_times'): - os.stat_float_times(False) def set_options(self, **options): """ @@ -231,7 +228,9 @@ Type 'yes' to continue, or 'no' to cancel: """ else: full_path = None # Skip the file if the source file is younger - if target_last_modified >= source_last_modified: + # Avoid sub-second precision (see #14665, #19540) + if (target_last_modified.replace(microsecond=0) + >= source_last_modified.replace(microsecond=0)): if not ((self.symlink and full_path and not os.path.islink(full_path)) or (not self.symlink and full_path From 55416e235d95b6168034236e5b1cdc581d544cc6 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 26 Jan 2013 13:47:11 +0100 Subject: [PATCH 208/870] Fixed #19589 -- assertRegexpMatches is deprecated in Python 3.3. --- django/utils/six.py | 6 ++++++ docs/topics/python3.txt | 8 +++++++- tests/modeltests/timezones/tests.py | 2 +- tests/regressiontests/admin_views/tests.py | 2 +- tests/regressiontests/i18n/commands/extraction.py | 3 ++- tests/regressiontests/version/tests.py | 3 ++- 6 files changed, 19 insertions(+), 5 deletions(-) diff --git a/django/utils/six.py b/django/utils/six.py index 73846358a1..b93dc5b164 100644 --- a/django/utils/six.py +++ b/django/utils/six.py @@ -393,9 +393,11 @@ def with_metaclass(meta, base=object): if PY3: _iterlists = "lists" _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" else: _iterlists = "iterlists" _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" def iterlists(d): @@ -407,5 +409,9 @@ def assertRaisesRegex(self, *args, **kwargs): return getattr(self, _assertRaisesRegex)(*args, **kwargs) +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + add_move(MovedModule("_dummy_thread", "dummy_thread")) add_move(MovedModule("_thread", "thread")) diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt index b44c180d7f..b1f3fa3277 100644 --- a/docs/topics/python3.txt +++ b/docs/topics/python3.txt @@ -402,7 +402,13 @@ The version of six bundled with Django includes one extra function: This replaces ``testcase.assertRaisesRegexp`` on Python 2, and ``testcase.assertRaisesRegex`` on Python 3. ``assertRaisesRegexp`` still - exists in current Python3 versions, but issues a warning. + exists in current Python 3 versions, but issues a warning. + +.. function:: assertRegex(testcase, *args, **kwargs) + + This replaces ``testcase.assertRegexpMatches`` on Python 2, and + ``testcase.assertRegex`` on Python 3. ``assertRegexpMatches`` still + exists in current Python 3 versions, but issues a warning. In addition to six' defaults moves, Django's version provides ``thread`` as diff --git a/tests/modeltests/timezones/tests.py b/tests/modeltests/timezones/tests.py index 29a490e3fb..4ae6bbd6a8 100644 --- a/tests/modeltests/timezones/tests.py +++ b/tests/modeltests/timezones/tests.py @@ -506,7 +506,7 @@ class SerializationTests(TestCase): def assert_yaml_contains_datetime(self, yaml, dt): # Depending on the yaml dumper, '!timestamp' might be absent - self.assertRegexpMatches(yaml, + six.assertRegex(self, yaml, r"- fields: {dt: !(!timestamp)? '%s'}" % re.escape(dt)) def test_naive_datetime(self): diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index e886078ae5..d7d01e6a92 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -1263,7 +1263,7 @@ class AdminViewDeletedObjectsTest(TestCase): """ pattern = re.compile(br"""
  • Plot: World Domination\s* {% endif %}{% endif %} {% endblock %} -{% csrf_token %}{% block form_top %}{% endblock %} +{% csrf_token %}{% block form_top %}{% endblock %}
    {% if is_popup %}{% endif %} {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %} diff --git a/django/contrib/admin/templatetags/admin_urls.py b/django/contrib/admin/templatetags/admin_urls.py index 90e81b0ef3..bca95d92ae 100644 --- a/django/contrib/admin/templatetags/admin_urls.py +++ b/django/contrib/admin/templatetags/admin_urls.py @@ -1,4 +1,3 @@ -from django.core.urlresolvers import reverse from django import template from django.contrib.admin.util import quote @@ -6,7 +5,7 @@ register = template.Library() @register.filter def admin_urlname(value, arg): - return 'admin:%s_%s_%s' % (value.app_label, value.module_name, arg) + return 'admin:%s_%s_%s' % (value.app_label, value.model_name, arg) @register.filter diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 07013d1d4b..133a8ad13e 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -116,7 +116,7 @@ def get_deleted_objects(objs, opts, user, admin_site, using): admin_url = reverse('%s:%s_%s_change' % (admin_site.name, opts.app_label, - opts.object_name.lower()), + opts.model_name), None, (quote(obj._get_pk_val()),)) p = '%s.%s' % (opts.app_label, opts.get_delete_permission()) diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index be7067ff61..4b296b3f4f 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -379,6 +379,6 @@ class ChangeList(object): def url_for_result(self, result): pk = getattr(result, self.pk_attname) return reverse('admin:%s_%s_change' % (self.opts.app_label, - self.opts.module_name), + self.opts.model_name), args=(quote(pk),), current_app=self.model_admin.admin_site.name) diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index a3887740d8..4b79401dbc 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -147,7 +147,7 @@ class ForeignKeyRawIdWidget(forms.TextInput): # The related object is registered with the same AdminSite related_url = reverse('admin:%s_%s_changelist' % (rel_to._meta.app_label, - rel_to._meta.module_name), + rel_to._meta.model_name), current_app=self.admin_site.name) params = self.url_parameters() @@ -247,7 +247,7 @@ class RelatedFieldWidgetWrapper(forms.Widget): def render(self, name, value, *args, **kwargs): rel_to = self.rel.to - info = (rel_to._meta.app_label, rel_to._meta.object_name.lower()) + info = (rel_to._meta.app_label, rel_to._meta.model_name) self.widget.choices = self.choices output = [self.widget.render(name, value, *args, **kwargs)] if self.can_add_related: diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index cb0c116416..ef2790f2db 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -189,7 +189,7 @@ def model_detail(request, app_label, model_name): raise Http404(_("App %r not found") % app_label) model = None for m in models.get_models(app_mod): - if m._meta.object_name.lower() == model_name: + if m._meta.model_name == model_name: model = m break if model is None: @@ -224,12 +224,12 @@ def model_detail(request, app_label, model_name): fields.append({ 'name': "%s.all" % field.name, "data_type": 'List', - 'verbose': utils.parse_rst(_("all %s") % verbose , 'model', _('model:') + opts.module_name), + 'verbose': utils.parse_rst(_("all %s") % verbose , 'model', _('model:') + opts.model_name), }) fields.append({ 'name' : "%s.count" % field.name, 'data_type' : 'Integer', - 'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.module_name), + 'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.model_name), }) # Gather model methods. @@ -243,7 +243,7 @@ def model_detail(request, app_label, model_name): continue verbose = func.__doc__ if verbose: - verbose = utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.module_name) + verbose = utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.model_name) fields.append({ 'name': func_name, 'data_type': get_return_data_type(func_name), @@ -257,12 +257,12 @@ def model_detail(request, app_label, model_name): fields.append({ 'name' : "%s.all" % accessor, 'data_type' : 'List', - 'verbose' : utils.parse_rst(_("all %s") % verbose , 'model', _('model:') + opts.module_name), + 'verbose' : utils.parse_rst(_("all %s") % verbose , 'model', _('model:') + opts.model_name), }) fields.append({ 'name' : "%s.count" % accessor, 'data_type' : 'Integer', - 'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.module_name), + 'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.model_name), }) return render_to_response('admin_doc/model_detail.html', { 'root_path': urlresolvers.reverse('admin:index'), diff --git a/django/contrib/auth/context_processors.py b/django/contrib/auth/context_processors.py index 3d17fe2754..b8ead73eb4 100644 --- a/django/contrib/auth/context_processors.py +++ b/django/contrib/auth/context_processors.py @@ -2,14 +2,14 @@ # the template system can understand. class PermLookupDict(object): - def __init__(self, user, module_name): - self.user, self.module_name = user, module_name + def __init__(self, user, app_label): + self.user, self.app_label = user, app_label def __repr__(self): return str(self.user.get_all_permissions()) def __getitem__(self, perm_name): - return self.user.has_perm("%s.%s" % (self.module_name, perm_name)) + return self.user.has_perm("%s.%s" % (self.app_label, perm_name)) def __iter__(self): # To fix 'item in perms.someapp' and __getitem__ iteraction we need to @@ -17,7 +17,7 @@ class PermLookupDict(object): raise TypeError("PermLookupDict is not iterable.") def __bool__(self): - return self.user.has_module_perms(self.module_name) + return self.user.has_module_perms(self.app_label) def __nonzero__(self): # Python 2 compatibility return type(self).__bool__(self) @@ -27,8 +27,8 @@ class PermWrapper(object): def __init__(self, user): self.user = user - def __getitem__(self, module_name): - return PermLookupDict(self.user, module_name) + def __getitem__(self, app_label): + return PermLookupDict(self.user, app_label) def __iter__(self): # I am large, I contain multitudes. @@ -41,8 +41,8 @@ class PermWrapper(object): if '.' not in perm_name: # The name refers to module. return bool(self[perm_name]) - module_name, perm_name = perm_name.split('.', 1) - return self[module_name][perm_name] + app_label, perm_name = perm_name.split('.', 1) + return self[app_label][perm_name] def auth(request): diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index a77bba0f73..475dd255d4 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -17,7 +17,7 @@ from django.utils.six.moves import input def _get_permission_codename(action, opts): - return '%s_%s' % (action, opts.object_name.lower()) + return '%s_%s' % (action, opts.model_name) def _get_all_permissions(opts, ctype): diff --git a/django/contrib/comments/moderation.py b/django/contrib/comments/moderation.py index 6c56d7a8a5..6648aebb59 100644 --- a/django/contrib/comments/moderation.py +++ b/django/contrib/comments/moderation.py @@ -302,7 +302,7 @@ class Moderator(object): model_or_iterable = [model_or_iterable] for model in model_or_iterable: if model in self._registry: - raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.module_name) + raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.model_name) self._registry[model] = moderation_class(model) def unregister(self, model_or_iterable): @@ -318,7 +318,7 @@ class Moderator(object): model_or_iterable = [model_or_iterable] for model in model_or_iterable: if model not in self._registry: - raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name) + raise NotModerated("The model '%s' is not currently being moderated" % model._meta.model_name) del self._registry[model] def pre_save_moderation(self, sender, comment, request, **kwargs): diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index 7c02b21b6a..befd326092 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -86,10 +86,10 @@ def post_comment(request, next=None, using=None): # These first two exist for purely historical reasons. # Django v1.0 and v1.1 allowed the underscore format for # preview templates, so we have to preserve that format. - "comments/%s_%s_preview.html" % (model._meta.app_label, model._meta.module_name), + "comments/%s_%s_preview.html" % (model._meta.app_label, model._meta.model_name), "comments/%s_preview.html" % model._meta.app_label, # Now the usual directory based template hierarchy. - "comments/%s/%s/preview.html" % (model._meta.app_label, model._meta.module_name), + "comments/%s/%s/preview.html" % (model._meta.app_label, model._meta.model_name), "comments/%s/preview.html" % model._meta.app_label, "comments/preview.html", ] diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index cda4d46fe8..d849d1607e 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -389,7 +389,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet): opts = self.model._meta self.instance = instance self.rel_name = '-'.join(( - opts.app_label, opts.object_name.lower(), + opts.app_label, opts.model_name, self.ct_field.name, self.ct_fk_field.name, )) if self.instance is None or self.instance.pk is None: @@ -409,7 +409,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet): @classmethod def get_default_prefix(cls): opts = cls.model._meta - return '-'.join((opts.app_label, opts.object_name.lower(), + return '-'.join((opts.app_label, opts.model_name, cls.ct_field.name, cls.ct_fk_field.name, )) diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py index 8329ab65d9..ddd7654ed7 100644 --- a/django/contrib/contenttypes/management.py +++ b/django/contrib/contenttypes/management.py @@ -21,7 +21,7 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, * # They all have the same app_label, get the first one. app_label = app_models[0]._meta.app_label app_models = dict( - (model._meta.object_name.lower(), model) + (model._meta.model_name, model) for model in app_models ) diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py index b658655bbb..f0bd109b00 100644 --- a/django/contrib/contenttypes/models.py +++ b/django/contrib/contenttypes/models.py @@ -25,7 +25,7 @@ class ContentTypeManager(models.Manager): return model._meta def _get_from_cache(self, opts): - key = (opts.app_label, opts.object_name.lower()) + key = (opts.app_label, opts.model_name) return self.__class__._cache[self.db][key] def get_for_model(self, model, for_concrete_model=True): @@ -43,7 +43,7 @@ class ContentTypeManager(models.Manager): # django.utils.functional.__proxy__ object. ct, created = self.get_or_create( app_label = opts.app_label, - model = opts.object_name.lower(), + model = opts.model_name, defaults = {'name': smart_text(opts.verbose_name_raw)}, ) self._add_to_cache(self.db, ct) @@ -67,7 +67,7 @@ class ContentTypeManager(models.Manager): ct = self._get_from_cache(opts) except KeyError: needed_app_labels.add(opts.app_label) - needed_models.add(opts.object_name.lower()) + needed_models.add(opts.model_name) needed_opts.add(opts) else: results[model] = ct @@ -86,7 +86,7 @@ class ContentTypeManager(models.Manager): # These weren't in the cache, or the DB, create them. ct = self.create( app_label=opts.app_label, - model=opts.object_name.lower(), + model=opts.model_name, name=smart_text(opts.verbose_name_raw), ) self._add_to_cache(self.db, ct) @@ -119,7 +119,7 @@ class ContentTypeManager(models.Manager): def _add_to_cache(self, using, ct): """Insert a ContentType into the cache.""" model = ct.model_class() - key = (model._meta.app_label, model._meta.object_name.lower()) + key = (model._meta.app_label, model._meta.model_name) self.__class__._cache.setdefault(using, {})[key] = ct self.__class__._cache.setdefault(using, {})[ct.id] = ct diff --git a/django/contrib/gis/sitemaps/kml.py b/django/contrib/gis/sitemaps/kml.py index db30606b04..837fe62b62 100644 --- a/django/contrib/gis/sitemaps/kml.py +++ b/django/contrib/gis/sitemaps/kml.py @@ -30,7 +30,7 @@ class KMLSitemap(Sitemap): for field in source._meta.fields: if isinstance(field, GeometryField): kml_sources.append((source._meta.app_label, - source._meta.module_name, + source._meta.model_name, field.name)) elif isinstance(source, (list, tuple)): if len(source) != 3: diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index c93bc90b18..5c9ea3e7bb 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -23,7 +23,7 @@ class Options(object): def __init__(self, table): self.db_table = table self.app_label = 'django_cache' - self.module_name = 'cacheentry' + self.model_name = 'cacheentry' self.verbose_name = 'cache entry' self.verbose_name_plural = 'cache entries' self.object_name = 'CacheEntry' diff --git a/django/core/management/sql.py b/django/core/management/sql.py index e46f4ae4f5..66df43e971 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -173,8 +173,8 @@ def custom_sql_for_model(model, style, connection): # Find custom SQL, if it's available. backend_name = connection.settings_dict['ENGINE'].split('.')[-1] - sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.object_name.lower(), backend_name)), - os.path.join(app_dir, "%s.sql" % opts.object_name.lower())] + sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.model_name, backend_name)), + os.path.join(app_dir, "%s.sql" % opts.model_name)] for sql_file in sql_files: if os.path.exists(sql_file): with codecs.open(sql_file, 'U', encoding=settings.FILE_CHARSET) as fp: diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index 37fa906280..5e07e2a006 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -143,7 +143,7 @@ def Deserializer(object_list, **options): def _get_model(model_identifier): """ - Helper to look up a model from an "app_label.module_name" string. + Helper to look up a model from an "app_label.model_name" string. """ try: Model = models.get_model(*model_identifier.split(".")) diff --git a/django/core/xheaders.py b/django/core/xheaders.py index b650a3a6d4..3766628c98 100644 --- a/django/core/xheaders.py +++ b/django/core/xheaders.py @@ -20,5 +20,5 @@ def populate_xheaders(request, response, model, object_id): if (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or (hasattr(request, 'user') and request.user.is_active and request.user.is_staff)): - response['X-Object-Type'] = "%s.%s" % (model._meta.app_label, model._meta.object_name.lower()) + response['X-Object-Type'] = "%s.%s" % (model._meta.app_label, model._meta.model_name) response['X-Object-Id'] = str(object_id) diff --git a/django/db/models/base.py b/django/db/models/base.py index 38afc60991..5f058654bf 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -191,7 +191,7 @@ class ModelBase(type): if base in o2o_map: field = o2o_map[base] elif not is_proxy: - attr_name = '%s_ptr' % base._meta.module_name + attr_name = '%s_ptr' % base._meta.model_name field = OneToOneField(base, name=attr_name, auto_created=True, parent_link=True) new_class.add_to_class(attr_name, field) @@ -973,7 +973,7 @@ def method_get_order(ordered_obj, self): ############################################## def get_absolute_url(opts, func, self, *args, **kwargs): - return settings.ABSOLUTE_URL_OVERRIDES.get('%s.%s' % (opts.app_label, opts.module_name), func)(self, *args, **kwargs) + return settings.ABSOLUTE_URL_OVERRIDES.get('%s.%s' % (opts.app_label, opts.model_name), func)(self, *args, **kwargs) ######## diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index ae792a30e7..bd2e288410 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -118,7 +118,7 @@ class RelatedField(object): self.do_related_class(other, cls) def set_attributes_from_rel(self): - self.name = self.name or (self.rel.to._meta.object_name.lower() + '_' + self.rel.to._meta.pk.name) + self.name = self.name or (self.rel.to._meta.model_name + '_' + self.rel.to._meta.pk.name) if self.verbose_name is None: self.verbose_name = self.rel.to._meta.verbose_name self.rel.field_name = self.rel.field_name or self.rel.to._meta.pk.name @@ -222,7 +222,7 @@ class RelatedField(object): # related object in a table-spanning query. It uses the lower-cased # object_name by default, but this can be overridden with the # "related_name" option. - return self.rel.related_name or self.opts.object_name.lower() + return self.rel.related_name or self.opts.model_name class SingleRelatedObjectDescriptor(object): @@ -983,7 +983,7 @@ class ForeignKey(RelatedField, Field): def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): try: - to_name = to._meta.object_name.lower() + to_name = to._meta.model_name except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) else: @@ -1174,7 +1174,7 @@ def create_many_to_many_intermediary_model(field, klass): from_ = 'from_%s' % to.lower() to = 'to_%s' % to.lower() else: - from_ = klass._meta.object_name.lower() + from_ = klass._meta.model_name to = to.lower() meta = type('Meta', (object,), { 'db_table': field._get_m2m_db_table(klass._meta), diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 56edc36bec..c027105c5b 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -239,7 +239,7 @@ class AppCache(object): for model in models: # Store as 'name: model' pair in a dictionary # in the app_models dictionary - model_name = model._meta.object_name.lower() + model_name = model._meta.model_name model_dict = self.app_models.setdefault(app_label, SortedDict()) if model_name in model_dict: # The same model may be imported via different paths (e.g. diff --git a/django/db/models/options.py b/django/db/models/options.py index 952596b514..a302e2d73a 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import re from bisect import bisect +import warnings from django.conf import settings from django.db.models.fields.related import ManyToManyRel @@ -28,7 +29,7 @@ class Options(object): def __init__(self, meta, app_label=None): self.local_fields, self.local_many_to_many = [], [] self.virtual_fields = [] - self.module_name, self.verbose_name = None, None + self.model_name, self.verbose_name = None, None self.verbose_name_plural = None self.db_table = '' self.ordering = [] @@ -78,7 +79,7 @@ class Options(object): self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS # First, construct the default values for these options. self.object_name = cls.__name__ - self.module_name = self.object_name.lower() + self.model_name = self.object_name.lower() self.verbose_name = get_verbose_name(self.object_name) # Next, apply any overridden values from 'class Meta'. @@ -116,11 +117,21 @@ class Options(object): self.verbose_name_plural = string_concat(self.verbose_name, 's') del self.meta - # If the db_table wasn't provided, use the app_label + module_name. + # If the db_table wasn't provided, use the app_label + model_name. if not self.db_table: - self.db_table = "%s_%s" % (self.app_label, self.module_name) + self.db_table = "%s_%s" % (self.app_label, self.model_name) self.db_table = truncate_name(self.db_table, connection.ops.max_name_length()) + @property + def module_name(self): + """ + This property has been deprecated in favor of `model_name`. refs #19689 + """ + warnings.warn( + "Options.module_name has been deprecated in favor of model_name", + PendingDeprecationWarning, stacklevel=2) + return self.model_name + def _prepare(self, model): if self.order_with_respect_to: self.order_with_respect_to = self.get_field(self.order_with_respect_to) @@ -193,7 +204,7 @@ class Options(object): return '' % self.object_name def __str__(self): - return "%s.%s" % (smart_text(self.app_label), smart_text(self.module_name)) + return "%s.%s" % (smart_text(self.app_label), smart_text(self.model_name)) def verbose_name_raw(self): """ @@ -217,7 +228,7 @@ class Options(object): case insensitive, so we make sure we are case insensitive here. """ if self.swappable: - model_label = '%s.%s' % (self.app_label, self.object_name.lower()) + model_label = '%s.%s' % (self.app_label, self.model_name) swapped_for = getattr(settings, self.swappable, None) if swapped_for: try: @@ -371,13 +382,13 @@ class Options(object): return cache def get_add_permission(self): - return 'add_%s' % self.object_name.lower() + return 'add_%s' % self.model_name def get_change_permission(self): - return 'change_%s' % self.object_name.lower() + return 'change_%s' % self.model_name def get_delete_permission(self): - return 'delete_%s' % self.object_name.lower() + return 'delete_%s' % self.model_name def get_all_related_objects(self, local_only=False, include_hidden=False, include_proxy_eq=False): diff --git a/django/db/models/related.py b/django/db/models/related.py index 26932137ad..53645bedb9 100644 --- a/django/db/models/related.py +++ b/django/db/models/related.py @@ -16,8 +16,8 @@ class RelatedObject(object): self.model = model self.opts = model._meta self.field = field - self.name = '%s:%s' % (self.opts.app_label, self.opts.module_name) - self.var_name = self.opts.object_name.lower() + self.name = '%s:%s' % (self.opts.app_label, self.opts.model_name) + self.var_name = self.opts.model_name def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH, limit_to_currently_related=False): @@ -31,7 +31,7 @@ class RelatedObject(object): queryset = self.model._default_manager.all() if limit_to_currently_related: queryset = queryset.complex_filter( - {'%s__isnull' % self.parent_model._meta.module_name: False}) + {'%s__isnull' % self.parent_model._meta.model_name: False}) lst = [(x._get_pk_val(), smart_text(x)) for x in queryset] return first_choice + lst @@ -56,9 +56,9 @@ class RelatedObject(object): # If this is a symmetrical m2m relation on self, there is no reverse accessor. if getattr(self.field.rel, 'symmetrical', False) and self.model == self.parent_model: return None - return self.field.rel.related_name or (self.opts.object_name.lower() + '_set') + return self.field.rel.related_name or (self.opts.model_name + '_set') else: - return self.field.rel.related_name or (self.opts.object_name.lower()) + return self.field.rel.related_name or (self.opts.model_name) def get_cache_name(self): return "_%s_cache" % self.get_accessor_name() diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py index c27b92b85e..58302bbe23 100644 --- a/django/views/generic/detail.py +++ b/django/views/generic/detail.py @@ -84,7 +84,7 @@ class SingleObjectMixin(ContextMixin): if self.context_object_name: return self.context_object_name elif isinstance(obj, models.Model): - return obj._meta.object_name.lower() + return obj._meta.model_name else: return None @@ -144,13 +144,13 @@ class SingleObjectTemplateResponseMixin(TemplateResponseMixin): if isinstance(self.object, models.Model): names.append("%s/%s%s.html" % ( self.object._meta.app_label, - self.object._meta.object_name.lower(), + self.object._meta.model_name, self.template_name_suffix )) elif hasattr(self, 'model') and self.model is not None and issubclass(self.model, models.Model): names.append("%s/%s%s.html" % ( self.model._meta.app_label, - self.model._meta.object_name.lower(), + self.model._meta.model_name, self.template_name_suffix )) return names diff --git a/django/views/generic/list.py b/django/views/generic/list.py index 1f286168f6..08c4bbcda0 100644 --- a/django/views/generic/list.py +++ b/django/views/generic/list.py @@ -97,7 +97,7 @@ class MultipleObjectMixin(ContextMixin): if self.context_object_name: return self.context_object_name elif hasattr(object_list, 'model'): - return '%s_list' % object_list.model._meta.object_name.lower() + return '%s_list' % object_list.model._meta.model_name else: return None @@ -177,7 +177,7 @@ class MultipleObjectTemplateResponseMixin(TemplateResponseMixin): # generated ones. if hasattr(self.object_list, 'model'): opts = self.object_list.model._meta - names.append("%s/%s%s.html" % (opts.app_label, opts.object_name.lower(), self.template_name_suffix)) + names.append("%s/%s%s.html" % (opts.app_label, opts.model_name, self.template_name_suffix)) return names diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index df3d84fdae..50b9aa3c19 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -327,6 +327,8 @@ these changes. :class:`django.middleware.common.BrokenLinkEmailsMiddleware` middleware to your :setting:`MIDDLEWARE_CLASSES` setting instead. +* ``Model._meta.module_name`` was renamed to ``model_name``. + 2.0 --- diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 32e5172878..f86d8b8108 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -127,3 +127,9 @@ from your settings. If you defined your own form widgets and defined the ``_has_changed`` method on a widget, you should now define this method on the form field itself. + +``module_name`` model meta attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Model._meta.module_name`` was renamed to ``model_name``. Despite being a +private API, it will go through a regular deprecation path. diff --git a/tests/regressiontests/admin_custom_urls/models.py b/tests/regressiontests/admin_custom_urls/models.py index ef04c2aa09..55fc064835 100644 --- a/tests/regressiontests/admin_custom_urls/models.py +++ b/tests/regressiontests/admin_custom_urls/models.py @@ -42,7 +42,7 @@ class ActionAdmin(admin.ModelAdmin): return self.admin_site.admin_view(view)(*args, **kwargs) return update_wrapper(wrapper, view) - info = self.model._meta.app_label, self.model._meta.module_name + info = self.model._meta.app_label, self.model._meta.model_name view_name = '%s_%s_add' % info From a097ee32d8364045a950d6a36b19630fc34397f1 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Tue, 5 Feb 2013 05:39:35 -0500 Subject: [PATCH 277/870] Fixed #17683 -- Make sure `BaseModelFormSet` respects defined widgets. --- django/forms/models.py | 6 +++++- .../model_formsets_regress/tests.py | 17 +++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/django/forms/models.py b/django/forms/models.py index 837da74814..e2d739fd1a 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -678,7 +678,11 @@ class BaseModelFormSet(BaseFormSet): else: qs = self.model._default_manager.get_query_set() qs = qs.using(form.instance._state.db) - form.fields[self._pk_field.name] = ModelChoiceField(qs, initial=pk_value, required=False, widget=HiddenInput) + if form._meta.widgets: + widget = form._meta.widgets.get(self._pk_field.name, HiddenInput) + else: + widget = HiddenInput + form.fields[self._pk_field.name] = ModelChoiceField(qs, initial=pk_value, required=False, widget=widget) super(BaseModelFormSet, self).add_fields(form, index) def modelformset_factory(model, form=ModelForm, formfield_callback=None, diff --git a/tests/regressiontests/model_formsets_regress/tests.py b/tests/regressiontests/model_formsets_regress/tests.py index 8cadcfc409..fd35eda854 100644 --- a/tests/regressiontests/model_formsets_regress/tests.py +++ b/tests/regressiontests/model_formsets_regress/tests.py @@ -261,14 +261,17 @@ class FormsetTests(TestCase): formset.save() -class CustomWidget(forms.CharField): +class CustomWidget(forms.widgets.TextInput): pass class UserSiteForm(forms.ModelForm): class Meta: model = UserSite - widgets = {'data': CustomWidget} + widgets = { + 'id': CustomWidget, + 'data': CustomWidget, + } class Callback(object): @@ -283,24 +286,27 @@ class Callback(object): class FormfieldCallbackTests(TestCase): """ - Regression for #13095: Using base forms with widgets - defined in Meta should not raise errors. + Regression for #13095 and #17683: Using base forms with widgets + defined in Meta should not raise errors and BaseModelForm should respect + the specified pk widget. """ def test_inlineformset_factory_default(self): Formset = inlineformset_factory(User, UserSite, form=UserSiteForm) form = Formset().forms[0] + self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) def test_modelformset_factory_default(self): Formset = modelformset_factory(UserSite, form=UserSiteForm) form = Formset().forms[0] + self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) def assertCallbackCalled(self, callback): id_field, user_field, data_field = UserSite._meta.fields expected_log = [ - (id_field, {}), + (id_field, {'widget': CustomWidget}), (user_field, {}), (data_field, {'widget': CustomWidget}), ] @@ -318,7 +324,6 @@ class FormfieldCallbackTests(TestCase): formfield_callback=callback) self.assertCallbackCalled(callback) - class BaseCustomDeleteFormSet(BaseFormSet): """ A formset mix-in that lets a form decide if it's to be deleted. From ea425ebcb2d42a60ab3934b3bac9378b08e39d12 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Wed, 6 Feb 2013 01:07:42 -0500 Subject: [PATCH 278/870] Fixed a documentation warning introduced by 3f1c7b7 --- docs/topics/i18n/translation.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index e2cc8fabce..122328e31b 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -1483,7 +1483,7 @@ selection based on data from the request. It customizes content for each user. ``'django.middleware.locale.LocaleMiddleware'``. .. versionchanged:: 1.6 - In previous versions, ``LocaleMiddleware` wasn't enabled by default. + In previous versions, ``LocaleMiddleware`` wasn't enabled by default. Because middleware order matters, you should follow these guidelines: From 2390fe3f4f8f52e24157d79b0c60247207c9716f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 6 Feb 2013 10:02:41 +0100 Subject: [PATCH 279/870] Fixed #19745 -- Forced resolution of verbose names in createsupersuser Thanks Baptiste Mispelon for the report and Preston Holmes for the review. --- .../management/commands/createsuperuser.py | 9 +++++---- django/contrib/auth/tests/basic.py | 20 ++++++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index 4169da248c..b3cd6f9653 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -11,7 +11,7 @@ from django.contrib.auth.management import get_default_username from django.core import exceptions from django.core.management.base import BaseCommand, CommandError from django.db import DEFAULT_DB_ALIAS -from django.utils.encoding import force_str +from django.utils.encoding import force_str, force_text from django.utils.six.moves import input from django.utils.text import capfirst @@ -80,9 +80,10 @@ class Command(BaseCommand): try: # Get a username + verbose_field_name = force_text(self.username_field.verbose_name) while username is None: if not username: - input_msg = capfirst(self.username_field.verbose_name) + input_msg = capfirst(verbose_field_name) if default_username: input_msg = "%s (leave blank to use '%s')" % ( input_msg, default_username) @@ -102,14 +103,14 @@ class Command(BaseCommand): pass else: self.stderr.write("Error: That %s is already taken." % - self.username_field.verbose_name) + verbose_field_name) username = None for field_name in self.UserModel.REQUIRED_FIELDS: field = self.UserModel._meta.get_field(field_name) user_data[field_name] = options.get(field_name) while user_data[field_name] is None: - raw_value = input(force_str('%s: ' % capfirst(field.verbose_name))) + raw_value = input(force_str('%s: ' % capfirst(force_text(field.verbose_name)))) try: user_data[field_name] = field.clean(raw_value, None) except exceptions.ValidationError as e: diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py index 8627329870..03af1fd7bb 100644 --- a/django/contrib/auth/tests/basic.py +++ b/django/contrib/auth/tests/basic.py @@ -12,6 +12,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core.management import call_command from django.test import TestCase from django.test.utils import override_settings +from django.utils.encoding import force_str from django.utils.six import StringIO @@ -30,8 +31,13 @@ def mock_inputs(inputs): # prompt should be encoded in Python 2. This line will raise an # Exception if prompt contains unencoded non-ascii on Python 2. prompt = str(prompt) - if str('leave blank to use') in prompt: - return inputs['username'] + assert str('__proxy__') not in prompt + response = '' + for key, val in inputs.items(): + if force_str(key) in prompt.lower(): + response = val + break + return response old_getpass = createsuperuser.getpass old_input = createsuperuser.input @@ -178,16 +184,20 @@ class BasicTestCase(TestCase): u = User.objects.get(username="nolocale@somewhere.org") self.assertEqual(u.email, 'nolocale@somewhere.org') - @mock_inputs({'password': "nopasswd", 'username': 'foo'}) + @mock_inputs({ + 'password': "nopasswd", + 'uživatel': 'foo', # username (cz) + 'email': 'nolocale@somewhere.org'}) def test_createsuperuser_non_ascii_verbose_name(self): + # Aliased so the string doesn't get extracted + from django.utils.translation import ugettext_lazy as ulazy username_field = User._meta.get_field('username') old_verbose_name = username_field.verbose_name - username_field.verbose_name = 'uživatel' + username_field.verbose_name = ulazy('uživatel') new_io = StringIO() try: call_command("createsuperuser", interactive=True, - email="nolocale@somewhere.org", stdout=new_io ) finally: From 5449240c548bb6877923791d02e800c6b25393f5 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Wed, 6 Feb 2013 05:25:35 -0500 Subject: [PATCH 280/870] Fixed #9800 -- Allow "isPermaLink" attribute in element of an RSS item. Thanks @rtnpro for the patch! --- django/contrib/syndication/views.py | 2 ++ django/utils/feedgenerator.py | 11 +++++-- docs/ref/contrib/syndication.txt | 12 ++++++++ tests/regressiontests/syndication/feeds.py | 13 ++++++++ tests/regressiontests/syndication/tests.py | 36 +++++++++++++++++++++- tests/regressiontests/syndication/urls.py | 4 +++ 6 files changed, 74 insertions(+), 4 deletions(-) diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py index 996b7dfb40..a80b9d1fae 100644 --- a/django/contrib/syndication/views.py +++ b/django/contrib/syndication/views.py @@ -184,6 +184,8 @@ class Feed(object): link = link, description = description, unique_id = self.__get_dynamic_attr('item_guid', item, link), + unique_id_is_permalink = self.__get_dynamic_attr( + 'item_guid_is_permalink', item), enclosure = enc, pubdate = pubdate, author_name = author_name, diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index f9126a6782..7eba842a89 100644 --- a/django/utils/feedgenerator.py +++ b/django/utils/feedgenerator.py @@ -113,8 +113,8 @@ class SyndicationFeed(object): def add_item(self, title, link, description, author_email=None, author_name=None, author_link=None, pubdate=None, comments=None, - unique_id=None, enclosure=None, categories=(), item_copyright=None, - ttl=None, **kwargs): + unique_id=None, unique_id_is_permalink=None, enclosure=None, + categories=(), item_copyright=None, ttl=None, **kwargs): """ Adds an item to the feed. All args are expected to be Python Unicode objects except pubdate, which is a datetime.datetime object, and @@ -136,6 +136,7 @@ class SyndicationFeed(object): 'pubdate': pubdate, 'comments': to_unicode(comments), 'unique_id': to_unicode(unique_id), + 'unique_id_is_permalink': unique_id_is_permalink, 'enclosure': enclosure, 'categories': categories or (), 'item_copyright': to_unicode(item_copyright), @@ -280,7 +281,11 @@ class Rss201rev2Feed(RssFeed): if item['comments'] is not None: handler.addQuickElement("comments", item['comments']) if item['unique_id'] is not None: - handler.addQuickElement("guid", item['unique_id']) + guid_attrs = {} + if isinstance(item.get('unique_id_is_permalink'), bool): + guid_attrs['isPermaLink'] = str( + item['unique_id_is_permalink']).lower() + handler.addQuickElement("guid", item['unique_id'], guid_attrs) if item['ttl'] is not None: handler.addQuickElement("ttl", item['ttl']) diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index 2955d7dad3..65aa7b57b4 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -624,6 +624,18 @@ This example illustrates all possible attributes and methods for a Takes an item, as return by items(), and returns the item's ID. """ + # ITEM_GUID_IS_PERMALINK -- The following method is optional. If + # provided, it sets the 'isPermaLink' attribute of an item's + # GUID element. This method is used only when 'item_guid' is + # specified. + + def item_guid_is_permalink(self, obj): + """ + Takes an item, as returned by items(), and returns a boolean. + """ + + item_guid_is_permalink = False # Hard coded value + # ITEM AUTHOR NAME -- One of the following three is optional. The # framework looks for them in this order. diff --git a/tests/regressiontests/syndication/feeds.py b/tests/regressiontests/syndication/feeds.py index 04a67f4bdb..25757057b9 100644 --- a/tests/regressiontests/syndication/feeds.py +++ b/tests/regressiontests/syndication/feeds.py @@ -42,6 +42,19 @@ class TestRss2Feed(views.Feed): item_copyright = 'Copyright (c) 2007, Sally Smith' +class TestRss2FeedWithGuidIsPermaLinkTrue(TestRss2Feed): + def item_guid_is_permalink(self, item): + return True + + +class TestRss2FeedWithGuidIsPermaLinkFalse(TestRss2Feed): + def item_guid(self, item): + return str(item.pk) + + def item_guid_is_permalink(self, item): + return False + + class TestRss091Feed(TestRss2Feed): feed_type = feedgenerator.RssUserland091Feed diff --git a/tests/regressiontests/syndication/tests.py b/tests/regressiontests/syndication/tests.py index 10413b4ddd..8885dc28c0 100644 --- a/tests/regressiontests/syndication/tests.py +++ b/tests/regressiontests/syndication/tests.py @@ -103,9 +103,43 @@ class SyndicationFeedTest(FeedTestCase): 'author': 'test@example.com (Sally Smith)', }) self.assertCategories(items[0], ['python', 'testing']) - for item in items: self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'category', 'pubDate', 'author']) + # Assert that does not have any 'isPermaLink' attribute + self.assertIsNone(item.getElementsByTagName( + 'guid')[0].attributes.get('isPermaLink')) + + def test_rss2_feed_guid_permalink_false(self): + """ + Test if the 'isPermaLink' attribute of element of an item + in the RSS feed is 'false'. + """ + response = self.client.get( + '/syndication/rss2/guid_ispermalink_false/') + doc = minidom.parseString(response.content) + chan = doc.getElementsByTagName( + 'rss')[0].getElementsByTagName('channel')[0] + items = chan.getElementsByTagName('item') + for item in items: + self.assertEqual( + item.getElementsByTagName('guid')[0].attributes.get( + 'isPermaLink').value, "false") + + def test_rss2_feed_guid_permalink_true(self): + """ + Test if the 'isPermaLink' attribute of element of an item + in the RSS feed is 'true'. + """ + response = self.client.get( + '/syndication/rss2/guid_ispermalink_true/') + doc = minidom.parseString(response.content) + chan = doc.getElementsByTagName( + 'rss')[0].getElementsByTagName('channel')[0] + items = chan.getElementsByTagName('item') + for item in items: + self.assertEqual( + item.getElementsByTagName('guid')[0].attributes.get( + 'isPermaLink').value, "true") def test_rss091_feed(self): """ diff --git a/tests/regressiontests/syndication/urls.py b/tests/regressiontests/syndication/urls.py index 57f9d81a73..ec3c8cc596 100644 --- a/tests/regressiontests/syndication/urls.py +++ b/tests/regressiontests/syndication/urls.py @@ -8,6 +8,10 @@ from . import feeds urlpatterns = patterns('django.contrib.syndication.views', (r'^syndication/complex/(?P.*)/$', feeds.ComplexFeed()), (r'^syndication/rss2/$', feeds.TestRss2Feed()), + (r'^syndication/rss2/guid_ispermalink_true/$', + feeds.TestRss2FeedWithGuidIsPermaLinkTrue()), + (r'^syndication/rss2/guid_ispermalink_false/$', + feeds.TestRss2FeedWithGuidIsPermaLinkFalse()), (r'^syndication/rss091/$', feeds.TestRss091Feed()), (r'^syndication/no_pubdate/$', feeds.TestNoPubdateFeed()), (r'^syndication/atom/$', feeds.TestAtomFeed()), From afa3e1633431137f4e76c7efc359b579f4d9c08e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 6 Feb 2013 08:23:18 -0500 Subject: [PATCH 281/870] Fixed #19743 - Documented some limitations of contrib.auth. Thanks Aymeric for the suggestion. --- docs/topics/auth/index.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/topics/auth/index.txt b/docs/topics/auth/index.txt index ddb2d2f992..8447d449ce 100644 --- a/docs/topics/auth/index.txt +++ b/docs/topics/auth/index.txt @@ -37,6 +37,14 @@ The auth system consists of: * Forms and view tools for logging in users, or restricting content * A pluggable backend system +The authentication system in Django aims to be very generic and doesn't provide +some features commonly found in web authentication systems. Solutions for some +of these common problems have been implemented in third-party packages: + +* Password strength checking +* Throttling of login attempts +* Authentication against third-parties (OAuth, for example) + Installation ============ From d7504a3d7b8645bdb979bab7ded0e9a9b6dccd0e Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 6 Feb 2013 21:20:43 +0100 Subject: [PATCH 282/870] Improved regex in strip_tags Thanks Pablo Recio for the report. Refs #19237. --- django/utils/html.py | 2 +- tests/regressiontests/utils/html.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/django/utils/html.py b/django/utils/html.py index ec7b28d330..a9ebd17935 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -33,7 +33,7 @@ link_target_attribute_re = re.compile(r'(]*?)target=[^\s>]+') html_gunk_re = re.compile(r'(?:
    |<\/i>|<\/b>|<\/em>|<\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE) hard_coded_bullets_re = re.compile(r'((?:

    (?:%s).*?[a-zA-Z].*?

    \s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL) trailing_empty_content_re = re.compile(r'(?:

    (?: |\s|
    )*?

    \s*)+\Z') -strip_tags_re = re.compile(r'])*?>', re.IGNORECASE) +strip_tags_re = re.compile(r']*=(\s*"[^"]*"|\s*\'[^\']*\'|\S*)|[^>])*?>', re.IGNORECASE) def escape(text): diff --git a/tests/regressiontests/utils/html.py b/tests/regressiontests/utils/html.py index a0226c4765..62c7dac24a 100644 --- a/tests/regressiontests/utils/html.py +++ b/tests/regressiontests/utils/html.py @@ -68,6 +68,7 @@ class TestUtilsHtml(unittest.TestCase): ('a

    b

    c', 'abc'), ('a

    b

    c', 'abc'), ('de

    f', 'def'), + ('foo
    bar', 'foobar'), ) for value, output in items: self.check_output(f, value, output) From d18f796a481e79a3800d4672d6189e4c496cce3d Mon Sep 17 00:00:00 2001 From: Alexey Boriskin Date: Sat, 2 Feb 2013 00:22:20 +0400 Subject: [PATCH 283/870] Fixed #19704 -- Make use of new ungettext_lazy function at appropriate places --- django/contrib/comments/admin.py | 16 +++++------ django/utils/timesince.py | 22 +++++++-------- .../tests/moderation_view_tests.py | 28 ++++++++++++++++++- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py index a651baaadf..bca638182c 100644 --- a/django/contrib/comments/admin.py +++ b/django/contrib/comments/admin.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.contrib import admin from django.contrib.auth import get_user_model from django.contrib.comments.models import Comment -from django.utils.translation import ugettext_lazy as _, ungettext +from django.utils.translation import ugettext_lazy as _, ungettext, ungettext_lazy from django.contrib.comments import get_model from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete @@ -52,17 +52,20 @@ class CommentsAdmin(admin.ModelAdmin): def flag_comments(self, request, queryset): self._bulk_flag(request, queryset, perform_flag, - lambda n: ungettext('flagged', 'flagged', n)) + ungettext_lazy('%d comment was successfully flagged', + '%d comments were successfully flagged')) flag_comments.short_description = _("Flag selected comments") def approve_comments(self, request, queryset): self._bulk_flag(request, queryset, perform_approve, - lambda n: ungettext('approved', 'approved', n)) + ungettext_lazy('%d comment was successfully approved', + '%d comments were successfully approved')) approve_comments.short_description = _("Approve selected comments") def remove_comments(self, request, queryset): self._bulk_flag(request, queryset, perform_delete, - lambda n: ungettext('removed', 'removed', n)) + ungettext_lazy('%d comment was successfully removed', + '%d comments were successfully removed')) remove_comments.short_description = _("Remove selected comments") def _bulk_flag(self, request, queryset, action, done_message): @@ -75,10 +78,7 @@ class CommentsAdmin(admin.ModelAdmin): action(request, comment) n_comments += 1 - msg = ungettext('1 comment was successfully %(action)s.', - '%(count)s comments were successfully %(action)s.', - n_comments) - self.message_user(request, msg % {'count': n_comments, 'action': done_message(n_comments)}) + self.message_user(request, done_message % n_comments) # Only register the default admin if the model is the built-in comment model # (this won't be true if there's a custom comment app). diff --git a/django/utils/timesince.py b/django/utils/timesince.py index 1721f097bd..d70ab2ffe1 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import datetime from django.utils.timezone import is_aware, utc -from django.utils.translation import ungettext, ugettext +from django.utils.translation import ugettext, ungettext_lazy def timesince(d, now=None, reversed=False): """ @@ -19,12 +19,12 @@ def timesince(d, now=None, reversed=False): Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since """ chunks = ( - (60 * 60 * 24 * 365, lambda n: ungettext('year', 'years', n)), - (60 * 60 * 24 * 30, lambda n: ungettext('month', 'months', n)), - (60 * 60 * 24 * 7, lambda n : ungettext('week', 'weeks', n)), - (60 * 60 * 24, lambda n : ungettext('day', 'days', n)), - (60 * 60, lambda n: ungettext('hour', 'hours', n)), - (60, lambda n: ungettext('minute', 'minutes', n)) + (60 * 60 * 24 * 365, ungettext_lazy('%d year', '%d years')), + (60 * 60 * 24 * 30, ungettext_lazy('%d month', '%d months')), + (60 * 60 * 24 * 7, ungettext_lazy('%d week', '%d weeks')), + (60 * 60 * 24, ungettext_lazy('%d day', '%d days')), + (60 * 60, ungettext_lazy('%d hour', '%d hours')), + (60, ungettext_lazy('%d minute', '%d minutes')) ) # Convert datetime.date to datetime.datetime for comparison. if not isinstance(d, datetime.datetime): @@ -40,19 +40,19 @@ def timesince(d, now=None, reversed=False): since = delta.days * 24 * 60 * 60 + delta.seconds if since <= 0: # d is in the future compared to now, stop processing. - return '0 ' + ugettext('minutes') + return ugettext('0 minutes') for i, (seconds, name) in enumerate(chunks): count = since // seconds if count != 0: break - s = ugettext('%(number)d %(type)s') % {'number': count, 'type': name(count)} + result = name % count if i + 1 < len(chunks): # Now get the second item seconds2, name2 = chunks[i + 1] count2 = (since - (seconds * count)) // seconds2 if count2 != 0: - s += ugettext(', %(number)d %(type)s') % {'number': count2, 'type': name2(count2)} - return s + result += ugettext(', ') + name2 % count2 + return result def timeuntil(d, now=None): """ diff --git a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py index 0f83d5e210..0abeff9687 100644 --- a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py +++ b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py @@ -1,9 +1,10 @@ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals from django.contrib.auth.models import User, Permission from django.contrib.comments import signals from django.contrib.comments.models import Comment, CommentFlag from django.contrib.contenttypes.models import ContentType +from django.utils import translation from . import CommentTestCase @@ -281,3 +282,28 @@ class AdminActionsTests(CommentTestCase): response = self.client.get('/admin2/comments/comment/') self.assertEqual(response.status_code, 200) self.assertNotContains(response, '
    diff --git a/django/contrib/admindocs/templates/admin_doc/template_tag_index.html b/django/contrib/admindocs/templates/admin_doc/template_tag_index.html index 568b118826..37209102fe 100644 --- a/django/contrib/admindocs/templates/admin_doc/template_tag_index.html +++ b/django/contrib/admindocs/templates/admin_doc/template_tag_index.html @@ -22,7 +22,7 @@

    {% firstof library.grouper "Built-in tags" %}

    {% if library.grouper %}

    To use these tags, put {% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %} in your template before using the tag.


    {% endif %} {% for tag in library.list|dictsort:"name" %} -

    {{ tag.name }}

    +

    {{ tag.name }}

    {{ tag.title|striptags }}

    {{ tag.body }} {% if not forloop.last %}
    {% endif %} @@ -43,7 +43,7 @@

    {% firstof library.grouper "Built-in tags" %}

    diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index ef2790f2db..6b1bea15f3 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -62,7 +62,7 @@ def template_tag_index(request): for key in metadata: metadata[key] = utils.parse_rst(metadata[key], 'tag', _('tag:') + tag_name) if library in template.builtins: - tag_library = None + tag_library = '' else: tag_library = module_name.split('.')[-1] tags.append({ @@ -97,7 +97,7 @@ def template_filter_index(request): for key in metadata: metadata[key] = utils.parse_rst(metadata[key], 'filter', _('filter:') + filter_name) if library in template.builtins: - tag_library = None + tag_library = '' else: tag_library = module_name.split('.')[-1] filters.append({ From e94f405d9499d310ef58b7409a98759a5f5512b0 Mon Sep 17 00:00:00 2001 From: Hiroki Kiyohara Date: Wed, 13 Feb 2013 09:55:43 +0100 Subject: [PATCH 319/870] Fixed #18558 -- Added url property to HttpResponseRedirect* Thanks coolRR for the report. --- AUTHORS | 1 + django/contrib/auth/tests/decorators.py | 2 +- django/contrib/auth/tests/views.py | 28 +++++++-------- .../tests/wizard/namedwizardtests/tests.py | 36 +++++++++---------- django/http/response.py | 2 ++ django/test/client.py | 2 +- django/test/testcases.py | 2 +- docs/ref/request-response.txt | 7 ++++ docs/releases/1.6.txt | 4 +++ tests/regressiontests/admin_views/tests.py | 2 +- tests/regressiontests/generic_views/base.py | 22 ++++++------ tests/regressiontests/httpwrappers/tests.py | 2 ++ tests/regressiontests/middleware/tests.py | 20 +++++------ .../urlpatterns_reverse/tests.py | 16 ++++----- tests/regressiontests/views/tests/i18n.py | 2 +- 15 files changed, 82 insertions(+), 66 deletions(-) diff --git a/AUTHORS b/AUTHORS index 6e79befd31..c43823ce33 100644 --- a/AUTHORS +++ b/AUTHORS @@ -306,6 +306,7 @@ answer newbie questions, and generally made Django that much better: Garth Kidd kilian Sune Kirkeby + Hiroki Kiyohara Bastian Kleineidam Cameron Knight (ckknight) Nena Kojadin diff --git a/django/contrib/auth/tests/decorators.py b/django/contrib/auth/tests/decorators.py index be99e7abb6..5aff375498 100644 --- a/django/contrib/auth/tests/decorators.py +++ b/django/contrib/auth/tests/decorators.py @@ -34,7 +34,7 @@ class LoginRequiredTestCase(AuthViewsTestCase): """ response = self.client.get(view_url) self.assertEqual(response.status_code, 302) - self.assertTrue(login_url in response['Location']) + self.assertTrue(login_url in response.url) self.login() response = self.client.get(view_url) self.assertEqual(response.status_code, 200) diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index 6040a2f5b5..6c508cf607 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -46,7 +46,7 @@ class AuthViewsTestCase(TestCase): 'password': password, }) self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'].endswith(settings.LOGIN_REDIRECT_URL)) + self.assertTrue(response.url.endswith(settings.LOGIN_REDIRECT_URL)) self.assertTrue(SESSION_KEY in self.client.session) def assertContainsEscaped(self, response, text, **kwargs): @@ -281,7 +281,7 @@ class ChangePasswordTest(AuthViewsTestCase): 'new_password2': 'password1', }) self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'].endswith('/password_change/done/')) + self.assertTrue(response.url.endswith('/password_change/done/')) self.fail_login() self.login(password='password1') @@ -293,13 +293,13 @@ class ChangePasswordTest(AuthViewsTestCase): 'new_password2': 'password1', }) self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'].endswith('/password_change/done/')) + self.assertTrue(response.url.endswith('/password_change/done/')) def test_password_change_done_fails(self): with self.settings(LOGIN_URL='/login/'): response = self.client.get('/password_change/done/') self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'].endswith('/login/?next=/password_change/done/')) + self.assertTrue(response.url.endswith('/login/?next=/password_change/done/')) @skipIfCustomUser @@ -336,7 +336,7 @@ class LoginTest(AuthViewsTestCase): 'password': password, }) self.assertEqual(response.status_code, 302) - self.assertFalse(bad_url in response['Location'], + self.assertFalse(bad_url in response.url, "%s should be blocked" % bad_url) # These URLs *should* still pass the security check @@ -357,7 +357,7 @@ class LoginTest(AuthViewsTestCase): 'password': password, }) self.assertEqual(response.status_code, 302) - self.assertTrue(good_url in response['Location'], + self.assertTrue(good_url in response.url, "%s should be allowed" % good_url) @@ -376,7 +376,7 @@ class LoginURLSettings(AuthViewsTestCase): settings.LOGIN_URL = login_url response = self.client.get('/login_required/') self.assertEqual(response.status_code, 302) - return response['Location'] + return response.url def test_standard_login_url(self): login_url = '/login/' @@ -444,11 +444,11 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/next_page/') self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'].endswith('/somewhere/')) + self.assertTrue(response.url.endswith('/somewhere/')) response = self.client.get('/logout/next_page/?next=/login/') self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'].endswith('/login/')) + self.assertTrue(response.url.endswith('/login/')) self.confirm_logged_out() @@ -457,7 +457,7 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/next_page/') self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'].endswith('/somewhere/')) + self.assertTrue(response.url.endswith('/somewhere/')) self.confirm_logged_out() def test_logout_with_redirect_argument(self): @@ -465,7 +465,7 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/?next=/login/') self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'].endswith('/login/')) + self.assertTrue(response.url.endswith('/login/')) self.confirm_logged_out() def test_logout_with_custom_redirect_argument(self): @@ -473,7 +473,7 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/custom_query/?follow=/somewhere/') self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'].endswith('/somewhere/')) + self.assertTrue(response.url.endswith('/somewhere/')) self.confirm_logged_out() def test_security_check(self, password='password'): @@ -492,7 +492,7 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get(nasty_url) self.assertEqual(response.status_code, 302) - self.assertFalse(bad_url in response['Location'], + self.assertFalse(bad_url in response.url, "%s should be blocked" % bad_url) self.confirm_logged_out() @@ -512,6 +512,6 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get(safe_url) self.assertEqual(response.status_code, 302) - self.assertTrue(good_url in response['Location'], + self.assertTrue(good_url in response.url, "%s should be allowed" % good_url) self.confirm_logged_out() diff --git a/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py b/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py index 7529d89a2c..214f19a04d 100644 --- a/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py +++ b/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py @@ -21,7 +21,7 @@ class NamedWizardTests(object): def test_initial_call(self): response = self.client.get(reverse('%s_start' % self.wizard_urlname)) self.assertEqual(response.status_code, 302) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) wizard = response.context['wizard'] self.assertEqual(wizard['steps'].current, 'form1') @@ -40,7 +40,7 @@ class NamedWizardTests(object): self.assertEqual(response.status_code, 302) # Test for proper redirect GET parameters - location = response['Location'] + location = response.url self.assertNotEqual(location.find('?'), -1) querydict = QueryDict(location[location.find('?') + 1:]) self.assertEqual(dict(querydict.items()), get_params) @@ -60,7 +60,7 @@ class NamedWizardTests(object): response = self.client.post( reverse(self.wizard_urlname, kwargs={'step': 'form1'}), self.wizard_step_data[0]) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) wizard = response.context['wizard'] @@ -79,7 +79,7 @@ class NamedWizardTests(object): response = self.client.post( reverse(self.wizard_urlname, kwargs={'step': 'form1'}), self.wizard_step_data[0]) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['wizard']['steps'].current, 'form2') @@ -88,7 +88,7 @@ class NamedWizardTests(object): reverse(self.wizard_urlname, kwargs={ 'step': response.context['wizard']['steps'].current }), {'wizard_goto_step': response.context['wizard']['steps'].prev}) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['wizard']['steps'].current, 'form1') @@ -116,7 +116,7 @@ class NamedWizardTests(object): reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), self.wizard_step_data[0]) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['wizard']['steps'].current, 'form2') @@ -128,7 +128,7 @@ class NamedWizardTests(object): reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), post_data) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['wizard']['steps'].current, 'form3') @@ -137,7 +137,7 @@ class NamedWizardTests(object): reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), self.wizard_step_data[2]) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['wizard']['steps'].current, 'form4') @@ -146,7 +146,7 @@ class NamedWizardTests(object): reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), self.wizard_step_data[3]) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) all_data = response.context['form_list'] @@ -169,7 +169,7 @@ class NamedWizardTests(object): reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), self.wizard_step_data[0]) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) post_data = self.wizard_step_data[1] @@ -178,7 +178,7 @@ class NamedWizardTests(object): reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), post_data) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) step2_url = reverse(self.wizard_urlname, kwargs={'step': 'form2'}) @@ -194,14 +194,14 @@ class NamedWizardTests(object): reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), self.wizard_step_data[2]) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) response = self.client.post( reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), self.wizard_step_data[3]) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) all_data = response.context['all_cleaned_data'] @@ -227,7 +227,7 @@ class NamedWizardTests(object): reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), self.wizard_step_data[0]) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) post_data = self.wizard_step_data[1] @@ -237,14 +237,14 @@ class NamedWizardTests(object): reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), post_data) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) response = self.client.post( reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), self.wizard_step_data[2]) - loc = response['Location'] + loc = response.url response = self.client.get(loc) self.assertEqual(response.status_code, 200, loc) @@ -263,7 +263,7 @@ class NamedWizardTests(object): response = self.client.post( reverse(self.wizard_urlname, kwargs={'step': 'form1'}), self.wizard_step_data[0]) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['wizard']['steps'].current, 'form2') @@ -271,7 +271,7 @@ class NamedWizardTests(object): '%s?reset=1' % reverse('%s_start' % self.wizard_urlname)) self.assertEqual(response.status_code, 302) - response = self.client.get(response['Location']) + response = self.client.get(response.url) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['wizard']['steps'].current, 'form1') diff --git a/django/http/response.py b/django/http/response.py index 48a401adcb..88ac8848c2 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -392,6 +392,8 @@ class HttpResponseRedirectBase(HttpResponse): super(HttpResponseRedirectBase, self).__init__(*args, **kwargs) self['Location'] = iri_to_uri(redirect_to) + url = property(lambda self: self['Location']) + class HttpResponseRedirect(HttpResponseRedirectBase): status_code = 302 diff --git a/django/test/client.py b/django/test/client.py index bb0f25e108..2506437023 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -580,7 +580,7 @@ class Client(RequestFactory): response.redirect_chain = [] while response.status_code in (301, 302, 303, 307): - url = response['Location'] + url = response.url redirect_chain = response.redirect_chain redirect_chain.append((url, response.status_code)) diff --git a/django/test/testcases.py b/django/test/testcases.py index f7c34a9f25..f9d028bb72 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -601,7 +601,7 @@ class TransactionTestCase(SimpleTestCase): " code was %d (expected %d)" % (response.status_code, status_code)) - url = response['Location'] + url = response.url scheme, netloc, path, query, fragment = urlsplit(url) redirect_response = response.client.get(path, QueryDict(query)) diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 717995aea2..30f5e87100 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -746,6 +746,13 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in domain (e.g. ``'/search/'``). See :class:`HttpResponse` for other optional constructor arguments. Note that this returns an HTTP status code 302. + .. attribute:: HttpResponseRedirect.url + + .. versionadded:: 1.6 + + This read-only attribute represents the URL the response will redirect + to (equivalent to the ``Location`` response header). + .. class:: HttpResponsePermanentRedirect Like :class:`HttpResponseRedirect`, but it returns a permanent redirect diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 5d615177f4..60537aca53 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -68,6 +68,10 @@ Minor features :class:`~django.views.generic.edit.DeletionMixin` is now interpolated with its ``object``\'s ``__dict__``. +* :class:`~django.http.HttpResponseRedirect` and + :class:`~django.http.HttpResponsePermanentRedirect` now provide an ``url`` + attribute (equivalent to the URL the response will redirect to). + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 1633fba6b5..e0cd7cdfa1 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -1641,7 +1641,7 @@ class SecureViewTests(TestCase): response = self.client.get(shortcut_url, follow=False) # Can't use self.assertRedirects() because User.get_absolute_url() is silly. self.assertEqual(response.status_code, 302) - self.assertEqual(response['Location'], 'http://example.com/users/super/') + self.assertEqual(response.url, 'http://example.com/users/super/') @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py index fd2abb0aa7..7f6f261cb5 100644 --- a/tests/regressiontests/generic_views/base.py +++ b/tests/regressiontests/generic_views/base.py @@ -329,66 +329,66 @@ class RedirectViewTest(unittest.TestCase): "Default is a permanent redirect" response = RedirectView.as_view(url='/bar/')(self.rf.get('/foo/')) self.assertEqual(response.status_code, 301) - self.assertEqual(response['Location'], '/bar/') + self.assertEqual(response.url, '/bar/') def test_temporary_redirect(self): "Permanent redirects are an option" response = RedirectView.as_view(url='/bar/', permanent=False)(self.rf.get('/foo/')) self.assertEqual(response.status_code, 302) - self.assertEqual(response['Location'], '/bar/') + self.assertEqual(response.url, '/bar/') def test_include_args(self): "GET arguments can be included in the redirected URL" response = RedirectView.as_view(url='/bar/')(self.rf.get('/foo/')) self.assertEqual(response.status_code, 301) - self.assertEqual(response['Location'], '/bar/') + self.assertEqual(response.url, '/bar/') response = RedirectView.as_view(url='/bar/', query_string=True)(self.rf.get('/foo/?pork=spam')) self.assertEqual(response.status_code, 301) - self.assertEqual(response['Location'], '/bar/?pork=spam') + self.assertEqual(response.url, '/bar/?pork=spam') def test_include_urlencoded_args(self): "GET arguments can be URL-encoded when included in the redirected URL" response = RedirectView.as_view(url='/bar/', query_string=True)( self.rf.get('/foo/?unicode=%E2%9C%93')) self.assertEqual(response.status_code, 301) - self.assertEqual(response['Location'], '/bar/?unicode=%E2%9C%93') + self.assertEqual(response.url, '/bar/?unicode=%E2%9C%93') def test_parameter_substitution(self): "Redirection URLs can be parameterized" response = RedirectView.as_view(url='/bar/%(object_id)d/')(self.rf.get('/foo/42/'), object_id=42) self.assertEqual(response.status_code, 301) - self.assertEqual(response['Location'], '/bar/42/') + self.assertEqual(response.url, '/bar/42/') def test_redirect_POST(self): "Default is a permanent redirect" response = RedirectView.as_view(url='/bar/')(self.rf.post('/foo/')) self.assertEqual(response.status_code, 301) - self.assertEqual(response['Location'], '/bar/') + self.assertEqual(response.url, '/bar/') def test_redirect_HEAD(self): "Default is a permanent redirect" response = RedirectView.as_view(url='/bar/')(self.rf.head('/foo/')) self.assertEqual(response.status_code, 301) - self.assertEqual(response['Location'], '/bar/') + self.assertEqual(response.url, '/bar/') def test_redirect_OPTIONS(self): "Default is a permanent redirect" response = RedirectView.as_view(url='/bar/')(self.rf.options('/foo/')) self.assertEqual(response.status_code, 301) - self.assertEqual(response['Location'], '/bar/') + self.assertEqual(response.url, '/bar/') def test_redirect_PUT(self): "Default is a permanent redirect" response = RedirectView.as_view(url='/bar/')(self.rf.put('/foo/')) self.assertEqual(response.status_code, 301) - self.assertEqual(response['Location'], '/bar/') + self.assertEqual(response.url, '/bar/') def test_redirect_DELETE(self): "Default is a permanent redirect" response = RedirectView.as_view(url='/bar/')(self.rf.delete('/foo/')) self.assertEqual(response.status_code, 301) - self.assertEqual(response['Location'], '/bar/') + self.assertEqual(response.url, '/bar/') def test_redirect_when_meta_contains_no_query_string(self): "regression for #16705" diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index c76d8eafe3..2d3240915e 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -410,6 +410,8 @@ class HttpResponseSubclassesTests(TestCase): content='The resource has temporarily moved', content_type='text/html') self.assertContains(response, 'The resource has temporarily moved', status_code=302) + # Test that url attribute is right + self.assertEqual(response.url, response['Location']) def test_not_modified(self): response = HttpResponseNotModified() diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py index 6c436415ab..73a6279e2c 100644 --- a/tests/regressiontests/middleware/tests.py +++ b/tests/regressiontests/middleware/tests.py @@ -69,7 +69,7 @@ class CommonMiddlewareTest(TestCase): request = self._get_request('slash') r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) - self.assertEqual(r['Location'], 'http://testserver/middleware/slash/') + self.assertEqual(r.url, 'http://testserver/middleware/slash/') @override_settings(APPEND_SLASH=True, DEBUG=True) def test_append_slash_no_redirect_on_POST_in_DEBUG(self): @@ -101,7 +101,7 @@ class CommonMiddlewareTest(TestCase): r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) self.assertEqual( - r['Location'], + r.url, 'http://testserver/middleware/needsquoting%23/') @override_settings(APPEND_SLASH=False, PREPEND_WWW=True) @@ -110,7 +110,7 @@ class CommonMiddlewareTest(TestCase): r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) self.assertEqual( - r['Location'], + r.url, 'http://www.testserver/middleware/path/') @override_settings(APPEND_SLASH=True, PREPEND_WWW=True) @@ -118,7 +118,7 @@ class CommonMiddlewareTest(TestCase): request = self._get_request('slash/') r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) - self.assertEqual(r['Location'], + self.assertEqual(r.url, 'http://www.testserver/middleware/slash/') @override_settings(APPEND_SLASH=True, PREPEND_WWW=True) @@ -126,7 +126,7 @@ class CommonMiddlewareTest(TestCase): request = self._get_request('slash') r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) - self.assertEqual(r['Location'], + self.assertEqual(r.url, 'http://www.testserver/middleware/slash/') @@ -171,7 +171,7 @@ class CommonMiddlewareTest(TestCase): self.assertFalse(r is None, "CommonMiddlware failed to return APPEND_SLASH redirect using request.urlconf") self.assertEqual(r.status_code, 301) - self.assertEqual(r['Location'], 'http://testserver/middleware/customurlconf/slash/') + self.assertEqual(r.url, 'http://testserver/middleware/customurlconf/slash/') @override_settings(APPEND_SLASH=True, DEBUG=True) def test_append_slash_no_redirect_on_POST_in_DEBUG_custom_urlconf(self): @@ -208,7 +208,7 @@ class CommonMiddlewareTest(TestCase): "CommonMiddlware failed to return APPEND_SLASH redirect using request.urlconf") self.assertEqual(r.status_code, 301) self.assertEqual( - r['Location'], + r.url, 'http://testserver/middleware/customurlconf/needsquoting%23/') @override_settings(APPEND_SLASH=False, PREPEND_WWW=True) @@ -218,7 +218,7 @@ class CommonMiddlewareTest(TestCase): r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) self.assertEqual( - r['Location'], + r.url, 'http://www.testserver/middleware/customurlconf/path/') @override_settings(APPEND_SLASH=True, PREPEND_WWW=True) @@ -227,7 +227,7 @@ class CommonMiddlewareTest(TestCase): request.urlconf = 'regressiontests.middleware.extra_urls' r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) - self.assertEqual(r['Location'], + self.assertEqual(r.url, 'http://www.testserver/middleware/customurlconf/slash/') @override_settings(APPEND_SLASH=True, PREPEND_WWW=True) @@ -236,7 +236,7 @@ class CommonMiddlewareTest(TestCase): request.urlconf = 'regressiontests.middleware.extra_urls' r = CommonMiddleware().process_request(request) self.assertEqual(r.status_code, 301) - self.assertEqual(r['Location'], + self.assertEqual(r.url, 'http://www.testserver/middleware/customurlconf/slash/') # Legacy tests for the 404 error reporting via email (to be removed in 1.8) diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py index eb3afe8201..9777710daf 100644 --- a/tests/regressiontests/urlpatterns_reverse/tests.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -270,31 +270,31 @@ class ReverseShortcutTests(TestCase): res = redirect(FakeObj()) self.assertTrue(isinstance(res, HttpResponseRedirect)) - self.assertEqual(res['Location'], '/hi-there/') + self.assertEqual(res.url, '/hi-there/') res = redirect(FakeObj(), permanent=True) self.assertTrue(isinstance(res, HttpResponsePermanentRedirect)) - self.assertEqual(res['Location'], '/hi-there/') + self.assertEqual(res.url, '/hi-there/') def test_redirect_to_view_name(self): res = redirect('hardcoded2') - self.assertEqual(res['Location'], '/hardcoded/doc.pdf') + self.assertEqual(res.url, '/hardcoded/doc.pdf') res = redirect('places', 1) - self.assertEqual(res['Location'], '/places/1/') + self.assertEqual(res.url, '/places/1/') res = redirect('headlines', year='2008', month='02', day='17') - self.assertEqual(res['Location'], '/headlines/2008.02.17/') + self.assertEqual(res.url, '/headlines/2008.02.17/') self.assertRaises(NoReverseMatch, redirect, 'not-a-view') def test_redirect_to_url(self): res = redirect('/foo/') - self.assertEqual(res['Location'], '/foo/') + self.assertEqual(res.url, '/foo/') res = redirect('http://example.com/') - self.assertEqual(res['Location'], 'http://example.com/') + self.assertEqual(res.url, 'http://example.com/') def test_redirect_view_object(self): from .views import absolute_kwargs_view res = redirect(absolute_kwargs_view) - self.assertEqual(res['Location'], '/absolute_arg_view/') + self.assertEqual(res.url, '/absolute_arg_view/') self.assertRaises(NoReverseMatch, redirect, absolute_kwargs_view, wrong_argument=None) diff --git a/tests/regressiontests/views/tests/i18n.py b/tests/regressiontests/views/tests/i18n.py index b1dc8808a1..0a091ed1b7 100644 --- a/tests/regressiontests/views/tests/i18n.py +++ b/tests/regressiontests/views/tests/i18n.py @@ -44,7 +44,7 @@ class I18NTests(TestCase): lang_code, lang_name = settings.LANGUAGES[0] post_data = dict(language=lang_code, next='//unsafe/redirection/') response = self.client.post('/views/i18n/setlang/', data=post_data) - self.assertEqual(response['Location'], 'http://testserver/') + self.assertEqual(response.url, 'http://testserver/') self.assertEqual(self.client.session['django_language'], lang_code) def test_setlang_reversal(self): From ac4faa6dc33407c93566884e53fa5d8ef44c0a2a Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 13 Feb 2013 18:24:49 +0100 Subject: [PATCH 320/870] Fixed #19693 -- Made truncatewords_html handle self-closing tags Thanks sneawo for the report and Jonathan Loy for the patch. --- django/utils/text.py | 2 +- tests/regressiontests/utils/text.py | 27 +++++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/django/utils/text.py b/django/utils/text.py index 6664b18249..02c3f10678 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -24,7 +24,7 @@ capfirst = allow_lazy(capfirst, six.text_type) # Set up regular expressions re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U|re.S) -re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>', re.S) +re_tag = re.compile(r'<(/)?([^ ]+?)(?:(\s*/)| .*?)?>', re.S) def wrap(text, width): diff --git a/tests/regressiontests/utils/text.py b/tests/regressiontests/utils/text.py index 3465282c7f..c9dde6b1b3 100644 --- a/tests/regressiontests/utils/text.py +++ b/tests/regressiontests/utils/text.py @@ -55,22 +55,33 @@ class TestUtilsText(SimpleTestCase): truncator.words(4, '[snip]')) def test_truncate_html_words(self): - truncator = text.Truncator('

    The quick brown fox jumped ' - 'over the lazy dog.

    ') - self.assertEqual('

    The quick brown fox jumped over the ' - 'lazy dog.

    ', truncator.words(10, html=True)) - self.assertEqual('

    The quick brown fox...' + truncator = text.Truncator('

    The quick brown fox' + ' jumped over the lazy dog.

    ') + self.assertEqual('

    The quick brown fox jumped over' + ' the lazy dog.

    ', truncator.words(10, html=True)) + self.assertEqual('

    The quick brown fox...' '

    ', truncator.words(4, html=True)) - self.assertEqual('

    The quick brown fox....' + self.assertEqual('

    The quick brown fox....' '

    ', truncator.words(4, '....', html=True)) - self.assertEqual('

    The quick brown fox' - '

    ', truncator.words(4, '', html=True)) + self.assertEqual('

    The quick brown fox' + '

    ', truncator.words(4, '', html=True)) + # Test with new line inside tag truncator = text.Truncator('

    The quick brown fox jumped over the lazy dog.

    ') self.assertEqual('

    The quick brown...

    ', truncator.words(3, '...', html=True)) + # Test self-closing tags + truncator = text.Truncator('
    The
    quick brown fox jumped over' + ' the lazy dog.') + self.assertEqual('
    The
    quick brown...', + truncator.words(3, '...', html=True )) + truncator = text.Truncator('
    The
    quick brown fox ' + 'jumped over the lazy dog.') + self.assertEqual('
    The
    quick brown...', + truncator.words(3, '...', html=True )) + def test_wrap(self): digits = '1234 67 9' self.assertEqual(text.wrap(digits, 100), '1234 67 9') From 142ec8b2835c242339b930c47a70a3c7036df91d Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 13 Feb 2013 23:10:43 +0100 Subject: [PATCH 321/870] Fixed #8404 -- Isolated auth password-related tests from custom templates --- django/contrib/auth/tests/views.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index 6c508cf607..42bea825f5 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -1,3 +1,4 @@ +import itertools import os import re @@ -49,8 +50,10 @@ class AuthViewsTestCase(TestCase): self.assertTrue(response.url.endswith(settings.LOGIN_REDIRECT_URL)) self.assertTrue(SESSION_KEY in self.client.session) - def assertContainsEscaped(self, response, text, **kwargs): - return self.assertContains(response, escape(force_text(text)), **kwargs) + def assertFormError(self, response, error): + """Assert that error is found in response.context['form'] errors""" + form_errors = list(itertools.chain(*response.context['form'].errors.values())) + self.assertIn(force_text(text), form_errors) @skipIfCustomUser @@ -87,7 +90,7 @@ class PasswordResetTest(AuthViewsTestCase): response = self.client.get('/password_reset/') self.assertEqual(response.status_code, 200) response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'}) - self.assertContainsEscaped(response, PasswordResetForm.error_messages['unknown']) + self.assertFormError(response, PasswordResetForm.error_messages['unknown']) self.assertEqual(len(mail.outbox), 0) def test_email_found(self): @@ -214,7 +217,7 @@ class PasswordResetTest(AuthViewsTestCase): url, path = self._test_confirm_start() response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'x'}) - self.assertContainsEscaped(response, SetPasswordForm.error_messages['password_mismatch']) + self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch']) @override_settings(AUTH_USER_MODEL='auth.CustomUser') @@ -248,7 +251,7 @@ class ChangePasswordTest(AuthViewsTestCase): 'username': 'testclient', 'password': password, }) - self.assertContainsEscaped(response, AuthenticationForm.error_messages['invalid_login'] % { + self.assertFormError(response, AuthenticationForm.error_messages['invalid_login'] % { 'username': User._meta.get_field('username').verbose_name }) @@ -262,7 +265,7 @@ class ChangePasswordTest(AuthViewsTestCase): 'new_password1': 'password1', 'new_password2': 'password1', }) - self.assertContainsEscaped(response, PasswordChangeForm.error_messages['password_incorrect']) + self.assertFormError(response, PasswordChangeForm.error_messages['password_incorrect']) def test_password_change_fails_with_mismatched_passwords(self): self.login() @@ -271,7 +274,7 @@ class ChangePasswordTest(AuthViewsTestCase): 'new_password1': 'password1', 'new_password2': 'donuts', }) - self.assertContainsEscaped(response, SetPasswordForm.error_messages['password_mismatch']) + self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch']) def test_password_change_succeeds(self): self.login() From f1029b308f3ea967a5d93aea2b730671898a56f5 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 14 Feb 2013 08:33:10 +0100 Subject: [PATCH 322/870] Fixed a misnamed variable introduced in commit 142ec8b283 Refs #8404. --- django/contrib/auth/tests/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index 42bea825f5..48dfc9ed76 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -53,7 +53,7 @@ class AuthViewsTestCase(TestCase): def assertFormError(self, response, error): """Assert that error is found in response.context['form'] errors""" form_errors = list(itertools.chain(*response.context['form'].errors.values())) - self.assertIn(force_text(text), form_errors) + self.assertIn(force_text(error), form_errors) @skipIfCustomUser From 138de533ff677b470a1e7b4b6ff084a5b7a7444b Mon Sep 17 00:00:00 2001 From: Michael van Tellingen Date: Thu, 14 Feb 2013 09:45:55 +0100 Subject: [PATCH 323/870] Fixed #19819 - Improved template filter errors handling. Wrap the Parser.compile_filter method call with a try/except and call the newly added Parser.compile_filter_error(). Overwrite this method in the DebugParser to throw the correct error. Since this error was otherwise catched by the compile_function try/except block the debugger highlighted the wrong line. --- django/template/base.py | 9 ++++++++- django/template/debug.py | 4 ++++ tests/regressiontests/templates/parser.py | 12 +++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/django/template/base.py b/django/template/base.py index 0a2b2c9437..9577d586d8 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -250,7 +250,11 @@ class Parser(object): elif token.token_type == 1: # TOKEN_VAR if not token.contents: self.empty_variable(token) - filter_expression = self.compile_filter(token.contents) + try: + filter_expression = self.compile_filter(token.contents) + except TemplateSyntaxError as e: + if not self.compile_filter_error(token, e): + raise var_node = self.create_variable_node(filter_expression) self.extend_nodelist(nodelist, var_node, token) elif token.token_type == 2: # TOKEN_BLOCK @@ -330,6 +334,9 @@ class Parser(object): def unclosed_block_tag(self, parse_until): raise self.error(None, "Unclosed tags: %s " % ', '.join(parse_until)) + def compile_filter_error(self, token, e): + pass + def compile_function_error(self, token, e): pass diff --git a/django/template/debug.py b/django/template/debug.py index c7ac007b48..043dd91b4e 100644 --- a/django/template/debug.py +++ b/django/template/debug.py @@ -64,6 +64,10 @@ class DebugParser(Parser): msg = "Unclosed tag '%s'. Looking for one of: %s " % (command, ', '.join(parse_until)) raise self.source_error(source, msg) + def compile_filter_error(self, token, e): + if not hasattr(e, 'django_template_source'): + e.django_template_source = token.source + def compile_function_error(self, token, e): if not hasattr(e, 'django_template_source'): e.django_template_source = token.source diff --git a/tests/regressiontests/templates/parser.py b/tests/regressiontests/templates/parser.py index 6c9deee9ff..9422da80d7 100644 --- a/tests/regressiontests/templates/parser.py +++ b/tests/regressiontests/templates/parser.py @@ -4,8 +4,10 @@ Testing some internals of the template processing. These are *not* examples to b from __future__ import unicode_literals from django.template import (TokenParser, FilterExpression, Parser, Variable, - TemplateSyntaxError) + Template, TemplateSyntaxError) +from django.test.utils import override_settings from django.utils.unittest import TestCase +from django.utils import six class ParserTests(TestCase): @@ -83,3 +85,11 @@ class ParserTests(TestCase): self.assertRaises(TemplateSyntaxError, Variable, "article._hidden" ) + + @override_settings(DEBUG=True, TEMPLATE_DEBUG=True) + def test_compile_filter_error(self): + # regression test for #19819 + msg = "Could not parse the remainder: '@bar' from 'foo@bar'" + with six.assertRaisesRegex(self, TemplateSyntaxError, msg) as cm: + Template("{% if 1 %}{{ foo@bar }}{% endif %}") + self.assertEqual(cm.exception.django_template_source[1], (10, 23)) From 668d0b8d499c45ef7d449b5e56f0adc97660d417 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 14 Feb 2013 11:22:33 +0100 Subject: [PATCH 324/870] Fixed #19823 -- Fixed memcached code example in cache docs --- docs/topics/cache.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 208fa3a5e2..e345b89dcd 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -137,7 +137,7 @@ on the IP addresses 172.19.26.240 (port 11211), 172.19.26.242 (port 11212), and 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'LOCATION': [ '172.19.26.240:11211', - '172.19.26.242:11211', + '172.19.26.242:11212', '172.19.26.244:11213', ] } From f179a5198e05e1be8ba8be87c1f0e1a8924cf005 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 14 Feb 2013 20:29:21 -0300 Subject: [PATCH 325/870] Fix filtering during collection of paths in setup.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Marek Brzóska for the report. --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index f4545b6a0c..6fff26d3af 100644 --- a/setup.py +++ b/setup.py @@ -68,9 +68,7 @@ django_dir = 'django' for dirpath, dirnames, filenames in os.walk(django_dir): # Ignore PEP 3147 cache dirs and those whose names start with '.' - for i, dirname in enumerate(dirnames): - if dirname.startswith('.') or dirname == '__pycache__': - del dirnames[i] + dirnames[:] = [d for d in dirnames if not d.startswith('.') and d != '__pycache__'] if '__init__.py' in filenames: packages.append('.'.join(fullsplit(dirpath))) elif filenames: From f5e4a699ca0f58818acbdf9081164060cee910fa Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 15 Feb 2013 09:00:55 +0800 Subject: [PATCH 326/870] Fixed #19822 -- Added validation for uniqueness on USERNAME_FIELD on custom User models. Thanks to Claude Peroz for the draft patch. --- django/contrib/auth/tests/custom_user.py | 22 ++++++++++++++++++++++ django/contrib/auth/tests/management.py | 18 ++++++++++++++++++ django/core/management/validation.py | 6 +++++- docs/topics/auth/customizing.txt | 5 ++++- 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py index 8cc57d4caf..0d324f0953 100644 --- a/django/contrib/auth/tests/custom_user.py +++ b/django/contrib/auth/tests/custom_user.py @@ -144,3 +144,25 @@ class IsActiveTestUser1(AbstractBaseUser): app_label = 'auth' # the is_active attr is provided by AbstractBaseUser + + +class CustomUserNonUniqueUsername(AbstractBaseUser): + "A user with a non-unique username" + username = models.CharField(max_length=30) + + USERNAME_FIELD = 'username' + + class Meta: + app_label = 'auth' + + +class CustomUserBadRequiredFields(AbstractBaseUser): + "A user with a non-unique username" + username = models.CharField(max_length=30, unique=True) + date_of_birth = models.DateField() + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['username', 'date_of_birth'] + + class Meta: + app_label = 'auth' diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index 42f14d6d5c..687a5c31cb 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -9,6 +9,8 @@ from django.contrib.auth.tests import CustomUser from django.contrib.auth.tests.utils import skipIfCustomUser from django.core.management import call_command from django.core.management.base import CommandError +from django.core.management.validation import get_validation_errors +from django.db.models.loading import get_app from django.test import TestCase from django.test.utils import override_settings from django.utils import six @@ -170,6 +172,22 @@ class CreatesuperuserManagementCommandTestCase(TestCase): self.assertEqual(CustomUser._default_manager.count(), 0) +class CustomUserModelValidationTestCase(TestCase): + @override_settings(AUTH_USER_MODEL='auth.CustomUserBadRequiredFields') + def test_username_not_in_required_fields(self): + "USERNAME_FIELD should not appear in REQUIRED_FIELDS." + new_io = StringIO() + get_validation_errors(new_io, get_app('auth')) + self.assertIn("The field named as the USERNAME_FIELD should not be included in REQUIRED_FIELDS on a swappable User model.", new_io.getvalue()) + + @override_settings(AUTH_USER_MODEL='auth.CustomUserNonUniqueUsername') + def test_username_non_unique(self): + "A non-unique USERNAME_FIELD should raise a model validation error." + new_io = StringIO() + get_validation_errors(new_io, get_app('auth')) + self.assertIn("The USERNAME_FIELD must be unique. Add unique=True to the field parameters.", new_io.getvalue()) + + class PermissionDuplicationTestCase(TestCase): def setUp(self): diff --git a/django/core/management/validation.py b/django/core/management/validation.py index f49a3c2232..587d3a0ad7 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -49,12 +49,16 @@ def get_validation_errors(outfile, app=None): # No need to perform any other validation checks on a swapped model. continue - # This is the current User model. Check known validation problems with User models + # If this is the current User model, check known validation problems with User models if settings.AUTH_USER_MODEL == '%s.%s' % (opts.app_label, opts.object_name): # Check that the USERNAME FIELD isn't included in REQUIRED_FIELDS. if cls.USERNAME_FIELD in cls.REQUIRED_FIELDS: e.add(opts, 'The field named as the USERNAME_FIELD should not be included in REQUIRED_FIELDS on a swappable User model.') + # Check that the username field is unique + if not opts.get_field(cls.USERNAME_FIELD).unique: + e.add(opts, 'The USERNAME_FIELD must be unique. Add unique=True to the field parameters.') + # Model isn't swapped; do field-specific validation. for f in opts.local_fields: if f.name == 'id' and not f.primary_key and opts.pk.name == 'id': diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 7f1eff6624..d1ce6eb7dc 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -492,7 +492,10 @@ password resets. You must then provide some key implementation details: A string describing the name of the field on the User model that is used as the unique identifier. This will usually be a username of some kind, but it can also be an email address, or any other unique - identifier. In the following example, the field `identifier` is used + identifier. The field *must* be unique (i.e., have ``unique=True`` + set in it's definition). + + In the following example, the field `identifier` is used as the identifying field:: class MyUser(AbstractBaseUser): From 7d5e35cdb46124e2471833b9570add1a00a1d9e0 Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Thu, 14 Feb 2013 23:29:15 -0800 Subject: [PATCH 327/870] Fixed #19829 -- Fixed index lookups for NumPy arrays in templates. --- django/template/base.py | 2 +- tests/regressiontests/templates/tests.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/django/template/base.py b/django/template/base.py index 9577d586d8..f43194de4a 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -763,7 +763,7 @@ class Variable(object): for bit in self.lookups: try: # dictionary lookup current = current[bit] - except (TypeError, AttributeError, KeyError): + except (TypeError, AttributeError, KeyError, ValueError): try: # attribute lookup current = getattr(current, bit) except (TypeError, AttributeError): diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 8d2a45b8fc..255671435a 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -54,6 +54,12 @@ except ImportError as e: else: raise +# NumPy installed? +try: + import numpy +except ImportError: + numpy = False + from . import filters ################################# @@ -1649,6 +1655,17 @@ class Templates(TestCase): 'verbatim-tag05': ('{% verbatim %}{% endverbatim %}{% verbatim %}{% endverbatim %}', {}, ''), 'verbatim-tag06': ("{% verbatim special %}Don't {% endverbatim %} just yet{% endverbatim special %}", {}, "Don't {% endverbatim %} just yet"), } + + if numpy: + tests.update({ + # Numpy's array-index syntax allows a template to access a certain item of a subscriptable object. + 'numpy-array-index01': ("{{ var.1 }}", {"var": numpy.array(["first item", "second item"])}, "second item"), + + # Fail silently when the array index is out of range. + 'numpy-array-index02': ("{{ var.5 }}", {"var": numpy.array(["first item", "second item"])}, ("", "INVALID")), + }) + + return tests class TemplateTagLoading(unittest.TestCase): From b8061397eac019f42f875b6fff99b26a6dfdf229 Mon Sep 17 00:00:00 2001 From: Alexey Boriskin Date: Fri, 15 Feb 2013 02:25:15 +0400 Subject: [PATCH 328/870] Put unicode_literals into all formats.py --- django/conf/locale/bn/formats.py | 1 + django/conf/locale/bs/formats.py | 1 + django/conf/locale/ca/formats.py | 1 + django/conf/locale/cy/formats.py | 1 + django/conf/locale/da/formats.py | 1 + django/conf/locale/de/formats.py | 1 + django/conf/locale/el/formats.py | 1 + django/conf/locale/en/formats.py | 1 + django/conf/locale/en_GB/formats.py | 1 + django/conf/locale/es/formats.py | 1 + django/conf/locale/es_AR/formats.py | 1 + django/conf/locale/es_NI/formats.py | 1 + django/conf/locale/eu/formats.py | 1 + django/conf/locale/fy_NL/formats.py | 1 + django/conf/locale/ga/formats.py | 1 + django/conf/locale/hi/formats.py | 1 + django/conf/locale/hr/formats.py | 1 + django/conf/locale/id/formats.py | 1 + django/conf/locale/is/formats.py | 1 + django/conf/locale/it/formats.py | 1 + django/conf/locale/ka/formats.py | 1 + django/conf/locale/kn/formats.py | 1 + django/conf/locale/mk/formats.py | 1 + django/conf/locale/ml/formats.py | 1 + django/conf/locale/mn/formats.py | 1 + django/conf/locale/nl/formats.py | 1 + django/conf/locale/pt/formats.py | 1 + django/conf/locale/ro/formats.py | 1 + django/conf/locale/sl/formats.py | 1 + django/conf/locale/sq/formats.py | 1 + django/conf/locale/sr/formats.py | 1 + django/conf/locale/sr_Latn/formats.py | 1 + django/conf/locale/ta/formats.py | 1 + django/conf/locale/te/formats.py | 1 + django/conf/locale/th/formats.py | 1 + django/conf/locale/tr/formats.py | 1 + django/conf/locale/zh_CN/formats.py | 1 + django/conf/locale/zh_TW/formats.py | 1 + 38 files changed, 38 insertions(+) diff --git a/django/conf/locale/bn/formats.py b/django/conf/locale/bn/formats.py index a99bea1533..eed24e605e 100644 --- a/django/conf/locale/bn/formats.py +++ b/django/conf/locale/bn/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/bs/formats.py b/django/conf/locale/bs/formats.py index 34e9eac197..454fab1e1e 100644 --- a/django/conf/locale/bs/formats.py +++ b/django/conf/locale/bs/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/ca/formats.py b/django/conf/locale/ca/formats.py index b9431b5b67..f870ca78aa 100644 --- a/django/conf/locale/ca/formats.py +++ b/django/conf/locale/ca/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/cy/formats.py b/django/conf/locale/cy/formats.py index 6e0e7027cd..ba4b275938 100644 --- a/django/conf/locale/cy/formats.py +++ b/django/conf/locale/cy/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/da/formats.py b/django/conf/locale/da/formats.py index 9022eb2ed4..f5ff44611e 100644 --- a/django/conf/locale/da/formats.py +++ b/django/conf/locale/da/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/de/formats.py b/django/conf/locale/de/formats.py index a75b806acb..ac35e64e35 100644 --- a/django/conf/locale/de/formats.py +++ b/django/conf/locale/de/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/el/formats.py b/django/conf/locale/el/formats.py index 9226490a38..a44b909b2e 100644 --- a/django/conf/locale/el/formats.py +++ b/django/conf/locale/el/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/en/formats.py b/django/conf/locale/en/formats.py index 6bd693e60e..041066e38b 100644 --- a/django/conf/locale/en/formats.py +++ b/django/conf/locale/en/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/en_GB/formats.py b/django/conf/locale/en_GB/formats.py index b594aafb74..a8c463b8a1 100644 --- a/django/conf/locale/en_GB/formats.py +++ b/django/conf/locale/en_GB/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/es/formats.py b/django/conf/locale/es/formats.py index 6241158338..541878306d 100644 --- a/django/conf/locale/es/formats.py +++ b/django/conf/locale/es/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/es_AR/formats.py b/django/conf/locale/es_AR/formats.py index 651690bfdf..b3e215fe33 100644 --- a/django/conf/locale/es_AR/formats.py +++ b/django/conf/locale/es_AR/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/es_NI/formats.py b/django/conf/locale/es_NI/formats.py index a22432a16a..34ced6b8bd 100644 --- a/django/conf/locale/es_NI/formats.py +++ b/django/conf/locale/es_NI/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals DATE_FORMAT = r'j \d\e F \d\e Y' TIME_FORMAT = 'H:i:s' diff --git a/django/conf/locale/eu/formats.py b/django/conf/locale/eu/formats.py index 5e045b2ae6..5b768eabca 100644 --- a/django/conf/locale/eu/formats.py +++ b/django/conf/locale/eu/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/fy_NL/formats.py b/django/conf/locale/fy_NL/formats.py index bf26f4b31c..4355928ca0 100644 --- a/django/conf/locale/fy_NL/formats.py +++ b/django/conf/locale/fy_NL/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/ga/formats.py b/django/conf/locale/ga/formats.py index 50c68a9c11..ce271ab533 100644 --- a/django/conf/locale/ga/formats.py +++ b/django/conf/locale/ga/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/hi/formats.py b/django/conf/locale/hi/formats.py index 95db623bd9..f4fabe00cd 100644 --- a/django/conf/locale/hi/formats.py +++ b/django/conf/locale/hi/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/hr/formats.py b/django/conf/locale/hr/formats.py index 9f4c74051e..a27b6e0613 100644 --- a/django/conf/locale/hr/formats.py +++ b/django/conf/locale/hr/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/id/formats.py b/django/conf/locale/id/formats.py index d609762117..4d564bac88 100644 --- a/django/conf/locale/id/formats.py +++ b/django/conf/locale/id/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/is/formats.py b/django/conf/locale/is/formats.py index e7f3418fb1..90f251abe7 100644 --- a/django/conf/locale/is/formats.py +++ b/django/conf/locale/is/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/it/formats.py b/django/conf/locale/it/formats.py index de81fa6cdf..a1814445a2 100644 --- a/django/conf/locale/it/formats.py +++ b/django/conf/locale/it/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/ka/formats.py b/django/conf/locale/ka/formats.py index c3552e0661..65916c707d 100644 --- a/django/conf/locale/ka/formats.py +++ b/django/conf/locale/ka/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/kn/formats.py b/django/conf/locale/kn/formats.py index 2af6f83a3f..ce1999ac93 100644 --- a/django/conf/locale/kn/formats.py +++ b/django/conf/locale/kn/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/mk/formats.py b/django/conf/locale/mk/formats.py index 44feb512bf..7e3e9adffa 100644 --- a/django/conf/locale/mk/formats.py +++ b/django/conf/locale/mk/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/ml/formats.py b/django/conf/locale/ml/formats.py index 6bd693e60e..041066e38b 100644 --- a/django/conf/locale/ml/formats.py +++ b/django/conf/locale/ml/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/mn/formats.py b/django/conf/locale/mn/formats.py index 54a46a903f..50ab9f101c 100644 --- a/django/conf/locale/mn/formats.py +++ b/django/conf/locale/mn/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/nl/formats.py b/django/conf/locale/nl/formats.py index be5a146104..af18379f56 100644 --- a/django/conf/locale/nl/formats.py +++ b/django/conf/locale/nl/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/pt/formats.py b/django/conf/locale/pt/formats.py index 2d6ca69647..fa40a22202 100644 --- a/django/conf/locale/pt/formats.py +++ b/django/conf/locale/pt/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/ro/formats.py b/django/conf/locale/ro/formats.py index 4be020f375..5435837483 100644 --- a/django/conf/locale/ro/formats.py +++ b/django/conf/locale/ro/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/sl/formats.py b/django/conf/locale/sl/formats.py index 0d6137e1ed..47b4cbe23f 100644 --- a/django/conf/locale/sl/formats.py +++ b/django/conf/locale/sl/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/sq/formats.py b/django/conf/locale/sq/formats.py index 6dd12e5800..1c04e2e5e8 100644 --- a/django/conf/locale/sq/formats.py +++ b/django/conf/locale/sq/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/sr/formats.py b/django/conf/locale/sr/formats.py index 227f20d723..db11e032c8 100644 --- a/django/conf/locale/sr/formats.py +++ b/django/conf/locale/sr/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/sr_Latn/formats.py b/django/conf/locale/sr_Latn/formats.py index 227f20d723..db11e032c8 100644 --- a/django/conf/locale/sr_Latn/formats.py +++ b/django/conf/locale/sr_Latn/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/ta/formats.py b/django/conf/locale/ta/formats.py index c03fb20183..1c08f17501 100644 --- a/django/conf/locale/ta/formats.py +++ b/django/conf/locale/ta/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/te/formats.py b/django/conf/locale/te/formats.py index 173b60bfba..02399d6c1b 100644 --- a/django/conf/locale/te/formats.py +++ b/django/conf/locale/te/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/th/formats.py b/django/conf/locale/th/formats.py index aa43108a40..7847a7c251 100644 --- a/django/conf/locale/th/formats.py +++ b/django/conf/locale/th/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/tr/formats.py b/django/conf/locale/tr/formats.py index 705b2ed659..40462ea619 100644 --- a/django/conf/locale/tr/formats.py +++ b/django/conf/locale/tr/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/zh_CN/formats.py b/django/conf/locale/zh_CN/formats.py index bf26f4b31c..4355928ca0 100644 --- a/django/conf/locale/zh_CN/formats.py +++ b/django/conf/locale/zh_CN/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date diff --git a/django/conf/locale/zh_TW/formats.py b/django/conf/locale/zh_TW/formats.py index bf26f4b31c..4355928ca0 100644 --- a/django/conf/locale/zh_TW/formats.py +++ b/django/conf/locale/zh_TW/formats.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- # This file is distributed under the same license as the Django package. # +from __future__ import unicode_literals # The *_FORMAT strings use the Django date format syntax, # see http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date From dcf8cd30aed682d42f388c3e8a03098403ef3a16 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 15 Feb 2013 09:36:07 +0100 Subject: [PATCH 329/870] Updated FormattingTests test case to use settings contexts --- tests/regressiontests/i18n/tests.py | 200 +++++++++++++--------------- 1 file changed, 92 insertions(+), 108 deletions(-) diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index 7b4fd0eff8..ba7415c053 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -337,14 +337,10 @@ class TranslationTests(TestCase): self.assertEqual(rendered, 'My other name is James.') +@override_settings(USE_L10N=True) class FormattingTests(TestCase): def setUp(self): - self.use_i18n = settings.USE_I18N - self.use_l10n = settings.USE_L10N - self.use_thousand_separator = settings.USE_THOUSAND_SEPARATOR - self.thousand_separator = settings.THOUSAND_SEPARATOR - self.number_grouping = settings.NUMBER_GROUPING self.n = decimal.Decimal('66666.666') self.f = 99999.999 self.d = datetime.date(2009, 12, 31) @@ -360,24 +356,16 @@ class FormattingTests(TestCase): 'l': self.l, }) - def tearDown(self): - # Restore defaults - settings.USE_I18N = self.use_i18n - settings.USE_L10N = self.use_l10n - settings.USE_THOUSAND_SEPARATOR = self.use_thousand_separator - settings.THOUSAND_SEPARATOR = self.thousand_separator - settings.NUMBER_GROUPING = self.number_grouping - def test_locale_independent(self): """ Localization of numbers """ - with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=False): + with self.settings(USE_THOUSAND_SEPARATOR=False): self.assertEqual('66666.66', nformat(self.n, decimal_sep='.', decimal_pos=2, grouping=3, thousand_sep=',')) self.assertEqual('66666A6', nformat(self.n, decimal_sep='A', decimal_pos=1, grouping=1, thousand_sep='B')) self.assertEqual('66666', nformat(self.n, decimal_sep='X', decimal_pos=0, grouping=1, thousand_sep='Y')) - with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True): + with self.settings(USE_THOUSAND_SEPARATOR=True): self.assertEqual('66,666.66', nformat(self.n, decimal_sep='.', decimal_pos=2, grouping=3, thousand_sep=',')) self.assertEqual('6B6B6B6B6A6', nformat(self.n, decimal_sep='A', decimal_pos=1, grouping=1, thousand_sep='B')) self.assertEqual('-66666.6', nformat(-66666.666, decimal_sep='.', decimal_pos=1)) @@ -390,12 +378,12 @@ class FormattingTests(TestCase): self.assertEqual('31.12.2009 в 20:50', Template('{{ dt|date:"d.m.Y в H:i" }}').render(self.ctxt)) self.assertEqual('⌚ 10:15', Template('{{ t|time:"⌚ H:i" }}').render(self.ctxt)) + @override_settings(USE_L10N=False) def test_l10n_disabled(self): """ Catalan locale with format i18n disabled translations will be used, but not formats """ - settings.USE_L10N = False with translation.override('ca', deactivate=True): self.assertEqual('N j, Y', get_format('DATE_FORMAT')) self.assertEqual(0, get_format('FIRST_DAY_OF_WEEK')) @@ -474,13 +462,12 @@ class FormattingTests(TestCase): fr_formats.FIRST_DAY_OF_WEEK = 0 with translation.override('fr'): - with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True, - THOUSAND_SEPARATOR='!'): + with self.settings(USE_THOUSAND_SEPARATOR=True, THOUSAND_SEPARATOR='!'): self.assertEqual('', get_format('THOUSAND_SEPARATOR')) # Even a second time (after the format has been cached)... self.assertEqual('', get_format('THOUSAND_SEPARATOR')) - with self.settings(USE_L10N=True, FIRST_DAY_OF_WEEK=1): + with self.settings(FIRST_DAY_OF_WEEK=1): self.assertEqual(0, get_format('FIRST_DAY_OF_WEEK')) # Even a second time (after the format has been cached)... self.assertEqual(0, get_format('FIRST_DAY_OF_WEEK')) @@ -490,7 +477,6 @@ class FormattingTests(TestCase): fr_formats.FIRST_DAY_OF_WEEK = backup_FIRST_DAY_OF_WEEK def test_l10n_enabled(self): - settings.USE_L10N = True # Catalan locale with translation.override('ca', deactivate=True): self.assertEqual('j \d\e F \d\e Y', get_format('DATE_FORMAT')) @@ -502,67 +488,70 @@ class FormattingTests(TestCase): self.assertEqual('31/12/2009 20:50', date_format(self.dt, 'SHORT_DATETIME_FORMAT')) self.assertEqual('No localizable', localize('No localizable')) - settings.USE_THOUSAND_SEPARATOR = True - self.assertEqual('66.666,666', localize(self.n)) - self.assertEqual('99.999,999', localize(self.f)) - self.assertEqual('10.000', localize(self.l)) - self.assertEqual('True', localize(True)) + with self.settings(USE_THOUSAND_SEPARATOR=True): + self.assertEqual('66.666,666', localize(self.n)) + self.assertEqual('99.999,999', localize(self.f)) + self.assertEqual('10.000', localize(self.l)) + self.assertEqual('True', localize(True)) - settings.USE_THOUSAND_SEPARATOR = False - self.assertEqual('66666,666', localize(self.n)) - self.assertEqual('99999,999', localize(self.f)) - self.assertEqual('10000', localize(self.l)) - self.assertEqual('31 de desembre de 2009', localize(self.d)) - self.assertEqual('31 de desembre de 2009 a les 20:50', localize(self.dt)) + with self.settings(USE_THOUSAND_SEPARATOR=False): + self.assertEqual('66666,666', localize(self.n)) + self.assertEqual('99999,999', localize(self.f)) + self.assertEqual('10000', localize(self.l)) + self.assertEqual('31 de desembre de 2009', localize(self.d)) + self.assertEqual('31 de desembre de 2009 a les 20:50', localize(self.dt)) - settings.USE_THOUSAND_SEPARATOR = True - self.assertEqual('66.666,666', Template('{{ n }}').render(self.ctxt)) - self.assertEqual('99.999,999', Template('{{ f }}').render(self.ctxt)) - self.assertEqual('10.000', Template('{{ l }}').render(self.ctxt)) + with self.settings(USE_THOUSAND_SEPARATOR=True): + self.assertEqual('66.666,666', Template('{{ n }}').render(self.ctxt)) + self.assertEqual('99.999,999', Template('{{ f }}').render(self.ctxt)) + self.assertEqual('10.000', Template('{{ l }}').render(self.ctxt)) - form3 = I18nForm({ - 'decimal_field': '66.666,666', - 'float_field': '99.999,999', - 'date_field': '31/12/2009', - 'datetime_field': '31/12/2009 20:50', - 'time_field': '20:50', - 'integer_field': '1.234', - }) - self.assertEqual(True, form3.is_valid()) - self.assertEqual(decimal.Decimal('66666.666'), form3.cleaned_data['decimal_field']) - self.assertEqual(99999.999, form3.cleaned_data['float_field']) - self.assertEqual(datetime.date(2009, 12, 31), form3.cleaned_data['date_field']) - self.assertEqual(datetime.datetime(2009, 12, 31, 20, 50), form3.cleaned_data['datetime_field']) - self.assertEqual(datetime.time(20, 50), form3.cleaned_data['time_field']) - self.assertEqual(1234, form3.cleaned_data['integer_field']) + with self.settings(USE_THOUSAND_SEPARATOR=True): + form3 = I18nForm({ + 'decimal_field': '66.666,666', + 'float_field': '99.999,999', + 'date_field': '31/12/2009', + 'datetime_field': '31/12/2009 20:50', + 'time_field': '20:50', + 'integer_field': '1.234', + }) + self.assertEqual(True, form3.is_valid()) + self.assertEqual(decimal.Decimal('66666.666'), form3.cleaned_data['decimal_field']) + self.assertEqual(99999.999, form3.cleaned_data['float_field']) + self.assertEqual(datetime.date(2009, 12, 31), form3.cleaned_data['date_field']) + self.assertEqual(datetime.datetime(2009, 12, 31, 20, 50), form3.cleaned_data['datetime_field']) + self.assertEqual(datetime.time(20, 50), form3.cleaned_data['time_field']) + self.assertEqual(1234, form3.cleaned_data['integer_field']) - settings.USE_THOUSAND_SEPARATOR = False - self.assertEqual('66666,666', Template('{{ n }}').render(self.ctxt)) - self.assertEqual('99999,999', Template('{{ f }}').render(self.ctxt)) - self.assertEqual('31 de desembre de 2009', Template('{{ d }}').render(self.ctxt)) - self.assertEqual('31 de desembre de 2009 a les 20:50', Template('{{ dt }}').render(self.ctxt)) - self.assertEqual('66666,67', Template('{{ n|floatformat:2 }}').render(self.ctxt)) - self.assertEqual('100000,0', Template('{{ f|floatformat }}').render(self.ctxt)) - self.assertEqual('10:15:48', Template('{{ t|time:"TIME_FORMAT" }}').render(self.ctxt)) - self.assertEqual('31/12/2009', Template('{{ d|date:"SHORT_DATE_FORMAT" }}').render(self.ctxt)) - self.assertEqual('31/12/2009 20:50', Template('{{ dt|date:"SHORT_DATETIME_FORMAT" }}').render(self.ctxt)) - self.assertEqual(date_format(datetime.datetime.now(), "DATE_FORMAT"), Template('{% now "DATE_FORMAT" %}').render(self.ctxt)) + with self.settings(USE_THOUSAND_SEPARATOR=False): + self.assertEqual('66666,666', Template('{{ n }}').render(self.ctxt)) + self.assertEqual('99999,999', Template('{{ f }}').render(self.ctxt)) + self.assertEqual('31 de desembre de 2009', Template('{{ d }}').render(self.ctxt)) + self.assertEqual('31 de desembre de 2009 a les 20:50', Template('{{ dt }}').render(self.ctxt)) + self.assertEqual('66666,67', Template('{{ n|floatformat:2 }}').render(self.ctxt)) + self.assertEqual('100000,0', Template('{{ f|floatformat }}').render(self.ctxt)) + self.assertEqual('10:15:48', Template('{{ t|time:"TIME_FORMAT" }}').render(self.ctxt)) + self.assertEqual('31/12/2009', Template('{{ d|date:"SHORT_DATE_FORMAT" }}').render(self.ctxt)) + self.assertEqual('31/12/2009 20:50', Template('{{ dt|date:"SHORT_DATETIME_FORMAT" }}').render(self.ctxt)) + self.assertEqual(date_format(datetime.datetime.now(), "DATE_FORMAT"), + Template('{% now "DATE_FORMAT" %}').render(self.ctxt)) - form4 = I18nForm({ - 'decimal_field': '66666,666', - 'float_field': '99999,999', - 'date_field': '31/12/2009', - 'datetime_field': '31/12/2009 20:50', - 'time_field': '20:50', - 'integer_field': '1234', - }) - self.assertEqual(True, form4.is_valid()) - self.assertEqual(decimal.Decimal('66666.666'), form4.cleaned_data['decimal_field']) - self.assertEqual(99999.999, form4.cleaned_data['float_field']) - self.assertEqual(datetime.date(2009, 12, 31), form4.cleaned_data['date_field']) - self.assertEqual(datetime.datetime(2009, 12, 31, 20, 50), form4.cleaned_data['datetime_field']) - self.assertEqual(datetime.time(20, 50), form4.cleaned_data['time_field']) - self.assertEqual(1234, form4.cleaned_data['integer_field']) + with self.settings(USE_THOUSAND_SEPARATOR=False): + form4 = I18nForm({ + 'decimal_field': '66666,666', + 'float_field': '99999,999', + 'date_field': '31/12/2009', + 'datetime_field': '31/12/2009 20:50', + 'time_field': '20:50', + 'integer_field': '1234', + }) + self.assertEqual(True, form4.is_valid()) + self.assertEqual(decimal.Decimal('66666.666'), form4.cleaned_data['decimal_field']) + self.assertEqual(99999.999, form4.cleaned_data['float_field']) + self.assertEqual(datetime.date(2009, 12, 31), form4.cleaned_data['date_field']) + self.assertEqual(datetime.datetime(2009, 12, 31, 20, 50), form4.cleaned_data['datetime_field']) + self.assertEqual(datetime.time(20, 50), form4.cleaned_data['time_field']) + self.assertEqual(1234, form4.cleaned_data['integer_field']) form5 = SelectDateForm({ 'date_field_month': '12', @@ -593,32 +582,32 @@ class FormattingTests(TestCase): self.assertEqual('12/31/2009 8:50 p.m.', date_format(self.dt, 'SHORT_DATETIME_FORMAT')) self.assertEqual('No localizable', localize('No localizable')) - settings.USE_THOUSAND_SEPARATOR = True - self.assertEqual('66,666.666', localize(self.n)) - self.assertEqual('99,999.999', localize(self.f)) - self.assertEqual('10,000', localize(self.l)) + with self.settings(USE_THOUSAND_SEPARATOR=True): + self.assertEqual('66,666.666', localize(self.n)) + self.assertEqual('99,999.999', localize(self.f)) + self.assertEqual('10,000', localize(self.l)) - settings.USE_THOUSAND_SEPARATOR = False - self.assertEqual('66666.666', localize(self.n)) - self.assertEqual('99999.999', localize(self.f)) - self.assertEqual('10000', localize(self.l)) - self.assertEqual('Dec. 31, 2009', localize(self.d)) - self.assertEqual('Dec. 31, 2009, 8:50 p.m.', localize(self.dt)) + with self.settings(USE_THOUSAND_SEPARATOR=False): + self.assertEqual('66666.666', localize(self.n)) + self.assertEqual('99999.999', localize(self.f)) + self.assertEqual('10000', localize(self.l)) + self.assertEqual('Dec. 31, 2009', localize(self.d)) + self.assertEqual('Dec. 31, 2009, 8:50 p.m.', localize(self.dt)) - settings.USE_THOUSAND_SEPARATOR = True - self.assertEqual('66,666.666', Template('{{ n }}').render(self.ctxt)) - self.assertEqual('99,999.999', Template('{{ f }}').render(self.ctxt)) - self.assertEqual('10,000', Template('{{ l }}').render(self.ctxt)) + with self.settings(USE_THOUSAND_SEPARATOR=True): + self.assertEqual('66,666.666', Template('{{ n }}').render(self.ctxt)) + self.assertEqual('99,999.999', Template('{{ f }}').render(self.ctxt)) + self.assertEqual('10,000', Template('{{ l }}').render(self.ctxt)) - settings.USE_THOUSAND_SEPARATOR = False - self.assertEqual('66666.666', Template('{{ n }}').render(self.ctxt)) - self.assertEqual('99999.999', Template('{{ f }}').render(self.ctxt)) - self.assertEqual('Dec. 31, 2009', Template('{{ d }}').render(self.ctxt)) - self.assertEqual('Dec. 31, 2009, 8:50 p.m.', Template('{{ dt }}').render(self.ctxt)) - self.assertEqual('66666.67', Template('{{ n|floatformat:2 }}').render(self.ctxt)) - self.assertEqual('100000.0', Template('{{ f|floatformat }}').render(self.ctxt)) - self.assertEqual('12/31/2009', Template('{{ d|date:"SHORT_DATE_FORMAT" }}').render(self.ctxt)) - self.assertEqual('12/31/2009 8:50 p.m.', Template('{{ dt|date:"SHORT_DATETIME_FORMAT" }}').render(self.ctxt)) + with self.settings(USE_THOUSAND_SEPARATOR=False): + self.assertEqual('66666.666', Template('{{ n }}').render(self.ctxt)) + self.assertEqual('99999.999', Template('{{ f }}').render(self.ctxt)) + self.assertEqual('Dec. 31, 2009', Template('{{ d }}').render(self.ctxt)) + self.assertEqual('Dec. 31, 2009, 8:50 p.m.', Template('{{ dt }}').render(self.ctxt)) + self.assertEqual('66666.67', Template('{{ n|floatformat:2 }}').render(self.ctxt)) + self.assertEqual('100000.0', Template('{{ f|floatformat }}').render(self.ctxt)) + self.assertEqual('12/31/2009', Template('{{ d|date:"SHORT_DATE_FORMAT" }}').render(self.ctxt)) + self.assertEqual('12/31/2009 8:50 p.m.', Template('{{ dt|date:"SHORT_DATETIME_FORMAT" }}').render(self.ctxt)) form5 = I18nForm({ 'decimal_field': '66666.666', @@ -652,7 +641,7 @@ class FormattingTests(TestCase): """ Check if sublocales fall back to the main locale """ - with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True): + with self.settings(USE_THOUSAND_SEPARATOR=True): with translation.override('de-at', deactivate=True): self.assertEqual('66.666,666', Template('{{ n }}').render(self.ctxt)) with translation.override('es-us', deactivate=True): @@ -662,7 +651,6 @@ class FormattingTests(TestCase): """ Tests if form input is correctly localized """ - settings.USE_L10N = True with translation.override('de-at', deactivate=True): form6 = CompanyForm({ 'name': 'acme', @@ -685,7 +673,6 @@ class FormattingTests(TestCase): """ Tests the iter_format_modules function. """ - settings.USE_L10N = True with translation.override('de-at', deactivate=True): de_format_mod = import_module('django.conf.locale.de.formats') self.assertEqual(list(iter_format_modules('de')), [de_format_mod]) @@ -698,19 +685,16 @@ class FormattingTests(TestCase): Tests the iter_format_modules function always yields format modules in a stable and correct order in presence of both base ll and ll_CC formats. """ - settings.USE_L10N = True en_format_mod = import_module('django.conf.locale.en.formats') en_gb_format_mod = import_module('django.conf.locale.en_GB.formats') self.assertEqual(list(iter_format_modules('en-gb')), [en_gb_format_mod, en_format_mod]) def test_get_format_modules_lang(self): - with self.settings(USE_L10N=True): - with translation.override('de', deactivate=True): - self.assertEqual('.', get_format('DECIMAL_SEPARATOR', lang='en')) + with translation.override('de', deactivate=True): + self.assertEqual('.', get_format('DECIMAL_SEPARATOR', lang='en')) def test_get_format_modules_stability(self): - with self.settings(USE_L10N=True, - FORMAT_MODULE_PATH='regressiontests.i18n.other.locale'): + with self.settings(FORMAT_MODULE_PATH='regressiontests.i18n.other.locale'): with translation.override('de', deactivate=True): old = str("%r") % get_format_modules(reverse=True) new = str("%r") % get_format_modules(reverse=True) # second try From 02e5909f7ae436dab8e4d13370670c467163b8aa Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 15 Feb 2013 15:44:27 +0100 Subject: [PATCH 330/870] Fixed #19807 -- Sanitized getpass input in createsuperuser Python 2 getpass on Windows doesn't accept unicode, even when containing only ascii chars. Thanks Semmel for the report and tests. --- .../contrib/auth/management/commands/createsuperuser.py | 2 +- django/contrib/auth/tests/basic.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index f462d95749..ac2835d6b3 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -122,7 +122,7 @@ class Command(BaseCommand): while password is None: if not password: password = getpass.getpass() - password2 = getpass.getpass('Password (again): ') + password2 = getpass.getpass(force_str('Password (again): ')) if password != password2: self.stderr.write("Error: Your passwords didn't match.") password = None diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py index 03af1fd7bb..3c7bcffc7d 100644 --- a/django/contrib/auth/tests/basic.py +++ b/django/contrib/auth/tests/basic.py @@ -13,7 +13,7 @@ from django.core.management import call_command from django.test import TestCase from django.test.utils import override_settings from django.utils.encoding import force_str -from django.utils.six import StringIO +from django.utils.six import binary_type, StringIO def mock_inputs(inputs): @@ -24,8 +24,11 @@ def mock_inputs(inputs): def inner(test_func): def wrapped(*args): class mock_getpass: - pass - mock_getpass.getpass = staticmethod(lambda p=None: inputs['password']) + @staticmethod + def getpass(prompt=b'Password: ', stream=None): + # getpass on Windows only supports prompt as bytestring (#19807) + assert isinstance(prompt, binary_type) + return inputs['password'] def mock_input(prompt): # prompt should be encoded in Python 2. This line will raise an From a8d1421dd9d0ada64b9d8a3e96ed0b431c66ac97 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 15 Feb 2013 16:09:31 +0100 Subject: [PATCH 331/870] Avoided unneeded assertion on Python 3 Fixes failure introduced in 02e5909f7a. --- django/contrib/auth/tests/basic.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py index 3c7bcffc7d..097bc90451 100644 --- a/django/contrib/auth/tests/basic.py +++ b/django/contrib/auth/tests/basic.py @@ -13,7 +13,7 @@ from django.core.management import call_command from django.test import TestCase from django.test.utils import override_settings from django.utils.encoding import force_str -from django.utils.six import binary_type, StringIO +from django.utils.six import binary_type, PY3, StringIO def mock_inputs(inputs): @@ -26,8 +26,9 @@ def mock_inputs(inputs): class mock_getpass: @staticmethod def getpass(prompt=b'Password: ', stream=None): - # getpass on Windows only supports prompt as bytestring (#19807) - assert isinstance(prompt, binary_type) + if not PY3: + # getpass on Windows only supports prompt as bytestring (#19807) + assert isinstance(prompt, binary_type) return inputs['password'] def mock_input(prompt): From b19d83fc12ebbabbaa9d72286f2e4bfa071ff784 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 15 Feb 2013 16:37:52 +0100 Subject: [PATCH 332/870] Improved input sanitizing with thousand separators For languages with non-breaking space as thousand separator, standard space input should also be allowed, as few people know how to enter non-breaking space on keyboards. Refs #17217. Thanks Alexey Boriskin for the report and initial patch. --- django/utils/formats.py | 24 +++++++++++++----------- tests/regressiontests/i18n/tests.py | 20 +++++++++++++++++++- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/django/utils/formats.py b/django/utils/formats.py index 03b9918edf..f0abdc2f7b 100644 --- a/django/utils/formats.py +++ b/django/utils/formats.py @@ -1,5 +1,6 @@ import decimal import datetime +import unicodedata from django.conf import settings from django.utils import dateformat, numberformat, datetime_safe @@ -192,16 +193,17 @@ def sanitize_separators(value): Sanitizes a value according to the current decimal and thousand separator setting. Used with form field input. """ - if settings.USE_L10N: + if settings.USE_L10N and isinstance(value, six.string_types): + parts = [] decimal_separator = get_format('DECIMAL_SEPARATOR') - if isinstance(value, six.string_types): - parts = [] - if decimal_separator in value: - value, decimals = value.split(decimal_separator, 1) - parts.append(decimals) - if settings.USE_THOUSAND_SEPARATOR: - parts.append(value.replace(get_format('THOUSAND_SEPARATOR'), '')) - else: - parts.append(value) - value = '.'.join(reversed(parts)) + if decimal_separator in value: + value, decimals = value.split(decimal_separator, 1) + parts.append(decimals) + if settings.USE_THOUSAND_SEPARATOR: + thousand_sep = get_format('THOUSAND_SEPARATOR') + for replacement in set([ + thousand_sep, unicodedata.normalize('NFKD', thousand_sep)]): + value = value.replace(replacement, '') + parts.append(value) + value = '.'.join(reversed(parts)) return value diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index ba7415c053..45d49d5766 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -15,7 +15,7 @@ from django.test.utils import override_settings from django.utils import translation from django.utils.formats import (get_format, date_format, time_format, localize, localize_input, iter_format_modules, get_format_modules, - number_format) + number_format, sanitize_separators) from django.utils.importlib import import_module from django.utils.numberformat import format as nformat from django.utils._os import upath @@ -669,6 +669,24 @@ class FormattingTests(TestCase): # Checking for the localized "products_delivered" field self.assertInHTML('', form6.as_ul()) + def test_sanitize_separators(self): + """ + Tests django.utils.formats.sanitize_separators. + """ + # Non-strings are untouched + self.assertEqual(sanitize_separators(123), 123) + + with translation.override('ru', deactivate=True): + # Russian locale has non-breaking space (\xa0) as thousand separator + # Check that usual space is accepted too when sanitizing inputs + with self.settings(USE_THOUSAND_SEPARATOR=True): + self.assertEqual(sanitize_separators('1\xa0234\xa0567'), '1234567') + self.assertEqual(sanitize_separators('77\xa0777,777'), '77777.777') + self.assertEqual(sanitize_separators('12 345'), '12345') + self.assertEqual(sanitize_separators('77 777,777'), '77777.777') + with self.settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=False): + self.assertEqual(sanitize_separators('12\xa0345'), '12\xa0345') + def test_iter_format_modules(self): """ Tests the iter_format_modules function. From 35185495e3f70f900b542bf95d744f51e5c5cb92 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 15 Feb 2013 17:12:14 +0100 Subject: [PATCH 333/870] Fixed #17066 -- Prevented TypeError in GeoIP.__del__ When garbaging GeoIP instances, it happens that GeoIP_delete is already None. Thanks mitar for the report and stefanw for tests. --- django/contrib/gis/geoip/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/django/contrib/gis/geoip/base.py b/django/contrib/gis/geoip/base.py index 944240c811..4b8b6e1ed3 100644 --- a/django/contrib/gis/geoip/base.py +++ b/django/contrib/gis/geoip/base.py @@ -125,6 +125,8 @@ class GeoIP(object): def __del__(self): # Cleaning any GeoIP file handles lying around. + if GeoIP_delete is None: + return if self._country: GeoIP_delete(self._country) if self._city: GeoIP_delete(self._city) From 87854b0bdf354059f949350a4d63a0ed071d564c Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 15 Feb 2013 20:11:46 +0100 Subject: [PATCH 334/870] Fixed geos test to prevent random failure Points in the test fixtures have 20 as max coordinate. --- django/contrib/gis/geos/tests/test_geos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index ec320f94ec..c15833fb5b 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -688,7 +688,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): for i in range(len(mp)): # Creating a random point. pnt = mp[i] - new = Point(random.randint(1, 100), random.randint(1, 100)) + new = Point(random.randint(21, 100), random.randint(21, 100)) # Testing the assignment mp[i] = new s = str(new) # what was used for the assignment is still accessible From 91c26eadc9b4efa5399ec0f6c84b56a3f8eb84f4 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 16 Feb 2013 10:21:05 +0800 Subject: [PATCH 335/870] Refs #14881 -- Document that User models need to have an integer primary key. Thanks to Kaloian Minkov for the reminder about this undocumented requirement. --- docs/topics/auth/customizing.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index d1ce6eb7dc..9c31445455 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -466,11 +466,13 @@ Specifying a custom User model Django expects your custom User model to meet some minimum requirements. -1. Your model must have a single unique field that can be used for +1. Your model must have an integer primary key. + +2. Your model must have a single unique field that can be used for identification purposes. This can be a username, an email address, or any other unique attribute. -2. Your model must provide a way to address the user in a "short" and +3. Your model must provide a way to address the user in a "short" and "long" form. The most common interpretation of this would be to use the user's given name as the "short" identifier, and the user's full name as the "long" identifier. However, there are no constraints on From e74e207cce54802f897adcb42149440ee154821e Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 10 Feb 2013 16:15:49 +0100 Subject: [PATCH 336/870] Fixed #17260 -- Added time zone aware aggregation and lookups. Thanks Carl Meyer for the review. Squashed commit of the following: commit 4f290bdb60b7d8534abf4ca901bd0844612dcbda Author: Aymeric Augustin Date: Wed Feb 13 21:21:30 2013 +0100 Used '0:00' instead of 'UTC' which doesn't always exist in Oracle. Thanks Ian Kelly for the suggestion. commit 01b6366f3ce67d57a58ca8f25e5be77911748638 Author: Aymeric Augustin Date: Wed Feb 13 13:38:43 2013 +0100 Made tzname a parameter of datetime_extract/trunc_sql. This is required to work around a bug in Oracle. commit 924a144ef8a80ba4daeeafbe9efaa826566e9d02 Author: Aymeric Augustin Date: Wed Feb 13 14:47:44 2013 +0100 Added support for parameters in SELECT clauses. commit b4351d2890cd1090d3ff2d203fe148937324c935 Author: Aymeric Augustin Date: Mon Feb 11 22:30:22 2013 +0100 Documented backwards incompatibilities in the two previous commits. commit 91ef84713c81bd455f559dacf790e586d08cacb9 Author: Aymeric Augustin Date: Mon Feb 11 09:42:31 2013 +0100 Used QuerySet.datetimes for the admin's date_hierarchy. commit 0d0de288a5210fa106cd4350961eb2006535cc5c Author: Aymeric Augustin Date: Mon Feb 11 09:29:38 2013 +0100 Used QuerySet.datetimes in date-based generic views. commit 9c0859ff7c0b00734afe7fc15609d43d83215072 Author: Aymeric Augustin Date: Sun Feb 10 21:43:25 2013 +0100 Implemented QuerySet.datetimes on Oracle. commit 68ab511a4ffbd2b811bf5da174d47e4dd90f28fc Author: Aymeric Augustin Date: Sun Feb 10 21:43:14 2013 +0100 Implemented QuerySet.datetimes on MySQL. commit 22d52681d347a8cdf568dc31ed032cbc61d049ef Author: Aymeric Augustin Date: Sun Feb 10 21:42:29 2013 +0100 Implemented QuerySet.datetimes on SQLite. commit f6800fd04c93722b45f9236976389e0b2fe436f5 Author: Aymeric Augustin Date: Sun Feb 10 21:43:03 2013 +0100 Implemented QuerySet.datetimes on PostgreSQL. commit 0c829c23f4cf4d6804cadcc93032dd4c26b8c65e Author: Aymeric Augustin Date: Sun Feb 10 21:41:08 2013 +0100 Added datetime-handling infrastructure in the ORM layers. commit 104d82a7778cf3f0f5d03dfa53709c26df45daad Author: Aymeric Augustin Date: Mon Feb 11 10:05:55 2013 +0100 Updated null_queries tests to avoid clashing with the __second lookup. commit c01bbb32358201b3ac8cb4291ef87b7612a2b8e6 Author: Aymeric Augustin Date: Sun Feb 10 23:07:41 2013 +0100 Updated tests of .dates(). Replaced .dates() by .datetimes() for DateTimeFields. Replaced dates with datetimes in the expected output for DateFields. commit 50fb7a52462fecf0127b38e7f3df322aeb287c43 Author: Aymeric Augustin Date: Sun Feb 10 21:40:09 2013 +0100 Updated and added tests for QuerySet.datetimes. commit a8451a5004c437190e264667b1e6fb8acc3c1eeb Author: Aymeric Augustin Date: Sun Feb 10 22:34:46 2013 +0100 Documented the new time lookups and updated the date lookups. commit 29413eab2bd1d5e004598900c0dadc0521bbf4d3 Author: Aymeric Augustin Date: Sun Feb 10 16:15:49 2013 +0100 Documented QuerySet.datetimes and updated QuerySet.dates. --- .../contrib/admin/templatetags/admin_list.py | 14 +- .../contrib/gis/db/backends/mysql/compiler.py | 3 + .../gis/db/backends/mysql/operations.py | 7 +- .../gis/db/backends/oracle/compiler.py | 3 + .../gis/db/backends/oracle/operations.py | 4 +- .../gis/db/backends/postgis/operations.py | 2 +- .../gis/db/backends/spatialite/operations.py | 2 +- django/contrib/gis/db/backends/util.py | 2 +- .../contrib/gis/db/models/sql/aggregates.py | 12 +- django/contrib/gis/db/models/sql/compiler.py | 63 +++++-- django/contrib/gis/db/models/sql/where.py | 5 +- .../contrib/gis/tests/geoapp/test_regress.py | 2 +- django/db/backends/__init__.py | 66 +++++--- django/db/backends/mysql/base.py | 47 +++++- django/db/backends/mysql/compiler.py | 3 + django/db/backends/oracle/base.py | 62 ++++++- django/db/backends/oracle/compiler.py | 3 + .../postgresql_psycopg2/operations.py | 25 +++ django/db/backends/sqlite3/base.py | 80 +++++++-- django/db/models/fields/__init__.py | 23 +-- django/db/models/manager.py | 3 + django/db/models/query.py | 51 +++++- django/db/models/query_utils.py | 2 +- django/db/models/sql/aggregates.py | 11 +- django/db/models/sql/compiler.py | 101 +++++++---- django/db/models/sql/constants.py | 3 +- django/db/models/sql/datastructures.py | 23 ++- django/db/models/sql/expressions.py | 4 +- django/db/models/sql/subqueries.py | 48 +++++- django/db/models/sql/where.py | 34 ++-- django/views/generic/dates.py | 9 +- docs/ref/models/querysets.txt | 157 +++++++++++++++--- docs/releases/1.6.txt | 35 ++++ tests/modeltests/aggregation/tests.py | 8 +- tests/modeltests/basic/tests.py | 18 +- tests/modeltests/many_to_one/tests.py | 56 +++---- tests/modeltests/reserved_names/tests.py | 4 +- tests/modeltests/timezones/tests.py | 128 ++++++++++---- .../aggregation_regress/tests.py | 4 +- tests/regressiontests/backends/tests.py | 6 +- tests/regressiontests/dates/models.py | 2 + tests/regressiontests/dates/tests.py | 38 ++--- tests/regressiontests/datetimes/__init__.py | 0 tests/regressiontests/datetimes/models.py | 28 ++++ tests/regressiontests/datetimes/tests.py | 83 +++++++++ tests/regressiontests/extra_regress/tests.py | 5 +- tests/regressiontests/generic_views/dates.py | 15 +- .../model_inheritance_regress/tests.py | 4 +- tests/regressiontests/null_queries/models.py | 3 +- tests/regressiontests/null_queries/tests.py | 6 +- tests/regressiontests/queries/tests.py | 24 +-- 51 files changed, 1041 insertions(+), 300 deletions(-) create mode 100644 tests/regressiontests/datetimes/__init__.py create mode 100644 tests/regressiontests/datetimes/models.py create mode 100644 tests/regressiontests/datetimes/tests.py diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index ce435dea81..c5bcad342b 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -292,6 +292,8 @@ def date_hierarchy(cl): """ if cl.date_hierarchy: field_name = cl.date_hierarchy + field = cl.opts.get_field_by_name(field_name)[0] + dates_or_datetimes = 'datetimes' if isinstance(field, models.DateTimeField) else 'dates' year_field = '%s__year' % field_name month_field = '%s__month' % field_name day_field = '%s__day' % field_name @@ -323,7 +325,8 @@ def date_hierarchy(cl): 'choices': [{'title': capfirst(formats.date_format(day, 'MONTH_DAY_FORMAT'))}] } elif year_lookup and month_lookup: - days = cl.query_set.filter(**{year_field: year_lookup, month_field: month_lookup}).dates(field_name, 'day') + days = cl.query_set.filter(**{year_field: year_lookup, month_field: month_lookup}) + days = getattr(days, dates_or_datetimes)(field_name, 'day') return { 'show': True, 'back': { @@ -336,11 +339,12 @@ def date_hierarchy(cl): } for day in days] } elif year_lookup: - months = cl.query_set.filter(**{year_field: year_lookup}).dates(field_name, 'month') + months = cl.query_set.filter(**{year_field: year_lookup}) + months = getattr(months, dates_or_datetimes)(field_name, 'month') return { - 'show' : True, + 'show': True, 'back': { - 'link' : link({}), + 'link': link({}), 'title': _('All dates') }, 'choices': [{ @@ -349,7 +353,7 @@ def date_hierarchy(cl): } for month in months] } else: - years = cl.query_set.dates(field_name, 'year') + years = getattr(cl.query_set, dates_or_datetimes)(field_name, 'year') return { 'show': True, 'choices': [{ diff --git a/django/contrib/gis/db/backends/mysql/compiler.py b/django/contrib/gis/db/backends/mysql/compiler.py index 7079db9f6a..f4654eff84 100644 --- a/django/contrib/gis/db/backends/mysql/compiler.py +++ b/django/contrib/gis/db/backends/mysql/compiler.py @@ -30,3 +30,6 @@ class SQLAggregateCompiler(compiler.SQLAggregateCompiler, GeoSQLCompiler): class SQLDateCompiler(compiler.SQLDateCompiler, GeoSQLCompiler): pass + +class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, GeoSQLCompiler): + pass diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index fa20ca07f4..14402ec0a3 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -56,12 +56,13 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations): lookup_info = self.geometry_functions.get(lookup_type, False) if lookup_info: - return "%s(%s, %s)" % (lookup_info, geo_col, - self.get_geom_placeholder(value, field.srid)) + sql = "%s(%s, %s)" % (lookup_info, geo_col, + self.get_geom_placeholder(value, field.srid)) + return sql, [] # TODO: Is this really necessary? MySQL can't handle NULL geometries # in its spatial indexes anyways. if lookup_type == 'isnull': - return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) + return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), [] raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) diff --git a/django/contrib/gis/db/backends/oracle/compiler.py b/django/contrib/gis/db/backends/oracle/compiler.py index 98da0163ba..d00af7fa71 100644 --- a/django/contrib/gis/db/backends/oracle/compiler.py +++ b/django/contrib/gis/db/backends/oracle/compiler.py @@ -20,3 +20,6 @@ class SQLAggregateCompiler(compiler.SQLAggregateCompiler, GeoSQLCompiler): class SQLDateCompiler(compiler.SQLDateCompiler, GeoSQLCompiler): pass + +class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, GeoSQLCompiler): + pass diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index 4e42b4cf00..18697ac8c0 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -262,7 +262,7 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations): return lookup_info.as_sql(geo_col, self.get_geom_placeholder(field, value)) elif lookup_type == 'isnull': # Handling 'isnull' lookup type - return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) + return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), [] raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) @@ -288,7 +288,7 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations): def spatial_ref_sys(self): from django.contrib.gis.db.backends.oracle.models import SpatialRefSys return SpatialRefSys - + def modify_insert_params(self, placeholders, params): """Drop out insert parameters for NULL placeholder. Needed for Oracle Spatial backend due to #10888 diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index aa23b974db..fe90343411 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -560,7 +560,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): elif lookup_type == 'isnull': # Handling 'isnull' lookup type - return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) + return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), [] raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 773ac0b57d..d2d75c1fff 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -358,7 +358,7 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): return op.as_sql(geo_col, self.get_geom_placeholder(field, geom)) elif lookup_type == 'isnull': # Handling 'isnull' lookup type - return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) + return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), [] raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) diff --git a/django/contrib/gis/db/backends/util.py b/django/contrib/gis/db/backends/util.py index 2fc9123d26..2612810659 100644 --- a/django/contrib/gis/db/backends/util.py +++ b/django/contrib/gis/db/backends/util.py @@ -16,7 +16,7 @@ class SpatialOperation(object): self.extra = kwargs def as_sql(self, geo_col, geometry='%s'): - return self.sql_template % self.params(geo_col, geometry) + return self.sql_template % self.params(geo_col, geometry), [] def params(self, geo_col, geometry): params = {'function' : self.function, diff --git a/django/contrib/gis/db/models/sql/aggregates.py b/django/contrib/gis/db/models/sql/aggregates.py index 9fcbb516d6..ae848c0894 100644 --- a/django/contrib/gis/db/models/sql/aggregates.py +++ b/django/contrib/gis/db/models/sql/aggregates.py @@ -22,13 +22,15 @@ class GeoAggregate(Aggregate): raise ValueError('Geospatial aggregates only allowed on geometry fields.') def as_sql(self, qn, connection): - "Return the aggregate, rendered as SQL." + "Return the aggregate, rendered as SQL with parameters." if connection.ops.oracle: self.extra['tolerance'] = self.tolerance + params = [] + if hasattr(self.col, 'as_sql'): - field_name = self.col.as_sql(qn, connection) + field_name, params = self.col.as_sql(qn, connection) elif isinstance(self.col, (list, tuple)): field_name = '.'.join([qn(c) for c in self.col]) else: @@ -36,13 +38,13 @@ class GeoAggregate(Aggregate): sql_template, sql_function = connection.ops.spatial_aggregate_sql(self) - params = { + substitutions = { 'function': sql_function, 'field': field_name } - params.update(self.extra) + substitutions.update(self.extra) - return sql_template % params + return sql_template % substitutions, params class Collect(GeoAggregate): pass diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py index fc53d08ffd..b488f59362 100644 --- a/django/contrib/gis/db/models/sql/compiler.py +++ b/django/contrib/gis/db/models/sql/compiler.py @@ -1,14 +1,16 @@ +import datetime try: from itertools import zip_longest except ImportError: from itertools import izip_longest as zip_longest -from django.utils.six.moves import zip - -from django.db.backends.util import truncate_name, typecast_timestamp +from django.conf import settings +from django.db.backends.util import truncate_name, typecast_date, typecast_timestamp from django.db.models.sql import compiler from django.db.models.sql.constants import MULTI from django.utils import six +from django.utils.six.moves import zip +from django.utils import timezone SQLCompiler = compiler.SQLCompiler @@ -31,6 +33,7 @@ class GeoSQLCompiler(compiler.SQLCompiler): qn2 = self.connection.ops.quote_name result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias)) for alias, col in six.iteritems(self.query.extra_select)] + params = [] aliases = set(self.query.extra_select.keys()) if with_aliases: col_aliases = aliases.copy() @@ -61,7 +64,9 @@ class GeoSQLCompiler(compiler.SQLCompiler): aliases.add(r) col_aliases.add(col[1]) else: - result.append(col.as_sql(qn, self.connection)) + col_sql, col_params = col.as_sql(qn, self.connection) + result.append(col_sql) + params.extend(col_params) if hasattr(col, 'alias'): aliases.add(col.alias) @@ -74,15 +79,13 @@ class GeoSQLCompiler(compiler.SQLCompiler): aliases.update(new_aliases) max_name_length = self.connection.ops.max_name_length() - result.extend([ - '%s%s' % ( - self.get_extra_select_format(alias) % aggregate.as_sql(qn, self.connection), - alias is not None - and ' AS %s' % qn(truncate_name(alias, max_name_length)) - or '' - ) - for alias, aggregate in self.query.aggregate_select.items() - ]) + for alias, aggregate in self.query.aggregate_select.items(): + agg_sql, agg_params = aggregate.as_sql(qn, self.connection) + if alias is None: + result.append(agg_sql) + else: + result.append('%s AS %s' % (agg_sql, qn(truncate_name(alias, max_name_length)))) + params.extend(agg_params) # This loop customized for GeoQuery. for (table, col), field in self.query.related_select_cols: @@ -98,7 +101,7 @@ class GeoSQLCompiler(compiler.SQLCompiler): col_aliases.add(col) self._select_aliases = aliases - return result + return result, params def get_default_columns(self, with_aliases=False, col_aliases=None, start_alias=None, opts=None, as_pairs=False, from_parent=None): @@ -280,5 +283,35 @@ class SQLDateCompiler(compiler.SQLDateCompiler, GeoSQLCompiler): if self.connection.ops.oracle: date = self.resolve_columns(row, fields)[offset] elif needs_string_cast: - date = typecast_timestamp(str(date)) + date = typecast_date(str(date)) + if isinstance(date, datetime.datetime): + date = date.date() yield date + +class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, GeoSQLCompiler): + """ + This is overridden for GeoDjango to properly cast date columns, since + `GeoQuery.resolve_columns` is used for spatial values. + See #14648, #16757. + """ + def results_iter(self): + if self.connection.ops.oracle: + from django.db.models.fields import DateTimeField + fields = [DateTimeField()] + else: + needs_string_cast = self.connection.features.needs_datetime_string_cast + + offset = len(self.query.extra_select) + for rows in self.execute_sql(MULTI): + for row in rows: + datetime = row[offset] + if self.connection.ops.oracle: + datetime = self.resolve_columns(row, fields)[offset] + elif needs_string_cast: + datetime = typecast_timestamp(str(datetime)) + # Datetimes are artifically returned in UTC on databases that + # don't support time zone. Restore the zone used in the query. + if settings.USE_TZ: + datetime = datetime.replace(tzinfo=None) + datetime = timezone.make_aware(datetime, self.query.tzinfo) + yield datetime diff --git a/django/contrib/gis/db/models/sql/where.py b/django/contrib/gis/db/models/sql/where.py index ec078aebed..6ef34db0a3 100644 --- a/django/contrib/gis/db/models/sql/where.py +++ b/django/contrib/gis/db/models/sql/where.py @@ -44,8 +44,9 @@ class GeoWhereNode(WhereNode): lvalue, lookup_type, value_annot, params_or_value = child if isinstance(lvalue, GeoConstraint): data, params = lvalue.process(lookup_type, params_or_value, connection) - spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field, qn) - return spatial_sql, params + spatial_sql, spatial_params = connection.ops.spatial_lookup_sql( + data, lookup_type, params_or_value, lvalue.field, qn) + return spatial_sql, spatial_params + params else: return super(GeoWhereNode, self).make_atom(child, qn, connection) diff --git a/django/contrib/gis/tests/geoapp/test_regress.py b/django/contrib/gis/tests/geoapp/test_regress.py index 15e8555741..a27b2d40f6 100644 --- a/django/contrib/gis/tests/geoapp/test_regress.py +++ b/django/contrib/gis/tests/geoapp/test_regress.py @@ -49,7 +49,7 @@ class GeoRegressionTests(TestCase): founded = datetime(1857, 5, 23) mansfield = PennsylvaniaCity.objects.create(name='Mansfield', county='Tioga', point='POINT(-77.071445 41.823881)', founded=founded) - self.assertEqual(founded, PennsylvaniaCity.objects.dates('founded', 'day')[0]) + self.assertEqual(founded, PennsylvaniaCity.objects.datetimes('founded', 'day')[0]) self.assertEqual(founded, PennsylvaniaCity.objects.aggregate(Min('founded'))['founded__min']) def test_empty_count(self): diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index bbb5a5b294..03b62f6413 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -1,3 +1,5 @@ +import datetime + from django.db.utils import DatabaseError try: @@ -14,7 +16,7 @@ from django.db.transaction import TransactionManagementError from django.utils.functional import cached_property from django.utils.importlib import import_module from django.utils import six -from django.utils.timezone import is_aware +from django.utils import timezone class BaseDatabaseWrapper(object): @@ -397,6 +399,9 @@ class BaseDatabaseFeatures(object): # Can datetimes with timezones be used? supports_timezones = True + # Does the database have a copy of the zoneinfo database? + has_zoneinfo_database = True + # When performing a GROUP BY, is an ORDER BY NULL required # to remove any ordering? requires_explicit_null_ordering_when_grouping = False @@ -523,7 +528,7 @@ class BaseDatabaseOperations(object): def date_trunc_sql(self, lookup_type, field_name): """ Given a lookup_type of 'year', 'month' or 'day', returns the SQL that - truncates the given date field field_name to a DATE object with only + truncates the given date field field_name to a date object with only the given specificity. """ raise NotImplementedError() @@ -537,6 +542,23 @@ class BaseDatabaseOperations(object): """ return "%s" + def datetime_extract_sql(self, lookup_type, field_name, tzname): + """ + Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or + 'second', returns the SQL that extracts a value from the given + datetime field field_name, and a tuple of parameters. + """ + raise NotImplementedError() + + def datetime_trunc_sql(self, lookup_type, field_name, tzname): + """ + Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or + 'second', returns the SQL that truncates the given datetime field + field_name to a datetime object with only the given specificity, and + a tuple of parameters. + """ + raise NotImplementedError() + def deferrable_sql(self): """ Returns the SQL necessary to make a constraint "initially deferred" @@ -853,7 +875,7 @@ class BaseDatabaseOperations(object): """ if value is None: return None - if is_aware(value): + if timezone.is_aware(value): raise ValueError("Django does not support timezone-aware times.") return six.text_type(value) @@ -866,29 +888,33 @@ class BaseDatabaseOperations(object): return None return util.format_number(value, max_digits, decimal_places) - def year_lookup_bounds(self, value): - """ - Returns a two-elements list with the lower and upper bound to be used - with a BETWEEN operator to query a field value using a year lookup - - `value` is an int, containing the looked-up year. - """ - first = '%s-01-01 00:00:00' - second = '%s-12-31 23:59:59.999999' - return [first % value, second % value] - def year_lookup_bounds_for_date_field(self, value): """ Returns a two-elements list with the lower and upper bound to be used - with a BETWEEN operator to query a DateField value using a year lookup + with a BETWEEN operator to query a DateField value using a year + lookup. `value` is an int, containing the looked-up year. - - By default, it just calls `self.year_lookup_bounds`. Some backends need - this hook because on their DB date fields can't be compared to values - which include a time part. """ - return self.year_lookup_bounds(value) + first = datetime.date(value, 1, 1) + second = datetime.date(value, 12, 31) + return [first, second] + + def year_lookup_bounds_for_datetime_field(self, value): + """ + Returns a two-elements list with the lower and upper bound to be used + with a BETWEEN operator to query a DateTimeField value using a year + lookup. + + `value` is an int, containing the looked-up year. + """ + first = datetime.datetime(value, 1, 1) + second = datetime.datetime(value, 12, 31, 23, 59, 59, 999999) + if settings.USE_TZ: + tz = timezone.get_current_timezone() + first = timezone.make_aware(first, tz) + second = timezone.make_aware(second, tz) + return [first, second] def convert_values(self, value, field): """ diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index f24df93bf4..9de2a4d62d 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -30,6 +30,7 @@ if (version < (1, 2, 1) or (version[:3] == (1, 2, 1) and from MySQLdb.converters import conversions, Thing2Literal from MySQLdb.constants import FIELD_TYPE, CLIENT +from django.conf import settings from django.db import utils from django.db.backends import * from django.db.backends.signals import connection_created @@ -193,6 +194,12 @@ class DatabaseFeatures(BaseDatabaseFeatures): "Confirm support for introspected foreign keys" return self._mysql_storage_engine != 'MyISAM' + @cached_property + def has_zoneinfo_database(self): + cursor = self.connection.cursor() + cursor.execute("SELECT 1 FROM mysql.time_zone LIMIT 1") + return cursor.fetchone() is not None + class DatabaseOperations(BaseDatabaseOperations): compiler_module = "django.db.backends.mysql.compiler" @@ -218,6 +225,39 @@ class DatabaseOperations(BaseDatabaseOperations): sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) return sql + def datetime_extract_sql(self, lookup_type, field_name, tzname): + if settings.USE_TZ: + field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name + params = [tzname] + else: + params = [] + # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html + if lookup_type == 'week_day': + # DAYOFWEEK() returns an integer, 1-7, Sunday=1. + # Note: WEEKDAY() returns 0-6, Monday=0. + sql = "DAYOFWEEK(%s)" % field_name + else: + sql = "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + return sql, params + + def datetime_trunc_sql(self, lookup_type, field_name, tzname): + if settings.USE_TZ: + field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name + params = [tzname] + else: + params = [] + fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] + format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. + format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') + try: + i = fields.index(lookup_type) + 1 + except ValueError: + sql = field_name + else: + format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) + sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) + return sql, params + def date_interval_sql(self, sql, connector, timedelta): return "(%s %s INTERVAL '%d 0:0:%d:%d' DAY_MICROSECOND)" % (sql, connector, timedelta.days, timedelta.seconds, timedelta.microseconds) @@ -314,11 +354,10 @@ class DatabaseOperations(BaseDatabaseOperations): # MySQL doesn't support microseconds return six.text_type(value.replace(microsecond=0)) - def year_lookup_bounds(self, value): + def year_lookup_bounds_for_datetime_field(self, value): # Again, no microseconds - first = '%s-01-01 00:00:00' - second = '%s-12-31 23:59:59.99' - return [first % value, second % value] + first, second = super(DatabaseOperations, self).year_lookup_bounds_for_datetime_field(value) + return [first.replace(microsecond=0), second.replace(microsecond=0)] def max_name_length(self): return 64 diff --git a/django/db/backends/mysql/compiler.py b/django/db/backends/mysql/compiler.py index d8e9b3a202..f4c5563eb2 100644 --- a/django/db/backends/mysql/compiler.py +++ b/django/db/backends/mysql/compiler.py @@ -31,3 +31,6 @@ class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler): class SQLDateCompiler(compiler.SQLDateCompiler, SQLCompiler): pass + +class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, SQLCompiler): + pass diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index e72a06472c..7bcfb46798 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals import datetime import decimal +import re import sys import warnings @@ -128,12 +129,12 @@ WHEN (new.%(col_name)s IS NULL) """ def date_extract_sql(self, lookup_type, field_name): - # http://download-east.oracle.com/docs/cd/B10501_01/server.920/a96540/functions42a.htm#1017163 if lookup_type == 'week_day': # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday. return "TO_CHAR(%s, 'D')" % field_name else: - return "EXTRACT(%s FROM %s)" % (lookup_type, field_name) + # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm + return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) def date_interval_sql(self, sql, connector, timedelta): """ @@ -150,13 +151,58 @@ WHEN (new.%(col_name)s IS NULL) timedelta.microseconds, day_precision) def date_trunc_sql(self, lookup_type, field_name): - # Oracle uses TRUNC() for both dates and numbers. - # http://download-east.oracle.com/docs/cd/B10501_01/server.920/a96540/functions155a.htm#SQLRF06151 - if lookup_type == 'day': - sql = 'TRUNC(%s)' % field_name + # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm#i1002084 + if lookup_type in ('year', 'month'): + return "TRUNC(%s, '%s')" % (field_name, lookup_type.upper()) else: - sql = "TRUNC(%s, '%s')" % (field_name, lookup_type) - return sql + return "TRUNC(%s)" % field_name + + # Oracle crashes with "ORA-03113: end-of-file on communication channel" + # if the time zone name is passed in parameter. Use interpolation instead. + # https://groups.google.com/forum/#!msg/django-developers/zwQju7hbG78/9l934yelwfsJ + # This regexp matches all time zone names from the zoneinfo database. + _tzname_re = re.compile(r'^[\w/:+-]+$') + + def _convert_field_to_tz(self, field_name, tzname): + if not self._tzname_re.match(tzname): + raise ValueError("Invalid time zone name: %s" % tzname) + # Convert from UTC to local time, returning TIMESTAMP WITH TIME ZONE. + result = "(FROM_TZ(%s, '0:00') AT TIME ZONE '%s')" % (field_name, tzname) + # Extracting from a TIMESTAMP WITH TIME ZONE ignore the time zone. + # Convert to a DATETIME, which is called DATE by Oracle. There's no + # built-in function to do that; the easiest is to go through a string. + result = "TO_CHAR(%s, 'YYYY-MM-DD HH24:MI:SS')" % result + result = "TO_DATE(%s, 'YYYY-MM-DD HH24:MI:SS')" % result + # Re-convert to a TIMESTAMP because EXTRACT only handles the date part + # on DATE values, even though they actually store the time part. + return "CAST(%s AS TIMESTAMP)" % result + + def datetime_extract_sql(self, lookup_type, field_name, tzname): + if settings.USE_TZ: + field_name = self._convert_field_to_tz(field_name, tzname) + if lookup_type == 'week_day': + # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday. + sql = "TO_CHAR(%s, 'D')" % field_name + else: + # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm + sql = "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + return sql, [] + + def datetime_trunc_sql(self, lookup_type, field_name, tzname): + if settings.USE_TZ: + field_name = self._convert_field_to_tz(field_name, tzname) + # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm#i1002084 + if lookup_type in ('year', 'month'): + sql = "TRUNC(%s, '%s')" % (field_name, lookup_type.upper()) + elif lookup_type == 'day': + sql = "TRUNC(%s)" % field_name + elif lookup_type == 'hour': + sql = "TRUNC(%s, 'HH24')" % field_name + elif lookup_type == 'minute': + sql = "TRUNC(%s, 'MI')" % field_name + else: + sql = field_name # Cast to DATE removes sub-second precision. + return sql, [] def convert_values(self, value, field): if isinstance(value, Database.LOB): diff --git a/django/db/backends/oracle/compiler.py b/django/db/backends/oracle/compiler.py index 24030cdffc..cbee27951c 100644 --- a/django/db/backends/oracle/compiler.py +++ b/django/db/backends/oracle/compiler.py @@ -71,3 +71,6 @@ class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler): class SQLDateCompiler(compiler.SQLDateCompiler, SQLCompiler): pass + +class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, SQLCompiler): + pass diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py index 40fe629110..8e87ed539f 100644 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ b/django/db/backends/postgresql_psycopg2/operations.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from django.conf import settings from django.db.backends import BaseDatabaseOperations @@ -36,6 +37,30 @@ class DatabaseOperations(BaseDatabaseOperations): # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) + def datetime_extract_sql(self, lookup_type, field_name, tzname): + if settings.USE_TZ: + field_name = "%s AT TIME ZONE %%s" % field_name + params = [tzname] + else: + params = [] + # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT + if lookup_type == 'week_day': + # For consistency across backends, we return Sunday=1, Saturday=7. + sql = "EXTRACT('dow' FROM %s) + 1" % field_name + else: + sql = "EXTRACT('%s' FROM %s)" % (lookup_type, field_name) + return sql, params + + def datetime_trunc_sql(self, lookup_type, field_name, tzname): + if settings.USE_TZ: + field_name = "%s AT TIME ZONE %%s" % field_name + params = [tzname] + else: + params = [] + # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC + sql = "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) + return sql, params + def deferrable_sql(self): return " DEFERRABLE INITIALLY DEFERRED" diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index f4fd1cc379..3b4ff4c5dd 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -35,6 +35,10 @@ except ImportError as exc: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured("Error loading either pysqlite2 or sqlite3 modules (tried in that order): %s" % exc) +try: + import pytz +except ImportError: + pytz = None DatabaseError = Database.DatabaseError IntegrityError = Database.IntegrityError @@ -117,6 +121,10 @@ class DatabaseFeatures(BaseDatabaseFeatures): cursor.execute('DROP TABLE STDDEV_TEST') return has_support + @cached_property + def has_zoneinfo_database(self): + return pytz is not None + class DatabaseOperations(BaseDatabaseOperations): def bulk_batch_size(self, fields, objs): """ @@ -142,10 +150,10 @@ class DatabaseOperations(BaseDatabaseOperations): def date_extract_sql(self, lookup_type, field_name): # sqlite doesn't support extract, so we fake it with the user-defined - # function django_extract that's registered in connect(). Note that + # function django_date_extract that's registered in connect(). Note that # single quotes are used because this is a string (and could otherwise # cause a collision with a field name). - return "django_extract('%s', %s)" % (lookup_type.lower(), field_name) + return "django_date_extract('%s', %s)" % (lookup_type.lower(), field_name) def date_interval_sql(self, sql, connector, timedelta): # It would be more straightforward if we could use the sqlite strftime @@ -154,7 +162,7 @@ class DatabaseOperations(BaseDatabaseOperations): # values differently. So instead we register our own function that # formats the datetime combined with the delta in a manner suitable # for comparisons. - return 'django_format_dtdelta(%s, "%s", "%d", "%d", "%d")' % (sql, + return 'django_format_dtdelta(%s, "%s", "%d", "%d", "%d")' % (sql, connector, timedelta.days, timedelta.seconds, timedelta.microseconds) def date_trunc_sql(self, lookup_type, field_name): @@ -164,6 +172,26 @@ class DatabaseOperations(BaseDatabaseOperations): # cause a collision with a field name). return "django_date_trunc('%s', %s)" % (lookup_type.lower(), field_name) + def datetime_extract_sql(self, lookup_type, field_name, tzname): + # Same comment as in date_extract_sql. + if settings.USE_TZ: + if pytz is None: + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured("This query requires pytz, " + "but it isn't installed.") + return "django_datetime_extract('%s', %s, %%s)" % ( + lookup_type.lower(), field_name), [tzname] + + def datetime_trunc_sql(self, lookup_type, field_name, tzname): + # Same comment as in date_trunc_sql. + if settings.USE_TZ: + if pytz is None: + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured("This query requires pytz, " + "but it isn't installed.") + return "django_datetime_trunc('%s', %s, %%s)" % ( + lookup_type.lower(), field_name), [tzname] + def drop_foreignkey_sql(self): return "" @@ -214,11 +242,6 @@ class DatabaseOperations(BaseDatabaseOperations): return six.text_type(value) - def year_lookup_bounds(self, value): - first = '%s-01-01' - second = '%s-12-31 23:59:59.999999' - return [first % value, second % value] - def convert_values(self, value, field): """SQLite returns floats when it should be returning decimals, and gets dates and datetimes wrong. @@ -310,9 +333,10 @@ class DatabaseWrapper(BaseDatabaseWrapper): def get_new_connection(self, conn_params): conn = Database.connect(**conn_params) - # Register extract, date_trunc, and regexp functions. - conn.create_function("django_extract", 2, _sqlite_extract) + conn.create_function("django_date_extract", 2, _sqlite_date_extract) conn.create_function("django_date_trunc", 2, _sqlite_date_trunc) + conn.create_function("django_datetime_extract", 3, _sqlite_datetime_extract) + conn.create_function("django_datetime_trunc", 3, _sqlite_datetime_trunc) conn.create_function("regexp", 2, _sqlite_regexp) conn.create_function("django_format_dtdelta", 5, _sqlite_format_dtdelta) return conn @@ -402,7 +426,7 @@ class SQLiteCursorWrapper(Database.Cursor): def convert_query(self, query): return FORMAT_QMARK_REGEX.sub('?', query).replace('%%','%') -def _sqlite_extract(lookup_type, dt): +def _sqlite_date_extract(lookup_type, dt): if dt is None: return None try: @@ -419,12 +443,46 @@ def _sqlite_date_trunc(lookup_type, dt): dt = util.typecast_timestamp(dt) except (ValueError, TypeError): return None + if lookup_type == 'year': + return "%i-01-01" % dt.year + elif lookup_type == 'month': + return "%i-%02i-01" % (dt.year, dt.month) + elif lookup_type == 'day': + return "%i-%02i-%02i" % (dt.year, dt.month, dt.day) + +def _sqlite_datetime_extract(lookup_type, dt, tzname): + if dt is None: + return None + try: + dt = util.typecast_timestamp(dt) + except (ValueError, TypeError): + return None + if tzname is not None: + dt = timezone.localtime(dt, pytz.timezone(tzname)) + if lookup_type == 'week_day': + return (dt.isoweekday() % 7) + 1 + else: + return getattr(dt, lookup_type) + +def _sqlite_datetime_trunc(lookup_type, dt, tzname): + try: + dt = util.typecast_timestamp(dt) + except (ValueError, TypeError): + return None + if tzname is not None: + dt = timezone.localtime(dt, pytz.timezone(tzname)) if lookup_type == 'year': return "%i-01-01 00:00:00" % dt.year elif lookup_type == 'month': return "%i-%02i-01 00:00:00" % (dt.year, dt.month) elif lookup_type == 'day': return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day) + elif lookup_type == 'hour': + return "%i-%02i-%02i %02i:00:00" % (dt.year, dt.month, dt.day, dt.hour) + elif lookup_type == 'minute': + return "%i-%02i-%02i %02i:%02i:00" % (dt.year, dt.month, dt.day, dt.hour, dt.minute) + elif lookup_type == 'second': + return "%i-%02i-%02i %02i:%02i:%02i" % (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) def _sqlite_format_dtdelta(dt, conn, days, secs, usecs): try: diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 94abfd784c..b70d235656 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -312,9 +312,10 @@ class Field(object): return value._prepare() if lookup_type in ( - 'regex', 'iregex', 'month', 'day', 'week_day', 'search', - 'contains', 'icontains', 'iexact', 'startswith', 'istartswith', - 'endswith', 'iendswith', 'isnull' + 'iexact', 'contains', 'icontains', + 'startswith', 'istartswith', 'endswith', 'iendswith', + 'month', 'day', 'week_day', 'hour', 'minute', 'second', + 'isnull', 'search', 'regex', 'iregex', ): return value elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte'): @@ -350,8 +351,8 @@ class Field(object): sql, params = value._as_sql(connection=connection) return QueryWrapper(('(%s)' % sql), params) - if lookup_type in ('regex', 'iregex', 'month', 'day', 'week_day', - 'search'): + if lookup_type in ('month', 'day', 'week_day', 'hour', 'minute', + 'second', 'search', 'regex', 'iregex'): return [value] elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte'): return [self.get_db_prep_value(value, connection=connection, @@ -370,10 +371,12 @@ class Field(object): elif lookup_type == 'isnull': return [] elif lookup_type == 'year': - if self.get_internal_type() == 'DateField': + if isinstance(self, DateTimeField): + return connection.ops.year_lookup_bounds_for_datetime_field(value) + elif isinstance(self, DateField): return connection.ops.year_lookup_bounds_for_date_field(value) else: - return connection.ops.year_lookup_bounds(value) + return [value] # this isn't supposed to happen def has_default(self): """ @@ -722,9 +725,9 @@ class DateField(Field): is_next=False)) def get_prep_lookup(self, lookup_type, value): - # For "__month", "__day", and "__week_day" lookups, convert the value - # to an int so the database backend always sees a consistent type. - if lookup_type in ('month', 'day', 'week_day'): + # For dates lookups, convert the value to an int + # so the database backend always sees a consistent type. + if lookup_type in ('month', 'day', 'week_day', 'hour', 'minute', 'second'): return int(value) return super(DateField, self).get_prep_lookup(lookup_type, value) diff --git a/django/db/models/manager.py b/django/db/models/manager.py index cee2131279..b1f2e10735 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -130,6 +130,9 @@ class Manager(object): def dates(self, *args, **kwargs): return self.get_query_set().dates(*args, **kwargs) + def datetimes(self, *args, **kwargs): + return self.get_query_set().datetimes(*args, **kwargs) + def distinct(self, *args, **kwargs): return self.get_query_set().distinct(*args, **kwargs) diff --git a/django/db/models/query.py b/django/db/models/query.py index 87bc6205a8..0f3a79a25d 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -7,6 +7,7 @@ import itertools import sys import warnings +from django.conf import settings from django.core import exceptions from django.db import connections, router, transaction, IntegrityError from django.db.models.constants import LOOKUP_SEP @@ -17,6 +18,7 @@ from django.db.models.deletion import Collector from django.db.models import sql from django.utils.functional import partition from django.utils import six +from django.utils import timezone # Used to control how many objects are worked with at once in some cases (e.g. # when deleting objects). @@ -629,16 +631,33 @@ class QuerySet(object): def dates(self, field_name, kind, order='ASC'): """ - Returns a list of datetime objects representing all available dates for + Returns a list of date objects representing all available dates for the given field_name, scoped to 'kind'. """ - assert kind in ("month", "year", "day"), \ + assert kind in ("year", "month", "day"), \ "'kind' must be one of 'year', 'month' or 'day'." assert order in ('ASC', 'DESC'), \ "'order' must be either 'ASC' or 'DESC'." return self._clone(klass=DateQuerySet, setup=True, _field_name=field_name, _kind=kind, _order=order) + def datetimes(self, field_name, kind, order='ASC', tzinfo=None): + """ + Returns a list of datetime objects representing all available + datetimes for the given field_name, scoped to 'kind'. + """ + assert kind in ("year", "month", "day", "hour", "minute", "second"), \ + "'kind' must be one of 'year', 'month', 'day', 'hour', 'minute' or 'second'." + assert order in ('ASC', 'DESC'), \ + "'order' must be either 'ASC' or 'DESC'." + if settings.USE_TZ: + if tzinfo is None: + tzinfo = timezone.get_current_timezone() + else: + tzinfo = None + return self._clone(klass=DateTimeQuerySet, setup=True, + _field_name=field_name, _kind=kind, _order=order, _tzinfo=tzinfo) + def none(self): """ Returns an empty QuerySet. @@ -1187,7 +1206,7 @@ class DateQuerySet(QuerySet): self.query.clear_deferred_loading() self.query = self.query.clone(klass=sql.DateQuery, setup=True) self.query.select = [] - self.query.add_date_select(self._field_name, self._kind, self._order) + self.query.add_select(self._field_name, self._kind, self._order) def _clone(self, klass=None, setup=False, **kwargs): c = super(DateQuerySet, self)._clone(klass, False, **kwargs) @@ -1198,6 +1217,32 @@ class DateQuerySet(QuerySet): return c +class DateTimeQuerySet(QuerySet): + def iterator(self): + return self.query.get_compiler(self.db).results_iter() + + def _setup_query(self): + """ + Sets up any special features of the query attribute. + + Called by the _clone() method after initializing the rest of the + instance. + """ + self.query.clear_deferred_loading() + self.query = self.query.clone(klass=sql.DateTimeQuery, setup=True, tzinfo=self._tzinfo) + self.query.select = [] + self.query.add_select(self._field_name, self._kind, self._order) + + def _clone(self, klass=None, setup=False, **kwargs): + c = super(DateTimeQuerySet, self)._clone(klass, False, **kwargs) + c._field_name = self._field_name + c._kind = self._kind + c._tzinfo = self._tzinfo + if setup and hasattr(c, '_setup_query'): + c._setup_query() + return c + + def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None, only_load=None, from_parent=None): """ diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index c1a690a524..c82cc45617 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -25,7 +25,7 @@ class QueryWrapper(object): parameters. Can be used to pass opaque data to a where-clause, for example. """ def __init__(self, sql, params): - self.data = sql, params + self.data = sql, list(params) def as_sql(self, qn=None, connection=None): return self.data diff --git a/django/db/models/sql/aggregates.py b/django/db/models/sql/aggregates.py index 75a330f22a..3c8720210b 100644 --- a/django/db/models/sql/aggregates.py +++ b/django/db/models/sql/aggregates.py @@ -73,22 +73,23 @@ class Aggregate(object): self.col = (change_map.get(self.col[0], self.col[0]), self.col[1]) def as_sql(self, qn, connection): - "Return the aggregate, rendered as SQL." + "Return the aggregate, rendered as SQL with parameters." + params = [] if hasattr(self.col, 'as_sql'): - field_name = self.col.as_sql(qn, connection) + field_name, params = self.col.as_sql(qn, connection) elif isinstance(self.col, (list, tuple)): field_name = '.'.join([qn(c) for c in self.col]) else: field_name = self.col - params = { + substitutions = { 'function': self.sql_function, 'field': field_name } - params.update(self.extra) + substitutions.update(self.extra) - return self.sql_template % params + return self.sql_template % substitutions, params class Avg(Aggregate): diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 79b5d99452..1b6654b670 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1,5 +1,6 @@ -from django.utils.six.moves import zip +import datetime +from django.conf import settings from django.core.exceptions import FieldError from django.db import transaction from django.db.backends.util import truncate_name @@ -12,6 +13,8 @@ from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.query import get_order_dir, Query from django.db.utils import DatabaseError from django.utils import six +from django.utils.six.moves import zip +from django.utils import timezone class SQLCompiler(object): @@ -71,7 +74,7 @@ class SQLCompiler(object): # as the pre_sql_setup will modify query state in a way that forbids # another run of it. self.refcounts_before = self.query.alias_refcount.copy() - out_cols = self.get_columns(with_col_aliases) + out_cols, s_params = self.get_columns(with_col_aliases) ordering, ordering_group_by = self.get_ordering() distinct_fields = self.get_distinct() @@ -94,6 +97,7 @@ class SQLCompiler(object): result.append(self.connection.ops.distinct_sql(distinct_fields)) result.append(', '.join(out_cols + self.query.ordering_aliases)) + params.extend(s_params) result.append('FROM') result.extend(from_) @@ -161,9 +165,10 @@ class SQLCompiler(object): def get_columns(self, with_aliases=False): """ - Returns the list of columns to use in the select statement. If no - columns have been specified, returns all columns relating to fields in - the model. + Returns the list of columns to use in the select statement, as well as + a list any extra parameters that need to be included. If no columns + have been specified, returns all columns relating to fields in the + model. If 'with_aliases' is true, any column names that are duplicated (without the table names) are given unique aliases. This is needed in @@ -172,6 +177,7 @@ class SQLCompiler(object): qn = self.quote_name_unless_alias qn2 = self.connection.ops.quote_name result = ['(%s) AS %s' % (col[0], qn2(alias)) for alias, col in six.iteritems(self.query.extra_select)] + params = [] aliases = set(self.query.extra_select.keys()) if with_aliases: col_aliases = aliases.copy() @@ -201,7 +207,9 @@ class SQLCompiler(object): aliases.add(r) col_aliases.add(col[1]) else: - result.append(col.as_sql(qn, self.connection)) + col_sql, col_params = col.as_sql(qn, self.connection) + result.append(col_sql) + params.extend(col_params) if hasattr(col, 'alias'): aliases.add(col.alias) @@ -214,15 +222,13 @@ class SQLCompiler(object): aliases.update(new_aliases) max_name_length = self.connection.ops.max_name_length() - result.extend([ - '%s%s' % ( - aggregate.as_sql(qn, self.connection), - alias is not None - and ' AS %s' % qn(truncate_name(alias, max_name_length)) - or '' - ) - for alias, aggregate in self.query.aggregate_select.items() - ]) + for alias, aggregate in self.query.aggregate_select.items(): + agg_sql, agg_params = aggregate.as_sql(qn, self.connection) + if alias is None: + result.append(agg_sql) + else: + result.append('%s AS %s' % (agg_sql, qn(truncate_name(alias, max_name_length)))) + params.extend(agg_params) for (table, col), _ in self.query.related_select_cols: r = '%s.%s' % (qn(table), qn(col)) @@ -237,7 +243,7 @@ class SQLCompiler(object): col_aliases.add(col) self._select_aliases = aliases - return result + return result, params def get_default_columns(self, with_aliases=False, col_aliases=None, start_alias=None, opts=None, as_pairs=False, from_parent=None): @@ -542,14 +548,16 @@ class SQLCompiler(object): seen = set() cols = self.query.group_by + select_cols for col in cols: + col_params = () if isinstance(col, (list, tuple)): sql = '%s.%s' % (qn(col[0]), qn(col[1])) elif hasattr(col, 'as_sql'): - sql = col.as_sql(qn, self.connection) + sql, col_params = col.as_sql(qn, self.connection) else: sql = '(%s)' % str(col) if sql not in seen: result.append(sql) + params.extend(col_params) seen.add(sql) # Still, we need to add all stuff in ordering (except if the backend can @@ -988,17 +996,44 @@ class SQLAggregateCompiler(SQLCompiler): if qn is None: qn = self.quote_name_unless_alias - sql = ('SELECT %s FROM (%s) subquery' % ( - ', '.join([ - aggregate.as_sql(qn, self.connection) - for aggregate in self.query.aggregate_select.values() - ]), - self.query.subquery) - ) - params = self.query.sub_params - return (sql, params) + sql, params = [], [] + for aggregate in self.query.aggregate_select.values(): + agg_sql, agg_params = aggregate.as_sql(qn, self.connection) + sql.append(agg_sql) + params.extend(agg_params) + sql = ', '.join(sql) + params = tuple(params) + + sql = 'SELECT %s FROM (%s) subquery' % (sql, self.query.subquery) + params = params + self.query.sub_params + return sql, params class SQLDateCompiler(SQLCompiler): + def results_iter(self): + """ + Returns an iterator over the results from executing this query. + """ + resolve_columns = hasattr(self, 'resolve_columns') + if resolve_columns: + from django.db.models.fields import DateField + fields = [DateField()] + else: + from django.db.backends.util import typecast_date + needs_string_cast = self.connection.features.needs_datetime_string_cast + + offset = len(self.query.extra_select) + for rows in self.execute_sql(MULTI): + for row in rows: + date = row[offset] + if resolve_columns: + date = self.resolve_columns(row, fields)[offset] + elif needs_string_cast: + date = typecast_date(str(date)) + if isinstance(date, datetime.datetime): + date = date.date() + yield date + +class SQLDateTimeCompiler(SQLCompiler): def results_iter(self): """ Returns an iterator over the results from executing this query. @@ -1014,13 +1049,17 @@ class SQLDateCompiler(SQLCompiler): offset = len(self.query.extra_select) for rows in self.execute_sql(MULTI): for row in rows: - date = row[offset] + datetime = row[offset] if resolve_columns: - date = self.resolve_columns(row, fields)[offset] + datetime = self.resolve_columns(row, fields)[offset] elif needs_string_cast: - date = typecast_timestamp(str(date)) - yield date - + datetime = typecast_timestamp(str(datetime)) + # Datetimes are artifically returned in UTC on databases that + # don't support time zone. Restore the zone used in the query. + if settings.USE_TZ: + datetime = datetime.replace(tzinfo=None) + datetime = timezone.make_aware(datetime, self.query.tzinfo) + yield datetime def order_modified_iter(cursor, trim, sentinel): """ diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py index 1764db7fcc..81bd646d69 100644 --- a/django/db/models/sql/constants.py +++ b/django/db/models/sql/constants.py @@ -11,7 +11,8 @@ import re QUERY_TERMS = set([ 'exact', 'iexact', 'contains', 'icontains', 'gt', 'gte', 'lt', 'lte', 'in', 'startswith', 'istartswith', 'endswith', 'iendswith', 'range', 'year', - 'month', 'day', 'week_day', 'isnull', 'search', 'regex', 'iregex', + 'month', 'day', 'week_day', 'hour', 'minute', 'second', 'isnull', 'search', + 'regex', 'iregex', ]) # Size of each "chunk" for get_iterator calls. diff --git a/django/db/models/sql/datastructures.py b/django/db/models/sql/datastructures.py index b8e06daf01..612eb8f2d9 100644 --- a/django/db/models/sql/datastructures.py +++ b/django/db/models/sql/datastructures.py @@ -40,4 +40,25 @@ class Date(object): col = '%s.%s' % tuple([qn(c) for c in self.col]) else: col = self.col - return connection.ops.date_trunc_sql(self.lookup_type, col) + return connection.ops.date_trunc_sql(self.lookup_type, col), [] + +class DateTime(object): + """ + Add a datetime selection column. + """ + def __init__(self, col, lookup_type, tzname): + self.col = col + self.lookup_type = lookup_type + self.tzname = tzname + + def relabel_aliases(self, change_map): + c = self.col + if isinstance(c, (list, tuple)): + self.col = (change_map.get(c[0], c[0]), c[1]) + + def as_sql(self, qn, connection): + if isinstance(self.col, (list, tuple)): + col = '%s.%s' % tuple([qn(c) for c in self.col]) + else: + col = self.col + return connection.ops.datetime_trunc_sql(self.lookup_type, col, self.tzname) diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py index a4c1d85c65..2a5008f067 100644 --- a/django/db/models/sql/expressions.py +++ b/django/db/models/sql/expressions.py @@ -94,9 +94,9 @@ class SQLEvaluator(object): if col is None: raise ValueError("Given node not found") if hasattr(col, 'as_sql'): - return col.as_sql(qn, connection), () + return col.as_sql(qn, connection) else: - return '%s.%s' % (qn(col[0]), qn(col[1])), () + return '%s.%s' % (qn(col[0]), qn(col[1])), [] def evaluate_date_modifier_node(self, node, qn, connection): timedelta = node.children.pop() diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 6072804697..6aac5c898c 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -2,22 +2,23 @@ Query subclasses which provide extra functionality beyond simple data retrieval. """ +from django.conf import settings from django.core.exceptions import FieldError from django.db import connections from django.db.models.constants import LOOKUP_SEP -from django.db.models.fields import DateField, FieldDoesNotExist +from django.db.models.fields import DateField, DateTimeField, FieldDoesNotExist from django.db.models.sql.constants import * -from django.db.models.sql.datastructures import Date +from django.db.models.sql.datastructures import Date, DateTime from django.db.models.sql.query import Query from django.db.models.sql.where import AND, Constraint -from django.utils.datastructures import SortedDict from django.utils.functional import Promise from django.utils.encoding import force_text from django.utils import six +from django.utils import timezone __all__ = ['DeleteQuery', 'UpdateQuery', 'InsertQuery', 'DateQuery', - 'AggregateQuery'] + 'DateTimeQuery', 'AggregateQuery'] class DeleteQuery(Query): """ @@ -223,9 +224,9 @@ class DateQuery(Query): compiler = 'SQLDateCompiler' - def add_date_select(self, field_name, lookup_type, order='ASC'): + def add_select(self, field_name, lookup_type, order='ASC'): """ - Converts the query into a date extraction query. + Converts the query into an extraction query. """ try: result = self.setup_joins( @@ -238,10 +239,9 @@ class DateQuery(Query): self.model._meta.object_name, field_name )) field = result[0] - assert isinstance(field, DateField), "%r isn't a DateField." \ - % field.name + self._check_field(field) # overridden in DateTimeQuery alias = result[3][-1] - select = Date((alias, field.column), lookup_type) + select = self._get_select((alias, field.column), lookup_type) self.clear_select_clause() self.select = [SelectInfo(select, None)] self.distinct = True @@ -250,6 +250,36 @@ class DateQuery(Query): if field.null: self.add_filter(("%s__isnull" % field_name, False)) + def _check_field(self, field): + assert isinstance(field, DateField), \ + "%r isn't a DateField." % field.name + if settings.USE_TZ: + assert not isinstance(field, DateTimeField), \ + "%r is a DateTimeField, not a DateField." % field.name + + def _get_select(self, col, lookup_type): + return Date(col, lookup_type) + +class DateTimeQuery(DateQuery): + """ + A DateTimeQuery is like a DateQuery but for a datetime field. If time zone + support is active, the tzinfo attribute contains the time zone to use for + converting the values before truncating them. Otherwise it's set to None. + """ + + compiler = 'SQLDateTimeCompiler' + + def _check_field(self, field): + assert isinstance(field, DateTimeField), \ + "%r isn't a DateTimeField." % field.name + + def _get_select(self, col, lookup_type): + if self.tzinfo is None: + tzname = None + else: + tzname = timezone._get_timezone_name(self.tzinfo) + return DateTime(col, lookup_type, tzname) + class AggregateQuery(Query): """ An AggregateQuery takes another query as a parameter to the FROM diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index cbb0546d6a..ef856893b5 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -8,11 +8,13 @@ import collections import datetime from itertools import repeat -from django.utils import tree -from django.db.models.fields import Field +from django.conf import settings +from django.db.models.fields import DateTimeField, Field from django.db.models.sql.datastructures import EmptyResultSet, Empty from django.db.models.sql.aggregates import Aggregate from django.utils.six.moves import xrange +from django.utils import timezone +from django.utils import tree # Connection types AND = 'AND' @@ -60,7 +62,8 @@ class WhereNode(tree.Node): # about the value(s) to the query construction. Specifically, datetime # and empty values need special handling. Other types could be used # here in the future (using Python types is suggested for consistency). - if isinstance(value, datetime.datetime): + if (isinstance(value, datetime.datetime) + or (isinstance(obj.field, DateTimeField) and lookup_type != 'isnull')): value_annotation = datetime.datetime elif hasattr(value, 'value_annotation'): value_annotation = value.value_annotation @@ -169,15 +172,13 @@ class WhereNode(tree.Node): if isinstance(lvalue, tuple): # A direct database column lookup. - field_sql = self.sql_for_columns(lvalue, qn, connection) + field_sql, field_params = self.sql_for_columns(lvalue, qn, connection), [] else: # A smart object with an as_sql() method. - field_sql = lvalue.as_sql(qn, connection) + field_sql, field_params = lvalue.as_sql(qn, connection) - if value_annotation is datetime.datetime: - cast_sql = connection.ops.datetime_cast_sql() - else: - cast_sql = '%s' + is_datetime_field = value_annotation is datetime.datetime + cast_sql = connection.ops.datetime_cast_sql() if is_datetime_field else '%s' if hasattr(params, 'as_sql'): extra, params = params.as_sql(qn, connection) @@ -185,6 +186,8 @@ class WhereNode(tree.Node): else: extra = '' + params = field_params + params + if (len(params) == 1 and params[0] == '' and lookup_type == 'exact' and connection.features.interprets_empty_strings_as_nulls): lookup_type = 'isnull' @@ -221,9 +224,14 @@ class WhereNode(tree.Node): params) elif lookup_type in ('range', 'year'): return ('%s BETWEEN %%s and %%s' % field_sql, params) + elif is_datetime_field and lookup_type in ('month', 'day', 'week_day', + 'hour', 'minute', 'second'): + tzname = timezone.get_current_timezone_name() if settings.USE_TZ else None + sql, tz_params = connection.ops.datetime_extract_sql(lookup_type, field_sql, tzname) + return ('%s = %%s' % sql, tz_params + params) elif lookup_type in ('month', 'day', 'week_day'): - return ('%s = %%s' % connection.ops.date_extract_sql(lookup_type, field_sql), - params) + return ('%s = %%s' + % connection.ops.date_extract_sql(lookup_type, field_sql), params) elif lookup_type == 'isnull': assert value_annotation in (True, False), "Invalid value_annotation for isnull" return ('%s IS %sNULL' % (field_sql, ('' if value_annotation else 'NOT ')), ()) @@ -238,7 +246,7 @@ class WhereNode(tree.Node): """ Returns the SQL fragment used for the left-hand side of a column constraint (for example, the "T1.foo" portion in the clause - "WHERE ... T1.foo = 6"). + "WHERE ... T1.foo = 6") and a list of parameters. """ table_alias, name, db_type = data if table_alias: @@ -331,7 +339,7 @@ class ExtraWhere(object): def as_sql(self, qn=None, connection=None): sqls = ["(%s)" % sql for sql in self.sqls] - return " AND ".join(sqls), tuple(self.params or ()) + return " AND ".join(sqls), list(self.params or ()) def clone(self): return self diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py index 9ffaca4470..29efc7dfac 100644 --- a/django/views/generic/dates.py +++ b/django/views/generic/dates.py @@ -379,15 +379,18 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): def get_date_list(self, queryset, date_type=None, ordering='ASC'): """ - Get a date list by calling `queryset.dates()`, checking along the way - for empty lists that aren't allowed. + Get a date list by calling `queryset.dates/datetimes()`, checking + along the way for empty lists that aren't allowed. """ date_field = self.get_date_field() allow_empty = self.get_allow_empty() if date_type is None: date_type = self.get_date_list_period() - date_list = queryset.dates(date_field, date_type, ordering) + if self.uses_datetime_field: + date_list = queryset.datetimes(date_field, date_type, ordering) + else: + date_list = queryset.dates(date_field, date_type, ordering) if date_list is not None and not date_list and not allow_empty: name = force_text(queryset.model._meta.verbose_name_plural) raise Http404(_("No %(verbose_name_plural)s available") % diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 171c2d3dcd..f77f87dd8e 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -550,14 +550,19 @@ dates .. method:: dates(field, kind, order='ASC') Returns a ``DateQuerySet`` — a ``QuerySet`` that evaluates to a list of -``datetime.datetime`` objects representing all available dates of a particular -kind within the contents of the ``QuerySet``. +:class:`datetime.date` objects representing all available dates of a +particular kind within the contents of the ``QuerySet``. -``field`` should be the name of a ``DateField`` or ``DateTimeField`` of your -model. +.. versionchanged:: 1.6 + ``dates`` used to return a list of :class:`datetime.datetime` objects. + +``field`` should be the name of a ``DateField`` of your model. + +.. versionchanged:: 1.6 + ``dates`` used to accept operating on a ``DateTimeField``. ``kind`` should be either ``"year"``, ``"month"`` or ``"day"``. Each -``datetime.datetime`` object in the result list is "truncated" to the given +``datetime.date`` object in the result list is "truncated" to the given ``type``. * ``"year"`` returns a list of all distinct year values for the field. @@ -572,21 +577,60 @@ model. Examples:: >>> Entry.objects.dates('pub_date', 'year') - [datetime.datetime(2005, 1, 1)] + [datetime.date(2005, 1, 1)] >>> Entry.objects.dates('pub_date', 'month') - [datetime.datetime(2005, 2, 1), datetime.datetime(2005, 3, 1)] + [datetime.date(2005, 2, 1), datetime.date(2005, 3, 1)] >>> Entry.objects.dates('pub_date', 'day') - [datetime.datetime(2005, 2, 20), datetime.datetime(2005, 3, 20)] + [datetime.date(2005, 2, 20), datetime.date(2005, 3, 20)] >>> Entry.objects.dates('pub_date', 'day', order='DESC') - [datetime.datetime(2005, 3, 20), datetime.datetime(2005, 2, 20)] + [datetime.date(2005, 3, 20), datetime.date(2005, 2, 20)] >>> Entry.objects.filter(headline__contains='Lennon').dates('pub_date', 'day') - [datetime.datetime(2005, 3, 20)] + [datetime.date(2005, 3, 20)] -.. warning:: +datetimes +~~~~~~~~~ - When :doc:`time zone support ` is enabled, Django - uses UTC in the database connection, which means the aggregation is - performed in UTC. This is a known limitation of the current implementation. +.. versionadded:: 1.6 + +.. method:: datetimes(field, kind, order='ASC', tzinfo=None) + +Returns a ``DateTimeQuerySet`` — a ``QuerySet`` that evaluates to a list of +:class:`datetime.datetime` objects representing all available dates of a +particular kind within the contents of the ``QuerySet``. + +``field`` should be the name of a ``DateTimeField`` of your model. + +``kind`` should be either ``"year"``, ``"month"``, ``"day"``, ``"hour"``, +``"minute"`` or ``"second"``. Each ``datetime.datetime`` object in the result +list is "truncated" to the given ``type``. + +``order``, which defaults to ``'ASC'``, should be either ``'ASC'`` or +``'DESC'``. This specifies how to order the results. + +``tzinfo`` defines the time zone to which datetimes are converted prior to +truncation. Indeed, a given datetime has different representations depending +on the time zone in use. This parameter must be a :class:`datetime.tzinfo` +object. If it's ``None``, Django uses the :ref:`current time zone +`. It has no effect when :setting:`USE_TZ` is +``False``. + +.. _database-time-zone-definitions: + +.. note:: + + This function performs time zone conversions directly in the database. + As a consequence, your database must be able to interpret the value of + ``tzinfo.tzname(None)``. This translates into the following requirements: + + - SQLite: install pytz_ — conversions are actually performed in Python. + - PostgreSQL: no requirements (see `Time Zones`_). + - Oracle: no requirements (see `Choosing a Time Zone File`_). + - MySQL: load the time zone tables with `mysql_tzinfo_to_sql`_. + + .. _pytz: http://pytz.sourceforge.net/ + .. _Time Zones: http://www.postgresql.org/docs/9.2/static/datatype-datetime.html#DATATYPE-TIMEZONES + .. _Choosing a Time Zone File: http://docs.oracle.com/cd/B19306_01/server.102/b14225/ch4datetime.htm#i1006667 + .. _mysql_tzinfo_to_sql: http://dev.mysql.com/doc/refman/5.5/en/mysql-tzinfo-to-sql.html none ~~~~ @@ -2020,7 +2064,7 @@ numbers and even characters. year ~~~~ -For date/datetime fields, exact year match. Takes a four-digit year. +For date and datetime fields, an exact year match. Takes an integer year. Example:: @@ -2032,6 +2076,9 @@ 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. + .. fieldlookup:: month month @@ -2050,12 +2097,15 @@ 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. + .. fieldlookup:: day day ~~~ -For date and datetime fields, an exact day match. +For date and datetime fields, an exact day match. Takes an integer day. Example:: @@ -2070,6 +2120,9 @@ SQL equivalent:: 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. + .. fieldlookup:: week_day week_day @@ -2091,12 +2144,74 @@ Note this will match any record with a ``pub_date`` that falls on a Monday (day 2 of the week), regardless of the month or year in which it occurs. Week days are indexed with day 1 being Sunday and day 7 being Saturday. -.. warning:: +When :setting:`USE_TZ` is ``True``, datetime fields are converted to the +current time zone before filtering. - When :doc:`time zone support ` is enabled, Django - uses UTC in the database connection, which means the ``year``, ``month``, - ``day`` and ``week_day`` lookups are performed in UTC. This is a known - limitation of the current implementation. +.. fieldlookup:: hour + +hour +~~~~ + +.. versionadded:: 1.6 + +For datetime fields, an exact hour match. Takes an integer between 0 and 23. + +Example:: + + Event.objects.filter(timestamp__hour=23) + +SQL equivalent:: + + SELECT ... WHERE EXTRACT('hour' FROM timestamp) = '23'; + +(The exact SQL syntax varies for each database engine.) + +When :setting:`USE_TZ` is ``True``, values are converted to the current time +zone before filtering. + +.. fieldlookup:: minute + +minute +~~~~~~ + +.. versionadded:: 1.6 + +For datetime fields, an exact minute match. Takes an integer between 0 and 59. + +Example:: + + Event.objects.filter(timestamp__minute=29) + +SQL equivalent:: + + SELECT ... WHERE EXTRACT('minute' FROM timestamp) = '29'; + +(The exact SQL syntax varies for each database engine.) + +When :setting:`USE_TZ` is ``True``, values are converted to the current time +zone before filtering. + +.. fieldlookup:: second + +second +~~~~~~ + +.. versionadded:: 1.6 + +For datetime fields, an exact second match. Takes an integer between 0 and 59. + +Example:: + + Event.objects.filter(timestamp__second=31) + +SQL equivalent:: + + SELECT ... WHERE EXTRACT('second' FROM timestamp) = '31'; + +(The exact SQL syntax varies for each database engine.) + +When :setting:`USE_TZ` is ``True``, values are converted to the current time +zone before filtering. .. fieldlookup:: isnull diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 60537aca53..9594481b9f 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -30,6 +30,16 @@ prevention ` are turned on. If the default templates don't suit your tastes, you can use :ref:`custom project and app templates `. +Time zone aware aggregation +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The support for :doc:`time zones ` introduced in +Django 1.4 didn't work well with :meth:`QuerySet.dates() +`: aggregation was always performed in +UTC. This limitation was lifted in Django 1.6. Use :meth:`QuerySet.datetimes() +` to perform time zone aware +aggregation on a :class:`~django.db.models.DateTimeField`. + Minor features ~~~~~~~~~~~~~~ @@ -47,6 +57,9 @@ Minor features * Added :meth:`~django.db.models.query.QuerySet.earliest` for symmetry with :meth:`~django.db.models.query.QuerySet.latest`. +* In addition to :lookup:`year`, :lookup:`month` and :lookup:`day`, the ORM + now supports :lookup:`hour`, :lookup:`minute` and :lookup:`second` lookups. + * The default widgets for :class:`~django.forms.EmailField` and :class:`~django.forms.URLField` use the new type attributes available in HTML5 (type='email', type='url'). @@ -80,6 +93,28 @@ Backwards incompatible changes in 1.6 :meth:`~django.db.models.query.QuerySet.none` has been called: ``isinstance(qs.none(), EmptyQuerySet)`` +* :meth:`QuerySet.dates() ` raises an + error if it's used on :class:`~django.db.models.DateTimeField` when time + zone support is active. Use :meth:`QuerySet.datetimes() + ` instead. + +* :meth:`QuerySet.dates() ` returns a + list of :class:`~datetime.date`. It used to return a list of + :class:`~datetime.datetime`. + +* The :attr:`~django.contrib.admin.ModelAdmin.date_hierarchy` feature of the + admin on a :class:`~django.db.models.DateTimeField` requires time zone + definitions in the database when :setting:`USE_TZ` is ``True``. + :ref:`Learn more `. + +* Accessing ``date_list`` in the context of a date-based generic view requires + time zone definitions in the database when the view is based on a + :class:`~django.db.models.DateTimeField` and :setting:`USE_TZ` is ``True``. + :ref:`Learn more `. + +* Model fields named ``hour``, ``minute`` or ``second`` may clash with the new + lookups. Append an explicit :lookup:`exact` lookup if this is an issue. + * If your CSS/Javascript code used to access HTML input widgets by type, you should review it as ``type='text'`` widgets might be now output as ``type='email'`` or ``type='url'`` depending on their corresponding field type. diff --git a/tests/modeltests/aggregation/tests.py b/tests/modeltests/aggregation/tests.py index c23b32fc85..c635e6ebb6 100644 --- a/tests/modeltests/aggregation/tests.py +++ b/tests/modeltests/aggregation/tests.py @@ -579,9 +579,9 @@ class BaseAggregateTestCase(TestCase): dates = Book.objects.annotate(num_authors=Count("authors")).dates('pubdate', 'year') self.assertQuerysetEqual( dates, [ - "datetime.datetime(1991, 1, 1, 0, 0)", - "datetime.datetime(1995, 1, 1, 0, 0)", - "datetime.datetime(2007, 1, 1, 0, 0)", - "datetime.datetime(2008, 1, 1, 0, 0)" + "datetime.date(1991, 1, 1)", + "datetime.date(1995, 1, 1)", + "datetime.date(2007, 1, 1)", + "datetime.date(2008, 1, 1)" ] ) diff --git a/tests/modeltests/basic/tests.py b/tests/modeltests/basic/tests.py index 1ca4f20dac..e408df8d46 100644 --- a/tests/modeltests/basic/tests.py +++ b/tests/modeltests/basic/tests.py @@ -266,34 +266,34 @@ class ModelTest(TestCase): # ... but there will often be more efficient ways if that is all you need: self.assertTrue(Article.objects.filter(id=a8.id).exists()) - # dates() returns a list of available dates of the given scope for + # datetimes() returns a list of available dates of the given scope for # the given field. self.assertQuerysetEqual( - Article.objects.dates('pub_date', 'year'), + Article.objects.datetimes('pub_date', 'year'), ["datetime.datetime(2005, 1, 1, 0, 0)"]) self.assertQuerysetEqual( - Article.objects.dates('pub_date', 'month'), + Article.objects.datetimes('pub_date', 'month'), ["datetime.datetime(2005, 7, 1, 0, 0)"]) self.assertQuerysetEqual( - Article.objects.dates('pub_date', 'day'), + Article.objects.datetimes('pub_date', 'day'), ["datetime.datetime(2005, 7, 28, 0, 0)", "datetime.datetime(2005, 7, 29, 0, 0)", "datetime.datetime(2005, 7, 30, 0, 0)", "datetime.datetime(2005, 7, 31, 0, 0)"]) self.assertQuerysetEqual( - Article.objects.dates('pub_date', 'day', order='ASC'), + Article.objects.datetimes('pub_date', 'day', order='ASC'), ["datetime.datetime(2005, 7, 28, 0, 0)", "datetime.datetime(2005, 7, 29, 0, 0)", "datetime.datetime(2005, 7, 30, 0, 0)", "datetime.datetime(2005, 7, 31, 0, 0)"]) self.assertQuerysetEqual( - Article.objects.dates('pub_date', 'day', order='DESC'), + Article.objects.datetimes('pub_date', 'day', order='DESC'), ["datetime.datetime(2005, 7, 31, 0, 0)", "datetime.datetime(2005, 7, 30, 0, 0)", "datetime.datetime(2005, 7, 29, 0, 0)", "datetime.datetime(2005, 7, 28, 0, 0)"]) - # dates() requires valid arguments. + # datetimes() requires valid arguments. self.assertRaises( TypeError, Article.objects.dates, @@ -324,10 +324,10 @@ class ModelTest(TestCase): order="bad order", ) - # Use iterator() with dates() to return a generator that lazily + # Use iterator() with datetimes() to return a generator that lazily # requests each result one at a time, to save memory. dates = [] - for article in Article.objects.dates('pub_date', 'day', order='DESC').iterator(): + for article in Article.objects.datetimes('pub_date', 'day', order='DESC').iterator(): dates.append(article) self.assertEqual(dates, [ datetime(2005, 7, 31, 0, 0), diff --git a/tests/modeltests/many_to_one/tests.py b/tests/modeltests/many_to_one/tests.py index 44ae689dd4..a4f87a3283 100644 --- a/tests/modeltests/many_to_one/tests.py +++ b/tests/modeltests/many_to_one/tests.py @@ -1,7 +1,7 @@ from __future__ import absolute_import from copy import deepcopy -from datetime import datetime +import datetime from django.core.exceptions import MultipleObjectsReturned, FieldError from django.test import TestCase @@ -20,7 +20,7 @@ class ManyToOneTests(TestCase): self.r2.save() # Create an Article. self.a = Article(id=None, headline="This is a test", - pub_date=datetime(2005, 7, 27), reporter=self.r) + pub_date=datetime.date(2005, 7, 27), reporter=self.r) self.a.save() def test_get(self): @@ -36,25 +36,25 @@ class ManyToOneTests(TestCase): # You can also instantiate an Article by passing the Reporter's ID # instead of a Reporter object. a3 = Article(id=None, headline="Third article", - pub_date=datetime(2005, 7, 27), reporter_id=self.r.id) + pub_date=datetime.date(2005, 7, 27), reporter_id=self.r.id) a3.save() self.assertEqual(a3.reporter.id, self.r.id) # Similarly, the reporter ID can be a string. a4 = Article(id=None, headline="Fourth article", - pub_date=datetime(2005, 7, 27), reporter_id=str(self.r.id)) + pub_date=datetime.date(2005, 7, 27), reporter_id=str(self.r.id)) a4.save() self.assertEqual(repr(a4.reporter), "") def test_add(self): # Create an Article via the Reporter object. new_article = self.r.article_set.create(headline="John's second story", - pub_date=datetime(2005, 7, 29)) + pub_date=datetime.date(2005, 7, 29)) self.assertEqual(repr(new_article), "") self.assertEqual(new_article.reporter.id, self.r.id) # Create a new article, and add it to the article set. - new_article2 = Article(headline="Paul's story", pub_date=datetime(2006, 1, 17)) + new_article2 = Article(headline="Paul's story", pub_date=datetime.date(2006, 1, 17)) self.r.article_set.add(new_article2) self.assertEqual(new_article2.reporter.id, self.r.id) self.assertQuerysetEqual(self.r.article_set.all(), @@ -80,9 +80,9 @@ class ManyToOneTests(TestCase): def test_assign(self): new_article = self.r.article_set.create(headline="John's second story", - pub_date=datetime(2005, 7, 29)) + pub_date=datetime.date(2005, 7, 29)) new_article2 = self.r2.article_set.create(headline="Paul's story", - pub_date=datetime(2006, 1, 17)) + pub_date=datetime.date(2006, 1, 17)) # Assign the article to the reporter directly using the descriptor. new_article2.reporter = self.r new_article2.save() @@ -118,9 +118,9 @@ class ManyToOneTests(TestCase): def test_selects(self): new_article = self.r.article_set.create(headline="John's second story", - pub_date=datetime(2005, 7, 29)) + pub_date=datetime.date(2005, 7, 29)) new_article2 = self.r2.article_set.create(headline="Paul's story", - pub_date=datetime(2006, 1, 17)) + pub_date=datetime.date(2006, 1, 17)) # Reporter objects have access to their related Article objects. self.assertQuerysetEqual(self.r.article_set.all(), [ "", @@ -237,9 +237,9 @@ class ManyToOneTests(TestCase): def test_reverse_selects(self): a3 = Article.objects.create(id=None, headline="Third article", - pub_date=datetime(2005, 7, 27), reporter_id=self.r.id) + pub_date=datetime.date(2005, 7, 27), reporter_id=self.r.id) a4 = Article.objects.create(id=None, headline="Fourth article", - pub_date=datetime(2005, 7, 27), reporter_id=str(self.r.id)) + pub_date=datetime.date(2005, 7, 27), reporter_id=str(self.r.id)) # Reporters can be queried self.assertQuerysetEqual(Reporter.objects.filter(id__exact=self.r.id), [""]) @@ -316,33 +316,33 @@ class ManyToOneTests(TestCase): # objects (Reporters). r1 = Reporter.objects.create(first_name='Mike', last_name='Royko', email='royko@suntimes.com') r2 = Reporter.objects.create(first_name='John', last_name='Kass', email='jkass@tribune.com') - a1 = Article.objects.create(headline='First', pub_date=datetime(1980, 4, 23), reporter=r1) - a2 = Article.objects.create(headline='Second', pub_date=datetime(1980, 4, 23), reporter=r2) + Article.objects.create(headline='First', pub_date=datetime.date(1980, 4, 23), reporter=r1) + Article.objects.create(headline='Second', pub_date=datetime.date(1980, 4, 23), reporter=r2) self.assertEqual(list(Article.objects.select_related().dates('pub_date', 'day')), [ - datetime(1980, 4, 23, 0, 0), - datetime(2005, 7, 27, 0, 0), + datetime.date(1980, 4, 23), + datetime.date(2005, 7, 27), ]) self.assertEqual(list(Article.objects.select_related().dates('pub_date', 'month')), [ - datetime(1980, 4, 1, 0, 0), - datetime(2005, 7, 1, 0, 0), + datetime.date(1980, 4, 1), + datetime.date(2005, 7, 1), ]) self.assertEqual(list(Article.objects.select_related().dates('pub_date', 'year')), [ - datetime(1980, 1, 1, 0, 0), - datetime(2005, 1, 1, 0, 0), + datetime.date(1980, 1, 1), + datetime.date(2005, 1, 1), ]) def test_delete(self): new_article = self.r.article_set.create(headline="John's second story", - pub_date=datetime(2005, 7, 29)) + pub_date=datetime.date(2005, 7, 29)) new_article2 = self.r2.article_set.create(headline="Paul's story", - pub_date=datetime(2006, 1, 17)) + pub_date=datetime.date(2006, 1, 17)) a3 = Article.objects.create(id=None, headline="Third article", - pub_date=datetime(2005, 7, 27), reporter_id=self.r.id) + pub_date=datetime.date(2005, 7, 27), reporter_id=self.r.id) a4 = Article.objects.create(id=None, headline="Fourth article", - pub_date=datetime(2005, 7, 27), reporter_id=str(self.r.id)) + pub_date=datetime.date(2005, 7, 27), reporter_id=str(self.r.id)) # If you delete a reporter, his articles will be deleted. self.assertQuerysetEqual(Article.objects.all(), [ @@ -383,7 +383,7 @@ class ManyToOneTests(TestCase): # for a ForeignKey. a2, created = Article.objects.get_or_create(id=None, headline="John's second test", - pub_date=datetime(2011, 5, 7), + pub_date=datetime.date(2011, 5, 7), reporter_id=self.r.id) self.assertTrue(created) self.assertEqual(a2.reporter.id, self.r.id) @@ -398,7 +398,7 @@ class ManyToOneTests(TestCase): # Create an Article by Paul for the same date. a3 = Article.objects.create(id=None, headline="Paul's commentary", - pub_date=datetime(2011, 5, 7), + pub_date=datetime.date(2011, 5, 7), reporter_id=self.r2.id) self.assertEqual(a3.reporter.id, self.r2.id) @@ -407,7 +407,7 @@ class ManyToOneTests(TestCase): Article.objects.get, reporter_id=self.r.id) self.assertEqual(repr(a3), repr(Article.objects.get(reporter_id=self.r2.id, - pub_date=datetime(2011, 5, 7)))) + pub_date=datetime.date(2011, 5, 7)))) def test_manager_class_caching(self): r1 = Reporter.objects.create(first_name='Mike') @@ -425,7 +425,7 @@ class ManyToOneTests(TestCase): email='john.smith@example.com') lazy = ugettext_lazy('test') reporter.article_set.create(headline=lazy, - pub_date=datetime(2011, 6, 10)) + pub_date=datetime.date(2011, 6, 10)) notlazy = six.text_type(lazy) article = reporter.article_set.get() self.assertEqual(article.headline, notlazy) diff --git a/tests/modeltests/reserved_names/tests.py b/tests/modeltests/reserved_names/tests.py index 87f7a42ec4..ddffe08d34 100644 --- a/tests/modeltests/reserved_names/tests.py +++ b/tests/modeltests/reserved_names/tests.py @@ -42,8 +42,8 @@ class ReservedNameTests(TestCase): self.generate() resp = Thing.objects.dates('where', 'year') self.assertEqual(list(resp), [ - datetime.datetime(2005, 1, 1, 0, 0), - datetime.datetime(2006, 1, 1, 0, 0), + datetime.date(2005, 1, 1), + datetime.date(2006, 1, 1), ]) def test_month_filter(self): diff --git a/tests/modeltests/timezones/tests.py b/tests/modeltests/timezones/tests.py index 4ae6bbd6a8..8786c1912f 100644 --- a/tests/modeltests/timezones/tests.py +++ b/tests/modeltests/timezones/tests.py @@ -189,13 +189,16 @@ class LegacyDatabaseTests(TestCase): self.assertEqual(Event.objects.filter(dt__gte=dt2).count(), 1) self.assertEqual(Event.objects.filter(dt__gt=dt2).count(), 0) - def test_query_date_related_filters(self): + def test_query_datetime_lookups(self): Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0)) Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0)) self.assertEqual(Event.objects.filter(dt__year=2011).count(), 2) self.assertEqual(Event.objects.filter(dt__month=1).count(), 2) self.assertEqual(Event.objects.filter(dt__day=1).count(), 2) self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 2) + self.assertEqual(Event.objects.filter(dt__hour=1).count(), 1) + self.assertEqual(Event.objects.filter(dt__minute=30).count(), 2) + self.assertEqual(Event.objects.filter(dt__second=0).count(), 2) def test_query_aggregation(self): # Only min and max make sense for datetimes. @@ -230,15 +233,30 @@ class LegacyDatabaseTests(TestCase): [afternoon_min_dt], transform=lambda d: d.dt) - def test_query_dates(self): + def test_query_datetimes(self): Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0)) Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0)) - self.assertQuerysetEqual(Event.objects.dates('dt', 'year'), - [datetime.datetime(2011, 1, 1)], transform=lambda d: d) - self.assertQuerysetEqual(Event.objects.dates('dt', 'month'), - [datetime.datetime(2011, 1, 1)], transform=lambda d: d) - self.assertQuerysetEqual(Event.objects.dates('dt', 'day'), - [datetime.datetime(2011, 1, 1)], transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'year'), + [datetime.datetime(2011, 1, 1, 0, 0, 0)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'month'), + [datetime.datetime(2011, 1, 1, 0, 0, 0)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'day'), + [datetime.datetime(2011, 1, 1, 0, 0, 0)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'hour'), + [datetime.datetime(2011, 1, 1, 1, 0, 0), + datetime.datetime(2011, 1, 1, 4, 0, 0)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'minute'), + [datetime.datetime(2011, 1, 1, 1, 30, 0), + datetime.datetime(2011, 1, 1, 4, 30, 0)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'second'), + [datetime.datetime(2011, 1, 1, 1, 30, 0), + datetime.datetime(2011, 1, 1, 4, 30, 0)], + transform=lambda d: d) def test_raw_sql(self): # Regression test for #17755 @@ -398,17 +416,32 @@ class NewDatabaseTests(TestCase): msg = str(warning.message) self.assertTrue(msg.startswith("DateTimeField received a naive datetime")) - def test_query_date_related_filters(self): - # These two dates fall in the same day in EAT, but in different days, - # years and months in UTC, and aggregation is performed in UTC when - # time zone support is enabled. This test could be changed if the - # implementation is changed to perform the aggregation is local time. + @skipUnlessDBFeature('has_zoneinfo_database') + def test_query_datetime_lookups(self): Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT)) Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT)) - self.assertEqual(Event.objects.filter(dt__year=2011).count(), 1) - self.assertEqual(Event.objects.filter(dt__month=1).count(), 1) - self.assertEqual(Event.objects.filter(dt__day=1).count(), 1) - self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 1) + self.assertEqual(Event.objects.filter(dt__year=2011).count(), 2) + self.assertEqual(Event.objects.filter(dt__month=1).count(), 2) + self.assertEqual(Event.objects.filter(dt__day=1).count(), 2) + self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 2) + self.assertEqual(Event.objects.filter(dt__hour=1).count(), 1) + self.assertEqual(Event.objects.filter(dt__minute=30).count(), 2) + self.assertEqual(Event.objects.filter(dt__second=0).count(), 2) + + @skipUnlessDBFeature('has_zoneinfo_database') + def test_query_datetime_lookups_in_other_timezone(self): + Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT)) + Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT)) + with timezone.override(UTC): + # These two dates fall in the same day in EAT, but in different days, + # years and months in UTC. + self.assertEqual(Event.objects.filter(dt__year=2011).count(), 1) + self.assertEqual(Event.objects.filter(dt__month=1).count(), 1) + self.assertEqual(Event.objects.filter(dt__day=1).count(), 1) + self.assertEqual(Event.objects.filter(dt__week_day=7).count(), 1) + self.assertEqual(Event.objects.filter(dt__hour=22).count(), 1) + self.assertEqual(Event.objects.filter(dt__minute=30).count(), 2) + self.assertEqual(Event.objects.filter(dt__second=0).count(), 2) def test_query_aggregation(self): # Only min and max make sense for datetimes. @@ -443,22 +476,61 @@ class NewDatabaseTests(TestCase): [afternoon_min_dt], transform=lambda d: d.dt) - def test_query_dates(self): - # Same comment as in test_query_date_related_filters. + @skipUnlessDBFeature('has_zoneinfo_database') + def test_query_datetimes(self): Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT)) Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT)) - self.assertQuerysetEqual(Event.objects.dates('dt', 'year'), - [datetime.datetime(2010, 1, 1, tzinfo=UTC), - datetime.datetime(2011, 1, 1, tzinfo=UTC)], + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'year'), + [datetime.datetime(2011, 1, 1, 0, 0, 0, tzinfo=EAT)], transform=lambda d: d) - self.assertQuerysetEqual(Event.objects.dates('dt', 'month'), - [datetime.datetime(2010, 12, 1, tzinfo=UTC), - datetime.datetime(2011, 1, 1, tzinfo=UTC)], + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'month'), + [datetime.datetime(2011, 1, 1, 0, 0, 0, tzinfo=EAT)], transform=lambda d: d) - self.assertQuerysetEqual(Event.objects.dates('dt', 'day'), - [datetime.datetime(2010, 12, 31, tzinfo=UTC), - datetime.datetime(2011, 1, 1, tzinfo=UTC)], + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'day'), + [datetime.datetime(2011, 1, 1, 0, 0, 0, tzinfo=EAT)], transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'hour'), + [datetime.datetime(2011, 1, 1, 1, 0, 0, tzinfo=EAT), + datetime.datetime(2011, 1, 1, 4, 0, 0, tzinfo=EAT)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'minute'), + [datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT), + datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'second'), + [datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT), + datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT)], + transform=lambda d: d) + + @skipUnlessDBFeature('has_zoneinfo_database') + def test_query_datetimes_in_other_timezone(self): + Event.objects.create(dt=datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=EAT)) + Event.objects.create(dt=datetime.datetime(2011, 1, 1, 4, 30, 0, tzinfo=EAT)) + with timezone.override(UTC): + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'year'), + [datetime.datetime(2010, 1, 1, 0, 0, 0, tzinfo=UTC), + datetime.datetime(2011, 1, 1, 0, 0, 0, tzinfo=UTC)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'month'), + [datetime.datetime(2010, 12, 1, 0, 0, 0, tzinfo=UTC), + datetime.datetime(2011, 1, 1, 0, 0, 0, tzinfo=UTC)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'day'), + [datetime.datetime(2010, 12, 31, 0, 0, 0, tzinfo=UTC), + datetime.datetime(2011, 1, 1, 0, 0, 0, tzinfo=UTC)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'hour'), + [datetime.datetime(2010, 12, 31, 22, 0, 0, tzinfo=UTC), + datetime.datetime(2011, 1, 1, 1, 0, 0, tzinfo=UTC)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'minute'), + [datetime.datetime(2010, 12, 31, 22, 30, 0, tzinfo=UTC), + datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=UTC)], + transform=lambda d: d) + self.assertQuerysetEqual(Event.objects.datetimes('dt', 'second'), + [datetime.datetime(2010, 12, 31, 22, 30, 0, tzinfo=UTC), + datetime.datetime(2011, 1, 1, 1, 30, 0, tzinfo=UTC)], + transform=lambda d: d) def test_raw_sql(self): # Regression test for #17755 diff --git a/tests/regressiontests/aggregation_regress/tests.py b/tests/regressiontests/aggregation_regress/tests.py index 596ebbfaec..076567538b 100644 --- a/tests/regressiontests/aggregation_regress/tests.py +++ b/tests/regressiontests/aggregation_regress/tests.py @@ -546,8 +546,8 @@ class AggregationTests(TestCase): qs = Book.objects.annotate(num_authors=Count('authors')).filter(num_authors=2).dates('pubdate', 'day') self.assertQuerysetEqual( qs, [ - datetime.datetime(1995, 1, 15, 0, 0), - datetime.datetime(2007, 12, 6, 0, 0) + datetime.date(1995, 1, 15), + datetime.date(2007, 12, 6), ], lambda b: b ) diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index 313fdc8351..fbe5026e12 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -144,11 +144,11 @@ class DateQuotingTest(TestCase): updated = datetime.datetime(2010, 2, 20) models.SchoolClass.objects.create(year=2009, last_updated=updated) years = models.SchoolClass.objects.dates('last_updated', 'year') - self.assertEqual(list(years), [datetime.datetime(2010, 1, 1, 0, 0)]) + self.assertEqual(list(years), [datetime.date(2010, 1, 1)]) - def test_django_extract(self): + def test_django_date_extract(self): """ - Test the custom ``django_extract method``, in particular against fields + Test the custom ``django_date_extract method``, in particular against fields which clash with strings passed to it (e.g. 'day') - see #12818__. __: http://code.djangoproject.com/ticket/12818 diff --git a/tests/regressiontests/dates/models.py b/tests/regressiontests/dates/models.py index e4bffb7199..23350755e7 100644 --- a/tests/regressiontests/dates/models.py +++ b/tests/regressiontests/dates/models.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db import models from django.utils.encoding import python_2_unicode_compatible diff --git a/tests/regressiontests/dates/tests.py b/tests/regressiontests/dates/tests.py index de28cac436..6c02d597de 100644 --- a/tests/regressiontests/dates/tests.py +++ b/tests/regressiontests/dates/tests.py @@ -1,6 +1,6 @@ from __future__ import absolute_import -from datetime import datetime +import datetime from django.test import TestCase @@ -11,32 +11,32 @@ class DatesTests(TestCase): def test_related_model_traverse(self): a1 = Article.objects.create( title="First one", - pub_date=datetime(2005, 7, 28), + pub_date=datetime.date(2005, 7, 28), ) a2 = Article.objects.create( title="Another one", - pub_date=datetime(2010, 7, 28), + pub_date=datetime.date(2010, 7, 28), ) a3 = Article.objects.create( title="Third one, in the first day", - pub_date=datetime(2005, 7, 28), + pub_date=datetime.date(2005, 7, 28), ) a1.comments.create( text="Im the HULK!", - pub_date=datetime(2005, 7, 28), + pub_date=datetime.date(2005, 7, 28), ) a1.comments.create( text="HULK SMASH!", - pub_date=datetime(2005, 7, 29), + pub_date=datetime.date(2005, 7, 29), ) a2.comments.create( text="LMAO", - pub_date=datetime(2010, 7, 28), + pub_date=datetime.date(2010, 7, 28), ) a3.comments.create( text="+1", - pub_date=datetime(2005, 8, 29), + pub_date=datetime.date(2005, 8, 29), ) c = Category.objects.create(name="serious-news") @@ -44,31 +44,31 @@ class DatesTests(TestCase): self.assertQuerysetEqual( Comment.objects.dates("article__pub_date", "year"), [ - datetime(2005, 1, 1), - datetime(2010, 1, 1), + datetime.date(2005, 1, 1), + datetime.date(2010, 1, 1), ], lambda d: d, ) self.assertQuerysetEqual( Comment.objects.dates("article__pub_date", "month"), [ - datetime(2005, 7, 1), - datetime(2010, 7, 1), + datetime.date(2005, 7, 1), + datetime.date(2010, 7, 1), ], lambda d: d ) self.assertQuerysetEqual( Comment.objects.dates("article__pub_date", "day"), [ - datetime(2005, 7, 28), - datetime(2010, 7, 28), + datetime.date(2005, 7, 28), + datetime.date(2010, 7, 28), ], lambda d: d ) self.assertQuerysetEqual( Article.objects.dates("comments__pub_date", "day"), [ - datetime(2005, 7, 28), - datetime(2005, 7, 29), - datetime(2005, 8, 29), - datetime(2010, 7, 28), + datetime.date(2005, 7, 28), + datetime.date(2005, 7, 29), + datetime.date(2005, 8, 29), + datetime.date(2010, 7, 28), ], lambda d: d ) @@ -77,7 +77,7 @@ class DatesTests(TestCase): ) self.assertQuerysetEqual( Category.objects.dates("articles__pub_date", "day"), [ - datetime(2005, 7, 28), + datetime.date(2005, 7, 28), ], lambda d: d, ) diff --git a/tests/regressiontests/datetimes/__init__.py b/tests/regressiontests/datetimes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/datetimes/models.py b/tests/regressiontests/datetimes/models.py new file mode 100644 index 0000000000..f21376aa1c --- /dev/null +++ b/tests/regressiontests/datetimes/models.py @@ -0,0 +1,28 @@ +from __future__ import unicode_literals + +from django.db import models +from django.utils.encoding import python_2_unicode_compatible + + +@python_2_unicode_compatible +class Article(models.Model): + title = models.CharField(max_length=100) + pub_date = models.DateTimeField() + + categories = models.ManyToManyField("Category", related_name="articles") + + def __str__(self): + return self.title + +@python_2_unicode_compatible +class Comment(models.Model): + article = models.ForeignKey(Article, related_name="comments") + text = models.TextField() + pub_date = models.DateTimeField() + approval_date = models.DateTimeField(null=True) + + def __str__(self): + return 'Comment to %s (%s)' % (self.article.title, self.pub_date) + +class Category(models.Model): + name = models.CharField(max_length=255) diff --git a/tests/regressiontests/datetimes/tests.py b/tests/regressiontests/datetimes/tests.py new file mode 100644 index 0000000000..58cb060f6b --- /dev/null +++ b/tests/regressiontests/datetimes/tests.py @@ -0,0 +1,83 @@ +from __future__ import absolute_import + +import datetime + +from django.test import TestCase + +from .models import Article, Comment, Category + + +class DateTimesTests(TestCase): + def test_related_model_traverse(self): + a1 = Article.objects.create( + title="First one", + pub_date=datetime.datetime(2005, 7, 28, 9, 0, 0), + ) + a2 = Article.objects.create( + title="Another one", + pub_date=datetime.datetime(2010, 7, 28, 10, 0, 0), + ) + a3 = Article.objects.create( + title="Third one, in the first day", + pub_date=datetime.datetime(2005, 7, 28, 17, 0, 0), + ) + + a1.comments.create( + text="Im the HULK!", + pub_date=datetime.datetime(2005, 7, 28, 9, 30, 0), + ) + a1.comments.create( + text="HULK SMASH!", + pub_date=datetime.datetime(2005, 7, 29, 1, 30, 0), + ) + a2.comments.create( + text="LMAO", + pub_date=datetime.datetime(2010, 7, 28, 10, 10, 10), + ) + a3.comments.create( + text="+1", + pub_date=datetime.datetime(2005, 8, 29, 10, 10, 10), + ) + + c = Category.objects.create(name="serious-news") + c.articles.add(a1, a3) + + self.assertQuerysetEqual( + Comment.objects.datetimes("article__pub_date", "year"), [ + datetime.datetime(2005, 1, 1), + datetime.datetime(2010, 1, 1), + ], + lambda d: d, + ) + self.assertQuerysetEqual( + Comment.objects.datetimes("article__pub_date", "month"), [ + datetime.datetime(2005, 7, 1), + datetime.datetime(2010, 7, 1), + ], + lambda d: d + ) + self.assertQuerysetEqual( + Comment.objects.datetimes("article__pub_date", "day"), [ + datetime.datetime(2005, 7, 28), + datetime.datetime(2010, 7, 28), + ], + lambda d: d + ) + self.assertQuerysetEqual( + Article.objects.datetimes("comments__pub_date", "day"), [ + datetime.datetime(2005, 7, 28), + datetime.datetime(2005, 7, 29), + datetime.datetime(2005, 8, 29), + datetime.datetime(2010, 7, 28), + ], + lambda d: d + ) + self.assertQuerysetEqual( + Article.objects.datetimes("comments__approval_date", "day"), [] + ) + self.assertQuerysetEqual( + Category.objects.datetimes("articles__pub_date", "day"), [ + datetime.datetime(2005, 7, 28), + ], + lambda d: d, + ) diff --git a/tests/regressiontests/extra_regress/tests.py b/tests/regressiontests/extra_regress/tests.py index 1bc6789edd..194b250c99 100644 --- a/tests/regressiontests/extra_regress/tests.py +++ b/tests/regressiontests/extra_regress/tests.py @@ -166,8 +166,9 @@ class ExtraRegressTests(TestCase): ) self.assertQuerysetEqual( - RevisionableModel.objects.extra(select={"the_answer": 'id'}).dates('when', 'month'), - ['datetime.datetime(2008, 9, 1, 0, 0)'] + RevisionableModel.objects.extra(select={"the_answer": 'id'}).datetimes('when', 'month'), + [datetime.datetime(2008, 9, 1, 0, 0)], + transform=lambda d: d, ) def test_values_with_extra(self): diff --git a/tests/regressiontests/generic_views/dates.py b/tests/regressiontests/generic_views/dates.py index 0c565daf9f..844b10bbcc 100644 --- a/tests/regressiontests/generic_views/dates.py +++ b/tests/regressiontests/generic_views/dates.py @@ -4,7 +4,7 @@ import time import datetime from django.core.exceptions import ImproperlyConfigured -from django.test import TestCase +from django.test import TestCase, skipUnlessDBFeature from django.test.utils import override_settings from django.utils import timezone from django.utils.unittest import skipUnless @@ -119,6 +119,7 @@ class ArchiveIndexViewTests(TestCase): self.assertEqual(res.status_code, 200) @requires_tz_support + @skipUnlessDBFeature('has_zoneinfo_database') @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_archive_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) @@ -140,7 +141,7 @@ class YearArchiveViewTests(TestCase): def test_year_view(self): res = self.client.get('/dates/books/2008/') self.assertEqual(res.status_code, 200) - self.assertEqual(list(res.context['date_list']), [datetime.datetime(2008, 10, 1)]) + self.assertEqual(list(res.context['date_list']), [datetime.date(2008, 10, 1)]) self.assertEqual(res.context['year'], datetime.date(2008, 1, 1)) self.assertTemplateUsed(res, 'generic_views/book_archive_year.html') @@ -151,7 +152,7 @@ class YearArchiveViewTests(TestCase): def test_year_view_make_object_list(self): res = self.client.get('/dates/books/2006/make_object_list/') self.assertEqual(res.status_code, 200) - self.assertEqual(list(res.context['date_list']), [datetime.datetime(2006, 5, 1)]) + self.assertEqual(list(res.context['date_list']), [datetime.date(2006, 5, 1)]) self.assertEqual(list(res.context['book_list']), list(Book.objects.filter(pubdate__year=2006))) self.assertEqual(list(res.context['object_list']), list(Book.objects.filter(pubdate__year=2006))) self.assertTemplateUsed(res, 'generic_views/book_archive_year.html') @@ -181,7 +182,7 @@ class YearArchiveViewTests(TestCase): res = self.client.get('/dates/books/%s/allow_future/' % year) self.assertEqual(res.status_code, 200) - self.assertEqual(list(res.context['date_list']), [datetime.datetime(year, 1, 1)]) + self.assertEqual(list(res.context['date_list']), [datetime.date(year, 1, 1)]) def test_year_view_paginated(self): res = self.client.get('/dates/books/2006/paginated/') @@ -204,6 +205,7 @@ class YearArchiveViewTests(TestCase): res = self.client.get('/dates/booksignings/2008/') self.assertEqual(res.status_code, 200) + @skipUnlessDBFeature('has_zoneinfo_database') @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_year_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) @@ -225,7 +227,7 @@ class MonthArchiveViewTests(TestCase): res = self.client.get('/dates/books/2008/oct/') self.assertEqual(res.status_code, 200) self.assertTemplateUsed(res, 'generic_views/book_archive_month.html') - self.assertEqual(list(res.context['date_list']), [datetime.datetime(2008, 10, 1)]) + self.assertEqual(list(res.context['date_list']), [datetime.date(2008, 10, 1)]) self.assertEqual(list(res.context['book_list']), list(Book.objects.filter(pubdate=datetime.date(2008, 10, 1)))) self.assertEqual(res.context['month'], datetime.date(2008, 10, 1)) @@ -268,7 +270,7 @@ class MonthArchiveViewTests(TestCase): # allow_future = True, valid future month res = self.client.get('/dates/books/%s/allow_future/' % urlbit) self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'][0].date(), b.pubdate) + self.assertEqual(res.context['date_list'][0], b.pubdate) self.assertEqual(list(res.context['book_list']), [b]) self.assertEqual(res.context['month'], future) @@ -328,6 +330,7 @@ class MonthArchiveViewTests(TestCase): res = self.client.get('/dates/booksignings/2008/apr/') self.assertEqual(res.status_code, 200) + @skipUnlessDBFeature('has_zoneinfo_database') @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_month_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 2, 1, 12, 0, tzinfo=timezone.utc)) diff --git a/tests/regressiontests/model_inheritance_regress/tests.py b/tests/regressiontests/model_inheritance_regress/tests.py index 6855d70071..8f741bbb7f 100644 --- a/tests/regressiontests/model_inheritance_regress/tests.py +++ b/tests/regressiontests/model_inheritance_regress/tests.py @@ -134,8 +134,8 @@ class ModelInheritanceTest(TestCase): obj = Child.objects.create( name='child', created=datetime.datetime(2008, 6, 26, 17, 0, 0)) - dates = list(Child.objects.dates('created', 'month')) - self.assertEqual(dates, [datetime.datetime(2008, 6, 1, 0, 0)]) + datetimes = list(Child.objects.datetimes('created', 'month')) + self.assertEqual(datetimes, [datetime.datetime(2008, 6, 1, 0, 0)]) def test_issue_7276(self): # Regression test for #7276: calling delete() on a model with diff --git a/tests/regressiontests/null_queries/models.py b/tests/regressiontests/null_queries/models.py index 25560fbab7..9070dd4873 100644 --- a/tests/regressiontests/null_queries/models.py +++ b/tests/regressiontests/null_queries/models.py @@ -28,4 +28,5 @@ class OuterB(models.Model): class Inner(models.Model): first = models.ForeignKey(OuterA) - second = models.ForeignKey(OuterB, null=True) + # second would clash with the __second lookup. + third = models.ForeignKey(OuterB, null=True) diff --git a/tests/regressiontests/null_queries/tests.py b/tests/regressiontests/null_queries/tests.py index 47c99fbcb3..93e72d55d8 100644 --- a/tests/regressiontests/null_queries/tests.py +++ b/tests/regressiontests/null_queries/tests.py @@ -55,17 +55,17 @@ class NullQueriesTests(TestCase): """ obj = OuterA.objects.create() self.assertQuerysetEqual( - OuterA.objects.filter(inner__second=None), + OuterA.objects.filter(inner__third=None), [''] ) self.assertQuerysetEqual( - OuterA.objects.filter(inner__second__data=None), + OuterA.objects.filter(inner__third__data=None), [''] ) inner_obj = Inner.objects.create(first=obj) self.assertQuerysetEqual( - Inner.objects.filter(first__inner__second=None), + Inner.objects.filter(first__inner__third=None), [''] ) diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py index 9d223970a0..ea54d18451 100644 --- a/tests/regressiontests/queries/tests.py +++ b/tests/regressiontests/queries/tests.py @@ -550,37 +550,37 @@ class Queries1Tests(BaseQuerysetTest): def test_tickets_6180_6203(self): # Dates with limits and/or counts self.assertEqual(Item.objects.count(), 4) - self.assertEqual(Item.objects.dates('created', 'month').count(), 1) - self.assertEqual(Item.objects.dates('created', 'day').count(), 2) - self.assertEqual(len(Item.objects.dates('created', 'day')), 2) - self.assertEqual(Item.objects.dates('created', 'day')[0], datetime.datetime(2007, 12, 19, 0, 0)) + self.assertEqual(Item.objects.datetimes('created', 'month').count(), 1) + self.assertEqual(Item.objects.datetimes('created', 'day').count(), 2) + self.assertEqual(len(Item.objects.datetimes('created', 'day')), 2) + self.assertEqual(Item.objects.datetimes('created', 'day')[0], datetime.datetime(2007, 12, 19, 0, 0)) def test_tickets_7087_12242(self): # Dates with extra select columns self.assertQuerysetEqual( - Item.objects.dates('created', 'day').extra(select={'a': 1}), + Item.objects.datetimes('created', 'day').extra(select={'a': 1}), ['datetime.datetime(2007, 12, 19, 0, 0)', 'datetime.datetime(2007, 12, 20, 0, 0)'] ) self.assertQuerysetEqual( - Item.objects.extra(select={'a': 1}).dates('created', 'day'), + Item.objects.extra(select={'a': 1}).datetimes('created', 'day'), ['datetime.datetime(2007, 12, 19, 0, 0)', 'datetime.datetime(2007, 12, 20, 0, 0)'] ) name="one" self.assertQuerysetEqual( - Item.objects.dates('created', 'day').extra(where=['name=%s'], params=[name]), + Item.objects.datetimes('created', 'day').extra(where=['name=%s'], params=[name]), ['datetime.datetime(2007, 12, 19, 0, 0)'] ) self.assertQuerysetEqual( - Item.objects.extra(where=['name=%s'], params=[name]).dates('created', 'day'), + Item.objects.extra(where=['name=%s'], params=[name]).datetimes('created', 'day'), ['datetime.datetime(2007, 12, 19, 0, 0)'] ) def test_ticket7155(self): # Nullable dates self.assertQuerysetEqual( - Item.objects.dates('modified', 'day'), + Item.objects.datetimes('modified', 'day'), ['datetime.datetime(2007, 12, 19, 0, 0)'] ) @@ -699,7 +699,7 @@ class Queries1Tests(BaseQuerysetTest): ) # Pickling of DateQuerySets used to fail - qs = Item.objects.dates('created', 'month') + qs = Item.objects.datetimes('created', 'month') _ = pickle.loads(pickle.dumps(qs)) def test_ticket9997(self): @@ -1235,8 +1235,8 @@ class Queries3Tests(BaseQuerysetTest): # field self.assertRaisesMessage( AssertionError, - "'name' isn't a DateField.", - Item.objects.dates, 'name', 'month' + "'name' isn't a DateTimeField.", + Item.objects.datetimes, 'name', 'month' ) class Queries4Tests(BaseQuerysetTest): From 632361611c6386696dc525ad3aecf065e6ed98ee Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 16 Feb 2013 13:26:36 +0100 Subject: [PATCH 337/870] Fixed #19833 -- Fixed import parameter encoding in get_runner Thanks Danilo Bargen for the report. --- django/test/utils.py | 7 ++++--- tests/regressiontests/test_runner/tests.py | 7 ++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/django/test/utils.py b/django/test/utils.py index 9413ea8dc4..a8ed3d6317 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -4,12 +4,13 @@ from xml.dom.minidom import parseString, Node from django.conf import settings, UserSettingsHolder from django.core import mail -from django.test.signals import template_rendered, setting_changed from django.template import Template, loader, TemplateDoesNotExist from django.template.loaders import cached -from django.utils.translation import deactivate +from django.test.signals import template_rendered, setting_changed +from django.utils.encoding import force_str from django.utils.functional import wraps from django.utils import six +from django.utils.translation import deactivate __all__ = ( @@ -133,7 +134,7 @@ def get_runner(settings, test_runner_class=None): test_module_name = '.'.join(test_path[:-1]) else: test_module_name = '.' - test_module = __import__(test_module_name, {}, {}, test_path[-1]) + test_module = __import__(test_module_name, {}, {}, force_str(test_path[-1])) test_runner = getattr(test_module, test_path[-1]) return test_runner diff --git a/tests/regressiontests/test_runner/tests.py b/tests/regressiontests/test_runner/tests.py index 93eabf74e3..5df421c54b 100644 --- a/tests/regressiontests/test_runner/tests.py +++ b/tests/regressiontests/test_runner/tests.py @@ -1,7 +1,7 @@ """ Tests for django test runner """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import sys from optparse import make_option @@ -150,6 +150,11 @@ class ManageCommandTests(unittest.TestCase): self.assertTrue(MockTestRunner.invoked, "The custom test runner has not been invoked") + def test_bad_test_runner(self): + with self.assertRaises(AttributeError): + call_command('test', 'sites', + testrunner='regressiontests.test_runner.NonExistentRunner') + class CustomOptionsTestRunner(simple.DjangoTestSuiteRunner): option_list = ( From 976dc07bafbd64f08c78ad6b1a4cbec5be9c85f4 Mon Sep 17 00:00:00 2001 From: Alex Hunley Date: Sat, 16 Feb 2013 14:30:55 -0500 Subject: [PATCH 338/870] Removed a misleading examples from documentations ala ticket #19719 --- docs/topics/forms/modelforms.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index d9e00d86cf..fec0d14836 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -222,11 +222,6 @@ supplied, ``save()`` will update that instance. If it's not supplied, # Save a new Article object from the form's data. >>> new_article = f.save() - # Create a form to edit an existing Article. - >>> a = Article.objects.get(pk=1) - >>> f = ArticleForm(instance=a) - >>> f.save() - # Create a form to edit an existing Article, but use # POST data to populate the form. >>> a = Article.objects.get(pk=1) From 7a80904b002a1983282c7dfa3ac05046098242ce Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 16 Feb 2013 18:23:39 -0500 Subject: [PATCH 339/870] Fixed #19812 - Removed a duplicate phrase in the widget docs. Thanks diegueus9 for the report and itsallvoodoo for the draft patch. --- docs/ref/forms/widgets.txt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index cb5224fd3c..970901a9ae 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -279,15 +279,10 @@ foundation for custom widgets. * A single value (e.g., a string) that is the "compressed" representation of a ``list`` of values. - If `value` is a list, output of :meth:`~MultiWidget.render` will be a - concatenation of rendered child widgets. If `value` is not a list, it - will be first processed by the method :meth:`~MultiWidget.decompress()` - to create the list and then processed as above. - - In the second case -- i.e., if the value is *not* a list -- - ``render()`` will first decompress the value into a ``list`` before - rendering it. It does so by calling the ``decompress()`` method, which - :class:`MultiWidget`'s subclasses must implement (see above). + If ``value`` is a list, the output of :meth:`~MultiWidget.render` will + be a concatenation of rendered child widgets. If ``value`` is not a + list, it will first be processed by the method + :meth:`~MultiWidget.decompress()` to create the list and then rendered. When ``render()`` executes its HTML rendering, each value in the list is rendered with the corresponding widget -- the first value is From 218bbef0c4890b3b853dee945a02215533b923b7 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 16 Feb 2013 18:31:54 -0500 Subject: [PATCH 340/870] Fixed #19824 - Corrected the class described for Field.primary_key from IntegerField to AutoField. Thanks Keryn Knight. --- docs/ref/models/fields.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 77b838622b..33ee05dd85 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -248,8 +248,8 @@ Alternatively you can use plain text and If ``True``, this field is the primary key for the model. -If you don't specify ``primary_key=True`` for any fields in your model, Django -will automatically add an :class:`IntegerField` to hold the primary key, so you +If you don't specify ``primary_key=True`` for any field in your model, Django +will automatically add an :class:`AutoField` to hold the primary key, so you don't need to set ``primary_key=True`` on any of your fields unless you want to override the default primary-key behavior. For more, see :ref:`automatic-primary-key-fields`. From 9c2066d567492a4a285c053039f671a2ca4a23d4 Mon Sep 17 00:00:00 2001 From: Simon Meers Date: Mon, 18 Feb 2013 00:33:29 +1100 Subject: [PATCH 341/870] Corrected INSTALLED_APPS syntax in 1.5 release notes. --- docs/releases/1.5.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index acf4f153ce..8813313035 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -662,7 +662,7 @@ Miscellaneous :doc:`django.contrib.redirects ` without enabling :doc:`django.contrib.sites `. This isn't allowed any longer. If you're using ``django.contrib.redirects``, make sure - :setting:``INSTALLED_APPS`` contains ``django.contrib.sites``. + :setting:`INSTALLED_APPS` contains ``django.contrib.sites``. Features deprecated in 1.5 ========================== From 5ec0405a09c4b05b7ee6c58bf52a06290143d788 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 18 Feb 2013 09:20:26 +0100 Subject: [PATCH 342/870] Fixed #19839 -- Isolated auth tests from customized TEMPLATE_LOADERS Thanks limscoder for the report. --- .../contrib/auth/tests/context_processors.py | 5 ++-- django/contrib/auth/tests/forms.py | 26 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/django/contrib/auth/tests/context_processors.py b/django/contrib/auth/tests/context_processors.py index f846a828dd..9e56cfce85 100644 --- a/django/contrib/auth/tests/context_processors.py +++ b/django/contrib/auth/tests/context_processors.py @@ -63,9 +63,10 @@ class PermWrapperTests(TestCase): @skipIfCustomUser @override_settings( + TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',), TEMPLATE_DIRS=( - os.path.join(os.path.dirname(upath(__file__)), 'templates'), - ), + os.path.join(os.path.dirname(upath(__file__)), 'templates'), + ), USE_TZ=False, # required for loading the fixture PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',), ) diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index 0c0973d543..c5a3fec7ce 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -341,18 +341,22 @@ class PasswordResetFormTest(TestCase): self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data['email'], email) + @override_settings( + TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',), + TEMPLATE_DIRS=( + os.path.join(os.path.dirname(upath(__file__)), 'templates'), + ), + ) def test_custom_email_subject(self): - template_path = os.path.join(os.path.dirname(upath(__file__)), 'templates') - with self.settings(TEMPLATE_DIRS=(template_path,)): - data = {'email': 'testclient@example.com'} - form = PasswordResetForm(data) - self.assertTrue(form.is_valid()) - # Since we're not providing a request object, we must provide a - # domain_override to prevent the save operation from failing in the - # potential case where contrib.sites is not installed. Refs #16412. - form.save(domain_override='example.com') - self.assertEqual(len(mail.outbox), 1) - self.assertEqual(mail.outbox[0].subject, 'Custom password reset on example.com') + data = {'email': 'testclient@example.com'} + form = PasswordResetForm(data) + self.assertTrue(form.is_valid()) + # Since we're not providing a request object, we must provide a + # domain_override to prevent the save operation from failing in the + # potential case where contrib.sites is not installed. Refs #16412. + form.save(domain_override='example.com') + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, 'Custom password reset on example.com') def test_bug_5605(self): # bug #5605, preserve the case of the user name (before the @ in the From 09ca0107689c6219ba311be58134407e49125235 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 18 Feb 2013 11:38:21 +0100 Subject: [PATCH 343/870] Removed an unecessary function. It was introduced by the refactoring in 5a4e63e6 and made redundant by the refactoring in 18934677. --- django/db/backends/sqlite3/base.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 3b4ff4c5dd..dd87972d5b 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -344,15 +344,13 @@ class DatabaseWrapper(BaseDatabaseWrapper): def init_connection_state(self): pass - def _sqlite_create_connection(self): - conn_params = self.get_connection_params() - self.connection = self.get_new_connection(conn_params) - self.init_connection_state() - connection_created.send(sender=self.__class__, connection=self) - def _cursor(self): if self.connection is None: - self._sqlite_create_connection() + conn_params = self.get_connection_params() + self.connection = self.get_new_connection(conn_params) + self.init_connection_state() + connection_created.send(sender=self.__class__, connection=self) + return self.connection.cursor(factory=SQLiteCursorWrapper) def check_constraints(self, table_names=None): From 92837ae56999a6f602d22130bfb3c49cd9b40242 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 18 Feb 2013 14:26:33 +0100 Subject: [PATCH 344/870] Avoided firing the request_finished signal in tests. * Avoided calling BaseHttpResponse.close(). The test client take care of that since acc5396e. * Disconnected the request_finished signal when this method must be called. The test client has a similar implementation since bacb097a. --- tests/regressiontests/httpwrappers/tests.py | 11 +++++++++++ tests/regressiontests/serializers_regress/tests.py | 1 - tests/regressiontests/views/tests/static.py | 6 ------ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index 2d3240915e..2964a86034 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -7,6 +7,8 @@ import pickle import warnings from django.core.exceptions import SuspiciousOperation +from django.core.signals import request_finished +from django.db import close_connection from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseNotAllowed, HttpResponseNotModified, StreamingHttpResponse, @@ -484,6 +486,15 @@ class StreamingHttpResponseTests(TestCase): r.tell() class FileCloseTests(TestCase): + + def setUp(self): + # Disable the request_finished signal during this test + # to avoid interfering with the database connection. + request_finished.disconnect(close_connection) + + def tearDown(self): + request_finished.connect(close_connection) + def test_response(self): filename = os.path.join(os.path.dirname(upath(__file__)), 'abc.txt') diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py index 2583a1ea04..67aec08af2 100644 --- a/tests/regressiontests/serializers_regress/tests.py +++ b/tests/regressiontests/serializers_regress/tests.py @@ -506,7 +506,6 @@ def streamTest(format, self): self.assertEqual(string_data, stream.getvalue()) else: self.assertEqual(string_data, stream.content.decode('utf-8')) - stream.close() for format in serializers.get_serializer_formats(): setattr(SerializerTests, 'test_' + format + '_serializer', curry(serializerTest, format)) diff --git a/tests/regressiontests/views/tests/static.py b/tests/regressiontests/views/tests/static.py index 8b8ef8ba9b..bdd9fbfc0b 100644 --- a/tests/regressiontests/views/tests/static.py +++ b/tests/regressiontests/views/tests/static.py @@ -27,7 +27,6 @@ class StaticTests(TestCase): for filename in media_files: response = self.client.get('/views/%s/%s' % (self.prefix, filename)) response_content = b''.join(response) - response.close() file_path = path.join(media_dir, filename) with open(file_path, 'rb') as fp: self.assertEqual(fp.read(), response_content) @@ -36,14 +35,12 @@ class StaticTests(TestCase): def test_unknown_mime_type(self): response = self.client.get('/views/%s/file.unknown' % self.prefix) - response.close() self.assertEqual('application/octet-stream', response['Content-Type']) def test_copes_with_empty_path_component(self): file_name = 'file.txt' response = self.client.get('/views/%s//%s' % (self.prefix, file_name)) response_content = b''.join(response) - response.close() with open(path.join(media_dir, file_name), 'rb') as fp: self.assertEqual(fp.read(), response_content) @@ -52,7 +49,6 @@ class StaticTests(TestCase): response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE='Thu, 1 Jan 1970 00:00:00 GMT') response_content = b''.join(response) - response.close() with open(path.join(media_dir, file_name), 'rb') as fp: self.assertEqual(fp.read(), response_content) @@ -77,7 +73,6 @@ class StaticTests(TestCase): response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE=invalid_date) response_content = b''.join(response) - response.close() with open(path.join(media_dir, file_name), 'rb') as fp: self.assertEqual(fp.read(), response_content) self.assertEqual(len(response_content), @@ -94,7 +89,6 @@ class StaticTests(TestCase): response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE=invalid_date) response_content = b''.join(response) - response.close() with open(path.join(media_dir, file_name), 'rb') as fp: self.assertEqual(fp.read(), response_content) self.assertEqual(len(response_content), From 64d0f89ab1dc6ef8a84814f567050fc063d67de2 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 18 Feb 2013 09:35:22 -0500 Subject: [PATCH 345/870] Fixed #19717 - Removed mentions of "root QuerySet" in docs. Thanks julien.aubert.mail@ for the report and James Pic for the patch. --- docs/topics/db/queries.txt | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index de898c8373..f3a8709a51 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -163,10 +163,9 @@ default. Access it directly via the model class, like so:: "record-level" operations. The :class:`~django.db.models.Manager` is the main source of ``QuerySets`` for -a model. It acts as a "root" :class:`~django.db.models.query.QuerySet` that -describes all objects in the model's database table. For example, -``Blog.objects`` is the initial :class:`~django.db.models.query.QuerySet` that -contains all ``Blog`` objects in the database. +a model. For example, ``Blog.objects.all()`` returns a +:class:`~django.db.models.query.QuerySet` that contains all ``Blog`` objects in +the database. Retrieving all objects ---------------------- @@ -180,20 +179,13 @@ this, use the :meth:`~django.db.models.query.QuerySet.all` method on a The :meth:`~django.db.models.query.QuerySet.all` method returns a :class:`~django.db.models.query.QuerySet` of all the objects in the database. -(If ``Entry.objects`` is a :class:`~django.db.models.query.QuerySet`, why can't -we just do ``Entry.objects``? That's because ``Entry.objects``, the root -:class:`~django.db.models.query.QuerySet`, is a special case that cannot be -evaluated. The :meth:`~django.db.models.query.QuerySet.all` method returns a -:class:`~django.db.models.query.QuerySet` that *can* be evaluated.) - - Retrieving specific objects with filters ---------------------------------------- -The root :class:`~django.db.models.query.QuerySet` provided by the -:class:`~django.db.models.Manager` describes all objects in the database -table. Usually, though, you'll need to select only a subset of the complete set -of objects. +The :class:`~django.db.models.query.QuerySet` returned by +:meth:`~django.db.models.query.QuerySet.all` describes all objects in the +database table. Usually, though, you'll need to select only a subset of the +complete set of objects. To create such a subset, you refine the initial :class:`~django.db.models.query.QuerySet`, adding filter conditions. The two @@ -216,10 +208,9 @@ so:: Entry.objects.filter(pub_date__year=2006) -We don't have to add an :meth:`~django.db.models.query.QuerySet.all` -- -``Entry.objects.all().filter(...)``. That would still work, but you only need -:meth:`~django.db.models.query.QuerySet.all` when you want all objects from the -root :class:`~django.db.models.query.QuerySet`. +With the default manager class, it is the same as:: + + Entry.objects.all().filter(pub_date__year=2006) .. _chaining-filters: From 29628e0b6e5b1c6324e0c06cc56a49a5aa0747e0 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 18 Feb 2013 17:12:42 +0100 Subject: [PATCH 346/870] Factored out common code in database backends. --- django/db/backends/__init__.py | 12 +++++++++++ django/db/backends/mysql/base.py | 10 +--------- django/db/backends/oracle/base.py | 20 ++++--------------- .../db/backends/postgresql_psycopg2/base.py | 8 +------- django/db/backends/sqlite3/base.py | 9 +-------- 5 files changed, 19 insertions(+), 40 deletions(-) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 03b62f6413..7a0a577ef1 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -11,6 +11,7 @@ from contextlib import contextmanager from django.conf import settings from django.db import DEFAULT_DB_ALIAS +from django.db.backends.signals import connection_created from django.db.backends import util from django.db.transaction import TransactionManagementError from django.utils.functional import cached_property @@ -52,6 +53,17 @@ class BaseDatabaseWrapper(object): __hash__ = object.__hash__ + def _valid_connection(self): + return self.connection is not None + + def _cursor(self): + if not self._valid_connection(): + conn_params = self.get_connection_params() + self.connection = self.get_new_connection(conn_params) + self.init_connection_state() + connection_created.send(sender=self.__class__, connection=self) + return self.create_cursor() + def _commit(self): if self.connection is not None: return self.connection.commit() diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 9de2a4d62d..eb823083f4 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -33,19 +33,16 @@ from MySQLdb.constants import FIELD_TYPE, CLIENT from django.conf import settings from django.db import utils from django.db.backends import * -from django.db.backends.signals import connection_created from django.db.backends.mysql.client import DatabaseClient from django.db.backends.mysql.creation import DatabaseCreation from django.db.backends.mysql.introspection import DatabaseIntrospection from django.db.backends.mysql.validation import DatabaseValidation from django.utils.encoding import force_str -from django.utils.functional import cached_property from django.utils.safestring import SafeBytes, SafeText from django.utils import six from django.utils import timezone # Raise exceptions for database warnings if DEBUG is on -from django.conf import settings if settings.DEBUG: warnings.filterwarnings("error", category=Database.Warning) @@ -454,12 +451,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): cursor.execute('SET SQL_AUTO_IS_NULL = 0') cursor.close() - def _cursor(self): - if not self._valid_connection(): - conn_params = self.get_connection_params() - self.connection = self.get_new_connection(conn_params) - self.init_connection_state() - connection_created.send(sender=self.__class__, connection=self) + def create_cursor(self): cursor = self.connection.cursor() return CursorWrapper(cursor) diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 7bcfb46798..e329ef3191 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -48,7 +48,6 @@ except ImportError as e: from django.conf import settings from django.db import utils from django.db.backends import * -from django.db.backends.signals import connection_created from django.db.backends.oracle.client import DatabaseClient from django.db.backends.oracle.creation import DatabaseCreation from django.db.backends.oracle.introspection import DatabaseIntrospection @@ -521,9 +520,6 @@ class DatabaseWrapper(BaseDatabaseWrapper): self.cursor().execute('SET CONSTRAINTS ALL IMMEDIATE') self.cursor().execute('SET CONSTRAINTS ALL DEFERRED') - def _valid_connection(self): - return self.connection is not None - def _connect_string(self): settings_dict = self.settings_dict if not settings_dict['HOST'].strip(): @@ -537,8 +533,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): return "%s/%s@%s" % (settings_dict['USER'], settings_dict['PASSWORD'], dsn) - def create_cursor(self, conn): - return FormatStylePlaceholderCursor(conn) + def create_cursor(self): + return FormatStylePlaceholderCursor(self.connection) def get_connection_params(self): conn_params = self.settings_dict['OPTIONS'].copy() @@ -551,7 +547,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): return Database.connect(conn_string, **conn_params) def init_connection_state(self): - cursor = self.create_cursor(self.connection) + cursor = self.create_cursor() # Set the territory first. The territory overrides NLS_DATE_FORMAT # and NLS_TIMESTAMP_FORMAT to the territory default. When all of # these are set in single statement it isn't clear what is supposed @@ -572,7 +568,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): # This check is performed only once per DatabaseWrapper # instance per thread, since subsequent connections will use # the same settings. - cursor = self.create_cursor(self.connection) + cursor = self.create_cursor() try: cursor.execute("SELECT 1 FROM DUAL WHERE DUMMY %s" % self._standard_operators['contains'], @@ -602,14 +598,6 @@ class DatabaseWrapper(BaseDatabaseWrapper): # stmtcachesize is available only in 4.3.2 and up. pass - def _cursor(self): - if not self._valid_connection(): - conn_params = self.get_connection_params() - self.connection = self.get_new_connection(conn_params) - self.init_connection_state() - connection_created.send(sender=self.__class__, connection=self) - return self.create_cursor(self.connection) - # Oracle doesn't support savepoint commits. Ignore them. def _savepoint_commit(self, sid): pass diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index b8d7fe3195..fb1ad5f991 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -8,7 +8,6 @@ import sys from django.db import utils from django.db.backends import * -from django.db.backends.signals import connection_created from django.db.backends.postgresql_psycopg2.operations import DatabaseOperations from django.db.backends.postgresql_psycopg2.client import DatabaseClient from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation @@ -205,12 +204,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): self.connection.set_isolation_level(self.isolation_level) self._get_pg_version() - def _cursor(self): - if self.connection is None: - conn_params = self.get_connection_params() - self.connection = self.get_new_connection(conn_params) - self.init_connection_state() - connection_created.send(sender=self.__class__, connection=self) + def create_cursor(self): cursor = self.connection.cursor() cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None return CursorWrapper(cursor) diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index dd87972d5b..7ddaaf8fe3 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -14,7 +14,6 @@ import sys from django.db import utils from django.db.backends import * -from django.db.backends.signals import connection_created from django.db.backends.sqlite3.client import DatabaseClient from django.db.backends.sqlite3.creation import DatabaseCreation from django.db.backends.sqlite3.introspection import DatabaseIntrospection @@ -344,13 +343,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): def init_connection_state(self): pass - def _cursor(self): - if self.connection is None: - conn_params = self.get_connection_params() - self.connection = self.get_new_connection(conn_params) - self.init_connection_state() - connection_created.send(sender=self.__class__, connection=self) - + def create_cursor(self): return self.connection.cursor(factory=SQLiteCursorWrapper) def check_constraints(self, table_names=None): From aea98e8c5357b15da214af311e8cb74b1503f958 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 18 Feb 2013 19:06:12 +0100 Subject: [PATCH 347/870] Simplified MySQL version checking. Django used to check the version of MySQL before handling the first request, which required: - opening a connection - closing it, to avoid holding it idle until the first request. This code isn't necessary any longer since Django dropped support for some versions of MySQL, and other database backends don't implement a similar dance. For consistency and maintenability, remove it. Reverts 4423757c0c50afbe2470434778c8d5e5b4a70925. Closes #18135. --- django/db/backends/mysql/base.py | 9 --------- tests/regressiontests/backends/tests.py | 6 ------ 2 files changed, 15 deletions(-) diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index eb823083f4..754e876701 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -464,16 +464,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): @cached_property def mysql_version(self): if not self.server_version: - new_connection = False - if not self._valid_connection(): - # Ensure we have a connection with the DB by using a temporary - # cursor - new_connection = True - self.cursor().close() server_info = self.connection.get_server_info() - if new_connection: - # Make sure we close the connection - self.close() m = server_version_re.match(server_info) if not m: raise Exception('Unable to determine MySQL version from version string %r' % server_info) diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index fbe5026e12..ed6f07691a 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -123,12 +123,6 @@ class MySQLTests(TestCase): else: self.assertFalse(found_reset) - @unittest.skipUnless(connection.vendor == 'mysql', - "Test valid only for MySQL") - def test_server_version_connections(self): - connection.close() - connection.mysql_version - self.assertTrue(connection.connection is None) class DateQuotingTest(TestCase): From 282b2f40cd0aa09815001e178f60c9e71667d847 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 18 Feb 2013 19:09:54 +0100 Subject: [PATCH 348/870] Fixed #15119 -- Stopped pinging the MySQL server. --- django/db/backends/__init__.py | 5 +---- django/db/backends/mysql/base.py | 9 --------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 7a0a577ef1..6e74c2b460 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -53,11 +53,8 @@ class BaseDatabaseWrapper(object): __hash__ = object.__hash__ - def _valid_connection(self): - return self.connection is not None - def _cursor(self): - if not self._valid_connection(): + if self.connection is None: conn_params = self.get_connection_params() self.connection = self.get_new_connection(conn_params) self.init_connection_state() diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 754e876701..5a23dce095 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -402,15 +402,6 @@ class DatabaseWrapper(BaseDatabaseWrapper): self.introspection = DatabaseIntrospection(self) self.validation = DatabaseValidation(self) - def _valid_connection(self): - if self.connection is not None: - try: - self.connection.ping() - return True - except DatabaseError: - self.close() - return False - def get_connection_params(self): kwargs = { 'conv': django_conversions, From 7b8529d206731bab01855a60f4a6f84e4221f2e1 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 18 Feb 2013 19:11:56 +0100 Subject: [PATCH 349/870] Removed duplicate caching of mysql_version. The manual caching in self.server_version and the cached_property decorator are redundant. --- django/db/backends/mysql/base.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 5a23dce095..3719b3a6a7 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -394,7 +394,6 @@ class DatabaseWrapper(BaseDatabaseWrapper): def __init__(self, *args, **kwargs): super(DatabaseWrapper, self).__init__(*args, **kwargs) - self.server_version = None self.features = DatabaseFeatures(self) self.ops = DatabaseOperations(self) self.client = DatabaseClient(self) @@ -454,13 +453,11 @@ class DatabaseWrapper(BaseDatabaseWrapper): @cached_property def mysql_version(self): - if not self.server_version: - server_info = self.connection.get_server_info() - m = server_version_re.match(server_info) - if not m: - raise Exception('Unable to determine MySQL version from version string %r' % server_info) - self.server_version = tuple([int(x) for x in m.groups()]) - return self.server_version + server_info = self.connection.get_server_info() + match = server_version_re.match(server_info) + if not match: + raise Exception('Unable to determine MySQL version from version string %r' % server_info) + return tuple([int(x) for x in match.groups()]) def disable_constraint_checking(self): """ From 21765c0a6c7cece4d052c8c7af6022ac9c61b7cf Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 18 Feb 2013 22:49:59 +0100 Subject: [PATCH 350/870] Implemented PostgreSQL version as a cached property. --- django/db/backends/postgresql_psycopg2/base.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index fb1ad5f991..831ad84843 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -14,6 +14,7 @@ from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation from django.db.backends.postgresql_psycopg2.version import get_version from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection from django.utils.encoding import force_str +from django.utils.functional import cached_property from django.utils.safestring import SafeText, SafeBytes from django.utils import six from django.utils.timezone import utc @@ -121,7 +122,6 @@ class DatabaseWrapper(BaseDatabaseWrapper): self.creation = DatabaseCreation(self) self.introspection = DatabaseIntrospection(self) self.validation = BaseDatabaseValidation(self) - self._pg_version = None def check_constraints(self, table_names=None): """ @@ -150,11 +150,9 @@ class DatabaseWrapper(BaseDatabaseWrapper): ) raise - def _get_pg_version(self): - if self._pg_version is None: - self._pg_version = get_version(self.connection) - return self._pg_version - pg_version = property(_get_pg_version) + @cached_property + def pg_version(self): + return get_version(self.connection) def get_connection_params(self): settings_dict = self.settings_dict @@ -202,7 +200,6 @@ class DatabaseWrapper(BaseDatabaseWrapper): self.connection.cursor().execute( self.ops.set_time_zone_sql(), [tz]) self.connection.set_isolation_level(self.isolation_level) - self._get_pg_version() def create_cursor(self): cursor = self.connection.cursor() From ffcfb19f47f5995406003cffca24cf62c6d234a8 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 18 Feb 2013 19:31:10 +0100 Subject: [PATCH 351/870] Added required methods in BaseDatabaseWrapper. I should have included this in 29628e0b6e5b1c6324e0c06cc56a49a5aa0747e0. --- django/db/backends/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 6e74c2b460..0f9423c1c3 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -53,6 +53,18 @@ class BaseDatabaseWrapper(object): __hash__ = object.__hash__ + def get_connection_params(self): + raise NotImplementedError + + def get_new_connection(self, conn_params): + raise NotImplementedError + + def init_connection_state(self): + raise NotImplementedError + + def create_cursor(self): + raise NotImplementedError + def _cursor(self): if self.connection is None: conn_params = self.get_connection_params() From b4492a8ca4a7ae4daa3a6b03c3d7a845fad74931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Mon, 18 Feb 2013 01:56:24 +0200 Subject: [PATCH 352/870] Fixed #19837 -- Refactored split_exclude() join generation The refactoring mainly concentrates on making sure the inner and outer query agree about the split position. The split position is where the multijoin happens, and thus the split position also determines the columns used in the "WHERE col1 IN (SELECT col2 from ...)" condition. This commit fixes a regression caused by #10790 and commit 69597e5bcc89aadafd1b76abf7efab30ee0b8b1a. The regression was caused by wrong cols in the split position. --- django/db/models/sql/datastructures.py | 6 +- django/db/models/sql/query.py | 135 +++++++++++------------- tests/regressiontests/queries/models.py | 14 +++ tests/regressiontests/queries/tests.py | 21 +++- tests/tmp.txt | 1 + 5 files changed, 103 insertions(+), 74 deletions(-) create mode 100644 tests/tmp.txt diff --git a/django/db/models/sql/datastructures.py b/django/db/models/sql/datastructures.py index 612eb8f2d9..4bc9e6ed34 100644 --- a/django/db/models/sql/datastructures.py +++ b/django/db/models/sql/datastructures.py @@ -12,8 +12,10 @@ class MultiJoin(Exception): multi-valued join was attempted (if the caller wants to treat that exceptionally). """ - def __init__(self, level): - self.level = level + def __init__(self, names_pos, path_with_names): + self.level = names_pos + # The path travelled, this includes the path to the multijoin. + self.names_with_path = path_with_names class Empty(object): pass diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index fb0f09efde..422029c5e0 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1200,7 +1200,7 @@ class Query(object): can_reuse.update(join_list) except MultiJoin as e: self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]), - can_reuse) + can_reuse, e.names_with_path) return if (lookup_type == 'isnull' and value is True and not negate and @@ -1324,7 +1324,7 @@ class Query(object): (the last used join field), and target (which is a field guaranteed to contain the same value as the final field). """ - path = [] + path, names_with_path = [], [] for pos, name in enumerate(names): if name == 'pk': name = opts.pk.name @@ -1361,16 +1361,17 @@ class Query(object): opts, final_field, False, True)) if hasattr(field, 'get_path_info'): pathinfos, opts, target, final_field = field.get_path_info() + if not allow_many: + for inner_pos, p in enumerate(pathinfos): + if p.m2m: + names_with_path.append((name, pathinfos[0:inner_pos + 1])) + raise MultiJoin(pos + 1, names_with_path) path.extend(pathinfos) + names_with_path.append((name, pathinfos)) else: # Local non-relational field. final_field = target = field break - multijoin_pos = None - for m2mpos, pathinfo in enumerate(path): - if pathinfo.m2m: - multijoin_pos = m2mpos - break if pos != len(names) - 1: if pos == len(names) - 2: @@ -1379,8 +1380,6 @@ class Query(object): "the lookup type?" % (name, names[pos + 1])) else: raise FieldError("Join on field %r not permitted." % name) - if multijoin_pos is not None and len(path) >= multijoin_pos and not allow_many: - raise MultiJoin(multijoin_pos + 1) return path, final_field, target def setup_joins(self, names, opts, alias, can_reuse=None, allow_many=True, @@ -1454,7 +1453,7 @@ class Query(object): break return target.column, joins[-1], joins - def split_exclude(self, filter_expr, prefix, can_reuse): + def split_exclude(self, filter_expr, prefix, can_reuse, names_with_path): """ When doing an exclude against any kind of N-to-many relation, we need to use a subquery. This method constructs the nested query, given the @@ -1462,11 +1461,10 @@ class Query(object): N-to-many relation field. As an example we could have original filter ~Q(child__name='foo'). - We would get here with filter_expr = child_name, prefix = child and - can_reuse is a set of joins we can reuse for filtering in the original - query. + We would get here with filter_expr = child__name, prefix = child and + can_reuse is a set of joins usable for filters in the original query. - We will turn this into + We will turn this into equivalent of: WHERE pk NOT IN (SELECT parent_id FROM thetable WHERE name = 'foo' AND parent_id IS NOT NULL) @@ -1474,44 +1472,48 @@ class Query(object): saner null handling, and is easier for the backend's optimizer to handle. """ + # Generate the inner query. query = Query(self.model) query.add_filter(filter_expr) query.bump_prefix() query.clear_ordering(True) - query.set_start(prefix) - # Adding extra check to make sure the selected field will not be null + # Try to have as simple as possible subquery -> trim leading joins from + # the subquery. + trimmed_joins = query.trim_start(names_with_path) + # Add extra check to make sure the selected field will not be null # since we are adding a IN clause. This prevents the # database from tripping over IN (...,NULL,...) selects and returning # nothing alias, col = query.select[0].col query.where.add((Constraint(alias, col, None), 'isnull', False), AND) - # We need to trim the last part from the prefix. - trimmed_prefix = LOOKUP_SEP.join(prefix.split(LOOKUP_SEP)[0:-1]) - if not trimmed_prefix: - rel, _, direct, m2m = self.model._meta.get_field_by_name(prefix) - if not m2m: - trimmed_prefix = rel.field.rel.field_name + + # Still make sure that the trimmed parts in the inner query and + # trimmed prefix are in sync. So, use the trimmed_joins to make sure + # as many path elements are in the prefix as there were trimmed joins. + # In addition, convert the path elements back to names so that + # add_filter() can handle them. + trimmed_prefix = [] + paths_in_prefix = trimmed_joins + for name, path in names_with_path: + if paths_in_prefix - len(path) > 0: + trimmed_prefix.append(name) + paths_in_prefix -= len(path) else: - if direct: - trimmed_prefix = rel.m2m_target_field_name() - else: - trimmed_prefix = rel.field.m2m_reverse_target_field_name() - + trimmed_prefix.append( + path[paths_in_prefix - len(path)].from_field.name) + break + trimmed_prefix = LOOKUP_SEP.join(trimmed_prefix) self.add_filter(('%s__in' % trimmed_prefix, query), negate=True, - can_reuse=can_reuse) + can_reuse=can_reuse) - # If there's more than one join in the inner query (before any initial - # bits were trimmed -- which means the last active table is more than - # two places into the alias list), we need to also handle the - # possibility that the earlier joins don't match anything by adding a - # comparison to NULL (e.g. in - # Tag.objects.exclude(parent__parent__name='t1'), a tag with no parent - # would otherwise be overlooked). - active_positions = len([count for count - in query.alias_refcount.items() if count]) - if active_positions > 1: + # If there's more than one join in the inner query, we need to also + # handle the possibility that the earlier joins don't match anything + # by adding a comparison to NULL (e.g. in + # Tag.objects.exclude(parent__parent__name='t1') + # a tag with no parent would otherwise be overlooked). + if trimmed_joins > 1: self.add_filter(('%s__isnull' % trimmed_prefix, False), negate=True, - can_reuse=can_reuse) + can_reuse=can_reuse) def set_empty(self): self.where = EmptyWhere() @@ -1869,42 +1871,33 @@ class Query(object): return self.extra extra_select = property(_extra_select) - def set_start(self, start): + def trim_start(self, names_with_path): """ - Sets the table from which to start joining. The start position is - specified by the related attribute from the base model. This will - automatically set to the select column to be the column linked from the - previous table. + Trims joins from the start of the join path. The candidates for trim + are the PathInfos in names_with_path structure. Outer joins are not + eligible for removal. Also sets the select column so the start + matches the join. - This method is primarily for internal use and the error checking isn't - as friendly as add_filter(). Mostly useful for querying directly - against the join table of many-to-many relation in a subquery. - """ - opts = self.model._meta - alias = self.get_initial_alias() - field, col, opts, joins, extra = self.setup_joins( - start.split(LOOKUP_SEP), opts, alias) - select_col = self.alias_map[joins[1]].lhs_join_col - select_alias = alias - - # The call to setup_joins added an extra reference to everything in - # joins. Reverse that. - for alias in joins: - self.unref_alias(alias) - - # We might be able to trim some joins from the front of this query, - # providing that we only traverse "always equal" connections (i.e. rhs - # is *always* the same value as lhs). - for alias in joins[1:]: - join_info = self.alias_map[alias] - if (join_info.lhs_join_col != select_col - or join_info.join_type != self.INNER): - break - self.unref_alias(select_alias) - select_alias = join_info.rhs_alias - select_col = join_info.rhs_join_col + This method is mostly useful for generating the subquery joins & col + in "WHERE somecol IN (subquery)". This construct is needed by + split_exclude(). + _""" + join_pos = 0 + for _, paths in names_with_path: + for path in paths: + peek = self.tables[join_pos + 1] + if self.alias_map[peek].join_type == self.LOUTER: + # Back up one level and break + select_alias = self.tables[join_pos] + select_col = path.from_field.column + break + select_alias = self.tables[join_pos + 1] + select_col = path.to_field.column + self.unref_alias(self.tables[join_pos]) + join_pos += 1 self.select = [SelectInfo((select_alias, select_col), None)] self.remove_inherited_models() + return join_pos def is_nullable(self, field): """ diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index 16583e891c..91edf71aeb 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -439,3 +439,17 @@ class BaseA(models.Model): a = models.ForeignKey(FK1, null=True) b = models.ForeignKey(FK2, null=True) c = models.ForeignKey(FK3, null=True) + +@python_2_unicode_compatible +class Identifier(models.Model): + name = models.CharField(max_length=100) + + def __str__(self): + return self.name + +class Program(models.Model): + identifier = models.OneToOneField(Identifier) + +class Channel(models.Model): + programs = models.ManyToManyField(Program) + identifier = models.OneToOneField(Identifier) diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py index ea54d18451..34bfea0b94 100644 --- a/tests/regressiontests/queries/tests.py +++ b/tests/regressiontests/queries/tests.py @@ -24,7 +24,7 @@ from .models import (Annotation, Article, Author, Celebrity, Child, Cover, Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory, SpecialCategory, OneToOneCategory, NullableName, ProxyCategory, SingleObject, RelatedObject, ModelA, ModelD, Responsibility, Job, - JobResponsibilities, BaseA) + JobResponsibilities, BaseA, Identifier, Program, Channel) class BaseQuerysetTest(TestCase): @@ -2612,3 +2612,22 @@ class DisjunctionPromotionTests(TestCase): qs = BaseA.objects.filter(Q(a__f1=F('c__f1')) | (Q(pk=1) & Q(pk=2))) self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 2) self.assertEqual(str(qs.query).count('INNER JOIN'), 0) + + +class ManyToManyExcludeTest(TestCase): + def test_exclude_many_to_many(self): + Identifier.objects.create(name='extra') + program = Program.objects.create(identifier=Identifier.objects.create(name='program')) + channel = Channel.objects.create(identifier=Identifier.objects.create(name='channel')) + channel.programs.add(program) + + # channel contains 'program1', so all Identifiers except that one + # should be returned + self.assertQuerysetEqual( + Identifier.objects.exclude(program__channel=channel).order_by('name'), + ['', ''] + ) + self.assertQuerysetEqual( + Identifier.objects.exclude(program__channel=None).order_by('name'), + [''] + ) diff --git a/tests/tmp.txt b/tests/tmp.txt new file mode 100644 index 0000000000..4e812b2c23 --- /dev/null +++ b/tests/tmp.txt @@ -0,0 +1 @@ +SELECT "queries_tag"."id", "queries_tag"."name", "queries_tag"."parent_id", "queries_tag"."category_id" FROM "queries_tag" WHERE NOT (("queries_tag"."id" IN (SELECT U0."id" FROM "queries_tag" U0 LEFT OUTER JOIN "queries_tag" U1 ON (U0."id" = U1."parent_id") WHERE (U1."id" IS NULL AND U0."id" IS NOT NULL)) AND "queries_tag"."id" IS NOT NULL)) ORDER BY "queries_tag"."name" ASC From 3e71368423b41c9418117328216e66f95cbaab03 Mon Sep 17 00:00:00 2001 From: Florian Hahn Date: Wed, 13 Feb 2013 18:37:40 +0100 Subject: [PATCH 353/870] Fixed #10870 -- Added aggreation + generic reverse relation test --- .../aggregation_regress/models.py | 18 +++++++++ .../aggregation_regress/tests.py | 40 ++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/tests/regressiontests/aggregation_regress/models.py b/tests/regressiontests/aggregation_regress/models.py index dd4ff50aec..b857ba62ac 100644 --- a/tests/regressiontests/aggregation_regress/models.py +++ b/tests/regressiontests/aggregation_regress/models.py @@ -1,4 +1,6 @@ # coding: utf-8 +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.encoding import python_2_unicode_compatible @@ -22,6 +24,13 @@ class Publisher(models.Model): return self.name +class TaggedItem(models.Model): + tag = models.CharField(max_length=100) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + + @python_2_unicode_compatible class Book(models.Model): isbn = models.CharField(max_length=9) @@ -33,6 +42,7 @@ class Book(models.Model): contact = models.ForeignKey(Author, related_name='book_contact_set') publisher = models.ForeignKey(Publisher) pubdate = models.DateField() + tags = generic.GenericRelation(TaggedItem) class Meta: ordering = ('name',) @@ -63,6 +73,14 @@ class Clues(models.Model): Clue = models.CharField(max_length=150) +class WithManualPK(models.Model): + # The generic relations regression test needs two different model + # classes with the same PK value, and there are some (external) + # DB backends that don't work nicely when assigning integer to AutoField + # column (MSSQL at least). + id = models.IntegerField(primary_key=True) + + @python_2_unicode_compatible class HardbackBook(Book): weight = models.FloatField() diff --git a/tests/regressiontests/aggregation_regress/tests.py b/tests/regressiontests/aggregation_regress/tests.py index 076567538b..94a02cf3b5 100644 --- a/tests/regressiontests/aggregation_regress/tests.py +++ b/tests/regressiontests/aggregation_regress/tests.py @@ -6,11 +6,13 @@ from decimal import Decimal 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.utils import six -from .models import Author, Book, Publisher, Clues, Entries, HardbackBook +from .models import (Author, Book, Publisher, Clues, Entries, HardbackBook, + TaggedItem, WithManualPK) class AggregationTests(TestCase): @@ -982,3 +984,39 @@ class AggregationTests(TestCase): def test_reverse_join_trimming(self): qs = Author.objects.annotate(Count('book_contact_set__contact')) self.assertIn(' JOIN ', str(qs.query)) + + def test_aggregation_with_generic_reverse_relation(self): + """ + Regression test for #10870: Aggregates with joins ignore extra + filters provided by setup_joins + + tests aggregations with generic reverse relations + """ + b = Book.objects.get(name='Practical Django Projects') + TaggedItem.objects.create(object_id=b.id, tag='intermediate', + content_type=ContentType.objects.get_for_model(b)) + TaggedItem.objects.create(object_id=b.id, tag='django', + content_type=ContentType.objects.get_for_model(b)) + # Assign a tag to model with same PK as the book above. If the JOIN + # used in aggregation doesn't have content type as part of the + # condition the annotation will also count the 'hi mom' tag for b. + wmpk = WithManualPK.objects.create(id=b.pk) + TaggedItem.objects.create(object_id=wmpk.id, tag='hi mom', + content_type=ContentType.objects.get_for_model(wmpk)) + b = Book.objects.get(name__startswith='Paradigms of Artificial Intelligence') + TaggedItem.objects.create(object_id=b.id, tag='intermediate', + content_type=ContentType.objects.get_for_model(b)) + + self.assertEqual(Book.objects.aggregate(Count('tags')), {'tags__count': 3}) + results = Book.objects.annotate(Count('tags')).order_by('-tags__count', 'name') + self.assertEqual( + [(b.name, b.tags__count) for b in results], + [ + ('Practical Django Projects', 2), + ('Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp', 1), + ('Artificial Intelligence: A Modern Approach', 0), + ('Python Web Development with Django', 0), + ('Sams Teach Yourself Django in 24 Hours', 0), + ('The Definitive Guide to Django: Web Development Done Right', 0) + ] + ) From 607772b942010d2b237c95d2ba74c958986def04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Tue, 19 Feb 2013 01:55:40 +0200 Subject: [PATCH 354/870] Removed accidentally committed file --- tests/tmp.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tests/tmp.txt diff --git a/tests/tmp.txt b/tests/tmp.txt deleted file mode 100644 index 4e812b2c23..0000000000 --- a/tests/tmp.txt +++ /dev/null @@ -1 +0,0 @@ -SELECT "queries_tag"."id", "queries_tag"."name", "queries_tag"."parent_id", "queries_tag"."category_id" FROM "queries_tag" WHERE NOT (("queries_tag"."id" IN (SELECT U0."id" FROM "queries_tag" U0 LEFT OUTER JOIN "queries_tag" U1 ON (U0."id" = U1."parent_id") WHERE (U1."id" IS NULL AND U0."id" IS NOT NULL)) AND "queries_tag"."id" IS NOT NULL)) ORDER BY "queries_tag"."name" ASC From 4b9fa49bc0cf5d2e01b6b98dec6d23fed774f254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Tue, 19 Feb 2013 03:12:37 +0200 Subject: [PATCH 355/870] Avoided related_name conflicts in tests --- tests/regressiontests/aggregation_regress/models.py | 4 ++-- tests/regressiontests/aggregation_regress/tests.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/regressiontests/aggregation_regress/models.py b/tests/regressiontests/aggregation_regress/models.py index b857ba62ac..047c871c39 100644 --- a/tests/regressiontests/aggregation_regress/models.py +++ b/tests/regressiontests/aggregation_regress/models.py @@ -24,7 +24,7 @@ class Publisher(models.Model): return self.name -class TaggedItem(models.Model): +class ItemTag(models.Model): tag = models.CharField(max_length=100) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() @@ -42,7 +42,7 @@ class Book(models.Model): contact = models.ForeignKey(Author, related_name='book_contact_set') publisher = models.ForeignKey(Publisher) pubdate = models.DateField() - tags = generic.GenericRelation(TaggedItem) + tags = generic.GenericRelation(ItemTag) class Meta: ordering = ('name',) diff --git a/tests/regressiontests/aggregation_regress/tests.py b/tests/regressiontests/aggregation_regress/tests.py index 94a02cf3b5..bb1eb59b07 100644 --- a/tests/regressiontests/aggregation_regress/tests.py +++ b/tests/regressiontests/aggregation_regress/tests.py @@ -12,7 +12,7 @@ from django.test import TestCase, Approximate, skipUnlessDBFeature from django.utils import six from .models import (Author, Book, Publisher, Clues, Entries, HardbackBook, - TaggedItem, WithManualPK) + ItemTag, WithManualPK) class AggregationTests(TestCase): @@ -993,18 +993,18 @@ class AggregationTests(TestCase): tests aggregations with generic reverse relations """ b = Book.objects.get(name='Practical Django Projects') - TaggedItem.objects.create(object_id=b.id, tag='intermediate', + ItemTag.objects.create(object_id=b.id, tag='intermediate', content_type=ContentType.objects.get_for_model(b)) - TaggedItem.objects.create(object_id=b.id, tag='django', + ItemTag.objects.create(object_id=b.id, tag='django', content_type=ContentType.objects.get_for_model(b)) # Assign a tag to model with same PK as the book above. If the JOIN # used in aggregation doesn't have content type as part of the # condition the annotation will also count the 'hi mom' tag for b. wmpk = WithManualPK.objects.create(id=b.pk) - TaggedItem.objects.create(object_id=wmpk.id, tag='hi mom', + ItemTag.objects.create(object_id=wmpk.id, tag='hi mom', content_type=ContentType.objects.get_for_model(wmpk)) b = Book.objects.get(name__startswith='Paradigms of Artificial Intelligence') - TaggedItem.objects.create(object_id=b.id, tag='intermediate', + ItemTag.objects.create(object_id=b.id, tag='intermediate', content_type=ContentType.objects.get_for_model(b)) self.assertEqual(Book.objects.aggregate(Count('tags')), {'tags__count': 3}) From 22d5e4b4af4a5913865bb3e4de4c25b6709cc4c5 Mon Sep 17 00:00:00 2001 From: "Stefan \"hr\" Berder" Date: Tue, 19 Feb 2013 16:01:06 +0800 Subject: [PATCH 356/870] Update docs/topics/class-based-views/generic-display.txt simple typo in "Making friendly template contexts" --- docs/topics/class-based-views/generic-display.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/class-based-views/generic-display.txt b/docs/topics/class-based-views/generic-display.txt index 835ca07459..8fe6cd0d65 100644 --- a/docs/topics/class-based-views/generic-display.txt +++ b/docs/topics/class-based-views/generic-display.txt @@ -172,7 +172,7 @@ context using the lower cased version of the model class' name. This is provided in addition to the default ``object_list`` entry, but contains exactly the same data, i.e. ``publisher_list``. -If the this still isn't a good match, you can manually set the name of the +If this still isn't a good match, you can manually set the name of the context variable. The ``context_object_name`` attribute on a generic view specifies the context variable to use:: From 9a3988ca5ad5808fad0d5bd8e25fe560d0d48ec0 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 19 Feb 2013 10:50:22 +0100 Subject: [PATCH 357/870] Implemented Oracle version as a cached property. --- django/db/backends/oracle/base.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index e329ef3191..3fdf67402a 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -52,6 +52,7 @@ from django.db.backends.oracle.client import DatabaseClient from django.db.backends.oracle.creation import DatabaseCreation from django.db.backends.oracle.introspection import DatabaseIntrospection from django.utils.encoding import force_bytes, force_text +from django.utils.functional import cached_property from django.utils import six from django.utils import timezone @@ -502,7 +503,6 @@ class DatabaseWrapper(BaseDatabaseWrapper): def __init__(self, *args, **kwargs): super(DatabaseWrapper, self).__init__(*args, **kwargs) - self.oracle_version = None self.features = DatabaseFeatures(self) use_returning_into = self.settings_dict["OPTIONS"].get('use_returning_into', True) self.features.can_return_id_from_insert = use_returning_into @@ -579,18 +579,15 @@ class DatabaseWrapper(BaseDatabaseWrapper): self.operators = self._standard_operators cursor.close() - try: - self.oracle_version = int(self.connection.version.split('.')[0]) - # There's no way for the DatabaseOperations class to know the - # currently active Oracle version, so we do some setups here. - # TODO: Multi-db support will need a better solution (a way to - # communicate the current version). - if self.oracle_version <= 9: - self.ops.regex_lookup = self.ops.regex_lookup_9 - else: - self.ops.regex_lookup = self.ops.regex_lookup_10 - except ValueError: - pass + # There's no way for the DatabaseOperations class to know the + # currently active Oracle version, so we do some setups here. + # TODO: Multi-db support will need a better solution (a way to + # communicate the current version). + if self.oracle_version is not None and self.oracle_version <= 9: + self.ops.regex_lookup = self.ops.regex_lookup_9 + else: + self.ops.regex_lookup = self.ops.regex_lookup_10 + try: self.connection.stmtcachesize = 20 except: @@ -624,6 +621,13 @@ class DatabaseWrapper(BaseDatabaseWrapper): six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2]) six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2]) + @cached_property + def oracle_version(self): + try: + return int(self.connection.version.split('.')[0]) + except ValueError: + return None + class OracleParam(object): """ From ebabd772911f732ef54e014f130f6f5530198e14 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 19 Feb 2013 10:51:24 +0100 Subject: [PATCH 358/870] Ensured a connection is established when checking the database version. Fixed a test broken by 21765c0a. Refs #18135. --- django/db/backends/__init__.py | 12 ++++++++++++ django/db/backends/mysql/base.py | 3 ++- django/db/backends/oracle/base.py | 4 +++- django/db/backends/postgresql_psycopg2/base.py | 3 ++- django/db/backends/postgresql_psycopg2/operations.py | 3 +-- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 0f9423c1c3..2a7a206c3b 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -352,6 +352,18 @@ class BaseDatabaseWrapper(object): def make_debug_cursor(self, cursor): return util.CursorDebugWrapper(cursor, self) + @contextmanager + def temporary_connection(self): + # Ensure a connection is established, and avoid leaving a dangling + # connection, for operations outside of the request-response cycle. + must_close = self.connection is None + cursor = self.cursor() + try: + yield + finally: + cursor.close() + if must_close: + self.close() class BaseDatabaseFeatures(object): allows_group_by_pk = False diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 3719b3a6a7..fc2ff31581 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -453,7 +453,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): @cached_property def mysql_version(self): - server_info = self.connection.get_server_info() + with self.temporary_connection(): + server_info = self.connection.get_server_info() match = server_version_re.match(server_info) if not match: raise Exception('Unable to determine MySQL version from version string %r' % server_info) diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 3fdf67402a..a9ae025146 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -623,8 +623,10 @@ class DatabaseWrapper(BaseDatabaseWrapper): @cached_property def oracle_version(self): + with self.temporary_connection(): + version = self.connection.version try: - return int(self.connection.version.split('.')[0]) + return int(version.split('.')[0]) except ValueError: return None diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index 831ad84843..85a0991402 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -152,7 +152,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): @cached_property def pg_version(self): - return get_version(self.connection) + with self.temporary_connection(): + return get_version(self.connection) def get_connection_params(self): settings_dict = self.settings_dict diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py index 8e87ed539f..56535e0865 100644 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ b/django/db/backends/postgresql_psycopg2/operations.py @@ -195,8 +195,7 @@ class DatabaseOperations(BaseDatabaseOperations): NotImplementedError if this is the database in use. """ if aggregate.sql_function in ('STDDEV_POP', 'VAR_POP'): - pg_version = self.connection.pg_version - if pg_version >= 80200 and pg_version <= 80204: + if 80200 <= self.connection.pg_version <= 80204: raise NotImplementedError('PostgreSQL 8.2 to 8.2.4 is known to have a faulty implementation of %s. Please upgrade your version of PostgreSQL.' % aggregate.sql_function) def max_name_length(self): From efa300088f4bdb7224d5f1200f6ff4dd526c47a7 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 19 Feb 2013 10:25:26 -0500 Subject: [PATCH 359/870] Fixed #18789 - Fixed some text wrap issues with methods in the docs. Thanks neixetis@ for the report. --- docs/_theme/djangodocs/static/djangodocs.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_theme/djangodocs/static/djangodocs.css b/docs/_theme/djangodocs/static/djangodocs.css index bab81cd919..c8d223382d 100644 --- a/docs/_theme/djangodocs/static/djangodocs.css +++ b/docs/_theme/djangodocs/static/djangodocs.css @@ -90,8 +90,8 @@ table.docutils thead th p { margin: 0; padding: 0; } table.docutils { border-collapse:collapse; } /*** code blocks ***/ -.literal { white-space:nowrap; } -.literal { color:#234f32; } +.literal { color:#234f32; white-space:nowrap; } +dt > tt.literal { white-space: normal; } #sidebar .literal { color:white; background:transparent; font-size:11px; } h4 .literal { color: #234f32; font-size: 13px; } pre { font-size:small; background:#E0FFB8; border:1px solid #94da3a; border-width:1px 0; margin: 1em 0; padding: .3em .4em; overflow: hidden; line-height: 1.3em;} From 00031b73bda7d910aa19876694ebb6778c4b3e70 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 19 Feb 2013 11:31:41 -0500 Subject: [PATCH 360/870] Updated a couple admonitions to use the warning directive. --- docs/ref/unicode.txt | 2 +- docs/topics/auth/customizing.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ref/unicode.txt b/docs/ref/unicode.txt index 784ff33398..92a446ff6b 100644 --- a/docs/ref/unicode.txt +++ b/docs/ref/unicode.txt @@ -68,7 +68,7 @@ Python 2 with unicode literals or Python 3:: See also :doc:`Python 3 compatibility `. -.. admonition:: Warning +.. warning:: A bytestring does not carry any information with it about its encoding. For that reason, we have to make an assumption, and Django assumes that all diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 9c31445455..2e7bf2e3db 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -408,7 +408,7 @@ This dotted pair describes the name of the Django app (which must be in your :setting:`INSTALLED_APPS`), and the name of the Django model that you wish to use as your User model. -.. admonition:: Warning +.. warning:: Changing :setting:`AUTH_USER_MODEL` has a big effect on your database structure. It changes the tables that are available, and it will affect the From 1add79bc4007fee658f193b65aea2af2347aab6b Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 19 Feb 2013 12:44:19 -0500 Subject: [PATCH 361/870] Fixed #19852 - Updated admin fieldset example for consistency. Thanks chris.freeman.pdx@ for the suggestion. --- docs/ref/contrib/admin/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 3f32d3bce4..cbf7d4215b 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -271,7 +271,7 @@ subclass:: Example:: { - 'classes': ['wide', 'extrapretty'], + 'classes': ('wide', 'extrapretty'), } Two useful classes defined by the default admin site stylesheet are From d51fb74360b94f2a856573174f8aae3cd905dd35 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 9 Feb 2013 10:17:01 -0700 Subject: [PATCH 362/870] Added a new required ALLOWED_HOSTS setting for HTTP host header validation. This is a security fix; disclosure and advisory coming shortly. --- django/conf/global_settings.py | 4 ++ .../project_template/project_name/settings.py | 4 ++ django/contrib/auth/tests/views.py | 1 + django/contrib/contenttypes/tests.py | 2 + django/contrib/sites/tests.py | 2 + django/http/request.py | 53 +++++++++++++-- django/test/utils.py | 6 ++ docs/ref/settings.txt | 36 ++++++++++ docs/releases/1.5.txt | 10 +++ docs/topics/security.txt | 67 +++++++++---------- tests/regressiontests/csrf_tests/tests.py | 4 ++ tests/regressiontests/requests/tests.py | 24 ++++++- 12 files changed, 169 insertions(+), 44 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 6a01493a72..659f2f42b7 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -29,6 +29,10 @@ ADMINS = () # * Receive x-headers INTERNAL_IPS = () +# Hosts/domain names that are valid for this site. +# "*" matches anything, ".example.com" matches example.com and all subdomains +ALLOWED_HOSTS = [] + # Local time zone for this installation. All choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all # systems may support all possibilities). When USE_TZ is True, this is diff --git a/django/conf/project_template/project_name/settings.py b/django/conf/project_template/project_name/settings.py index 8815dc6bc0..d46f327922 100644 --- a/django/conf/project_template/project_name/settings.py +++ b/django/conf/project_template/project_name/settings.py @@ -25,6 +25,10 @@ DEBUG = True TEMPLATE_DEBUG = True +# Hosts/domain names that are valid for this site; required if DEBUG is False +# See https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#allowed-hosts +ALLOWED_HOSTS = [] + # Application definition diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index 48dfc9ed76..229e294398 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -108,6 +108,7 @@ class PasswordResetTest(AuthViewsTestCase): self.assertEqual(len(mail.outbox), 1) self.assertEqual("staffmember@example.com", mail.outbox[0].from_email) + @override_settings(ALLOWED_HOSTS=['adminsite.com']) def test_admin_reset(self): "If the reset view is marked as being for admin, the HTTP_HOST header is used for a domain override." response = self.client.post('/admin_password_reset/', diff --git a/django/contrib/contenttypes/tests.py b/django/contrib/contenttypes/tests.py index 10311fae92..7937873a00 100644 --- a/django/contrib/contenttypes/tests.py +++ b/django/contrib/contenttypes/tests.py @@ -6,6 +6,7 @@ from django.contrib.contenttypes.views import shortcut from django.contrib.sites.models import Site, get_current_site from django.http import HttpRequest, Http404 from django.test import TestCase +from django.test.utils import override_settings from django.utils.http import urlquote from django.utils import six from django.utils.encoding import python_2_unicode_compatible @@ -203,6 +204,7 @@ class ContentTypesTests(TestCase): }) + @override_settings(ALLOWED_HOSTS=['example.com']) def test_shortcut_view(self): """ Check that the shortcut view (used for the admin "view on site" diff --git a/django/contrib/sites/tests.py b/django/contrib/sites/tests.py index 1bb2495e6b..cdbd78b80d 100644 --- a/django/contrib/sites/tests.py +++ b/django/contrib/sites/tests.py @@ -5,6 +5,7 @@ from django.contrib.sites.models import Site, RequestSite, get_current_site from django.core.exceptions import ObjectDoesNotExist from django.http import HttpRequest from django.test import TestCase +from django.test.utils import override_settings class SitesFrameworkTests(TestCase): @@ -41,6 +42,7 @@ class SitesFrameworkTests(TestCase): site = Site.objects.get_current() self.assertEqual("Example site", site.name) + @override_settings(ALLOWED_HOSTS=['example.com']) def test_get_current_site(self): # Test that the correct Site object is returned request = HttpRequest() diff --git a/django/http/request.py b/django/http/request.py index a8eb14d154..2c19e4ee8c 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -64,11 +64,12 @@ class HttpRequest(object): if server_port != ('443' if self.is_secure() else '80'): host = '%s:%s' % (host, server_port) - # Disallow potentially poisoned hostnames. - if not host_validation_re.match(host.lower()): - raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host) - - return host + allowed_hosts = ['*'] if settings.DEBUG else settings.ALLOWED_HOSTS + if validate_host(host, allowed_hosts): + return host + else: + raise SuspiciousOperation( + "Invalid HTTP_HOST header (you may need to set ALLOWED_HOSTS): %s" % host) def get_full_path(self): # RFC 3986 requires query string arguments to be in the ASCII range. @@ -450,3 +451,45 @@ def bytes_to_text(s, encoding): return six.text_type(s, encoding, 'replace') else: return s + + +def validate_host(host, allowed_hosts): + """ + Validate the given host header value for this site. + + Check that the host looks valid and matches a host or host pattern in the + given list of ``allowed_hosts``. Any pattern beginning with a period + matches a domain and all its subdomains (e.g. ``.example.com`` matches + ``example.com`` and any subdomain), ``*`` matches anything, and anything + else must match exactly. + + Return ``True`` for a valid host, ``False`` otherwise. + + """ + # All validation is case-insensitive + host = host.lower() + + # Basic sanity check + if not host_validation_re.match(host): + return False + + # Validate only the domain part. + if host[-1] == ']': + # It's an IPv6 address without a port. + domain = host + else: + domain = host.rsplit(':', 1)[0] + + for pattern in allowed_hosts: + pattern = pattern.lower() + match = ( + pattern == '*' or + pattern.startswith('.') and ( + domain.endswith(pattern) or domain == pattern[1:] + ) or + pattern == domain + ) + if match: + return True + + return False diff --git a/django/test/utils.py b/django/test/utils.py index a8ed3d6317..5d20120f58 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -78,6 +78,9 @@ def setup_test_environment(): mail.original_email_backend = settings.EMAIL_BACKEND settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + settings._original_allowed_hosts = settings.ALLOWED_HOSTS + settings.ALLOWED_HOSTS = ['*'] + mail.outbox = [] deactivate() @@ -96,6 +99,9 @@ def teardown_test_environment(): settings.EMAIL_BACKEND = mail.original_email_backend del mail.original_email_backend + settings.ALLOWED_HOSTS = settings._original_allowed_hosts + del settings._original_allowed_hosts + del mail.outbox diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 25818184f6..bba936d837 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -56,6 +56,42 @@ of (Full name, email address). Example:: Note that Django will email *all* of these people whenever an error happens. See :doc:`/howto/error-reporting` for more information. +.. setting:: ALLOWED_HOSTS + +ALLOWED_HOSTS +------------- + +Default: ``[]`` (Empty list) + +A list of strings representing the host/domain names that this Django site can +serve. This is a security measure to prevent an attacker from poisoning caches +and password reset emails with links to malicious hosts by submitting requests +with a fake HTTP ``Host`` header, which is possible even under many +seemingly-safe webserver configurations. + +Values in this list can be fully qualified names (e.g. ``'www.example.com'``), +in which case they will be matched against the request's ``Host`` header +exactly (case-insensitive, not including port). A value beginning with a period +can be used as a subdomain wildcard: ``'.example.com'`` will match +``example.com``, ``www.example.com``, and any other subdomain of +``example.com``. A value of ``'*'`` will match anything; in this case you are +responsible to provide your own validation of the ``Host`` header (perhaps in a +middleware; if so this middleware must be listed first in +:setting:`MIDDLEWARE_CLASSES`). + +If the ``Host`` header (or ``X-Forwarded-Host`` if +:setting:`USE_X_FORWARDED_HOST` is enabled) does not match any value in this +list, the :meth:`django.http.HttpRequest.get_host()` method will raise +:exc:`~django.core.exceptions.SuspiciousOperation`. + +When :setting:`DEBUG` is ``True`` or when running tests, host validation is +disabled; any host will be accepted. Thus it's usually only necessary to set it +in production. + +This validation only applies via :meth:`~django.http.HttpRequest.get_host()`; +if your code accesses the ``Host`` header directly from ``request.META`` you +are bypassing this security protection. + .. setting:: ALLOWED_INCLUDE_ROOTS ALLOWED_INCLUDE_ROOTS diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 8813313035..63f9758762 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -354,6 +354,16 @@ Backwards incompatible changes in 1.5 deprecation timeline for a given feature, its removal may appear as a backwards incompatible change. +``ALLOWED_HOSTS`` required in production +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The new :setting:`ALLOWED_HOSTS` setting validates the request's ``Host`` +header and protects against host-poisoning attacks. This setting is now +required whenever :setting:`DEBUG` is ``False``, or else +:meth:`django.http.HttpRequest.get_host()` will raise +:exc:`~django.core.exceptions.SuspiciousOperation`. For more details see the +:setting:`full documentation` for the new setting. + Managers on abstract models ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/security.txt b/docs/topics/security.txt index 07b8ebcdd2..566202eefa 100644 --- a/docs/topics/security.txt +++ b/docs/topics/security.txt @@ -160,47 +160,40 @@ server, there are some additional steps you may need: .. _host-headers-virtual-hosting: -Host headers and virtual hosting -================================ +Host header validation +====================== -Django uses the ``Host`` header provided by the client to construct URLs -in certain cases. While these values are sanitized to prevent Cross -Site Scripting attacks, they can be used for Cross-Site Request -Forgery and cache poisoning attacks in some circumstances. We -recommend you ensure your Web server is configured such that: +Django uses the ``Host`` header provided by the client to construct URLs in +certain cases. While these values are sanitized to prevent Cross Site Scripting +attacks, a fake ``Host`` value can be used for Cross-Site Request Forgery, +cache poisoning attacks, and poisoning links in emails. -* It always validates incoming HTTP ``Host`` headers against the expected - host name. -* Disallows requests with no ``Host`` header. -* Is *not* configured with a catch-all virtual host that forwards requests - to a Django application. +Because even seemingly-secure webserver configurations are susceptible to fake +``Host`` headers, Django validates ``Host`` headers against the +:setting:`ALLOWED_HOSTS` setting in the +:meth:`django.http.HttpRequest.get_host()` method. + +This validation only applies via :meth:`~django.http.HttpRequest.get_host()`; +if your code accesses the ``Host`` header directly from ``request.META`` you +are bypassing this security protection. + +For more details see the full :setting:`ALLOWED_HOSTS` documentation. + +.. warning:: + + Previous versions of this document recommended configuring your webserver to + ensure it validates incoming HTTP ``Host`` headers. While this is still + recommended, in many common webservers a configuration that seems to + validate the ``Host`` header may not in fact do so. For instance, even if + Apache is configured such that your Django site is served from a non-default + virtual host with the ``ServerName`` set, it is still possible for an HTTP + request to match this virtual host and supply a fake ``Host`` header. Thus, + Django now requires that you set :setting:`ALLOWED_HOSTS` explicitly rather + than relying on webserver configuration. Additionally, as of 1.3.1, Django requires you to explicitly enable support for -the ``X-Forwarded-Host`` header if your configuration requires it. - -Configuration for Apache ------------------------- - -The easiest way to get the described behavior in Apache is as follows. Create -a `virtual host`_ using the ServerName_ and ServerAlias_ directives to restrict -the domains Apache reacts to. Please keep in mind that while the directives do -support ports the match is only performed against the hostname. This means that -the ``Host`` header could still contain a port pointing to another webserver on -the same machine. The next step is to make sure that your newly created virtual -host is not also the default virtual host. Apache uses the first virtual host -found in the configuration file as default virtual host. As such you have to -ensure that you have another virtual host which will act as catch-all virtual -host. Just add one if you do not have one already, there is nothing special -about it aside from ensuring it is the first virtual host in the configuration -file. Debian/Ubuntu users usually don't have to take any action, since Apache -ships with a default virtual host in ``sites-available`` which is linked into -``sites-enabled`` as ``000-default`` and included from ``apache2.conf``. Just -make sure not to name your site ``000-abc``, since files are included in -alphabetical order. - -.. _virtual host: http://httpd.apache.org/docs/2.2/vhosts/ -.. _ServerName: http://httpd.apache.org/docs/2.2/mod/core.html#servername -.. _ServerAlias: http://httpd.apache.org/docs/2.2/mod/core.html#serveralias +the ``X-Forwarded-Host`` header (via the :setting:`USE_X_FORWARDED_HOST` +setting) if your configuration requires it. .. _additional-security-topics: diff --git a/tests/regressiontests/csrf_tests/tests.py b/tests/regressiontests/csrf_tests/tests.py index 3719108962..5300b2172a 100644 --- a/tests/regressiontests/csrf_tests/tests.py +++ b/tests/regressiontests/csrf_tests/tests.py @@ -7,6 +7,7 @@ from django.http import HttpRequest, HttpResponse from django.middleware.csrf import CsrfViewMiddleware, CSRF_KEY_LENGTH from django.template import RequestContext, Template from django.test import TestCase +from django.test.utils import override_settings from django.views.decorators.csrf import csrf_exempt, requires_csrf_token, ensure_csrf_cookie @@ -269,6 +270,7 @@ class CsrfViewMiddlewareTest(TestCase): csrf_cookie = resp2.cookies[settings.CSRF_COOKIE_NAME] self._check_token_present(resp, csrf_id=csrf_cookie.value) + @override_settings(ALLOWED_HOSTS=['www.example.com']) def test_https_bad_referer(self): """ Test that a POST HTTPS request with a bad referer is rejected @@ -281,6 +283,7 @@ class CsrfViewMiddlewareTest(TestCase): self.assertNotEqual(None, req2) self.assertEqual(403, req2.status_code) + @override_settings(ALLOWED_HOSTS=['www.example.com']) def test_https_good_referer(self): """ Test that a POST HTTPS request with a good referer is accepted @@ -292,6 +295,7 @@ class CsrfViewMiddlewareTest(TestCase): req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) self.assertEqual(None, req2) + @override_settings(ALLOWED_HOSTS=['www.example.com']) def test_https_good_referer_2(self): """ Test that a POST HTTPS request with a good referer is accepted diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py index 84928f39ba..345376d2df 100644 --- a/tests/regressiontests/requests/tests.py +++ b/tests/regressiontests/requests/tests.py @@ -84,7 +84,13 @@ class RequestsTests(unittest.TestCase): self.assertEqual(request.build_absolute_uri(location="/path/with:colons"), 'http://www.example.com/path/with:colons') - @override_settings(USE_X_FORWARDED_HOST=False) + @override_settings( + USE_X_FORWARDED_HOST=False, + ALLOWED_HOSTS=[ + 'forward.com', 'example.com', 'internal.com', '12.34.56.78', + '[2001:19f0:feee::dead:beef:cafe]', 'xn--4ca9at.com', + '.multitenant.com', 'INSENSITIVE.com', + ]) def test_http_get_host(self): # Check if X_FORWARDED_HOST is provided. request = HttpRequest() @@ -131,6 +137,9 @@ class RequestsTests(unittest.TestCase): '[2001:19f0:feee::dead:beef:cafe]', '[2001:19f0:feee::dead:beef:cafe]:8080', 'xn--4ca9at.com', # Punnycode for öäü.com + 'anything.multitenant.com', + 'multitenant.com', + 'insensitive.com', ] poisoned_hosts = [ @@ -139,6 +148,7 @@ class RequestsTests(unittest.TestCase): 'example.com:dr.frankenstein@evil.tld:80', 'example.com:80/badpath', 'example.com: recovermypassword.com', + 'other.com', # not in ALLOWED_HOSTS ] for host in legit_hosts: @@ -156,7 +166,7 @@ class RequestsTests(unittest.TestCase): } request.get_host() - @override_settings(USE_X_FORWARDED_HOST=True) + @override_settings(USE_X_FORWARDED_HOST=True, ALLOWED_HOSTS=['*']) def test_http_get_host_with_x_forwarded_host(self): # Check if X_FORWARDED_HOST is provided. request = HttpRequest() @@ -229,6 +239,16 @@ class RequestsTests(unittest.TestCase): request.get_host() + @override_settings(DEBUG=True, ALLOWED_HOSTS=[]) + def test_host_validation_disabled_in_debug_mode(self): + """If ALLOWED_HOSTS is empty and DEBUG is True, all hosts pass.""" + request = HttpRequest() + request.META = { + 'HTTP_HOST': 'example.com', + } + self.assertEqual(request.get_host(), 'example.com') + + def test_near_expiration(self): "Cookie will expire when an near expiration time is provided" response = HttpResponse() From c6d69c12ea7ee9ad35abc7dbf95e00d624d0df5d Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 11 Feb 2013 21:54:53 -0700 Subject: [PATCH 363/870] Restrict the XML deserializer to prevent network and entity-expansion DoS attacks. This is a security fix. Disclosure and advisory coming shortly. --- django/core/serializers/xml_serializer.py | 95 ++++++++++++++++++- .../serializers_regress/tests.py | 15 +++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index ea333a22bd..9dd4eee282 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -10,6 +10,8 @@ from django.db import models, DEFAULT_DB_ALIAS from django.utils.xmlutils import SimplerXMLGenerator from django.utils.encoding import smart_text from xml.dom import pulldom +from xml.sax import handler +from xml.sax.expatreader import ExpatParser as _ExpatParser class Serializer(base.Serializer): """ @@ -151,9 +153,13 @@ class Deserializer(base.Deserializer): def __init__(self, stream_or_string, **options): super(Deserializer, self).__init__(stream_or_string, **options) - self.event_stream = pulldom.parse(self.stream) + self.event_stream = pulldom.parse(self.stream, self._make_parser()) self.db = options.pop('using', DEFAULT_DB_ALIAS) + def _make_parser(self): + """Create a hardened XML parser (no custom/external entities).""" + return DefusedExpatParser() + def __next__(self): for event, node in self.event_stream: if event == "START_ELEMENT" and node.nodeName == "object": @@ -292,3 +298,90 @@ def getInnerText(node): else: pass return "".join(inner_text) + + +# Below code based on Christian Heimes' defusedxml + + +class DefusedExpatParser(_ExpatParser): + """ + An expat parser hardened against XML bomb attacks. + + Forbids DTDs, external entity references + + """ + def __init__(self, *args, **kwargs): + _ExpatParser.__init__(self, *args, **kwargs) + self.setFeature(handler.feature_external_ges, False) + self.setFeature(handler.feature_external_pes, False) + + def start_doctype_decl(self, name, sysid, pubid, has_internal_subset): + raise DTDForbidden(name, sysid, pubid) + + def entity_decl(self, name, is_parameter_entity, value, base, + sysid, pubid, notation_name): + raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name) + + def unparsed_entity_decl(self, name, base, sysid, pubid, notation_name): + # expat 1.2 + raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name) + + def external_entity_ref_handler(self, context, base, sysid, pubid): + raise ExternalReferenceForbidden(context, base, sysid, pubid) + + def reset(self): + _ExpatParser.reset(self) + parser = self._parser + parser.StartDoctypeDeclHandler = self.start_doctype_decl + parser.EntityDeclHandler = self.entity_decl + parser.UnparsedEntityDeclHandler = self.unparsed_entity_decl + parser.ExternalEntityRefHandler = self.external_entity_ref_handler + + +class DefusedXmlException(ValueError): + """Base exception.""" + def __repr__(self): + return str(self) + + +class DTDForbidden(DefusedXmlException): + """Document type definition is forbidden.""" + def __init__(self, name, sysid, pubid): + super(DTDForbidden, self).__init__() + self.name = name + self.sysid = sysid + self.pubid = pubid + + def __str__(self): + tpl = "DTDForbidden(name='{}', system_id={!r}, public_id={!r})" + return tpl.format(self.name, self.sysid, self.pubid) + + +class EntitiesForbidden(DefusedXmlException): + """Entity definition is forbidden.""" + def __init__(self, name, value, base, sysid, pubid, notation_name): + super(EntitiesForbidden, self).__init__() + self.name = name + self.value = value + self.base = base + self.sysid = sysid + self.pubid = pubid + self.notation_name = notation_name + + def __str__(self): + tpl = "EntitiesForbidden(name='{}', system_id={!r}, public_id={!r})" + return tpl.format(self.name, self.sysid, self.pubid) + + +class ExternalReferenceForbidden(DefusedXmlException): + """Resolving an external reference is forbidden.""" + def __init__(self, context, base, sysid, pubid): + super(ExternalReferenceForbidden, self).__init__() + self.context = context + self.base = base + self.sysid = sysid + self.pubid = pubid + + def __str__(self): + tpl = "ExternalReferenceForbidden(system_id='{}', public_id={})" + return tpl.format(self.sysid, self.pubid) diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py index 67aec08af2..e586d76e7f 100644 --- a/tests/regressiontests/serializers_regress/tests.py +++ b/tests/regressiontests/serializers_regress/tests.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, unicode_literals import datetime import decimal +from django.core.serializers.xml_serializer import DTDForbidden try: import yaml @@ -514,3 +515,17 @@ for format in serializers.get_serializer_formats(): if format != 'python': setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format)) + +class XmlDeserializerSecurityTests(TestCase): + + def test_no_dtd(self): + """ + The XML deserializer shouldn't allow a DTD. + + This is the most straightforward way to prevent all entity definitions + and avoid both external entities and entity-expansion attacks. + + """ + xml = '' + with self.assertRaises(DTDForbidden): + next(serializers.deserialize('xml', xml)) From 1f39eafd60761bf6a60b74d9e9859621da1b9363 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 4 Feb 2013 16:57:59 -0700 Subject: [PATCH 364/870] Checked object permissions on admin history view. This is a security fix. Disclosure and advisory coming shortly. Patch by Russell Keith-Magee. --- django/contrib/admin/options.py | 14 +++++--- tests/regressiontests/admin_views/tests.py | 40 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 8de31121e0..ff7873b40c 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1354,15 +1354,21 @@ class ModelAdmin(BaseModelAdmin): def history_view(self, request, object_id, extra_context=None): "The 'history' admin view for this model." from django.contrib.admin.models import LogEntry + # First check if the user can see this history. model = self.model + obj = get_object_or_404(model, pk=unquote(object_id)) + + if not self.has_change_permission(request, obj): + raise PermissionDenied + + # Then get the history for this object. opts = model._meta app_label = opts.app_label action_list = LogEntry.objects.filter( - object_id = unquote(object_id), - content_type__id__exact = ContentType.objects.get_for_model(model).id + object_id=unquote(object_id), + content_type__id__exact=ContentType.objects.get_for_model(model).id ).select_related().order_by('action_time') - # If no history was found, see whether this object even exists. - obj = get_object_or_404(model, pk=unquote(object_id)) + context = { 'title': _('Change history: %s') % force_text(obj), 'action_list': action_list, diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index e0cd7cdfa1..3d07f85721 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -1103,6 +1103,46 @@ class AdminViewPermissionsTest(TestCase): self.assertContains(response, 'login-form') self.client.get('/test_admin/admin/logout/') + def testHistoryView(self): + """History view should restrict access.""" + + # add user shoud not be able to view the list of article or change any of them + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.adduser_login) + response = self.client.get('/test_admin/admin/admin_views/article/1/history/') + self.assertEqual(response.status_code, 403) + self.client.get('/test_admin/admin/logout/') + + # change user can view all items and edit them + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.changeuser_login) + response = self.client.get('/test_admin/admin/admin_views/article/1/history/') + self.assertEqual(response.status_code, 200) + + # Test redirection when using row-level change permissions. Refs #11513. + RowLevelChangePermissionModel.objects.create(id=1, name="odd id") + RowLevelChangePermissionModel.objects.create(id=2, name="even id") + for login_dict in [self.super_login, self.changeuser_login, self.adduser_login, self.deleteuser_login]: + self.client.post('/test_admin/admin/', login_dict) + response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/history/') + self.assertEqual(response.status_code, 403) + + response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/history/') + self.assertEqual(response.status_code, 200) + + self.client.get('/test_admin/admin/logout/') + + for login_dict in [self.joepublic_login, self.no_username_login]: + self.client.post('/test_admin/admin/', login_dict) + response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/history/') + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'login-form') + response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/history/') + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'login-form') + + self.client.get('/test_admin/admin/logout/') + def testConditionallyShowAddSectionLink(self): """ The foreign key widget should only show the "add related" button if the From 35c991aa06aa34fa458f01eac49275ff4c2d76f9 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 12 Feb 2013 11:22:41 +0100 Subject: [PATCH 365/870] Added a default limit to the maximum number of forms in a formset. This is a security fix. Disclosure and advisory coming shortly. --- django/forms/formsets.py | 23 +++--- docs/topics/forms/formsets.txt | 4 +- docs/topics/forms/modelforms.txt | 4 +- tests/regressiontests/forms/tests/formsets.py | 70 +++++++++++++++++-- .../generic_inline_admin/tests.py | 3 +- 5 files changed, 85 insertions(+), 19 deletions(-) diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 1addbc617b..81b75f2796 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -21,6 +21,9 @@ MAX_NUM_FORM_COUNT = 'MAX_NUM_FORMS' ORDERING_FIELD_NAME = 'ORDER' DELETION_FIELD_NAME = 'DELETE' +# default maximum number of forms in a formset, to prevent memory exhaustion +DEFAULT_MAX_NUM = 1000 + class ManagementForm(Form): """ ``ManagementForm`` is used to keep track of how many form instances @@ -97,11 +100,10 @@ class BaseFormSet(object): total_forms = initial_forms + self.extra # Allow all existing related objects/inlines to be displayed, # but don't allow extra beyond max_num. - if self.max_num is not None: - if initial_forms > self.max_num >= 0: - total_forms = initial_forms - elif total_forms > self.max_num >= 0: - total_forms = self.max_num + if initial_forms > self.max_num >= 0: + total_forms = initial_forms + elif total_forms > self.max_num >= 0: + total_forms = self.max_num return total_forms def initial_form_count(self): @@ -111,14 +113,14 @@ class BaseFormSet(object): else: # Use the length of the inital data if it's there, 0 otherwise. initial_forms = self.initial and len(self.initial) or 0 - if self.max_num is not None and (initial_forms > self.max_num >= 0): + if initial_forms > self.max_num >= 0: initial_forms = self.max_num return initial_forms def _construct_forms(self): # instantiate all the forms and put them in self.forms self.forms = [] - for i in xrange(self.total_form_count()): + for i in xrange(min(self.total_form_count(), self.absolute_max)): self.forms.append(self._construct_form(i)) def _construct_form(self, i, **kwargs): @@ -367,9 +369,14 @@ class BaseFormSet(object): def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False, max_num=None): """Return a FormSet for the given form class.""" + if max_num is None: + max_num = DEFAULT_MAX_NUM + # hard limit on forms instantiated, to prevent memory-exhaustion attacks + # limit defaults to DEFAULT_MAX_NUM, but developer can increase it via max_num + absolute_max = max(DEFAULT_MAX_NUM, max_num) attrs = {'form': form, 'extra': extra, 'can_order': can_order, 'can_delete': can_delete, - 'max_num': max_num} + 'max_num': max_num, 'absolute_max': absolute_max} return type(form.__name__ + str('FormSet'), (formset,), attrs) def all_valid(formsets): diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index e2a2b00c7d..d2d102b5d6 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -98,8 +98,8 @@ If the value of ``max_num`` is greater than the number of existing objects, up to ``extra`` additional blank forms will be added to the formset, so long as the total number of forms does not exceed ``max_num``. -A ``max_num`` value of ``None`` (the default) puts no limit on the number of -forms displayed. +A ``max_num`` value of ``None`` (the default) puts a high limit on the number +of forms displayed (1000). In practice this is equivalent to no limit. Formset validation ------------------ diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index fec0d14836..62020e461e 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -738,8 +738,8 @@ so long as the total number of forms does not exceed ``max_num``:: -A ``max_num`` value of ``None`` (the default) puts no limit on the number of -forms displayed. +A ``max_num`` value of ``None`` (the default) puts a high limit on the number +of forms displayed (1000). In practice this is equivalent to no limit. Using a model formset in a view ------------------------------- diff --git a/tests/regressiontests/forms/tests/formsets.py b/tests/regressiontests/forms/tests/formsets.py index ef6f40c3e3..573a8f6a6d 100644 --- a/tests/regressiontests/forms/tests/formsets.py +++ b/tests/regressiontests/forms/tests/formsets.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.forms import (CharField, DateField, FileField, Form, IntegerField, - ValidationError) + ValidationError, formsets) from django.forms.formsets import BaseFormSet, formset_factory from django.forms.util import ErrorList from django.test import TestCase @@ -51,7 +51,7 @@ class FormsFormsetTestCase(TestCase): # for adding data. By default, it displays 1 blank form. It can display more, # but we'll look at how to do so later. formset = ChoiceFormSet(auto_id=False, prefix='choices') - self.assertHTMLEqual(str(formset), """ + self.assertHTMLEqual(str(formset), """ Choice: Votes:""") @@ -654,8 +654,8 @@ class FormsFormsetTestCase(TestCase): # Limiting the maximum number of forms ######################################## # Base case for max_num. - # When not passed, max_num will take its default value of None, i.e. unlimited - # number of forms, only controlled by the value of the extra parameter. + # When not passed, max_num will take a high default value, leaving the + # number of forms only controlled by the value of the extra parameter. LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3) formset = LimitedFavoriteDrinkFormSet() @@ -702,8 +702,8 @@ class FormsFormsetTestCase(TestCase): def test_max_num_with_initial_data(self): # max_num with initial data - # When not passed, max_num will take its default value of None, i.e. unlimited - # number of forms, only controlled by the values of the initial and extra + # When not passed, max_num will take a high default value, leaving the + # number of forms only controlled by the value of the initial and extra # parameters. initial = [ @@ -878,6 +878,64 @@ class FormsFormsetTestCase(TestCase): self.assertTrue(formset.is_valid()) self.assertTrue(all([form.is_valid_called for form in formset.forms])) + def test_hard_limit_on_instantiated_forms(self): + """A formset has a hard limit on the number of forms instantiated.""" + # reduce the default limit of 1000 temporarily for testing + _old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM + try: + formsets.DEFAULT_MAX_NUM = 3 + ChoiceFormSet = formset_factory(Choice) + # someone fiddles with the mgmt form data... + formset = ChoiceFormSet( + { + 'choices-TOTAL_FORMS': '4', + 'choices-INITIAL_FORMS': '0', + 'choices-MAX_NUM_FORMS': '4', + 'choices-0-choice': 'Zero', + 'choices-0-votes': '0', + 'choices-1-choice': 'One', + 'choices-1-votes': '1', + 'choices-2-choice': 'Two', + 'choices-2-votes': '2', + 'choices-3-choice': 'Three', + 'choices-3-votes': '3', + }, + prefix='choices', + ) + # But we still only instantiate 3 forms + self.assertEqual(len(formset.forms), 3) + finally: + formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM + + def test_increase_hard_limit(self): + """Can increase the built-in forms limit via a higher max_num.""" + # reduce the default limit of 1000 temporarily for testing + _old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM + try: + formsets.DEFAULT_MAX_NUM = 3 + # for this form, we want a limit of 4 + ChoiceFormSet = formset_factory(Choice, max_num=4) + formset = ChoiceFormSet( + { + 'choices-TOTAL_FORMS': '4', + 'choices-INITIAL_FORMS': '0', + 'choices-MAX_NUM_FORMS': '4', + 'choices-0-choice': 'Zero', + 'choices-0-votes': '0', + 'choices-1-choice': 'One', + 'choices-1-votes': '1', + 'choices-2-choice': 'Two', + 'choices-2-votes': '2', + 'choices-3-choice': 'Three', + 'choices-3-votes': '3', + }, + prefix='choices', + ) + # This time four forms are instantiated + self.assertEqual(len(formset.forms), 4) + finally: + formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM + data = { 'choices-TOTAL_FORMS': '1', # the number of forms rendered diff --git a/tests/regressiontests/generic_inline_admin/tests.py b/tests/regressiontests/generic_inline_admin/tests.py index f03641d292..8ba1700c76 100644 --- a/tests/regressiontests/generic_inline_admin/tests.py +++ b/tests/regressiontests/generic_inline_admin/tests.py @@ -6,6 +6,7 @@ from django.contrib import admin from django.contrib.admin.sites import AdminSite from django.contrib.contenttypes.generic import ( generic_inlineformset_factory, GenericTabularInline) +from django.forms.formsets import DEFAULT_MAX_NUM from django.forms.models import ModelForm from django.test import TestCase from django.test.utils import override_settings @@ -244,7 +245,7 @@ class GenericInlineModelAdminTest(TestCase): # Create a formset with default arguments formset = media_inline.get_formset(request) - self.assertEqual(formset.max_num, None) + self.assertEqual(formset.max_num, DEFAULT_MAX_NUM) self.assertEqual(formset.can_order, False) # Create a formset with custom keyword arguments From 8fbea5e1881e8c310a462599a191619688ba67dd Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 12 Feb 2013 16:06:03 -0700 Subject: [PATCH 366/870] Update 1.5 release notes for XML and formset fixes. --- docs/releases/1.5.txt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 63f9758762..73986d226f 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -628,6 +628,25 @@ your routers allow synchronizing content types and permissions to only one of them. See the docs on the :ref:`behavior of contrib apps with multiple databases ` for more information. +XML deserializer will not parse documents with a DTD +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to prevent exposure to denial-of-service attacks related to external +entity references and entity expansion, the XML model deserializer now refuses +to parse XML documents containing a DTD (DOCTYPE definition). Since the XML +serializer does not output a DTD, this will not impact typical usage, only +cases where custom-created XML documents are passed to Django's model +deserializer. + +Formsets default ``max_num`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A (default) value of ``None`` for the ``max_num`` argument to a formset factory +no longer defaults to allowing any number of forms in the formset. Instead, in +order to prevent memory-exhaustion attacks, it now defaults to a limit of 1000 +forms. This limit can be raised by explicitly setting a higher value for +``max_num``. + Miscellaneous ~~~~~~~~~~~~~ From 3f49d91463fcb74611d8b3eb19f5b68c6aae6812 Mon Sep 17 00:00:00 2001 From: Justin Turner Arthur Date: Tue, 19 Feb 2013 17:03:33 -0600 Subject: [PATCH 367/870] Fixes typo introduced by django/django@08dc90bccf7c4ffa8b04064d74b54c1150af5ff9. This is described in Trac ticket:19855. --- docs/howto/legacy-databases.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/legacy-databases.txt b/docs/howto/legacy-databases.txt index 67bce7e976..6846e4b2df 100644 --- a/docs/howto/legacy-databases.txt +++ b/docs/howto/legacy-databases.txt @@ -70,7 +70,7 @@ If you wanted to modify existing data on your ``CENSUS_PERSONS`` SQL table with Django you'd need to change the ``managed`` option highlighted above to ``True`` (or simply remove it to let it because ``True`` is its default value). -This servers as an explicit opt-in to give your nascent Django project write +This serves as an explicit opt-in to give your nascent Django project write access to your precious data on a model by model basis. .. versionchanged:: 1.6 From 132d5822b0651bd0f192388693cb22263e68ddf5 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 19 Feb 2013 18:19:50 -0500 Subject: [PATCH 368/870] Fixed #19728 - Updated API stability doc to reflect current meaning of "stable". --- docs/misc/api-stability.txt | 101 +++--------------------------------- 1 file changed, 8 insertions(+), 93 deletions(-) diff --git a/docs/misc/api-stability.txt b/docs/misc/api-stability.txt index 3c265be04f..8517866769 100644 --- a/docs/misc/api-stability.txt +++ b/docs/misc/api-stability.txt @@ -4,17 +4,19 @@ API stability :doc:`The release of Django 1.0 ` comes with a promise of API stability and forwards-compatibility. In a nutshell, this means that code you -develop against Django 1.0 will continue to work against 1.1 unchanged, and you -should need to make only minor changes for any 1.X release. +develop against a 1.X version of Django will continue to work with future +1.X releases. You may need to make minor changes when upgrading the version of +Django your project uses: see the "Backwards incompatible changes" section of +the :doc:`release note ` for the version or versions to which +you are upgrading. What "stable" means =================== In this context, stable means: -- All the public APIs -- everything documented in the linked documents below, - and all methods that don't begin with an underscore -- will not be moved or - renamed without providing backwards-compatible aliases. +- All the public APIs (everything in this documentation) will not be moved + or renamed without providing backwards-compatible aliases. - If new features are added to these APIs -- which is quite possible -- they will not break or change the meaning of existing methods. In other @@ -35,77 +37,7 @@ Stable APIs =========== In general, everything covered in the documentation -- with the exception of -anything in the :doc:`internals area ` is considered stable as -of 1.0. This includes these APIs: - -- :doc:`Authorization ` - -- :doc:`Caching `. - -- :doc:`Model definition, managers, querying and transactions - ` - -- :doc:`Sending email `. - -- :doc:`File handling and storage ` - -- :doc:`Forms ` - -- :doc:`HTTP request/response handling `, including file - uploads, middleware, sessions, URL resolution, view, and shortcut APIs. - -- :doc:`Generic views `. - -- :doc:`Internationalization `. - -- :doc:`Pagination ` - -- :doc:`Serialization ` - -- :doc:`Signals ` - -- :doc:`Templates `, including the language, Python-level - :doc:`template APIs `, and :doc:`custom template tags - and libraries `. We may add new template - tags in the future and the names may inadvertently clash with - external template tags. Before adding any such tags, we'll ensure that - Django raises an error if it tries to load tags with duplicate names. - -- :doc:`Testing ` - -- :doc:`django-admin utility `. - -- :doc:`Built-in middleware ` - -- :doc:`Request/response objects `. - -- :doc:`Settings `. Note, though that while the :doc:`list of - built-in settings ` can be considered complete we may -- and - probably will -- add new settings in future versions. This is one of those - places where "'stable' does not mean 'complete.'" - -- :doc:`Built-in signals `. Like settings, we'll probably add - new signals in the future, but the existing ones won't break. - -- :doc:`Unicode handling `. - -- Everything covered by the :doc:`HOWTO guides `. - -``django.utils`` ----------------- - -Most of the modules in ``django.utils`` are designed for internal use. Only -the following parts of :doc:`django.utils ` can be considered stable: - -- ``django.utils.cache`` -- ``django.utils.datastructures.SortedDict`` -- only this single class; the - rest of the module is for internal use. -- ``django.utils.encoding`` -- ``django.utils.feedgenerator`` -- ``django.utils.http`` -- ``django.utils.safestring`` -- ``django.utils.translation`` -- ``django.utils.tzinfo`` +anything in the :doc:`internals area ` is considered stable. Exceptions ========== @@ -121,23 +53,6 @@ If we become aware of a security problem -- hopefully by someone following our everything necessary to fix it. This might mean breaking backwards compatibility; security trumps the compatibility guarantee. -Contributed applications (``django.contrib``) ---------------------------------------------- - -While we'll make every effort to keep these APIs stable -- and have no plans to -break any contrib apps -- this is an area that will have more flux between -releases. As the Web evolves, Django must evolve with it. - -However, any changes to contrib apps will come with an important guarantee: -we'll make sure it's always possible to use an older version of a contrib app if -we need to make changes. Thus, if Django 1.5 ships with a backwards-incompatible -``django.contrib.flatpages``, we'll make sure you can still use the Django 1.4 -version alongside Django 1.5. This will continue to allow for easy upgrades. - -Historically, apps in ``django.contrib`` have been more stable than the core, so -in practice we probably won't have to ever make this exception. However, it's -worth noting if you're building apps that depend on ``django.contrib``. - APIs marked as internal ----------------------- From fd3a066ae319956d14a35445d83293fecdfdcce7 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 19 Feb 2013 18:05:02 -0800 Subject: [PATCH 369/870] This function is unused and should have been removed a few releases ago. --- django/db/backends/mysql/creation.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py index eb31dc66d1..8d72d11921 100644 --- a/django/db/backends/mysql/creation.py +++ b/django/db/backends/mysql/creation.py @@ -41,26 +41,3 @@ class DatabaseCreation(BaseDatabaseCreation): def sql_for_inline_foreign_key_references(self, model, field, known_models, style): "All inline references are pending under MySQL" return [], True - - def sql_for_inline_many_to_many_references(self, model, field, style): - from django.db import models - opts = model._meta - qn = self.connection.ops.quote_name - - table_output = [ - ' %s %s %s,' % - (style.SQL_FIELD(qn(field.m2m_column_name())), - style.SQL_COLTYPE(models.ForeignKey(model).db_type(connection=self.connection)), - style.SQL_KEYWORD('NOT NULL')), - ' %s %s %s,' % - (style.SQL_FIELD(qn(field.m2m_reverse_name())), - style.SQL_COLTYPE(models.ForeignKey(field.rel.to).db_type(connection=self.connection)), - style.SQL_KEYWORD('NOT NULL')) - ] - deferred = [ - (field.m2m_db_table(), field.m2m_column_name(), opts.db_table, - opts.pk.column), - (field.m2m_db_table(), field.m2m_reverse_name(), - field.rel.to._meta.db_table, field.rel.to._meta.pk.column) - ] - return table_output, deferred From ae2d04f7265bd804e82c601e9cbb52965c564418 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 19 Feb 2013 17:01:29 +0100 Subject: [PATCH 370/870] Add test for collapsible fieldset functionality in admin --- tests/regressiontests/admin_views/admin.py | 10 +++++++++ tests/regressiontests/admin_views/tests.py | 24 ++++++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/tests/regressiontests/admin_views/admin.py b/tests/regressiontests/admin_views/admin.py index 435883e637..d4348968e0 100644 --- a/tests/regressiontests/admin_views/admin.py +++ b/tests/regressiontests/admin_views/admin.py @@ -71,6 +71,16 @@ class ChapterXtra1Admin(admin.ModelAdmin): class ArticleAdmin(admin.ModelAdmin): list_display = ('content', 'date', callable_year, 'model_year', 'modeladmin_year') list_filter = ('date', 'section') + fieldsets=( + ('Some fields', { + 'classes': ('collapse',), + 'fields': ('title', 'content') + }), + ('Some other fields', { + 'classes': ('wide',), + 'fields': ('date', 'section') + }) + ) def changelist_view(self, request): "Test that extra_context works" diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 3d07f85721..667f3bc183 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -3195,12 +3195,12 @@ class PrePopulatedTest(TestCase): @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase): +class SeleniumAdminViewsFirefoxTests(AdminSeleniumWebDriverTestCase): webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver' urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml'] - def test_basic(self): + def test_prepopulated_fields(self): """ Ensure that the JavaScript-automated prepopulated fields work with the main form and with stacked and tabular inlines. @@ -3310,12 +3310,28 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase): slug2='option-one-tabular-inline-ignored-characters', ) + def test_collapsible_fieldset(self): + """ + Test that the 'collapse' class in fieldsets definition allows to + show/hide the appropriate field section. + """ + self.admin_login(username='super', password='secret', login_url='/test_admin/admin/') + self.selenium.get('%s%s' % (self.live_server_url, + '/test_admin/admin/admin_views/article/add/')) + self.assertFalse(self.selenium.find_element_by_id('id_title').is_displayed()) + self.selenium.find_elements_by_link_text('Show')[0].click() + self.assertTrue(self.selenium.find_element_by_id('id_title').is_displayed()) + self.assertEqual( + self.selenium.find_element_by_id('fieldsetcollapser0').text, + "Hide" + ) -class SeleniumPrePopulatedChromeTests(SeleniumPrePopulatedFirefoxTests): + +class SeleniumAdminViewsChromeTests(SeleniumAdminViewsFirefoxTests): webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver' -class SeleniumPrePopulatedIETests(SeleniumPrePopulatedFirefoxTests): +class SeleniumAdminViewsIETests(SeleniumAdminViewsFirefoxTests): webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver' From 8bbca211b6d4457e2f7acc3d41e54373f03ecddd Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 18 Feb 2013 22:02:33 +0100 Subject: [PATCH 371/870] Fixed #14571 -- Updated embedded jQuery from 1.4.2 to 1.9.1 Thanks dArignac for the initial patch. --- .../contrib/admin/static/admin/js/actions.js | 12 +- .../admin/static/admin/js/actions.min.js | 6 +- .../contrib/admin/static/admin/js/inlines.js | 8 +- .../admin/static/admin/js/inlines.min.js | 2 +- .../contrib/admin/static/admin/js/jquery.js | 12677 ++++++++++------ .../admin/static/admin/js/jquery.min.js | 159 +- 6 files changed, 8036 insertions(+), 4828 deletions(-) diff --git a/django/contrib/admin/static/admin/js/actions.js b/django/contrib/admin/static/admin/js/actions.js index 8494970aa1..1992a702c2 100644 --- a/django/contrib/admin/static/admin/js/actions.js +++ b/django/contrib/admin/static/admin/js/actions.js @@ -9,7 +9,7 @@ } else { reset(); } - $(actionCheckboxes).attr("checked", checked) + $(actionCheckboxes).prop("checked", checked) .parent().parent().toggleClass(options.selectedClass, checked); }, updateCounter = function() { @@ -19,7 +19,7 @@ sel: sel, cnt: _actions_icnt }, true)); - $(options.allToggle).attr("checked", function() { + $(options.allToggle).prop("checked", function() { if (sel == actionCheckboxes.length) { value = true; showQuestion(); @@ -64,7 +64,7 @@ } }); $(options.allToggle).show().click(function() { - checker($(this).attr("checked")); + checker($(this).prop("checked")); updateCounter(); }); $("div.actions span.question a").click(function(event) { @@ -74,7 +74,7 @@ }); $("div.actions span.clear a").click(function(event) { event.preventDefault(); - $(options.allToggle).attr("checked", false); + $(options.allToggle).prop("checked", false); clearAcross(); checker(0); updateCounter(); @@ -85,14 +85,14 @@ var target = event.target ? event.target : event.srcElement; if (lastChecked && $.data(lastChecked) != $.data(target) && event.shiftKey === true) { var inrange = false; - $(lastChecked).attr("checked", target.checked) + $(lastChecked).prop("checked", target.checked) .parent().parent().toggleClass(options.selectedClass, target.checked); $(actionCheckboxes).each(function() { if ($.data(this) == $.data(lastChecked) || $.data(this) == $.data(target)) { inrange = (inrange) ? false : true; } if (inrange) { - $(this).attr("checked", target.checked) + $(this).prop("checked", target.checked) .parent().parent().toggleClass(options.selectedClass, target.checked); } }); diff --git a/django/contrib/admin/static/admin/js/actions.min.js b/django/contrib/admin/static/admin/js/actions.min.js index 6b1947cefe..4d2c5f7f1c 100644 --- a/django/contrib/admin/static/admin/js/actions.min.js +++ b/django/contrib/admin/static/admin/js/actions.min.js @@ -1,6 +1,6 @@ -(function(a){a.fn.actions=function(n){var b=a.extend({},a.fn.actions.defaults,n),e=a(this),g=false,k=function(c){c?i():j();a(e).attr("checked",c).parent().parent().toggleClass(b.selectedClass,c)},f=function(){var c=a(e).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},true));a(b.allToggle).attr("checked",function(){if(c==e.length){value=true;i()}else{value=false;l()}return value})},i= +(function(a){a.fn.actions=function(n){var b=a.extend({},a.fn.actions.defaults,n),e=a(this),g=false,k=function(c){c?i():j();a(e).prop("checked",c).parent().parent().toggleClass(b.selectedClass,c)},f=function(){var c=a(e).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},true));a(b.allToggle).prop("checked",function(){if(c==e.length){value=true;i()}else{value=false;l()}return value})},i= function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()},m=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()},j=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()},l=function(){j();a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)};a(b.counterContainer).show(); -a(this).filter(":checked").each(function(){a(this).parent().parent().toggleClass(b.selectedClass);f();a(b.acrossInput).val()==1&&m()});a(b.allToggle).show().click(function(){k(a(this).attr("checked"));f()});a("div.actions span.question a").click(function(c){c.preventDefault();a(b.acrossInput).val(1);m()});a("div.actions span.clear a").click(function(c){c.preventDefault();a(b.allToggle).attr("checked",false);l();k(0);f()});lastChecked=null;a(e).click(function(c){if(!c)c=window.event;var d=c.target? -c.target:c.srcElement;if(lastChecked&&a.data(lastChecked)!=a.data(d)&&c.shiftKey===true){var h=false;a(lastChecked).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(e).each(function(){if(a.data(this)==a.data(lastChecked)||a.data(this)==a.data(d))h=h?false:true;h&&a(this).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,d.checked);lastChecked=d;f()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){g= +a(this).filter(":checked").each(function(){a(this).parent().parent().toggleClass(b.selectedClass);f();a(b.acrossInput).val()==1&&m()});a(b.allToggle).show().click(function(){k(a(this).prop("checked"));f()});a("div.actions span.question a").click(function(c){c.preventDefault();a(b.acrossInput).val(1);m()});a("div.actions span.clear a").click(function(c){c.preventDefault();a(b.allToggle).prop("checked",false);l();k(0);f()});lastChecked=null;a(e).click(function(c){if(!c)c=window.event;var d=c.target? +c.target:c.srcElement;if(lastChecked&&a.data(lastChecked)!=a.data(d)&&c.shiftKey===true){var h=false;a(lastChecked).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(e).each(function(){if(a.data(this)==a.data(lastChecked)||a.data(this)==a.data(d))h=h?false:true;h&&a(this).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,d.checked);lastChecked=d;f()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){g= true});a('form#changelist-form button[name="index"]').click(function(){if(g)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))});a('form#changelist-form input[name="_save"]').click(function(){var c=false;a("div.actions select option:selected").each(function(){if(a(this).val())c=true});if(c)return g?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")): confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})};a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery); diff --git a/django/contrib/admin/static/admin/js/inlines.js b/django/contrib/admin/static/admin/js/inlines.js index 4dc9459ff3..64307951bb 100644 --- a/django/contrib/admin/static/admin/js/inlines.js +++ b/django/contrib/admin/static/admin/js/inlines.js @@ -22,8 +22,8 @@ var updateElementIndex = function(el, prefix, ndx) { var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); var replacement = prefix + "-" + ndx; - if ($(el).attr("for")) { - $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); + if ($(el).prop("for")) { + $(el).prop("for", $(el).prop("for").replace(id_regex, replacement)); } if (el.id) { el.id = el.id.replace(id_regex, replacement); @@ -32,9 +32,9 @@ el.name = el.name.replace(id_regex, replacement); } }; - var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off"); + var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off"); var nextIndex = parseInt(totalForms.val(), 10); - var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off"); + var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off"); // only show the add button if we are allowed to add more items, // note that max_num = None translates to a blank string. var showAddButton = maxForms.val() === '' || (maxForms.val()-totalForms.val()) > 0; diff --git a/django/contrib/admin/static/admin/js/inlines.min.js b/django/contrib/admin/static/admin/js/inlines.min.js index d48ee0a098..0745bf1910 100644 --- a/django/contrib/admin/static/admin/js/inlines.min.js +++ b/django/contrib/admin/static/admin/js/inlines.min.js @@ -1,4 +1,4 @@ -(function(b){b.fn.formset=function(d){var a=b.extend({},b.fn.formset.defaults,d),c=b(this),d=c.parent(),i=function(a,e,g){var d=RegExp("("+e+"-(\\d+|__prefix__))"),e=e+"-"+g;b(a).attr("for")&&b(a).attr("for",b(a).attr("for").replace(d,e));a.id&&(a.id=a.id.replace(d,e));a.name&&(a.name=a.name.replace(d,e))},f=b("#id_"+a.prefix+"-TOTAL_FORMS").attr("autocomplete","off"),g=parseInt(f.val(),10),e=b("#id_"+a.prefix+"-MAX_NUM_FORMS").attr("autocomplete","off"),f=""===e.val()||0'+a.addText+""),h=d.find("tr:last a")):(c.filter(":last").after('"),h=c.filter(":last").next().find("a"));h.click(function(d){d.preventDefault();var f=b("#id_"+a.prefix+"-TOTAL_FORMS"),d=b("#"+a.prefix+ "-empty"),c=d.clone(true);c.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+g);c.is("tr")?c.children(":last").append('"):c.is("ul")||c.is("ol")?c.append('
  • '+a.deleteText+"
  • "):c.children(":first").append(''+a.deleteText+"");c.find("*").each(function(){i(this, a.prefix,f.val())});c.insertBefore(b(d));b(f).val(parseInt(f.val(),10)+1);g=g+1;e.val()!==""&&e.val()-f.val()<=0&&h.parent().hide();c.find("a."+a.deleteCssClass).click(function(d){d.preventDefault();d=b(this).parents("."+a.formCssClass);d.remove();g=g-1;a.removed&&a.removed(d);d=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(d.length);(e.val()===""||e.val()-d.length>0)&&h.parent().show();for(var c=0,f=d.length;c type pairs + class2type = {}, - // A central reference to the root jQuery(document) - rootjQuery, + // List of deleted data cache ids, so we can reuse them + core_deletedIds = [], - // A simple way to check for HTML strings or ID strings - // (both of which we optimize for) - quickExpr = /^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/, - - // Is it a simple selector - isSimple = /^.[^:#\[\.,]*$/, - - // Check if a string has a non-whitespace character in it - rnotwhite = /\S/, - - // Used for trimming whitespace - rtrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, - - // Keep a UserAgent string for use with jQuery.browser - userAgent = navigator.userAgent, - - // For matching the engine and version of the browser - browserMatch, - - // Has the ready events already been bound? - readyBound = false, - - // The functions to execute on DOM ready - readyList = [], - - // The ready event handler - DOMContentLoaded, + core_version = "1.9.1", // Save a reference to some core methods - toString = Object.prototype.toString, - hasOwnProperty = Object.prototype.hasOwnProperty, - push = Array.prototype.push, - slice = Array.prototype.slice, - indexOf = Array.prototype.indexOf; + core_concat = core_deletedIds.concat, + core_push = core_deletedIds.push, + core_slice = core_deletedIds.slice, + core_indexOf = core_deletedIds.indexOf, + core_toString = class2type.toString, + core_hasOwn = class2type.hasOwnProperty, + core_trim = core_version.trim, + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Used for matching numbers + core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, + + // Used for splitting on whitespace + core_rnotwhite = /\S+/g, + + // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, + rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }, + + // The ready event handler + completed = function( event ) { + + // readyState === "complete" is good enough for us to call the dom ready in oldIE + if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { + detach(); + jQuery.ready(); + } + }, + // Clean-up method for dom ready events + detach = function() { + if ( document.addEventListener ) { + document.removeEventListener( "DOMContentLoaded", completed, false ); + window.removeEventListener( "load", completed, false ); + + } else { + document.detachEvent( "onreadystatechange", completed ); + window.detachEvent( "onload", completed ); + } + }; jQuery.fn = jQuery.prototype = { - init: function( selector, context ) { - var match, elem, ret, doc; + // The current version of jQuery being used + jquery: core_version, - // Handle $(""), $(null), or $(undefined) + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) if ( !selector ) { return this; } - // Handle $(DOMElement) - if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - } - - // The body element only exists once, optimize finding it - if ( selector === "body" && !context ) { - this.context = document; - this[0] = document.body; - this.selector = "body"; - this.length = 1; - return this; - } - // Handle HTML strings if ( typeof selector === "string" ) { - // Are we dealing with HTML string or an ID? - match = quickExpr.exec( selector ); + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; - // Verify a match, and that no context was specified for #id + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id if ( match && (match[1] || !context) ) { // HANDLE: $(html) -> $(array) if ( match[1] ) { - doc = (context ? context.ownerDocument || context : document); + context = context instanceof jQuery ? context[0] : context; - // If a single string is passed in and it's a single tag - // just do a createElement and skip the rest - ret = rsingleTag.exec( selector ); + // scripts is true for back-compat + jQuery.merge( this, jQuery.parseHTML( + match[1], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); - if ( ret ) { - if ( jQuery.isPlainObject( context ) ) { - selector = [ document.createElement( ret[1] ) ]; - jQuery.fn.attr.call( selector, context, true ); + // HANDLE: $(html, props) + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); - } else { - selector = [ doc.createElement( ret[1] ) ]; + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } } - - } else { - ret = buildFragment( [ match[1] ], [ doc ] ); - selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes; } - - return jQuery.merge( this, selector ); - - // HANDLE: $("#id") + + return this; + + // HANDLE: $(#id) } else { elem = document.getElementById( match[2] ); - if ( elem ) { + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { // Handle the case where IE and Opera return items // by name instead of ID if ( elem.id !== match[2] ) { @@ -149,30 +191,29 @@ jQuery.fn = jQuery.prototype = { return this; } - // HANDLE: $("TAG") - } else if ( !context && /^\w+$/.test( selector ) ) { - this.selector = selector; - this.context = document; - selector = document.getElementsByTagName( selector ); - return jQuery.merge( this, selector ); - // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { - return (context || rootjQuery).find( selector ); + return ( context || rootjQuery ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else { - return jQuery( context ).find( selector ); + return this.constructor( context ).find( selector ); } + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + // HANDLE: $(function) // Shortcut for document ready } else if ( jQuery.isFunction( selector ) ) { return rootjQuery.ready( selector ); } - if (selector.selector !== undefined) { + if ( selector.selector !== undefined ) { this.selector = selector.selector; this.context = selector.context; } @@ -183,9 +224,6 @@ jQuery.fn = jQuery.prototype = { // Start with an empty selector selector: "", - // The current version of jQuery being used - jquery: "1.4.2", - // The default length of a jQuery object is 0 length: 0, @@ -195,7 +233,7 @@ jQuery.fn = jQuery.prototype = { }, toArray: function() { - return slice.call( this, 0 ); + return core_slice.call( this ); }, // Get the Nth element in the matched element set OR @@ -207,33 +245,20 @@ jQuery.fn = jQuery.prototype = { this.toArray() : // Return just the object - ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] ); + ( num < 0 ? this[ this.length + num ] : this[ num ] ); }, // Take an array of elements and push it onto the stack // (returning the new matched element set) - pushStack: function( elems, name, selector ) { - // Build a new jQuery matched element set - var ret = jQuery(); + pushStack: function( elems ) { - if ( jQuery.isArray( elems ) ) { - push.apply( ret, elems ); - - } else { - jQuery.merge( ret, elems ); - } + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); // Add the old object onto the stack (as a reference) ret.prevObject = this; - ret.context = this.context; - if ( name === "find" ) { - ret.selector = this.selector + (this.selector ? " " : "") + selector; - } else if ( name ) { - ret.selector = this.selector + "." + name + "(" + selector + ")"; - } - // Return the newly-formed element set return ret; }, @@ -244,29 +269,16 @@ jQuery.fn = jQuery.prototype = { each: function( callback, args ) { return jQuery.each( this, callback, args ); }, - + ready: function( fn ) { - // Attach the listeners - jQuery.bindReady(); - - // If the DOM is already ready - if ( jQuery.isReady ) { - // Execute the function immediately - fn.call( document, jQuery ); - - // Otherwise, remember the function for later - } else if ( readyList ) { - // Add the function to the wait list - readyList.push( fn ); - } + // Add the callback + jQuery.ready.promise().done( fn ); return this; }, - - eq: function( i ) { - return i === -1 ? - this.slice( i ) : - this.slice( i, +i + 1 ); + + slice: function() { + return this.pushStack( core_slice.apply( this, arguments ) ); }, first: function() { @@ -277,9 +289,10 @@ jQuery.fn = jQuery.prototype = { return this.eq( -1 ); }, - slice: function() { - return this.pushStack( slice.apply( this, arguments ), - "slice", slice.call(arguments).join(",") ); + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); }, map: function( callback ) { @@ -287,14 +300,14 @@ jQuery.fn = jQuery.prototype = { return callback.call( elem, i, elem ); })); }, - + end: function() { - return this.prevObject || jQuery(null); + return this.prevObject || this.constructor(null); }, // For internal use only. // Behaves like an Array's method, not like a jQuery method. - push: push, + push: core_push, sort: [].sort, splice: [].splice }; @@ -303,8 +316,11 @@ jQuery.fn = jQuery.prototype = { jQuery.fn.init.prototype = jQuery.fn; jQuery.extend = jQuery.fn.extend = function() { - // copy reference to target object - var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy; + var src, copyIsArray, copy, name, options, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; // Handle a deep copy situation if ( typeof target === "boolean" ) { @@ -338,10 +354,15 @@ jQuery.extend = jQuery.fn.extend = function() { continue; } - // Recurse if we're merging object literal values or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || jQuery.isArray(copy) ) ) { - var clone = src && ( jQuery.isPlainObject(src) || jQuery.isArray(src) ) ? src - : jQuery.isArray(copy) ? [] : {}; + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); @@ -360,90 +381,60 @@ jQuery.extend = jQuery.fn.extend = function() { jQuery.extend({ noConflict: function( deep ) { - window.$ = _$; + if ( window.$ === jQuery ) { + window.$ = _$; + } - if ( deep ) { + if ( deep && window.jQuery === jQuery ) { window.jQuery = _jQuery; } return jQuery; }, - + // Is the DOM ready to be used? Set to true once it occurs. isReady: false, - - // Handle when the DOM is ready - ready: function() { - // Make sure that the DOM is not already loaded - if ( !jQuery.isReady ) { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready, 13 ); - } - // Remember that the DOM is ready - jQuery.isReady = true; + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, - // If there are functions bound, to execute - if ( readyList ) { - // Execute all of them - var fn, i = 0; - while ( (fn = readyList[ i++ ]) ) { - fn.call( document, jQuery ); - } - - // Reset the list of functions - readyList = null; - } - - // Trigger any bound ready events - if ( jQuery.fn.triggerHandler ) { - jQuery( document ).triggerHandler( "ready" ); - } + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); } }, - - bindReady: function() { - if ( readyBound ) { + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { return; } - readyBound = true; - - // Catch cases where $(document).ready() is called after the - // browser event has already occurred. - if ( document.readyState === "complete" ) { - return jQuery.ready(); + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready ); } - // Mozilla, Opera and webkit nightlies currently support this event - if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", jQuery.ready, false ); + // Remember that the DOM is ready + jQuery.isReady = true; - // If IE event model is used - } else if ( document.attachEvent ) { - // ensure firing before onload, - // maybe late but safe also for iframes - document.attachEvent("onreadystatechange", DOMContentLoaded); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", jQuery.ready ); + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } - // If IE and not a frame - // continually check to see if the document is ready - var toplevel = false; + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); - try { - toplevel = window.frameElement == null; - } catch(e) {} - - if ( document.documentElement.doScroll && toplevel ) { - doScrollCheck(); - } + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger("ready").off("ready"); } }, @@ -451,117 +442,200 @@ jQuery.extend({ // Since version 1.3, DOM methods and functions like alert // aren't supported. They return false on IE (#2968). isFunction: function( obj ) { - return toString.call(obj) === "[object Function]"; + return jQuery.type(obj) === "function"; }, - isArray: function( obj ) { - return toString.call(obj) === "[object Array]"; + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + if ( obj == null ) { + return String( obj ); + } + return typeof obj === "object" || typeof obj === "function" ? + class2type[ core_toString.call(obj) ] || "object" : + typeof obj; }, isPlainObject: function( obj ) { // Must be an Object. // Because of IE, we also have to check the presence of the constructor property. // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval ) { + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { return false; } - - // Not own constructor property must be Object - if ( obj.constructor - && !hasOwnProperty.call(obj, "constructor") - && !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf") ) { + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !core_hasOwn.call(obj, "constructor") && + !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 return false; } - + // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. - + var key; for ( key in obj ) {} - - return key === undefined || hasOwnProperty.call( obj, key ); + + return key === undefined || core_hasOwn.call( obj, key ); }, isEmptyObject: function( obj ) { - for ( var name in obj ) { + var name; + for ( name in obj ) { return false; } return true; }, - + error: function( msg ) { - throw msg; + throw new Error( msg ); }, - - parseJSON: function( data ) { - if ( typeof data !== "string" || !data ) { + + // data: string of html + // context (optional): If specified, the fragment will be created in this context, defaults to document + // keepScripts (optional): If true, will include scripts passed in the html string + parseHTML: function( data, context, keepScripts ) { + if ( !data || typeof data !== "string" ) { return null; } - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); - - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( /^[\],:{}\s]*$/.test(data.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@") - .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]") - .replace(/(?:^|:|,)(?:\s*\[)+/g, "")) ) { - - // Try to use the native JSON parser first - return window.JSON && window.JSON.parse ? - window.JSON.parse( data ) : - (new Function("return " + data))(); - - } else { - jQuery.error( "Invalid JSON: " + data ); + if ( typeof context === "boolean" ) { + keepScripts = context; + context = false; } + context = context || document; + + var parsed = rsingleTag.exec( data ), + scripts = !keepScripts && []; + + // Single tag + if ( parsed ) { + return [ context.createElement( parsed[1] ) ]; + } + + parsed = jQuery.buildFragment( [ data ], context, scripts ); + if ( scripts ) { + jQuery( scripts ).remove(); + } + return jQuery.merge( [], parsed.childNodes ); + }, + + parseJSON: function( data ) { + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + if ( data === null ) { + return data; + } + + if ( typeof data === "string" ) { + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + if ( data ) { + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + } + } + } + + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + var xml, tmp; + if ( !data || typeof data !== "string" ) { + return null; + } + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; }, noop: function() {}, - // Evalulates a script in a global context + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context globalEval: function( data ) { - if ( data && rnotwhite.test(data) ) { - // Inspired by code by Andrea Giammarchi - // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html - var head = document.getElementsByTagName("head")[0] || document.documentElement, - script = document.createElement("script"); - - script.type = "text/javascript"; - - if ( jQuery.support.scriptEval ) { - script.appendChild( document.createTextNode( data ) ); - } else { - script.text = data; - } - - // Use insertBefore instead of appendChild to circumvent an IE6 bug. - // This arises when a base node is used (#2709). - head.insertBefore( script, head.firstChild ); - head.removeChild( script ); + if ( data && jQuery.trim( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); } }, + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); }, // args is for internal usage only - each: function( object, callback, args ) { - var name, i = 0, - length = object.length, - isObj = length === undefined || jQuery.isFunction(object); + each: function( obj, callback, args ) { + var value, + i = 0, + length = obj.length, + isArray = isArraylike( obj ); if ( args ) { - if ( isObj ) { - for ( name in object ) { - if ( callback.apply( object[ name ], args ) === false ) { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { break; } } } else { - for ( ; i < length; ) { - if ( callback.apply( object[ i++ ], args ) === false ) { + for ( i in obj ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { break; } } @@ -569,51 +643,77 @@ jQuery.extend({ // A special, fast, case for the most common use of each } else { - if ( isObj ) { - for ( name in object ) { - if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { break; } } } else { - for ( var value = object[0]; - i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {} + for ( i in obj ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } } } - return object; + return obj; }, - trim: function( text ) { - return (text || "").replace( rtrim, "" ); - }, + // Use native String.trim function wherever possible + trim: core_trim && !core_trim.call("\uFEFF\xA0") ? + function( text ) { + return text == null ? + "" : + core_trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, // results is for internal usage only - makeArray: function( array, results ) { + makeArray: function( arr, results ) { var ret = results || []; - if ( array != null ) { - // The window, strings (and functions) also have 'length' - // The extra typeof function check is to prevent crashes - // in Safari 2 (See: #3039) - if ( array.length == null || typeof array === "string" || jQuery.isFunction(array) || (typeof array !== "function" && array.setInterval) ) { - push.call( ret, array ); + if ( arr != null ) { + if ( isArraylike( Object(arr) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); } else { - jQuery.merge( ret, array ); + core_push.call( ret, arr ); } } return ret; }, - inArray: function( elem, array ) { - if ( array.indexOf ) { - return array.indexOf( elem ); - } + inArray: function( elem, arr, i ) { + var len; - for ( var i = 0, length = array.length; i < length; i++ ) { - if ( array[ i ] === elem ) { - return i; + if ( arr ) { + if ( core_indexOf ) { + return core_indexOf.call( arr, elem, i ); + } + + len = arr.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in arr && arr[ i ] === elem ) { + return i; + } } } @@ -621,13 +721,14 @@ jQuery.extend({ }, merge: function( first, second ) { - var i = first.length, j = 0; + var l = second.length, + i = first.length, + j = 0; - if ( typeof second.length === "number" ) { - for ( var l = second.length; j < l; j++ ) { + if ( typeof l === "number" ) { + for ( ; j < l; j++ ) { first[ i++ ] = second[ j ]; } - } else { while ( second[j] !== undefined ) { first[ i++ ] = second[ j++ ]; @@ -640,12 +741,17 @@ jQuery.extend({ }, grep: function( elems, callback, inv ) { - var ret = []; + var retVal, + ret = [], + i = 0, + length = elems.length; + inv = !!inv; // Go through the array, only saving the items // that pass the validator function - for ( var i = 0, length = elems.length; i < length; i++ ) { - if ( !inv !== !callback( elems[ i ], i ) ) { + for ( ; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { ret.push( elems[ i ] ); } } @@ -655,194 +761,577 @@ jQuery.extend({ // arg is for internal usage only map: function( elems, callback, arg ) { - var ret = [], value; + var value, + i = 0, + length = elems.length, + isArray = isArraylike( elems ), + ret = []; // Go through the array, translating each of the items to their - // new value (or values). - for ( var i = 0, length = elems.length; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); - if ( value != null ) { - ret[ ret.length ] = value; + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } } } - return ret.concat.apply( [], ret ); + // Flatten any nested arrays + return core_concat.apply( [], ret ); }, // A global GUID counter for objects guid: 1, - proxy: function( fn, proxy, thisObject ) { - if ( arguments.length === 2 ) { - if ( typeof proxy === "string" ) { - thisObject = fn; - fn = thisObject[ proxy ]; - proxy = undefined; + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var args, proxy, tmp; - } else if ( proxy && !jQuery.isFunction( proxy ) ) { - thisObject = proxy; - proxy = undefined; - } + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; } - if ( !proxy && fn ) { - proxy = function() { - return fn.apply( thisObject || this, arguments ); - }; + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; } + // Simulated bind + args = core_slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) ); + }; + // Set the guid of unique handler to the same of original handler, so it can be removed - if ( fn ) { - proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - } + proxy.guid = fn.guid = fn.guid || jQuery.guid++; - // So proxy can be declared as an argument return proxy; }, - // Use of jQuery.browser is frowned upon. - // More details: http://docs.jquery.com/Utilities/jQuery.browser - uaMatch: function( ua ) { - ua = ua.toLowerCase(); + // Multifunctional method to get and set values of a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + length = elems.length, + bulk = key == null; - var match = /(webkit)[ \/]([\w.]+)/.exec( ua ) || - /(opera)(?:.*version)?[ \/]([\w.]+)/.exec( ua ) || - /(msie) ([\w.]+)/.exec( ua ) || - !/compatible/.test( ua ) && /(mozilla)(?:.*? rv:([\w.]+))?/.exec( ua ) || - []; + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); + } - return { browser: match[1] || "", version: match[2] || "0" }; + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < length; i++ ) { + fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; }, - browser: {} + now: function() { + return ( new Date() ).getTime(); + } }); -browserMatch = jQuery.uaMatch( userAgent ); -if ( browserMatch.browser ) { - jQuery.browser[ browserMatch.browser ] = true; - jQuery.browser.version = browserMatch.version; -} +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { -// Deprecated, use jQuery.browser.webkit instead -if ( jQuery.browser.webkit ) { - jQuery.browser.safari = true; -} + readyList = jQuery.Deferred(); -if ( indexOf ) { - jQuery.inArray = function( elem, array ) { - return indexOf.call( array, elem ); - }; + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready ); + + // Standards-based browsers support DOMContentLoaded + } else if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed, false ); + + // If IE event model is used + } else { + // Ensure firing before onload, maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", completed ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", completed ); + + // If IE and not a frame + // continually check to see if the document is ready + var top = false; + + try { + top = window.frameElement == null && document.documentElement; + } catch(e) {} + + if ( top && top.doScroll ) { + (function doScrollCheck() { + if ( !jQuery.isReady ) { + + try { + // Use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + top.doScroll("left"); + } catch(e) { + return setTimeout( doScrollCheck, 50 ); + } + + // detach all dom ready events + detach(); + + // and execute any waiting functions + jQuery.ready(); + } + })(); + } + } + } + return readyList.promise( obj ); +}; + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +function isArraylike( obj ) { + var length = obj.length, + type = jQuery.type( obj ); + + if ( jQuery.isWindow( obj ) ) { + return false; + } + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === "array" || type !== "function" && + ( length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj ); } // All jQuery objects should point back to these rootjQuery = jQuery(document); +// String to Object options format cache +var optionsCache = {}; -// Cleanup functions for the document ready method -if ( document.addEventListener ) { - DOMContentLoaded = function() { - document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - jQuery.ready(); - }; - -} else if ( document.attachEvent ) { - DOMContentLoaded = function() { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( document.readyState === "complete" ) { - document.detachEvent( "onreadystatechange", DOMContentLoaded ); - jQuery.ready(); - } - }; +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; } -// The DOM ready check for Internet Explorer -function doScrollCheck() { - if ( jQuery.isReady ) { - return; - } +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { - try { - // If IE is used, use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - document.documentElement.doScroll("left"); - } catch( error ) { - setTimeout( doScrollCheck, 1 ); - return; - } + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); - // and execute any waiting functions - jQuery.ready(); -} + var // Flag to know if list is currently firing + firing, + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // First callback to fire (used internally by add and fireWith) + firingStart, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( list && ( !fired || stack ) ) { + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; -function evalScript( i, elem ) { - if ( elem.src ) { - jQuery.ajax({ - url: elem.src, - async: false, - dataType: "script" + return self; +}; +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function() { + deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; }); - } else { - jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); - } - if ( elem.parentNode ) { - elem.parentNode.removeChild( elem ); - } -} + // Make the deferred a promise + promise.promise( deferred ); -// Mutifunctional method to get and set values to a collection -// The value/s can be optionally by executed if its a function -function access( elems, key, value, exec, fn, pass ) { - var length = elems.length; - - // Setting many attributes - if ( typeof key === "object" ) { - for ( var k in key ) { - access( elems, k, key[k], exec, fn, value ); + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); } - return elems; - } - - // Setting one attribute - if ( value !== undefined ) { - // Optionally, function values get executed if exec is true - exec = !pass && exec && jQuery.isFunction(value); - - for ( var i = 0; i < length; i++ ) { - fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = core_slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; + if( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } } - - return elems; + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); } - - // Getting an attribute - return length ? fn( elems[0], key ) : undefined; -} +}); +jQuery.support = (function() { -function now() { - return (new Date).getTime(); -} -(function() { + var support, all, a, + input, select, fragment, + opt, eventName, isSupported, i, + div = document.createElement("div"); - jQuery.support = {}; + // Setup + div.setAttribute( "className", "t" ); + div.innerHTML = "
    a"; - var root = document.documentElement, - script = document.createElement("script"), - div = document.createElement("div"), - id = "script" + now(); - - div.style.display = "none"; - div.innerHTML = "
    a"; - - var all = div.getElementsByTagName("*"), - a = div.getElementsByTagName("a")[0]; - - // Can't get basic test support - if ( !all || !all.length || !a ) { - return; + // Support tests won't run in some limited or non-browser environments + all = div.getElementsByTagName("*"); + a = div.getElementsByTagName("a")[ 0 ]; + if ( !all || !a || !all.length ) { + return {}; } - jQuery.support = { + // First batch of tests + select = document.createElement("select"); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName("input")[ 0 ]; + + a.style.cssText = "top:1px;float:left;opacity:.5"; + support = { + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + // IE strips leading whitespace when .innerHTML is used leadingWhitespace: div.firstChild.nodeType === 3, @@ -855,8 +1344,8 @@ function now() { htmlSerialize: !!div.getElementsByTagName("link").length, // Get the style information from getAttribute - // (IE uses .cssText insted) - style: /red/.test( a.getAttribute("style") ), + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), // Make sure that URLs aren't manipulated // (IE normalizes it by default) @@ -865,246 +1354,481 @@ function now() { // Make sure that element opacity exists // (IE uses filter instead) // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.55$/.test( a.style.opacity ), + opacity: /^0.5/.test( a.style.opacity ), // Verify style float existence // (IE uses styleFloat instead of cssFloat) cssFloat: !!a.style.cssFloat, - // Make sure that if no value is specified for a checkbox - // that it defaults to "on". - // (WebKit defaults to "" instead) - checkOn: div.getElementsByTagName("input")[0].value === "on", + // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) + checkOn: !!input.value, // Make sure that a selected-by-default option has a working selected property. // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - optSelected: document.createElement("select").appendChild( document.createElement("option") ).selected, + optSelected: opt.selected, - parentNode: div.removeChild( div.appendChild( document.createElement("div") ) ).parentNode === null, + // Tests for enctype support on a form (#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + + // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode + boxModel: document.compatMode === "CSS1Compat", // Will be defined later deleteExpando: true, - checkClone: false, - scriptEval: false, noCloneEvent: true, - boxModel: null + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true, + boxSizingReliable: true, + pixelPosition: false }; - script.type = "text/javascript"; + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Support: IE<9 try { - script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); - } catch(e) {} - - root.insertBefore( script, root.firstChild ); - - // Make sure that the execution of code works by injecting a script - // tag with appendChild/createTextNode - // (IE doesn't support this, fails, and uses .text instead) - if ( window[ id ] ) { - jQuery.support.scriptEval = true; - delete window[ id ]; + delete div.test; + } catch( e ) { + support.deleteExpando = false; } - // Test to see if it's possible to delete an expando from an element - // Fails in Internet Explorer - try { - delete script.test; - - } catch(e) { - jQuery.support.deleteExpando = false; - } + // Check if we can trust getAttribute("value") + input = document.createElement("input"); + input.setAttribute( "value", "" ); + support.input = input.getAttribute( "value" ) === ""; - root.removeChild( script ); + // Check if an input maintains its value after becoming a radio + input.value = "t"; + input.setAttribute( "type", "radio" ); + support.radioValue = input.value === "t"; - if ( div.attachEvent && div.fireEvent ) { - div.attachEvent("onclick", function click() { - // Cloning a node shouldn't copy over any - // bound event handlers (IE does this) - jQuery.support.noCloneEvent = false; - div.detachEvent("onclick", click); - }); - div.cloneNode(true).fireEvent("onclick"); - } + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "checked", "t" ); + input.setAttribute( "name", "t" ); - div = document.createElement("div"); - div.innerHTML = ""; + fragment = document.createDocumentFragment(); + fragment.appendChild( input ); - var fragment = document.createDocumentFragment(); - fragment.appendChild( div.firstChild ); + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; // WebKit doesn't clone checked state correctly in fragments - jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - // Figure out if the W3C box model works as expected - // document.body must exist before we can do this + // Support: IE<9 + // Opera does not clone events (and typeof div.attachEvent === undefined). + // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() + if ( div.attachEvent ) { + div.attachEvent( "onclick", function() { + support.noCloneEvent = false; + }); + + div.cloneNode( true ).click(); + } + + // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) + // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP), test/csp.php + for ( i in { submit: true, change: true, focusin: true }) { + div.setAttribute( eventName = "on" + i, "t" ); + + support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; + } + + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + // Run tests that need a body at doc ready jQuery(function() { - var div = document.createElement("div"); - div.style.width = div.style.paddingLeft = "1px"; + var container, marginDiv, tds, + divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", + body = document.getElementsByTagName("body")[0]; - document.body.appendChild( div ); - jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; - document.body.removeChild( div ).style.display = 'none'; + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } - div = null; + container = document.createElement("div"); + container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; + + body.appendChild( container ).appendChild( div ); + + // Support: IE8 + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + div.innerHTML = "
    t
    "; + tds = div.getElementsByTagName("td"); + tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Support: IE8 + // Check if empty table cells still have offsetWidth/Height + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check box-sizing and margin behavior + div.innerHTML = ""; + div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; + support.boxSizing = ( div.offsetWidth === 4 ); + support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 ); + + // Use window.getComputedStyle because jsdom on node.js will break without it. + if ( window.getComputedStyle ) { + support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; + support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. (#3333) + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + marginDiv = div.appendChild( document.createElement("div") ); + marginDiv.style.cssText = div.style.cssText = divReset; + marginDiv.style.marginRight = marginDiv.style.width = "0"; + div.style.width = "1px"; + + support.reliableMarginRight = + !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + } + + if ( typeof div.style.zoom !== core_strundefined ) { + // Support: IE<8 + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + div.innerHTML = ""; + div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Support: IE6 + // Check if elements with layout shrink-wrap their children + div.style.display = "block"; + div.innerHTML = "
    "; + div.firstChild.style.width = "5px"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + + if ( support.inlineBlockNeedsLayout ) { + // Prevent IE 6 from affecting layout for positioned elements #11048 + // Prevent IE from shrinking the body in IE 7 mode #12869 + // Support: IE<8 + body.style.zoom = 1; + } + } + + body.removeChild( container ); + + // Null elements to avoid leaks in IE + container = div = tds = marginDiv = null; }); - // Technique from Juriy Zaytsev - // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ - var eventSupported = function( eventName ) { - var el = document.createElement("div"); - eventName = "on" + eventName; + // Null elements to avoid leaks in IE + all = select = fragment = opt = a = input = null; - var isSupported = (eventName in el); - if ( !isSupported ) { - el.setAttribute(eventName, "return;"); - isSupported = typeof el[eventName] === "function"; - } - el = null; - - return isSupported; - }; - - jQuery.support.submitBubbles = eventSupported("submit"); - jQuery.support.changeBubbles = eventSupported("change"); - - // release memory in IE - root = script = div = all = a = null; + return support; })(); -jQuery.props = { - "for": "htmlFor", - "class": "className", - readonly: "readOnly", - maxlength: "maxLength", - cellspacing: "cellSpacing", - rowspan: "rowSpan", - colspan: "colSpan", - tabindex: "tabIndex", - usemap: "useMap", - frameborder: "frameBorder" -}; -var expando = "jQuery" + now(), uuid = 0, windowData = {}; +var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, + rmultiDash = /([A-Z])/g; + +function internalData( elem, name, data, pvt /* Internal Use Only */ ){ + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = core_deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; +} + +function internalRemoveData( elem, name, pvt ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var i, l, thisCache, + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } + } + } else { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = name.concat( jQuery.map( name, jQuery.camelCase ) ); + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; + } + } + + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); + + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + } else if ( jQuery.support.deleteExpando || cache != cache.window ) { + delete cache[ id ]; + + // When all else fails, null + } else { + cache[ id ] = null; + } +} jQuery.extend({ cache: {}, - - expando:expando, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), // The following elements throw uncatchable exceptions if you // attempt to add expando properties to them. noData: { "embed": true, - "object": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", "applet": true }, + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + data: function( elem, name, data ) { - if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { - return; - } - - elem = elem == window ? - windowData : - elem; - - var id = elem[ expando ], cache = jQuery.cache, thisCache; - - if ( !id && typeof name === "string" && data === undefined ) { - return null; - } - - // Compute a unique ID for the element - if ( !id ) { - id = ++uuid; - } - - // Avoid generating a new cache unless none exists and we - // want to manipulate it. - if ( typeof name === "object" ) { - elem[ expando ] = id; - thisCache = cache[ id ] = jQuery.extend(true, {}, name); - - } else if ( !cache[ id ] ) { - elem[ expando ] = id; - cache[ id ] = {}; - } - - thisCache = cache[ id ]; - - // Prevent overriding the named cache with undefined values - if ( data !== undefined ) { - thisCache[ name ] = data; - } - - return typeof name === "string" ? thisCache[ name ] : thisCache; + return internalData( elem, name, data ); }, removeData: function( elem, name ) { - if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { - return; + return internalRemoveData( elem, name ); + }, + + // For internal use only. + _data: function( elem, name, data ) { + return internalData( elem, name, data, true ); + }, + + _removeData: function( elem, name ) { + return internalRemoveData( elem, name, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + // Do not set data on non-element because it will not be cleared (#8335). + if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { + return false; } - elem = elem == window ? - windowData : - elem; + var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; - var id = elem[ expando ], cache = jQuery.cache, thisCache = cache[ id ]; - - // If we want to remove a specific section of the element's data - if ( name ) { - if ( thisCache ) { - // Remove the section of cache data - delete thisCache[ name ]; - - // If we've removed all the data, remove the element's cache - if ( jQuery.isEmptyObject(thisCache) ) { - jQuery.removeData( elem ); - } - } - - // Otherwise, we want to remove all of the element's data - } else { - if ( jQuery.support.deleteExpando ) { - delete elem[ jQuery.expando ]; - - } else if ( elem.removeAttribute ) { - elem.removeAttribute( jQuery.expando ); - } - - // Completely remove the data cache - delete cache[ id ]; - } + // nodes accept data unless otherwise specified; rejection can be conditional + return !noData || noData !== true && elem.getAttribute("classid") === noData; } }); jQuery.fn.extend({ data: function( key, value ) { - if ( typeof key === "undefined" && this.length ) { - return jQuery.data( this[0] ); + var attrs, name, + elem = this[0], + i = 0, + data = null; - } else if ( typeof key === "object" ) { + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attrs = elem.attributes; + for ( ; i < attrs.length; i++ ) { + name = attrs[i].name; + + if ( !name.indexOf( "data-" ) ) { + name = jQuery.camelCase( name.slice(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { return this.each(function() { jQuery.data( this, key ); }); } - var parts = key.split("."); - parts[1] = parts[1] ? "." + parts[1] : ""; + return jQuery.access( this, function( value ) { - if ( value === undefined ) { - var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); - - if ( data === undefined && this.length ) { - data = jQuery.data( this[0], key ); + if ( value === undefined ) { + // Try to fetch any internally stored data first + return elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; } - return data === undefined && parts[1] ? - this.data( parts[0] ) : - data; - } else { - return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function() { + + this.each(function() { jQuery.data( this, key, value ); }); - } + }, null, value, arguments.length > 1, null, true ); }, removeData: function( key ) { @@ -1113,146 +1837,269 @@ jQuery.fn.extend({ }); } }); -jQuery.extend({ - queue: function( elem, type, data ) { - if ( !elem ) { - return; - } - type = (type || "fx") + "queue"; - var q = jQuery.data( elem, type ); +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { - // Speed up dequeue by getting out quickly if this is just a lookup - if ( !data ) { - return q || []; - } + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - if ( !q || jQuery.isArray(data) ) { - q = jQuery.data( elem, type, jQuery.makeArray(data) ); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); } else { - q.push( data ); + data = undefined; } + } - return q; + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } }, dequeue: function( elem, type ) { type = type || "fx"; - var queue = jQuery.queue( elem, type ), fn = queue.shift(); + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; // If the fx queue is dequeued, always remove the progress sentinel if ( fn === "inprogress" ) { fn = queue.shift(); + startLength--; } + hooks.cur = fn; if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being // automatically dequeued if ( type === "fx" ) { - queue.unshift("inprogress"); + queue.unshift( "inprogress" ); } - fn.call(elem, function() { - jQuery.dequeue(elem, type); - }); + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery._removeData( elem, type + "queue" ); + jQuery._removeData( elem, key ); + }) + }); } }); jQuery.fn.extend({ queue: function( type, data ) { + var setter = 2; + if ( typeof type !== "string" ) { data = type; type = "fx"; + setter--; } - if ( data === undefined ) { + if ( arguments.length < setter ) { return jQuery.queue( this[0], type ); } - return this.each(function( i, elem ) { - var queue = jQuery.queue( this, type, data ); - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); }, dequeue: function( type ) { return this.each(function() { jQuery.dequeue( this, type ); }); }, - // Based off of the plugin by Clint Helfers, with permission. // http://blindsignals.com/index.php/2009/07/jquery-delay/ delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; type = type || "fx"; - return this.queue( type, function() { - var elem = this; - setTimeout(function() { - jQuery.dequeue( elem, type ); - }, time ); + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; }); }, - clearQueue: function( type ) { return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while( i-- ) { + tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); } }); -var rclass = /[\n\t]/g, - rspace = /\s+/, +var nodeHook, boolHook, + rclass = /[\t\r\n]/g, rreturn = /\r/g, - rspecialurl = /href|src|style/, - rtype = /(button|input)/i, - rfocusable = /(button|input|object|select|textarea)/i, - rclickable = /^(a|area)$/i, - rradiocheck = /radio|checkbox/; + rfocusable = /^(?:input|select|textarea|button|object)$/i, + rclickable = /^(?:a|area)$/i, + rboolean = /^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i, + ruseDefault = /^(?:checked|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + getSetInput = jQuery.support.input; jQuery.fn.extend({ attr: function( name, value ) { - return access( this, name, value, true, jQuery.attr ); + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); }, - removeAttr: function( name, fn ) { - return this.each(function(){ - jQuery.attr( this, name, "" ); - if ( this.nodeType === 1 ) { - this.removeAttribute( name ); - } + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} }); }, addClass: function( value ) { - if ( jQuery.isFunction(value) ) { - return this.each(function(i) { - var self = jQuery(this); - self.addClass( value.call(this, i, self.attr("class")) ); + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call( this, j, this.className ) ); }); } - if ( value && typeof value === "string" ) { - var classNames = (value || "").split( rspace ); + if ( proceed ) { + // The disjunction here is for better compressibility (see removeClass) + classes = ( value || "" ).match( core_rnotwhite ) || []; - for ( var i = 0, l = this.length; i < l; i++ ) { - var elem = this[i]; + for ( ; i < len; i++ ) { + elem = this[ i ]; + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + " " + ); - if ( elem.nodeType === 1 ) { - if ( !elem.className ) { - elem.className = value; - - } else { - var className = " " + elem.className + " ", setClass = elem.className; - for ( var c = 0, cl = classNames.length; c < cl; c++ ) { - if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { - setClass += " " + classNames[c]; - } + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; } - elem.className = jQuery.trim( setClass ); } + elem.className = jQuery.trim( cur ); + } } } @@ -1261,30 +2108,36 @@ jQuery.fn.extend({ }, removeClass: function( value ) { - if ( jQuery.isFunction(value) ) { - return this.each(function(i) { - var self = jQuery(this); - self.removeClass( value.call(this, i, self.attr("class")) ); + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = arguments.length === 0 || typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call( this, j, this.className ) ); }); } + if ( proceed ) { + classes = ( value || "" ).match( core_rnotwhite ) || []; - if ( (value && typeof value === "string") || value === undefined ) { - var classNames = (value || "").split(rspace); + for ( ; i < len; i++ ) { + elem = this[ i ]; + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + "" + ); - for ( var i = 0, l = this.length; i < l; i++ ) { - var elem = this[i]; - - if ( elem.nodeType === 1 && elem.className ) { - if ( value ) { - var className = (" " + elem.className + " ").replace(rclass, " "); - for ( var c = 0, cl = classNames.length; c < cl; c++ ) { - className = className.replace(" " + classNames[c] + " ", " "); + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { + cur = cur.replace( " " + clazz + " ", " " ); } - elem.className = jQuery.trim( className ); - - } else { - elem.className = ""; } + elem.className = value ? jQuery.trim( cur ) : ""; } } } @@ -1293,44 +2146,52 @@ jQuery.fn.extend({ }, toggleClass: function( value, stateVal ) { - var type = typeof value, isBool = typeof stateVal === "boolean"; + var type = typeof value, + isBool = typeof stateVal === "boolean"; if ( jQuery.isFunction( value ) ) { - return this.each(function(i) { - var self = jQuery(this); - self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); }); } return this.each(function() { if ( type === "string" ) { // toggle individual class names - var className, i = 0, self = jQuery(this), + var className, + i = 0, + self = jQuery( this ), state = stateVal, - classNames = value.split( rspace ); + classNames = value.match( core_rnotwhite ) || []; while ( (className = classNames[ i++ ]) ) { - // check each className given, space seperated list + // check each className given, space separated list state = isBool ? state : !self.hasClass( className ); self[ state ? "addClass" : "removeClass" ]( className ); } - } else if ( type === "undefined" || type === "boolean" ) { + // Toggle whole class name + } else if ( type === core_strundefined || type === "boolean" ) { if ( this.className ) { // store className if set - jQuery.data( this, "__className__", this.className ); + jQuery._data( this, "__className__", this.className ); } - // toggle whole className - this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || ""; + // If the element has a class name or if we're passed "false", + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; } }); }, hasClass: function( selector ) { - var className = " " + selector + " "; - for ( var i = 0, l = this.length; i < l; i++ ) { - if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { return true; } } @@ -1339,95 +2200,60 @@ jQuery.fn.extend({ }, val: function( value ) { - if ( value === undefined ) { - var elem = this[0]; + var ret, hooks, isFunction, + elem = this[0]; + if ( !arguments.length ) { if ( elem ) { - if ( jQuery.nodeName( elem, "option" ) ) { - return (elem.attributes.value || {}).specified ? elem.value : elem.text; + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; } - // We need to handle select boxes special - if ( jQuery.nodeName( elem, "select" ) ) { - var index = elem.selectedIndex, - values = [], - options = elem.options, - one = elem.type === "select-one"; - - // Nothing was selected - if ( index < 0 ) { - return null; - } - - // Loop through all the selected options - for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { - var option = options[ i ]; - - if ( option.selected ) { - // Get the specifc value for the option - value = jQuery(option).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - } - - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { - return elem.getAttribute("value") === null ? "on" : elem.value; - } - - - // Everything else, we just grab the value - return (elem.value || "").replace(rreturn, ""); + ret = elem.value; + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; } - return undefined; + return; } - var isFunction = jQuery.isFunction(value); + isFunction = jQuery.isFunction( value ); - return this.each(function(i) { - var self = jQuery(this), val = value; + return this.each(function( i ) { + var val, + self = jQuery(this); if ( this.nodeType !== 1 ) { return; } if ( isFunction ) { - val = value.call(this, i, self.val()); - } - - // Typecast each time if the value is a Function and the appended - // value is therefore different each time. - if ( typeof val === "number" ) { - val += ""; - } - - if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { - this.checked = jQuery.inArray( self.val(), val ) >= 0; - - } else if ( jQuery.nodeName( this, "select" ) ) { - var values = jQuery.makeArray(val); - - jQuery( "option", this ).each(function() { - this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; - }); - - if ( !values.length ) { - this.selectedIndex = -1; - } - + val = value.call( this, i, self.val() ); } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { this.value = val; } }); @@ -1435,212 +2261,535 @@ jQuery.fn.extend({ }); jQuery.extend({ - attrFn: { - val: true, - css: true, - html: true, - text: true, - data: true, - width: true, - height: true, - offset: true + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, option, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one" || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? + max : + one ? index : 0; + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // oldIE doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + // Don't return options that are disabled or in a disabled optgroup + ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && + ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } }, - - attr: function( elem, name, value, pass ) { - // don't set attributes on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { - return undefined; - } - if ( pass && name in jQuery.attrFn ) { - return jQuery(elem)[name](value); - } + attr: function( elem, name, value ) { + var hooks, notxml, ret, + nType = elem.nodeType; - var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), - // Whether we are setting (or getting) - set = value !== undefined; - - // Try to normalize/fix the name - name = notxml && jQuery.props[ name ] || name; - - // Only do all the following if this is a node (faster for style) - if ( elem.nodeType === 1 ) { - // These attributes require special treatment - var special = rspecialurl.test( name ); - - // Safari mis-reports the default selected property of an option - // Accessing the parent's selectedIndex property fixes it - if ( name === "selected" && !jQuery.support.optSelected ) { - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } - - // If applicable, access the attribute via the DOM 0 way - if ( name in elem && notxml && !special ) { - if ( set ) { - // We can't allow the type property to be changed (since it causes problems in IE) - if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { - jQuery.error( "type property can't be changed" ); - } - - elem[ name ] = value; - } - - // browsers index elements by id/name on forms, give priority to attributes. - if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { - return elem.getAttributeNode( name ).nodeValue; - } - - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - if ( name === "tabIndex" ) { - var attributeNode = elem.getAttributeNode( "tabIndex" ); - - return attributeNode && attributeNode.specified ? - attributeNode.value : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; - } - - return elem[ name ]; - } - - if ( !jQuery.support.style && notxml && name === "style" ) { - if ( set ) { - elem.style.cssText = "" + value; - } - - return elem.style.cssText; - } - - if ( set ) { - // convert the value to a string (all browsers do this but IE) see #1070 - elem.setAttribute( name, "" + value ); - } - - var attr = !jQuery.support.hrefNormalized && notxml && special ? - // Some attributes require a special call on IE - elem.getAttribute( name, 2 ) : - elem.getAttribute( name ); - - // Non-existent attributes return null, we normalize to undefined - return attr === null ? undefined : attr; - } - - // elem is actually elem.style ... set the style - // Using attr for specific style information is now deprecated. Use style instead. - return jQuery.style( elem, name, value ); - } -}); -var rnamespaces = /\.(.*)$/, - fcleanup = function( nm ) { - return nm.replace(/[^\w\s\.\|`]/g, function( ch ) { - return "\\" + ch; - }); - }; - -/* - * A number of helper functions used for managing events. - * Many of the ideas behind this code originated from - * Dean Edwards' addEvent library. - */ -jQuery.event = { - - // Bind an event to an element - // Original by Dean Edwards - add: function( elem, types, handler, data ) { - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { return; } - // For whatever reason, IE has trouble passing the window object - // around, causing it to be cloned in the process - if ( elem.setInterval && ( elem !== window && !elem.frameElement ) ) { - elem = window; + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === core_strundefined ) { + return jQuery.prop( elem, name, value ); } - var handleObjIn, handleObj; + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); } - // Make sure that the function being executed has a unique ID - if ( !handler.guid ) { - handler.guid = jQuery.guid++; + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + + } else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, value + "" ); + return value; + } + + } else if ( hooks && notxml && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + // In IE9+, Flash objects don't have .getAttribute (#12945) + // Support: IE9+ + if ( typeof elem.getAttribute !== core_strundefined ) { + ret = elem.getAttribute( name ); + } + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var name, propName, + i = 0, + attrNames = value && value.match( core_rnotwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( (name = attrNames[i++]) ) { + propName = jQuery.propFix[ name ] || name; + + // Boolean attributes get special treatment (#10870) + if ( rboolean.test( name ) ) { + // Set corresponding property to false for boolean attributes + // Also clear defaultChecked/defaultSelected (if appropriate) for IE<8 + if ( !getSetAttribute && ruseDefault.test( name ) ) { + elem[ jQuery.camelCase( "default-" + name ) ] = + elem[ propName ] = false; + } else { + elem[ propName ] = false; + } + + // See #9699 for explanation of this approach (setting first, then removal) + } else { + jQuery.attr( elem, name, "" ); + } + + elem.removeAttribute( getSetAttribute ? name : propName ); + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to default in case type is set after value during creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; } - // Init the element's event structure - var elemData = jQuery.data( elem ); + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - // If no elemData is found then we must be trying to bind to one of the - // banned noData elements + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + var + // Use .prop to determine if this attribute is understood as boolean + prop = jQuery.prop( elem, name ), + + // Fetch it accordingly + attr = typeof prop === "boolean" && elem.getAttribute( name ), + detail = typeof prop === "boolean" ? + + getSetInput && getSetAttribute ? + attr != null : + // oldIE fabricates an empty string for missing boolean attributes + // and conflates checked/selected into attroperties + ruseDefault.test( name ) ? + elem[ jQuery.camelCase( "default-" + name ) ] : + !!attr : + + // fetch an attribute node for properties not recognized as boolean + elem.getAttributeNode( name ); + + return detail && detail.value !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + // IE<8 needs the *property* name + elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); + + // Use defaultChecked and defaultSelected for oldIE + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; + } + + return name; + } +}; + +// fix oldIE value attroperty +if ( !getSetInput || !getSetAttribute ) { + jQuery.attrHooks.value = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return jQuery.nodeName( elem, "input" ) ? + + // Ignore the value *property* by using defaultValue + elem.defaultValue : + + ret && ret.specified ? ret.value : undefined; + }, + set: function( elem, value, name ) { + if ( jQuery.nodeName( elem, "input" ) ) { + // Does not return so that setAttribute is also used + elem.defaultValue = value; + } else { + // Use nodeHook if defined (#1954); otherwise setAttribute is fine + return nodeHook && nodeHook.set( elem, value, name ); + } + } + }; +} + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return ret && ( name === "id" || name === "name" || name === "coords" ? ret.value !== "" : ret.specified ) ? + ret.value : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + elem.setAttributeNode( + (ret = elem.ownerDocument.createAttribute( name )) + ); + } + + ret.value = value += ""; + + // Break association with cloned elements by also using setAttribute (#9646) + return name === "value" || value === elem.getAttribute( name ) ? + value : + undefined; + } + }; + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + nodeHook.set( elem, value === "" ? false : value, name ); + } + }; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); +} + + +// Some attributes require a special call on IE +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret == null ? undefined : ret; + } + }); + }); + + // href/src property should get the full normalized URL (#10299/#12915) + jQuery.each([ "href", "src" ], function( i, name ) { + jQuery.propHooks[ name ] = { + get: function( elem ) { + return elem.getAttribute( name, 4 ); + } + }; + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Note: IE uppercases css property names, but if we were to .toLowerCase() + // .cssText, that would destroy case senstitivity in URL's, like in "background" + return elem.style.cssText || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = value + "" ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); +var rformElems = /^(?:input|select|textarea)$/i, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + var tmp, events, t, handleObjIn, + special, eventHandle, handleObj, + handlers, type, namespaces, origType, + elemData = jQuery._data( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) if ( !elemData ) { return; } - var events = elemData.events = elemData.events || {}, - eventHandle = elemData.handle, eventHandle; - - if ( !eventHandle ) { - elemData.handle = eventHandle = function() { - // Handle the second event of a trigger and when - // an event is called after a page has unloaded - return typeof jQuery !== "undefined" && !jQuery.event.triggered ? - jQuery.event.handle.apply( eventHandle.elem, arguments ) : - undefined; - }; + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; } - // Add elem as a property of the handle function - // This is to prevent a memory leak with non-native events in IE. - eventHandle.elem = elem; + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } // Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); - types = types.split(" "); + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); - var type, i = 0, namespaces; + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; - while ( (type = types[ i++ ]) ) { - handleObj = handleObjIn ? - jQuery.extend({}, handleObjIn) : - { handler: handler, data: data }; + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; - // Namespaced event handlers - if ( type.indexOf(".") > -1 ) { - namespaces = type.split("."); - type = namespaces.shift(); - handleObj.namespace = namespaces.slice(0).sort().join("."); + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; - } else { - namespaces = []; - handleObj.namespace = ""; - } + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); - handleObj.type = type; - handleObj.guid = handler.guid; - - // Get the current list of functions bound to this event - var handlers = events[ type ], - special = jQuery.event.special[ type ] || {}; - - // Init the event handler queue - if ( !handlers ) { + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { handlers = events[ type ] = []; + handlers.delegateCount = 0; - // Check for a special event handler - // Only use addEventListener/attachEvent if the special - // events handler returns false + // Only use addEventListener/attachEvent if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { @@ -1651,19 +2800,23 @@ jQuery.event = { } } } - - if ( special.add ) { - special.add.call( elem, handleObj ); + + if ( special.add ) { + special.add.call( elem, handleObj ); if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } - // Add the function to the element's handler list - handlers.push( handleObj ); + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } - // Keep track of which events have been used, for global triggering + // Keep track of which events have ever been used, for event optimization jQuery.event.global[ type ] = true; } @@ -1671,280 +2824,206 @@ jQuery.event = { elem = null; }, - global: {}, - // Detach an event or set of events from an element - remove: function( elem, types, handler, pos ) { - // don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + remove: function( elem, types, handler, selector, mappedTypes ) { + var j, handleObj, tmp, + origCount, t, events, + special, handlers, type, + namespaces, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + if ( !elemData || !(events = elemData.events) ) { return; } - var ret, type, fn, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, - elemData = jQuery.data( elem ), - events = elemData && elemData.events; + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); - if ( !elemData || !events ) { - return; - } - - // types is actually an event object here - if ( types && types.type ) { - handler = types.handler; - types = types.type; - } - - // Unbind all events for the element - if ( !types || typeof types === "string" && types.charAt(0) === "." ) { - types = types || ""; - - for ( type in events ) { - jQuery.event.remove( elem, type + types ); - } - - return; - } - - // Handle multiple events separated by a space - // jQuery(...).unbind("mouseover mouseout", fn); - types = types.split(" "); - - while ( (type = types[ i++ ]) ) { - origType = type; - handleObj = null; - all = type.indexOf(".") < 0; - namespaces = []; - - if ( !all ) { - // Namespaced event handlers - namespaces = type.split("."); - type = namespaces.shift(); - - namespace = new RegExp("(^|\\.)" + - jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)") - } - - eventType = events[ type ]; - - if ( !eventType ) { - continue; - } - - if ( !handler ) { - for ( var j = 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; - - if ( all || namespace.test( handleObj.namespace ) ) { - jQuery.event.remove( elem, origType, handleObj.handler, j ); - eventType.splice( j--, 1 ); - } + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } - continue; } special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); - for ( var j = pos || 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; - if ( handler.guid === handleObj.guid ) { - // remove the given handler for the given type - if ( all || namespace.test( handleObj.namespace ) ) { - if ( pos == null ) { - eventType.splice( j--, 1 ); - } + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } + if ( handleObj.selector ) { + handlers.delegateCount--; } - - if ( pos != null ) { - break; + if ( special.remove ) { + special.remove.call( elem, handleObj ); } } } - // remove generic event handler if no more handlers exist - if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { - if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { - removeEvent( elem, type, elemData.handle ); + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); } - ret = null; delete events[ type ]; } } // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { - var handle = elemData.handle; - if ( handle ) { - handle.elem = null; - } - - delete elemData.events; delete elemData.handle; - if ( jQuery.isEmptyObject( elemData ) ) { - jQuery.removeData( elem ); - } + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery._removeData( elem, "events" ); } }, - // bubbling is internal - trigger: function( event, data, elem /*, bubbling */ ) { - // Event object or event type - var type = event.type || event, - bubbling = arguments[3]; + trigger: function( event, data, elem, onlyHandlers ) { + var handle, ontype, cur, + bubbleType, special, tmp, i, + eventPath = [ elem || document ], + type = core_hasOwn.call( event, "type" ) ? event.type : event, + namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; - if ( !bubbling ) { - event = typeof event === "object" ? - // jQuery.Event object - event[expando] ? event : - // Object literal - jQuery.extend( jQuery.Event(type), event ) : - // Just the event type (string) - jQuery.Event(type); + cur = tmp = elem = elem || document; - if ( type.indexOf("!") >= 0 ) { - event.type = type = type.slice(0, -1); - event.exclusive = true; - } + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } - // Handle a global trigger - if ( !elem ) { - // Don't bubble custom events when global (to avoid too much overhead) - event.stopPropagation(); + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } - // Only trigger if we've ever bound an event for it - if ( jQuery.event.global[ type ] ) { - jQuery.each( jQuery.cache, function() { - if ( this.events && this.events[type] ) { - jQuery.event.trigger( event, data, this.handle.elem ); - } - }); - } - } + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; - // Handle triggering a single element + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); - // don't do events on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { - return undefined; - } + event.isTrigger = true; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; - // Clean up in case it is reused - event.result = undefined; + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { event.target = elem; - - // Clone the incoming data, if any - data = jQuery.makeArray( data ); - data.unshift( event ); } - event.currentTarget = elem; + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); - // Trigger the event, it is assumed that "handle" is a function - var handle = jQuery.data( elem, "handle" ); - if ( handle ) { - handle.apply( elem, data ); + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; } - var parent = elem.parentNode || elem.ownerDocument; + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - // Trigger an inline bound script - try { - if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { - if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { - event.result = false; - } + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; } - // prevent IE from throwing an error for some elements with some event types, see #3533 - } catch (e) {} - - if ( !event.isPropagationStopped() && parent ) { - jQuery.event.trigger( event, data, parent, true ); - - } else if ( !event.isDefaultPrevented() ) { - var target = event.target, old, - isClick = jQuery.nodeName(target, "a") && type === "click", - special = jQuery.event.special[ type ] || {}; - - if ( (!special._default || special._default.call( elem, event ) === false) && - !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { - - try { - if ( target[ type ] ) { - // Make sure that we don't accidentally re-trigger the onFOO events - old = target[ "on" + type ]; - - if ( old ) { - target[ "on" + type ] = null; - } - - jQuery.event.triggered = true; - target[ type ](); - } - - // prevent IE from throwing an error for some elements with some event types, see #3533 - } catch (e) {} - - if ( old ) { - target[ "on" + type ] = old; - } - - jQuery.event.triggered = false; + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); } } - }, - handle: function( event ) { - var all, handlers, namespaces, namespace, events; + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { - event = arguments[0] = jQuery.event.fix( event || window.event ); - event.currentTarget = this; + event.type = i > 1 ? + bubbleType : + special.bindType || type; - // Namespaced event handlers - all = event.type.indexOf(".") < 0 && !event.exclusive; + // jQuery handler + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } - if ( !all ) { - namespaces = event.type.split("."); - event.type = namespaces.shift(); - namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)"); + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } } + event.type = type; - var events = jQuery.data(this, "events"), handlers = events[ event.type ]; + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { - if ( events && handlers ) { - // Clone the handlers to prevent manipulation - handlers = handlers.slice(0); + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { - for ( var j = 0, l = handlers.length; j < l; j++ ) { - var handleObj = handlers[ j ]; + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { - // Filter the functions by class - if ( all || namespace.test( handleObj.namespace ) ) { - // Pass in a reference to the handler function itself - // So that we can later remove it - event.handler = handleObj.handler; - event.data = handleObj.data; - event.handleObj = handleObj; - - var ret = handleObj.handler.apply( this, arguments ); + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; - if ( ret !== undefined ) { - event.result = ret; - if ( ret === false ) { - event.preventDefault(); - event.stopPropagation(); - } + if ( tmp ) { + elem[ ontype ] = null; } - if ( event.isImmediatePropagationStopped() ) { - break; + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + try { + elem[ type ](); + } catch ( e ) { + // IE<9 dies on focus/blur to hidden element (#1486,#12518) + // only reproducible on winXP IE8 native, not IE9 in IE8 mode + } + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; } } } @@ -1953,1802 +3032,2563 @@ jQuery.event = { return event.result; }, - props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, ret, handleObj, matched, j, + handlerQueue = [], + args = core_slice.call( arguments ), + handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var sel, handleObj, matches, i, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + + for ( ; cur != this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } + + return handlerQueue; + }, fix: function( event ) { - if ( event[ expando ] ) { + if ( event[ jQuery.expando ] ) { return event; } - // store a copy of the original event object - // and "clone" to set read-only properties - var originalEvent = event; - event = jQuery.Event( originalEvent ); + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; - for ( var i = this.props.length, prop; i; ) { - prop = this.props[ --i ]; + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; event[ prop ] = originalEvent[ prop ]; } - // Fix target property, if necessary + // Support: IE<9 + // Fix target property (#1925) if ( !event.target ) { - event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either + event.target = originalEvent.srcElement || document; } - // check if target is a textnode (safari) + // Support: Chrome 23+, Safari? + // Target should not be a text node (#504, #13143) if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } - // Add relatedTarget, if necessary - if ( !event.relatedTarget && event.fromElement ) { - event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; - } + // Support: IE<9 + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) + event.metaKey = !!event.metaKey; - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && event.clientX != null ) { - var doc = document.documentElement, body = document.body; - event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); - event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); - } - - // Add which for key events - if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) ) { - event.which = event.charCode || event.keyCode; - } - - // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) - if ( !event.metaKey && event.ctrlKey ) { - event.metaKey = event.ctrlKey; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && event.button !== undefined ) { - event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); - } - - return event; + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; }, - // Deprecated, use jQuery.guid instead - guid: 1E8, + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), - // Deprecated, use jQuery.proxy instead - proxy: jQuery.proxy, + fixHooks: {}, - special: { - ready: { - // Make sure the ready event is setup - setup: jQuery.bindReady, - teardown: jQuery.noop - }, + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { - live: { - add: function( handleObj ) { - jQuery.event.add( this, handleObj.origType, jQuery.extend({}, handleObj, {handler: liveHandler}) ); - }, - - remove: function( handleObj ) { - var remove = true, - type = handleObj.origType.replace(rnamespaces, ""); - - jQuery.each( jQuery.data(this, "events").live || [], function() { - if ( type === this.origType.replace(rnamespaces, "") ) { - remove = false; - return false; - } - }); - - if ( remove ) { - jQuery.event.remove( this, handleObj.origType, liveHandler ); - } + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; } + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var body, eventDoc, doc, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { + this.click(); + return false; + } + } + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== document.activeElement && this.focus ) { + try { + this.focus(); + return false; + } catch ( e ) { + // Support: IE<9 + // If we error on focus to hidden element (#1486, #12518), + // let .trigger() run the handlers + } + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === document.activeElement && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" }, beforeunload: { - setup: function( data, namespaces, eventHandle ) { - // We only want to do this special case on windows - if ( this.setInterval ) { - this.onbeforeunload = eventHandle; - } + postDispatch: function( event ) { - return false; - }, - teardown: function( namespaces, eventHandle ) { - if ( this.onbeforeunload === eventHandle ) { - this.onbeforeunload = null; + // Even when returnValue equals to undefined Firefox will still show alert + if ( event.result !== undefined ) { + event.originalEvent.returnValue = event.result; } } } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } } }; -var removeEvent = document.removeEventListener ? +jQuery.removeEvent = document.removeEventListener ? function( elem, type, handle ) { - elem.removeEventListener( type, handle, false ); - } : + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : function( elem, type, handle ) { - elem.detachEvent( "on" + type, handle ); + var name = "on" + type; + + if ( elem.detachEvent ) { + + // #8545, #7054, preventing memory leaks for custom events in IE6-8 + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === core_strundefined ) { + elem[ name ] = null; + } + + elem.detachEvent( name, handle ); + } }; -jQuery.Event = function( src ) { +jQuery.Event = function( src, props ) { // Allow instantiation without the 'new' keyword - if ( !this.preventDefault ) { - return new jQuery.Event( src ); + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); } // Event object if ( src && src.type ) { this.originalEvent = src; this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + // Event type } else { this.type = src; } - // timeStamp is buggy for some events on Firefox(#3843) - // So we won't rely on the native value - this.timeStamp = now(); + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); // Mark it as fixed - this[ expando ] = true; + this[ jQuery.expando ] = true; }; -function returnFalse() { - return false; -} -function returnTrue() { - return true; -} - // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html jQuery.Event.prototype = { - preventDefault: function() { - this.isDefaultPrevented = returnTrue; + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + preventDefault: function() { var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; if ( !e ) { return; } - - // if preventDefault exists run it on the original event + + // If preventDefault exists, run it on the original event if ( e.preventDefault ) { e.preventDefault(); + + // Support: IE + // Otherwise set the returnValue property of the original event to false + } else { + e.returnValue = false; } - // otherwise set the returnValue property of the original event to false (IE) - e.returnValue = false; }, stopPropagation: function() { - this.isPropagationStopped = returnTrue; - var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; if ( !e ) { return; } - // if stopPropagation exists run it on the original event + // If stopPropagation exists, run it on the original event if ( e.stopPropagation ) { e.stopPropagation(); } - // otherwise set the cancelBubble property of the original event to true (IE) + + // Support: IE + // Set the cancelBubble property of the original event to true e.cancelBubble = true; }, stopImmediatePropagation: function() { this.isImmediatePropagationStopped = returnTrue; this.stopPropagation(); - }, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse + } }; -// Checks if an event happened on an element within another element -// Used in jQuery.event.special.mouseenter and mouseleave handlers -var withinElement = function( event ) { - // Check if mouse(over|out) are still within the same parent element - var parent = event.relatedTarget; - - // Firefox sometimes assigns relatedTarget a XUL element - // which we cannot access the parentNode property of - try { - // Traverse up the tree - while ( parent && parent !== this ) { - parent = parent.parentNode; - } - - if ( parent !== this ) { - // set the correct event type - event.type = event.data; - - // handle event if we actually just moused on to a non sub-element - jQuery.event.handle.apply( this, arguments ); - } - - // assuming we've left the element since we most likely mousedover a xul element - } catch(e) { } -}, - -// In case of event delegation, we only need to rename the event.type, -// liveHandler will take care of the rest. -delegate = function( event ) { - event.type = event.data; - jQuery.event.handle.apply( this, arguments ); -}; - -// Create mouseenter and mouseleave events +// Create mouseenter/leave events using mouseover/out and event-time checks jQuery.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { - setup: function( data ) { - jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); - }, - teardown: function( data ) { - jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; } }; }); -// submit delegation +// IE submit delegation if ( !jQuery.support.submitBubbles ) { jQuery.event.special.submit = { - setup: function( data, namespaces ) { - if ( this.nodeName.toLowerCase() !== "form" ) { - jQuery.event.add(this, "click.specialSubmit", function( e ) { - var elem = e.target, type = elem.type; - - if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { - return trigger( "submit", this, arguments ); - } - }); - - jQuery.event.add(this, "keypress.specialSubmit", function( e ) { - var elem = e.target, type = elem.type; - - if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { - return trigger( "submit", this, arguments ); - } - }); - - } else { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { return false; } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "submitBubbles" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "submitBubbles", true ); + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } }, - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialSubmit" ); + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); } }; - } -// change delegation, happens here so we have bind. +// IE change delegation and checkbox/radio fix if ( !jQuery.support.changeBubbles ) { - var formElems = /textarea|input|select/i, - - changeFilters, - - getVal = function( elem ) { - var type = elem.type, val = elem.value; - - if ( type === "radio" || type === "checkbox" ) { - val = elem.checked; - - } else if ( type === "select-multiple" ) { - val = elem.selectedIndex > -1 ? - jQuery.map( elem.options, function( elem ) { - return elem.selected; - }).join("-") : - ""; - - } else if ( elem.nodeName.toLowerCase() === "select" ) { - val = elem.selectedIndex; - } - - return val; - }, - - testChange = function testChange( e ) { - var elem = e.target, data, val; - - if ( !formElems.test( elem.nodeName ) || elem.readOnly ) { - return; - } - - data = jQuery.data( elem, "_change_data" ); - val = getVal(elem); - - // the current data will be also retrieved by beforeactivate - if ( e.type !== "focusout" || elem.type !== "radio" ) { - jQuery.data( elem, "_change_data", val ); - } - - if ( data === undefined || val === data ) { - return; - } - - if ( data != null || val ) { - e.type = "change"; - return jQuery.event.trigger( e, arguments[1], elem ); - } - }; - jQuery.event.special.change = { - filters: { - focusout: testChange, - click: function( e ) { - var elem = e.target, type = elem.type; + setup: function() { - if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { - return testChange.call( this, e ); + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); } - }, - - // Change has to be called before submit - // Keydown will be called before keypress, which is used in submit-event delegation - keydown: function( e ) { - var elem = e.target, type = elem.type; - - if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || - (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || - type === "select-multiple" ) { - return testChange.call( this, e ); - } - }, - - // Beforeactivate happens also before the previous element is blurred - // with this event you can't trigger a change event, but you can store - // information/focus[in] is not needed anymore - beforeactivate: function( e ) { - var elem = e.target; - jQuery.data( elem, "_change_data", getVal(elem) ); - } - }, - - setup: function( data, namespaces ) { - if ( this.type === "file" ) { return false; } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; - for ( var type in changeFilters ) { - jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); - } - - return formElems.test( this.nodeName ); + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "changeBubbles", true ); + } + }); }, - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialChange" ); + handle: function( event ) { + var elem = event.target; - return formElems.test( this.nodeName ); + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); } }; - - changeFilters = jQuery.event.special.change.filters; -} - -function trigger( type, elem, args ) { - args[0].type = type; - return jQuery.event.handle.apply( elem, args ); } // Create "bubbling" focus and blur events -if ( document.addEventListener ) { +if ( !jQuery.support.focusinBubbles ) { jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + jQuery.event.special[ fix ] = { setup: function() { - this.addEventListener( orig, handler, true ); - }, - teardown: function() { - this.removeEventListener( orig, handler, true ); + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } } }; - - function handler( e ) { - e = jQuery.event.fix( e ); - e.type = fix; - return jQuery.event.handle.call( this, e ); - } }); } -jQuery.each(["bind", "one"], function( i, name ) { - jQuery.fn[ name ] = function( type, data, fn ) { - // Handle object literals - if ( typeof type === "object" ) { - for ( var key in type ) { - this[ name ](key, data, type[key], fn); +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var type, origFn; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); } return this; } - - if ( jQuery.isFunction( data ) ) { - fn = data; - data = undefined; - } - var handler = name === "one" ? jQuery.proxy( fn, function( event ) { - jQuery( this ).unbind( event, handler ); - return fn.apply( this, arguments ); - }) : fn; - - if ( type === "unload" && name !== "one" ) { - this.one( type, data, fn ); - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.add( this[i], type, handler, data ); + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; } } - - return this; - }; -}); - -jQuery.fn.extend({ - unbind: function( type, fn ) { - // Handle object literals - if ( typeof type === "object" && !type.preventDefault ) { - for ( var key in type ) { - this.unbind(key, type[key]); - } - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.remove( this[i], type, fn ); - } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; } - return this; + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); }, - + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + delegate: function( selector, types, data, fn ) { - return this.live( types, data, fn, selector ); + return this.on( types, selector, data, fn ); }, - undelegate: function( selector, types, fn ) { - if ( arguments.length === 0 ) { - return this.unbind( "live" ); - - } else { - return this.die( types, null, fn, selector ); - } + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); }, - + trigger: function( type, data ) { return this.each(function() { jQuery.event.trigger( type, data, this ); }); }, - triggerHandler: function( type, data ) { - if ( this[0] ) { - var event = jQuery.Event( type ); - event.preventDefault(); - event.stopPropagation(); - jQuery.event.trigger( event, data, this[0] ); - return event.result; + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); } - }, - - toggle: function( fn ) { - // Save reference to arguments for access in closure - var args = arguments, i = 1; - - // link all the functions, so any of them can unbind this click handler - while ( i < args.length ) { - jQuery.proxy( fn, args[ i++ ] ); - } - - return this.click( jQuery.proxy( fn, function( event ) { - // Figure out which function to execute - var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; - jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); - - // Make sure that clicks stop - event.preventDefault(); - - // and execute the function - return args[ lastToggle ].apply( this, arguments ) || false; - })); - }, - - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); } }); +/*! + * Sizzle CSS Selector Engine + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license + * http://sizzlejs.com/ + */ +(function( window, undefined ) { -var liveMap = { - focus: "focusin", - blur: "focusout", - mouseenter: "mouseover", - mouseleave: "mouseout" -}; +var i, + cachedruns, + Expr, + getText, + isXML, + compile, + hasDuplicate, + outermostContext, -jQuery.each(["live", "die"], function( i, name ) { - jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { - var type, i = 0, match, namespaces, preType, - selector = origSelector || this.selector, - context = origSelector ? this : jQuery( this.context ); + // Local document vars + setDocument, + document, + docElem, + documentIsXML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + sortOrder, - if ( jQuery.isFunction( data ) ) { - fn = data; - data = undefined; - } + // Instance-specific data + expando = "sizzle" + -(new Date()), + preferredDoc = window.document, + support = {}, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), - types = (types || "").split(" "); + // General-purpose constants + strundefined = typeof undefined, + MAX_NEGATIVE = 1 << 31, - while ( (type = types[ i++ ]) != null ) { - match = rnamespaces.exec( type ); - namespaces = ""; - - if ( match ) { - namespaces = match[0]; - type = type.replace( rnamespaces, "" ); - } - - if ( type === "hover" ) { - types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); - continue; - } - - preType = type; - - if ( type === "focus" || type === "blur" ) { - types.push( liveMap[ type ] + namespaces ); - type = type + namespaces; - - } else { - type = (liveMap[ type ] || type) + namespaces; - } - - if ( name === "live" ) { - // bind live handler - context.each(function(){ - jQuery.event.add( this, liveConvert( type, selector ), - { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); - }); - - } else { - // unbind live handler - context.unbind( liveConvert( type, selector ), fn ); + // Array methods + arr = [], + pop = arr.pop, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf if we can't use a native one + indexOf = arr.indexOf || function( elem ) { + var i = 0, + len = this.length; + for ( ; i < len; i++ ) { + if ( this[i] === elem ) { + return i; } } - - return this; - } -}); + return -1; + }, -function liveHandler( event ) { - var stop, elems = [], selectors = [], args = arguments, - related, match, handleObj, elem, j, i, l, data, - events = jQuery.data( this, "events" ); - // Make sure we avoid non-left-click bubbling in Firefox (#3861) - if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) { - return; - } + // Regular expressions - event.liveFired = this; + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - var live = events.live.slice(0); + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace( "w", "w#" ), - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; + // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors + operators = "([*^$|!~]?=)", + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + + "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", - if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { - selectors.push( handleObj.selector ); + // Prefer arguments quoted, + // then not containing pseudos/brackets, + // then attribute selectors/non-parenthetical expressions, + // then anything else + // These preferences are here to reduce the number of selectors + // needing tokenize in the PSEUDO preFilter + pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", - } else { - live.splice( j--, 1 ); - } - } + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), - match = jQuery( event.target ).closest( selectors, event.currentTarget ); + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ), + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), - for ( i = 0, l = match.length; i < l; i++ ) { - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; + matchExpr = { + "ID": new RegExp( "^#(" + characterEncoding + ")" ), + "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), + "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), + "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, - if ( match[i].selector === handleObj.selector ) { - elem = match[i].elem; - related = null; + rsibling = /[\x20\t\r\n\f]*[+~]/, - // Those two events require additional checking - if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { - related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; - } + rnative = /^[^{]+\{\s*\[native code/, - if ( !related || related !== elem ) { - elems.push({ elem: elem, handleObj: handleObj }); - } - } - } - } + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - for ( i = 0, l = elems.length; i < l; i++ ) { - match = elems[i]; - event.currentTarget = match.elem; - event.data = match.handleObj.data; - event.handleObj = match.handleObj; + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, - if ( match.handleObj.origHandler.apply( match.elem, args ) === false ) { - stop = false; - break; - } - } + rescape = /'|\\/g, + rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, - return stop; -} - -function liveConvert( type, selector ) { - return "live." + (type && type !== "*" ? type + "." : "") + selector.replace(/\./g, "`").replace(/ /g, "&"); -} - -jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup error").split(" "), function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( fn ) { - return fn ? this.bind( name, fn ) : this.trigger( name ); + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = /\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g, + funescape = function( _, escaped ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + return high !== high ? + escaped : + // BMP codepoint + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); }; - if ( jQuery.attrFn ) { - jQuery.attrFn[ name ] = true; - } -}); - -// Prevent memory leaks in IE -// Window isn't included so as not to unbind existing unload events -// More info: -// - http://isaacschlueter.com/2006/10/msie-memory-leaks/ -if ( window.attachEvent && !window.addEventListener ) { - window.attachEvent("onunload", function() { - for ( var id in jQuery.cache ) { - if ( jQuery.cache[ id ].handle ) { - // Try/Catch is to handle iframes being unloaded, see #4280 - try { - jQuery.event.remove( jQuery.cache[ id ].handle.elem ); - } catch(e) {} - } +// Use a stripped-down slice if we can't use a native one +try { + slice.call( preferredDoc.documentElement.childNodes, 0 )[0].nodeType; +} catch ( e ) { + slice = function( i ) { + var elem, + results = []; + while ( (elem = this[i++]) ) { + results.push( elem ); } + return results; + }; +} + +/** + * For feature detection + * @param {Function} fn The function to test for native support + */ +function isNative( fn ) { + return rnative.test( fn + "" ); +} + +/** + * Create key-value caches of limited size + * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var cache, + keys = []; + + return (cache = function( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key += " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key ] = value); }); } -/*! - * Sizzle CSS Selector Engine - v1.0 - * Copyright 2009, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * More information: http://sizzlejs.com/ + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark */ -(function(){ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} -var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, - done = 0, - toString = Object.prototype.toString, - hasDuplicate = false, - baseHasDuplicate = true; +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); -// Here we check if the JavaScript engine is using some sort of -// optimization where it does not always call our comparision -// function. If that is the case, discard the hasDuplicate value. -// Thus far that includes Google Chrome. -[0, 0].sort(function(){ - baseHasDuplicate = false; - return 0; -}); - -var Sizzle = function(selector, context, results, seed) { - results = results || []; - var origContext = context = context || document; - - if ( context.nodeType !== 1 && context.nodeType !== 9 ) { - return []; + try { + return fn( div ); + } catch (e) { + return false; + } finally { + // release memory in IE + div = null; } - +} + +function Sizzle( selector, context, results, seed ) { + var match, elem, m, nodeType, + // QSA vars + i, groups, old, nid, newContext, newSelector; + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + + context = context || document; + results = results || []; + if ( !selector || typeof selector !== "string" ) { return results; } - var parts = [], m, set, checkSet, extra, prune = true, contextXML = isXML(context), - soFar = selector; - - // Reset the position of the chunker regexp (start from head) - while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) { - soFar = m[3]; - - parts.push( m[1] ); - - if ( m[2] ) { - extra = m[3]; + if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { + return []; + } + + if ( !documentIsXML && !seed ) { + + // Shortcuts + if ( (match = rquickExpr.exec( selector )) ) { + // Speed-up: Sizzle("#ID") + if ( (m = match[1]) ) { + if ( nodeType === 9 ) { + elem = context.getElementById( m ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && + contains( context, elem ) && elem.id === m ) { + results.push( elem ); + return results; + } + } + + // Speed-up: Sizzle("TAG") + } else if ( match[2] ) { + push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) ); + return results; + + // Speed-up: Sizzle(".CLASS") + } else if ( (m = match[3]) && support.getByClassName && context.getElementsByClassName ) { + push.apply( results, slice.call(context.getElementsByClassName( m ), 0) ); + return results; + } + } + + // QSA path + if ( support.qsa && !rbuggyQSA.test(selector) ) { + old = true; + nid = expando; + newContext = context; + newSelector = nodeType === 9 && selector; + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + groups = tokenize( selector ); + + if ( (old = context.getAttribute("id")) ) { + nid = old.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", nid ); + } + nid = "[id='" + nid + "'] "; + + i = groups.length; + while ( i-- ) { + groups[i] = nid + toSelector( groups[i] ); + } + newContext = rsibling.test( selector ) && context.parentNode || context; + newSelector = groups.join(","); + } + + if ( newSelector ) { + try { + push.apply( results, slice.call( newContext.querySelectorAll( + newSelector + ), 0 ) ); + return results; + } catch(qsaError) { + } finally { + if ( !old ) { + context.removeAttribute("id"); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Detect xml + * @param {Element|Object} elem An element or a document + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var doc = node ? node.ownerDocument || node : preferredDoc; + + // If no document and documentElement is available, return + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Set our document + document = doc; + docElem = doc.documentElement; + + // Support tests + documentIsXML = isXML( doc ); + + // Check if getElementsByTagName("*") returns only elements + support.tagNameNoComments = assert(function( div ) { + div.appendChild( doc.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Check if attributes should be retrieved by attribute nodes + support.attributes = assert(function( div ) { + div.innerHTML = ""; + var type = typeof div.lastChild.getAttribute("multiple"); + // IE8 returns a string for some attributes even when not present + return type !== "boolean" && type !== "string"; + }); + + // Check if getElementsByClassName can be trusted + support.getByClassName = assert(function( div ) { + // Opera can't find a second classname (in 9.6) + div.innerHTML = ""; + if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) { + return false; + } + + // Safari 3.2 caches class attributes and doesn't catch changes + div.lastChild.className = "e"; + return div.getElementsByClassName("e").length === 2; + }); + + // Check if getElementById returns elements by name + // Check if getElementsByName privileges form controls or returns elements by ID + support.getByName = assert(function( div ) { + // Inject content + div.id = expando + 0; + div.innerHTML = "
    "; + docElem.insertBefore( div, docElem.firstChild ); + + // Test + var pass = doc.getElementsByName && + // buggy browsers will return fewer than the correct 2 + doc.getElementsByName( expando ).length === 2 + + // buggy browsers will return more than the correct 0 + doc.getElementsByName( expando + 0 ).length; + support.getIdNotName = !doc.getElementById( expando ); + + // Cleanup + docElem.removeChild( div ); + + return pass; + }); + + // IE6/7 return modified attributes + Expr.attrHandle = assert(function( div ) { + div.innerHTML = ""; + return div.firstChild && typeof div.firstChild.getAttribute !== strundefined && + div.firstChild.getAttribute("href") === "#"; + }) ? + {} : + { + "href": function( elem ) { + return elem.getAttribute( "href", 2 ); + }, + "type": function( elem ) { + return elem.getAttribute("type"); + } + }; + + // ID find and filter + if ( support.getIdNotName ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && !documentIsXML ) { + var m = context.getElementById( id ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && !documentIsXML ) { + var m = context.getElementById( id ); + + return m ? + m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ? + [m] : + undefined : + []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.tagNameNoComments ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== strundefined ) { + return context.getElementsByTagName( tag ); + } + } : + function( tag, context ) { + var elem, + tmp = [], + i = 0, + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Name + Expr.find["NAME"] = support.getByName && function( tag, context ) { + if ( typeof context.getElementsByName !== strundefined ) { + return context.getElementsByName( name ); + } + }; + + // Class + Expr.find["CLASS"] = support.getByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== strundefined && !documentIsXML ) { + return context.getElementsByClassName( className ); + } + }; + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21), + // no need to also add to buggyMatches since matches checks buggyQSA + // A support test would require too much code (would include document ready) + rbuggyQSA = [ ":focus" ]; + + if ( (support.qsa = isNative(doc.querySelectorAll)) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explictly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = ""; + + // IE8 - Some boolean attributes are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + }); + + assert(function( div ) { + + // Opera 10-12/IE8 - ^= $= *= and empty values + // Should not select anything + div.innerHTML = ""; + if ( div.querySelectorAll("[i^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = isNative( (matches = docElem.matchesSelector || + docElem.mozMatchesSelector || + docElem.webkitMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = new RegExp( rbuggyMatches.join("|") ); + + // Element contains another + // Purposefully does not implement inclusive descendent + // As in, an element does not contain itself + contains = isNative(docElem.contains) || docElem.compareDocumentPosition ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + // Document order sorting + sortOrder = docElem.compareDocumentPosition ? + function( a, b ) { + var compare; + + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( (compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b )) ) { + if ( compare & 1 || a.parentNode && a.parentNode.nodeType === 11 ) { + if ( a === doc || contains( preferredDoc, a ) ) { + return -1; + } + if ( b === doc || contains( preferredDoc, b ) ) { + return 1; + } + return 0; + } + return compare & 4 ? -1 : 1; + } + + return a.compareDocumentPosition ? -1 : 1; + } : + function( a, b ) { + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Parentless nodes are either documents or disconnected + } else if ( !aup || !bup ) { + return a === doc ? -1 : + b === doc ? 1 : + aup ? -1 : + bup ? 1 : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + // Always assume the presence of duplicates if sort doesn't + // pass them to our comparison function (as in Google Chrome). + hasDuplicate = false; + [0, 0].sort( sortOrder ); + support.detectDuplicates = hasDuplicate; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + // rbuggyQSA always contains :focus, so no need for an existence check + if ( support.matchesSelector && !documentIsXML && (!rbuggyMatches || !rbuggyMatches.test(expr)) && !rbuggyQSA.test(expr) ) { + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch(e) {} + } + + return Sizzle( expr, document, null, [elem] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + var val; + + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + if ( !documentIsXML ) { + name = name.toLowerCase(); + } + if ( (val = Expr.attrHandle[ name ]) ) { + return val( elem ); + } + if ( documentIsXML || support.attributes ) { + return elem.getAttribute( name ); + } + return ( (val = elem.getAttributeNode( name )) || elem.getAttribute( name ) ) && elem[ name ] === true ? + name : + val && val.specified ? val.value : null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +// Document sorting and removing duplicates +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + i = 1, + j = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( ; (elem = results[i]); i++ ) { + if ( elem === results[ i - 1 ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + return results; +}; + +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && ( ~b.sourceIndex || MAX_NEGATIVE ) - ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +// Returns a function to use in pseudos for input types +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +// Returns a function to use in pseudos for buttons +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +// Returns a function to use in pseudos for positionals +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + for ( ; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (see #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[5] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[4] ) { + match[2] = match[4]; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeName ) { + if ( nodeName === "*" ) { + return function() { return true; }; + } + + nodeName = nodeName.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, outerCache, node, diff, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || (parent[ expando ] = {}); + cache = outerCache[ type ] || []; + nodeIndex = cache[0] === dirruns && cache[1]; + diff = cache[0] === dirruns && cache[2]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + // Use previously-cached element index if available + } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { + diff = cache[1]; + + // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) + } else { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { + // Cache the index of each encountered element + if ( useCache ) { + (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifider + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsXML ? + elem.getAttribute("xml:lang") || elem.getAttribute("lang") : + elem.lang) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), + // not comment, processing instructions, or others + // Thanks to Diego Perini for the nodeName shortcut + // Greater than "@" means alpha characters (specifically not starting with "#" or "?") + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( tokens = [] ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push( { + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { break; } } - if ( parts.length > 1 && origPOS.exec( selector ) ) { - if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context ); - } else { - set = Expr.relative[ parts[0] ] ? - [ context ] : - Sizzle( parts.shift(), context ); + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +} - while ( parts.length ) { - selector = parts.shift(); - - if ( Expr.relative[ selector ] ) { - selector += parts.shift(); - } - - set = posProcess( selector, set ); - } - } - } else { - // Take a shortcut and set the context if the root selector is an ID - // (but not if it'll be faster if the inner selector is an ID) - if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && - Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { - var ret = Sizzle.find( parts.shift(), context, contextXML ); - context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; - } - - if ( context ) { - var ret = seed ? - { expr: parts.pop(), set: makeArray(seed) } : - Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); - set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; - - if ( parts.length > 0 ) { - checkSet = makeArray(set); - } else { - prune = false; - } - - while ( parts.length ) { - var cur = parts.pop(), pop = cur; - - if ( !Expr.relative[ cur ] ) { - cur = ""; - } else { - pop = parts.pop(); - } - - if ( pop == null ) { - pop = context; - } - - Expr.relative[ cur ]( checkSet, pop, contextXML ); - } - } else { - checkSet = parts = []; - } +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; } + return selector; +} - if ( !checkSet ) { - checkSet = set; - } +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; - if ( !checkSet ) { - Sizzle.error( cur || selector ); - } - - if ( toString.call(checkSet) === "[object Array]" ) { - if ( !prune ) { - results.push.apply( results, checkSet ); - } else if ( context && context.nodeType === 1 ) { - for ( var i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) { - results.push( set[i] ); + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); } } - } else { - for ( var i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && checkSet[i].nodeType === 1 ) { - results.push( set[i] ); - } - } - } - } else { - makeArray( checkSet, results ); - } + } : - if ( extra ) { - Sizzle( extra, origContext, results, seed ); - Sizzle.uniqueSort( results ); - } + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var data, cache, outerCache, + dirkey = dirruns + " " + doneName; - return results; -}; - -Sizzle.uniqueSort = function(results){ - if ( sortOrder ) { - hasDuplicate = baseHasDuplicate; - results.sort(sortOrder); - - if ( hasDuplicate ) { - for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[i-1] ) { - results.splice(i--, 1); - } - } - } - } - - return results; -}; - -Sizzle.matches = function(expr, set){ - return Sizzle(expr, null, null, set); -}; - -Sizzle.find = function(expr, context, isXML){ - var set, match; - - if ( !expr ) { - return []; - } - - for ( var i = 0, l = Expr.order.length; i < l; i++ ) { - var type = Expr.order[i], match; - - if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - var left = match[1]; - match.splice(1,1); - - if ( left.substr( left.length - 1 ) !== "\\" ) { - match[1] = (match[1] || "").replace(/\\/g, ""); - set = Expr.find[ type ]( match, context, isXML ); - if ( set != null ) { - expr = expr.replace( Expr.match[ type ], "" ); - break; - } - } - } - } - - if ( !set ) { - set = context.getElementsByTagName("*"); - } - - return {set: set, expr: expr}; -}; - -Sizzle.filter = function(expr, set, inplace, not){ - var old = expr, result = [], curLoop = set, match, anyFound, - isXMLFilter = set && set[0] && isXML(set[0]); - - while ( expr && set.length ) { - for ( var type in Expr.filter ) { - if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - var filter = Expr.filter[ type ], found, item, left = match[1]; - anyFound = false; - - match.splice(1,1); - - if ( left.substr( left.length - 1 ) === "\\" ) { - continue; - } - - if ( curLoop === result ) { - result = []; - } - - if ( Expr.preFilter[ type ] ) { - match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); - - if ( !match ) { - anyFound = found = true; - } else if ( match === true ) { - continue; + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } } } - - if ( match ) { - for ( var i = 0; (item = curLoop[i]) != null; i++ ) { - if ( item ) { - found = filter( item, match, i, curLoop ); - var pass = not ^ !!found; - - if ( inplace && found != null ) { - if ( pass ) { - anyFound = true; - } else { - curLoop[i] = false; - } - } else if ( pass ) { - result.push( item ); - anyFound = true; + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { + if ( (data = cache[1]) === true || data === cachedruns ) { + return data === true; + } + } else { + cache = outerCache[ dir ] = [ dirkey ]; + cache[1] = matcher( elem, context, xml ) || cachedruns; + if ( cache[1] === true ) { + return true; } } } } - - if ( found !== undefined ) { - if ( !inplace ) { - curLoop = result; - } - - expr = expr.replace( Expr.match[ type ], "" ); - - if ( !anyFound ) { - return []; - } - - break; - } } - } + }; +} - // Improper expression - if ( expr === old ) { - if ( anyFound == null ) { - Sizzle.error( expr ); - } else { - break; - } - } - - old = expr; - } - - return curLoop; -}; - -Sizzle.error = function( msg ) { - throw "Syntax error, unrecognized expression: " + msg; -}; - -var Expr = Sizzle.selectors = { - order: [ "ID", "NAME", "TAG" ], - match: { - ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/, - CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/, - NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/, - ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, - TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/, - CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/, - POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/, - PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ - }, - leftMatch: {}, - attrMap: { - "class": "className", - "for": "htmlFor" - }, - attrHandle: { - href: function(elem){ - return elem.getAttribute("href"); - } - }, - relative: { - "+": function(checkSet, part){ - var isPartStr = typeof part === "string", - isTag = isPartStr && !/\W/.test(part), - isPartStrNotTag = isPartStr && !isTag; - - if ( isTag ) { - part = part.toLowerCase(); - } - - for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { - if ( (elem = checkSet[i]) ) { - while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} - - checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? - elem || false : - elem === part; - } - } - - if ( isPartStrNotTag ) { - Sizzle.filter( part, checkSet, true ); - } - }, - ">": function(checkSet, part){ - var isPartStr = typeof part === "string"; - - if ( isPartStr && !/\W/.test(part) ) { - part = part.toLowerCase(); - - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - var parent = elem.parentNode; - checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; - } - } - } else { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - checkSet[i] = isPartStr ? - elem.parentNode : - elem.parentNode === part; - } - } - - if ( isPartStr ) { - Sizzle.filter( part, checkSet, true ); - } - } - }, - "": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck; - - if ( typeof part === "string" && !/\W/.test(part) ) { - var nodeCheck = part = part.toLowerCase(); - checkFn = dirNodeCheck; - } - - checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); - }, - "~": function(checkSet, part, isXML){ - var doneName = done++, checkFn = dirCheck; - - if ( typeof part === "string" && !/\W/.test(part) ) { - var nodeCheck = part = part.toLowerCase(); - checkFn = dirNodeCheck; - } - - checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); - } - }, - find: { - ID: function(match, context, isXML){ - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - return m ? [m] : []; - } - }, - NAME: function(match, context){ - if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], results = context.getElementsByName(match[1]); - - for ( var i = 0, l = results.length; i < l; i++ ) { - if ( results[i].getAttribute("name") === match[1] ) { - ret.push( results[i] ); - } - } - - return ret.length === 0 ? null : ret; - } - }, - TAG: function(match, context){ - return context.getElementsByTagName(match[1]); - } - }, - preFilter: { - CLASS: function(match, curLoop, inplace, result, not, isXML){ - match = " " + match[1].replace(/\\/g, "") + " "; - - if ( isXML ) { - return match; - } - - for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { - if ( elem ) { - if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) { - if ( !inplace ) { - result.push( elem ); - } - } else if ( inplace ) { - curLoop[i] = false; - } - } - } - - return false; - }, - ID: function(match){ - return match[1].replace(/\\/g, ""); - }, - TAG: function(match, curLoop){ - return match[1].toLowerCase(); - }, - CHILD: function(match){ - if ( match[1] === "nth" ) { - // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' - var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( - match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || - !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); - - // calculate the numbers (first)n+(last) including if they are negative - match[2] = (test[1] + (test[2] || 1)) - 0; - match[3] = test[3] - 0; - } - - // TODO: Move to normal caching system - match[0] = done++; - - return match; - }, - ATTR: function(match, curLoop, inplace, result, not, isXML){ - var name = match[1].replace(/\\/g, ""); - - if ( !isXML && Expr.attrMap[name] ) { - match[1] = Expr.attrMap[name]; - } - - if ( match[2] === "~=" ) { - match[4] = " " + match[4] + " "; - } - - return match; - }, - PSEUDO: function(match, curLoop, inplace, result, not){ - if ( match[1] === "not" ) { - // If we're dealing with a complex expression, or a simple one - if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { - match[3] = Sizzle(match[3], null, null, curLoop); - } else { - var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); - if ( !inplace ) { - result.push.apply( result, ret ); - } +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { return false; } - } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { - return true; } - - return match; - }, - POS: function(match){ - match.unshift( true ); - return match; - } - }, - filters: { - enabled: function(elem){ - return elem.disabled === false && elem.type !== "hidden"; - }, - disabled: function(elem){ - return elem.disabled === true; - }, - checked: function(elem){ - return elem.checked === true; - }, - selected: function(elem){ - // Accessing this property makes selected-by-default - // options in Safari work properly - elem.parentNode.selectedIndex; - return elem.selected === true; - }, - parent: function(elem){ - return !!elem.firstChild; - }, - empty: function(elem){ - return !elem.firstChild; - }, - has: function(elem, i, match){ - return !!Sizzle( match[3], elem ).length; - }, - header: function(elem){ - return /h\d/i.test( elem.nodeName ); - }, - text: function(elem){ - return "text" === elem.type; - }, - radio: function(elem){ - return "radio" === elem.type; - }, - checkbox: function(elem){ - return "checkbox" === elem.type; - }, - file: function(elem){ - return "file" === elem.type; - }, - password: function(elem){ - return "password" === elem.type; - }, - submit: function(elem){ - return "submit" === elem.type; - }, - image: function(elem){ - return "image" === elem.type; - }, - reset: function(elem){ - return "reset" === elem.type; - }, - button: function(elem){ - return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; - }, - input: function(elem){ - return /input|select|textarea|button/i.test(elem.nodeName); - } - }, - setFilters: { - first: function(elem, i){ - return i === 0; - }, - last: function(elem, i, match, array){ - return i === array.length - 1; - }, - even: function(elem, i){ - return i % 2 === 0; - }, - odd: function(elem, i){ - return i % 2 === 1; - }, - lt: function(elem, i, match){ - return i < match[3] - 0; - }, - gt: function(elem, i, match){ - return i > match[3] - 0; - }, - nth: function(elem, i, match){ - return match[3] - 0 === i; - }, - eq: function(elem, i, match){ - return match[3] - 0 === i; - } - }, - filter: { - PSEUDO: function(elem, match, i, array){ - var name = match[1], filter = Expr.filters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; - } else if ( name === "not" ) { - var not = match[3]; - - for ( var i = 0, l = not.length; i < l; i++ ) { - if ( not[i] === elem ) { - return false; - } - } - - return true; - } else { - Sizzle.error( "Syntax error, unrecognized expression: " + name ); - } - }, - CHILD: function(elem, match){ - var type = match[1], node = elem; - switch (type) { - case 'only': - case 'first': - while ( (node = node.previousSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - if ( type === "first" ) { - return true; - } - node = elem; - case 'last': - while ( (node = node.nextSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - return true; - case 'nth': - var first = match[2], last = match[3]; - - if ( first === 1 && last === 0 ) { - return true; - } - - var doneName = match[0], - parent = elem.parentNode; - - if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { - var count = 0; - for ( node = parent.firstChild; node; node = node.nextSibling ) { - if ( node.nodeType === 1 ) { - node.nodeIndex = ++count; - } - } - parent.sizcache = doneName; - } - - var diff = elem.nodeIndex - last; - if ( first === 0 ) { - return diff === 0; - } else { - return ( diff % first === 0 && diff / first >= 0 ); - } - } - }, - ID: function(elem, match){ - return elem.nodeType === 1 && elem.getAttribute("id") === match; - }, - TAG: function(elem, match){ - return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; - }, - CLASS: function(elem, match){ - return (" " + (elem.className || elem.getAttribute("class")) + " ") - .indexOf( match ) > -1; - }, - ATTR: function(elem, match){ - var name = match[1], - result = Expr.attrHandle[ name ] ? - Expr.attrHandle[ name ]( elem ) : - elem[ name ] != null ? - elem[ name ] : - elem.getAttribute( name ), - value = result + "", - type = match[2], - check = match[4]; - - return result == null ? - type === "!=" : - type === "=" ? - value === check : - type === "*=" ? - value.indexOf(check) >= 0 : - type === "~=" ? - (" " + value + " ").indexOf(check) >= 0 : - !check ? - value && result !== false : - type === "!=" ? - value !== check : - type === "^=" ? - value.indexOf(check) === 0 : - type === "$=" ? - value.substr(value.length - check.length) === check : - type === "|=" ? - value === check || value.substr(0, check.length + 1) === check + "-" : - false; - }, - POS: function(elem, match, i, array){ - var name = match[2], filter = Expr.setFilters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - } - } - } -}; - -var origPOS = Expr.match.POS; - -for ( var type in Expr.match ) { - Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source ); - Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, function(all, num){ - return "\\" + (num - 0 + 1); - })); + return true; + } : + matchers[0]; } -var makeArray = function(array, results) { - array = Array.prototype.slice.call( array, 0 ); +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; - if ( results ) { - results.push.apply( results, array ); - return results; + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } } - - return array; -}; -// Perform a simple check to determine if the browser is capable of -// converting a NodeList to an array using builtin methods. -// Also verifies that the returned array holds DOM nodes -// (which is not the case in the Blackberry browser) -try { - Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + return newUnmatched; +} -// Provide a fallback method if it does not work -} catch(e){ - makeArray = function(array, results) { - var ret = results || []; +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, - if ( toString.call(array) === "[object Array]" ) { - Array.prototype.push.apply( ret, array ); + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined } else { - if ( typeof array.length === "number" ) { - for ( var i = 0, l = array.length; i < l; i++ ) { - ret.push( array[i] ); - } + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); } else { - for ( var i = 0; array[i]; i++ ) { - ret.push( array[i] ); - } + push.apply( results, matcherOut ); } } - - return ret; - }; + }); } -var sortOrder; +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, -if ( document.documentElement.compareDocumentPosition ) { - sortOrder = function( a, b ) { - if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { - if ( a == b ) { - hasDuplicate = true; - } - return a.compareDocumentPosition ? -1 : 1; - } + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + } ]; - var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} else if ( "sourceIndex" in document.documentElement ) { - sortOrder = function( a, b ) { - if ( !a.sourceIndex || !b.sourceIndex ) { - if ( a == b ) { - hasDuplicate = true; - } - return a.sourceIndex ? -1 : 1; - } + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); - var ret = a.sourceIndex - b.sourceIndex; - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} else if ( document.createRange ) { - sortOrder = function( a, b ) { - if ( !a.ownerDocument || !b.ownerDocument ) { - if ( a == b ) { - hasDuplicate = true; - } - return a.ownerDocument ? -1 : 1; - } - - var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); - aRange.setStart(a, 0); - aRange.setEnd(a, 0); - bRange.setStart(b, 0); - bRange.setEnd(b, 0); - var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange); - if ( ret === 0 ) { - hasDuplicate = true; - } - return ret; - }; -} - -// Utility function for retreiving the text value of an array of DOM nodes -function getText( elems ) { - var ret = "", elem; - - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; - - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; - - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += getText( elem.childNodes ); - } - } - - return ret; -} - -// Check to see if the browser returns elements by name when -// querying by getElementById (and provide a workaround) -(function(){ - // We're going to inject a fake input element with a specified name - var form = document.createElement("div"), - id = "script" + (new Date).getTime(); - form.innerHTML = ""; - - // Inject it into the root element, check its status, and remove it quickly - var root = document.documentElement; - root.insertBefore( form, root.firstChild ); - - // The workaround has to do additional checks after a getElementById - // Which slows things down for other browsers (hence the branching) - if ( document.getElementById( id ) ) { - Expr.find.ID = function(match, context, isXML){ - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; - } - }; - - Expr.filter.ID = function(elem, match){ - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); - return elem.nodeType === 1 && node && node.nodeValue === match; - }; - } - - root.removeChild( form ); - root = form = null; // release memory in IE -})(); - -(function(){ - // Check to see if the browser returns only elements - // when doing getElementsByTagName("*") - - // Create a fake element - var div = document.createElement("div"); - div.appendChild( document.createComment("") ); - - // Make sure no comments are found - if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function(match, context){ - var results = context.getElementsByTagName(match[1]); - - // Filter out possible comments - if ( match[1] === "*" ) { - var tmp = []; - - for ( var i = 0; results[i]; i++ ) { - if ( results[i].nodeType === 1 ) { - tmp.push( results[i] ); - } - } - - results = tmp; - } - - return results; - }; - } - - // Check to see if an attribute returns normalized href attributes - div.innerHTML = ""; - if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && - div.firstChild.getAttribute("href") !== "#" ) { - Expr.attrHandle.href = function(elem){ - return elem.getAttribute("href", 2); - }; - } - - div = null; // release memory in IE -})(); - -if ( document.querySelectorAll ) { - (function(){ - var oldSizzle = Sizzle, div = document.createElement("div"); - div.innerHTML = "

    "; - - // Safari can't handle uppercase or unicode characters when - // in quirks mode. - if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { - return; - } - - Sizzle = function(query, context, extra, seed){ - context = context || document; - - // Only use querySelectorAll on non-XML documents - // (ID selectors don't work in non-HTML documents) - if ( !seed && context.nodeType === 9 && !isXML(context) ) { - try { - return makeArray( context.querySelectorAll(query), extra ); - } catch(e){} - } - - return oldSizzle(query, context, extra, seed); - }; - - for ( var prop in oldSizzle ) { - Sizzle[ prop ] = oldSizzle[ prop ]; - } - - div = null; // release memory in IE - })(); -} - -(function(){ - var div = document.createElement("div"); - - div.innerHTML = "
    "; - - // Opera can't find a second classname (in 9.6) - // Also, make sure that getElementsByClassName actually exists - if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { - return; - } - - // Safari caches class attributes, doesn't catch changes (in 3.2) - div.lastChild.className = "e"; - - if ( div.getElementsByClassName("e").length === 1 ) { - return; - } - - Expr.order.splice(1, 0, "CLASS"); - Expr.find.CLASS = function(match, context, isXML) { - if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { - return context.getElementsByClassName(match[1]); - } - }; - - div = null; // release memory in IE -})(); - -function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - elem = elem[dir]; - var match = false; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 && !isXML ){ - elem.sizcache = doneName; - elem.sizset = i; - } - - if ( elem.nodeName.toLowerCase() === cur ) { - match = elem; - break; - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - if ( elem ) { - elem = elem[dir]; - var match = false; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 ) { - if ( !isXML ) { - elem.sizcache = doneName; - elem.sizset = i; - } - if ( typeof cur !== "string" ) { - if ( elem === cur ) { - match = true; - break; - } - - } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { - match = elem; + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { break; } } - - elem = elem[dir]; + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); } - - checkSet[i] = match; + matchers.push( matcher ); } } + + return elementMatcher( matchers ); } -var contains = document.compareDocumentPosition ? function(a, b){ - return !!(a.compareDocumentPosition(b) & 16); -} : function(a, b){ - return a !== b && (a.contains ? a.contains(b) : true); -}; +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + // A counter to specify which element is currently being matched + var matcherCachedRuns = 0, + bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, expandContext ) { + var elem, j, matcher, + setMatched = [], + matchedCount = 0, + i = "0", + unmatched = seed && [], + outermost = expandContext != null, + contextBackup = outermostContext, + // We must always have either seed elements or context + elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); -var isXML = function(elem){ - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; + if ( outermost ) { + outermostContext = context !== document && context; + cachedruns = matcherCachedRuns; + } -var posProcess = function(selector, context){ - var tmpSet = [], later = "", match, - root = context.nodeType ? [context] : context; - - // Position selectors must be done after the filter - // And so must :not(positional) so we move all PSEUDOs to the end - while ( (match = Expr.match.PSEUDO.exec( selector )) ) { - later += match[0]; - selector = selector.replace( Expr.match.PSEUDO, "" ); - } - - selector = Expr.relative[selector] ? selector + "*" : selector; - - for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet ); - } - - return Sizzle.filter( later, tmpSet ); -}; - -// EXPOSE -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.filters; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = getText; -jQuery.isXMLDoc = isXML; -jQuery.contains = contains; - -return; - -window.Sizzle = Sizzle; - -})(); -var runtil = /Until$/, - rparentsprev = /^(?:parents|prevUntil|prevAll)/, - // Note: This RegExp should be improved, or likely pulled from Sizzle - rmultiselector = /,/, - slice = Array.prototype.slice; - -// Implement the identical functionality for filter and not -var winnow = function( elements, qualifier, keep ) { - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep(elements, function( elem, i ) { - return !!qualifier.call( elem, i, elem ) === keep; - }); - - } else if ( qualifier.nodeType ) { - return jQuery.grep(elements, function( elem, i ) { - return (elem === qualifier) === keep; - }); - - } else if ( typeof qualifier === "string" ) { - var filtered = jQuery.grep(elements, function( elem ) { - return elem.nodeType === 1; - }); - - if ( isSimple.test( qualifier ) ) { - return jQuery.filter(qualifier, filtered, !keep); - } else { - qualifier = jQuery.filter( qualifier, filtered ); - } - } - - return jQuery.grep(elements, function( elem, i ) { - return (jQuery.inArray( elem, qualifier ) >= 0) === keep; - }); -}; - -jQuery.fn.extend({ - find: function( selector ) { - var ret = this.pushStack( "", "find", selector ), length = 0; - - for ( var i = 0, l = this.length; i < l; i++ ) { - length = ret.length; - jQuery.find( selector, this[i], ret ); - - if ( i > 0 ) { - // Make sure that the results are unique - for ( var n = length; n < ret.length; n++ ) { - for ( var r = 0; r < length; r++ ) { - if ( ret[r] === ret[n] ) { - ret.splice(n--, 1); + // Add elements passing elementMatchers directly to results + // Keep `i` a string if there are no elements so `matchedCount` will be "00" below + for ( ; (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context, xml ) ) { + results.push( elem ); break; } } + if ( outermost ) { + dirruns = dirrunsUnique; + cachedruns = ++matcherCachedRuns; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // Apply set filters to unmatched elements + matchedCount += i; + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !group ) { + group = tokenize( selector ); + } + i = group.length; + while ( i-- ) { + cached = matcherFromTokens( group[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + } + return cached; +}; + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function select( selector, context, results, seed ) { + var i, tokens, token, type, find, + match = tokenize( selector ); + + if ( !seed ) { + // Try to minimize operations if there is only one group + if ( match.length === 1 ) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + context.nodeType === 9 && !documentIsXML && + Expr.relative[ tokens[1].type ] ) { + + context = Expr.find["ID"]( token.matches[0].replace( runescape, funescape ), context )[0]; + if ( !context ) { + return results; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && context.parentNode || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, slice.call( seed, 0 ) ); + return results; + } + + break; + } } } } + } + // Compile and execute a filtering function + // Provide `match` to avoid retokenization if we modified the selector above + compile( selector, match )( + seed, + context, + documentIsXML, + results, + rsibling.test( selector ) + ); + return results; +} + +// Deprecated +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Easy API for creating new setFilters +function setFilters() {} +Expr.filters = setFilters.prototype = Expr.pseudos; +Expr.setFilters = new setFilters(); + +// Initialize with the default document +setDocument(); + +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})( window ); +var runtil = /Until$/, + rparentsprev = /^(?:parents|prev(?:Until|All))/, + isSimple = /^.[^:#\[\.,]*$/, + rneedsContext = jQuery.expr.match.needsContext, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var i, ret, self, + len = this.length; + + if ( typeof selector !== "string" ) { + self = this; + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + ret = []; + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, this[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = ( this.selector ? this.selector + " " : "" ) + selector; return ret; }, has: function( target ) { - var targets = jQuery( target ); + var i, + targets = jQuery( target, this ), + len = targets.length; + return this.filter(function() { - for ( var i = 0, l = targets.length; i < l; i++ ) { + for ( i = 0; i < len; i++ ) { if ( jQuery.contains( this, targets[i] ) ) { return true; } @@ -3757,71 +5597,62 @@ jQuery.fn.extend({ }, not: function( selector ) { - return this.pushStack( winnow(this, selector, false), "not", selector); + return this.pushStack( winnow(this, selector, false) ); }, filter: function( selector ) { - return this.pushStack( winnow(this, selector, true), "filter", selector ); + return this.pushStack( winnow(this, selector, true) ); }, - + is: function( selector ) { - return !!selector && jQuery.filter( selector, this ).length > 0; + return !!selector && ( + typeof selector === "string" ? + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + rneedsContext.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); }, closest: function( selectors, context ) { - if ( jQuery.isArray( selectors ) ) { - var ret = [], cur = this[0], match, matches = {}, selector; + var cur, + i = 0, + l = this.length, + ret = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; - if ( cur && selectors.length ) { - for ( var i = 0, l = selectors.length; i < l; i++ ) { - selector = selectors[i]; + for ( ; i < l; i++ ) { + cur = this[i]; - if ( !matches[selector] ) { - matches[selector] = jQuery.expr.match.POS.test( selector ) ? - jQuery( selector, context || this.context ) : - selector; - } - } - - while ( cur && cur.ownerDocument && cur !== context ) { - for ( selector in matches ) { - match = matches[selector]; - - if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { - ret.push({ selector: selector, elem: cur }); - delete matches[selector]; - } - } - cur = cur.parentNode; - } - } - - return ret; - } - - var pos = jQuery.expr.match.POS.test( selectors ) ? - jQuery( selectors, context || this.context ) : null; - - return this.map(function( i, cur ) { - while ( cur && cur.ownerDocument && cur !== context ) { - if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selectors) ) { - return cur; + while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; } cur = cur.parentNode; } - return null; - }); + } + + return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret ); }, - + // Determine the position of an element within // the matched set of elements index: function( elem ) { - if ( !elem || typeof elem === "string" ) { - return jQuery.inArray( this[0], - // If it receives a string, the selector is used - // If it receives nothing, the siblings are used - elem ? jQuery( elem ) : this.parent().children() ); + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + // Locate the position of the desired element return jQuery.inArray( // If it receives a jQuery object, the first element is used @@ -3830,24 +5661,28 @@ jQuery.fn.extend({ add: function( selector, context ) { var set = typeof selector === "string" ? - jQuery( selector, context || this.context ) : - jQuery.makeArray( selector ), + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), all = jQuery.merge( this.get(), set ); - return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? - all : - jQuery.unique( all ) ); + return this.pushStack( jQuery.unique(all) ); }, - andSelf: function() { - return this.add( this.prevObject ); + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); } }); -// A painfully simple check to see if an element is disconnected -// from a document (should be improved, where feasible). -function isDisconnected( node ) { - return !node || !node.parentNode || node.parentNode.nodeType === 11; +jQuery.fn.andSelf = jQuery.fn.addBack; + +function sibling( cur, dir ) { + do { + cur = cur[ dir ]; + } while ( cur && cur.nodeType !== 1 ); + + return cur; } jQuery.each({ @@ -3862,10 +5697,10 @@ jQuery.each({ return jQuery.dir( elem, "parentNode", until ); }, next: function( elem ) { - return jQuery.nth( elem, 2, "nextSibling" ); + return sibling( elem, "nextSibling" ); }, prev: function( elem ) { - return jQuery.nth( elem, 2, "previousSibling" ); + return sibling( elem, "previousSibling" ); }, nextAll: function( elem ) { return jQuery.dir( elem, "nextSibling" ); @@ -3880,7 +5715,7 @@ jQuery.each({ return jQuery.dir( elem, "previousSibling", until ); }, siblings: function( elem ) { - return jQuery.sibling( elem.parentNode.firstChild, elem ); + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); }, children: function( elem ) { return jQuery.sibling( elem.firstChild ); @@ -3888,12 +5723,12 @@ jQuery.each({ contents: function( elem ) { return jQuery.nodeName( elem, "iframe" ) ? elem.contentDocument || elem.contentWindow.document : - jQuery.makeArray( elem.childNodes ); + jQuery.merge( [], elem.childNodes ); } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { var ret = jQuery.map( this, fn, until ); - + if ( !runtil.test( name ) ) { selector = until; } @@ -3902,13 +5737,13 @@ jQuery.each({ ret = jQuery.filter( selector, ret ); } - ret = this.length > 1 ? jQuery.unique( ret ) : ret; + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; - if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + if ( this.length > 1 && rparentsprev.test( name ) ) { ret = ret.reverse(); } - return this.pushStack( ret, name, slice.call(arguments).join(",") ); + return this.pushStack( ret ); }; }); @@ -3918,11 +5753,15 @@ jQuery.extend({ expr = ":not(" + expr + ")"; } - return jQuery.find.matches(expr, elems); + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); }, - + dir: function( elem, dir, until ) { - var matched = [], cur = elem[dir]; + var matched = [], + cur = elem[ dir ]; + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { if ( cur.nodeType === 1 ) { matched.push( cur ); @@ -3932,19 +5771,6 @@ jQuery.extend({ return matched; }, - nth: function( cur, result, dir, elem ) { - result = result || 1; - var num = 0; - - for ( ; cur; cur = cur[dir] ) { - if ( cur.nodeType === 1 && ++num === result ) { - break; - } - } - - return cur; - }, - sibling: function( n, elem ) { var r = []; @@ -3957,54 +5783,101 @@ jQuery.extend({ return r; } }); -var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), rleadingWhitespace = /^\s+/, - rxhtmlTag = /(<([\w:]+)[^>]*?)\/>/g, - rselfClosing = /^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, rtagName = /<([\w:]+)/, rtbody = /"; - }, + rnoInnerhtml = /<(?:script|style|link)/i, + manipulation_rcheckableType = /^(?:checkbox|radio)$/i, + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /^$|\/(?:java|ecma)script/i, + rscriptTypeMasked = /^true\/(.*)/, + rcleanScript = /^\s*\s*$/g, + + // We have to close these tags to support XHTML (#13200) wrapMap = { option: [ 1, "" ], legend: [ 1, "
    ", "
    " ], + area: [ 1, "", "" ], + param: [ 1, "", "" ], thead: [ 1, "", "
    " ], tr: [ 2, "", "
    " ], - td: [ 3, "", "
    " ], col: [ 2, "", "
    " ], - area: [ 1, "", "" ], - _default: [ 0, "", "" ] - }; + td: [ 3, "", "
    " ], + + // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, + // unless wrapped in a div with non-breaking characters in front of it. + _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
    ", "
    " ] + }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; -// IE can't serialize and '); parse_html(''' + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    + + + + + +
    + + + + +
    + + +
    + + + + + + + + + + + +
    + + +
    + + + + + + + + + +
    +
    + +
    +
    + +
    +
    +
    + + + +
      + +
    • + Pull Request +
    • + +
    • +
      + +
      + + + + Watch + + + +
      +
      +
      + Notification status + +
      + +
      + +
      + +
      + +

      Not watching

      + You only receive notifications for discussions in which you participate or are @mentioned. + + + Watch + +
      +
      + +
      + +
      + +

      Watching

      + You receive notifications for all discussions in this repository. + + + Unwatch + +
      +
      + +
      + +
      + +

      Ignoring

      + You do not receive any notifications for discussions in this repository. + + + Stop ignoring + +
      +
      + +
      + +
      +
      +
      + +
      +
    • + +
    • + + + Unstar + + + + Star + + +
    • + +
    • + + + Fork + + +
    • + + +
    + +

    + public + + + / + django +

    +
    + + + + +
    + + + + + + +
    + + +
    + + + tree: + d7504a3d7b + + +
    + +
    +
    + Switch branches/tags + +
    + +
    +
    + +
    +
    + +
    +
    + + + + +
    +
    + +
    + + 1.5c2 +
    +
    + + 1.5c1 +
    +
    + + 1.5b2 +
    +
    + + 1.5b1 +
    +
    + + 1.5a1 +
    +
    + + 1.5.1 +
    +
    + + 1.5 +
    +
    + + 1.4.5 +
    +
    + + 1.4.4 +
    +
    + + 1.4.3 +
    +
    + + 1.4.2 +
    +
    + + 1.4.1 +
    +
    + + 1.4 +
    +
    + + 1.3.7 +
    +
    + + 1.3.6 +
    +
    + + 1.3.5 +
    +
    + + 1.3.4 +
    +
    + + 1.3.3 +
    +
    + + 1.3.2 +
    +
    + + 1.3.1 +
    +
    + + 1.3 +
    +
    + + 1.2.7 +
    +
    + + 1.2.6 +
    +
    + + 1.2.5 +
    +
    + + 1.2.4 +
    +
    + + 1.2.3 +
    +
    + + 1.2.2 +
    +
    + + 1.2.1 +
    +
    + + 1.2 +
    +
    + + 1.1.4 +
    +
    + + 1.1.3 +
    +
    + + 1.1.2 +
    +
    + + 1.1.1 +
    +
    + + 1.1 +
    +
    + + 1.0.4 +
    +
    + + 1.0.3 +
    +
    + + 1.0.2 +
    +
    + + 1.0.1 +
    +
    + + 1.0 +
    +
    + +
    Nothing to show
    + +
    + +
    +
    +
    + +
    + + + +
    + + + + + + + +
    +
    + +
    + + + +
    + Browse code + +

    + Improved regex in strip_tags +

    + +
    Thanks Pablo Recio for the report. Refs #19237.
    + +
    + commit d7504a3d7b8645bdb979bab7ded0e9a9b6dccd0e + + + 1 parent + + afa3e16 + + +
    + + + authored + +
    +
    +
    + + + +
    +

    + Showing 2 changed files + with 2 additions + and 1 deletion. + Show Diff Stats + Hide Diff Stats

    + +
      +
    1. + + + 2 +  + + + django/utils/html.py +
    2. +
    3. + + + 1 +  + + + tests/regressiontests/utils/html.py +
    4. +
    +
    + + +
    +
    +
    +
    + 2  + + + django/utils/html.py + + +
    +
    + + + + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + ... + + ... + + + @@ -33,7 +33,7 @@ +
    + 33 + + 33 + + +  html_gunk_re = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<strong><\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE) +
    + 34 + + 34 + + +  hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL) +
    + 35 + + 35 + + +  trailing_empty_content_re = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z') +
    + 36 + +   + + + -strip_tags_re = re.compile(r'</?\S([^=]*=(\s*"[^"]*"|\s*\'[^\']*\'|\S*)|[^>])*?>', re.IGNORECASE) +
    +   + + 36 + + + +strip_tags_re = re.compile(r'</?\S([^=>]*=(\s*"[^"]*"|\s*\'[^\']*\'|\S*)|[^>])*?>', re.IGNORECASE) +
    + 37 + + 37 + + +   +
    + 38 + + 38 + + +   +
    + 39 + + 39 + + +  def escape(text): +
    +
    + +
    +
    +
    +
    +
    + 1  + + + tests/regressiontests/utils/html.py + + +
    +
    + + + + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + ... + + ... + + + @@ -68,6 +68,7 @@ def test_strip_tags(self): +
    + 68 + + 68 + + +              ('a<p onclick="alert(\'<test>\')">b</p>c', 'abc'), +
    + 69 + + 69 + + +              ('a<p a >b</p>c', 'abc'), +
    + 70 + + 70 + + +              ('d<a:b c:d>e</p>f', 'def'), +
    +   + + 71 + + + +            ('<strong>foo</strong><a href="http://example.com">bar</a>', 'foobar'), +
    + 71 + + 72 + + +          ) +
    + 72 + + 73 + + +          for value, output in items: +
    + 73 + + 74 + + +              self.check_output(f, value, output) +
    +
    + +
    +
    +
    + + +
    + + +

    + 0 notes + on commit d7504a3 + +

    +
    +
    + +
    + + +
    +
    +
    + + + +
    +
    + + + Comments are parsed with GitHub Flavored Markdown + +
    + + +
    + + + + + + + + +

    + + Attach images by dragging & dropping them or + + choose an image + + + Octocat-spinner-32 Uploading your images now… + + + Unfortunately we don't support that file type yet. Try image files less than 5MB. + + + This browser doesn't support image attachments. + + + Something went really wrong and we can't process that image. + +

    + +
    +
    + +
    + + + +
    +
    +
    +

    Nothing to preview

    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + Commit_comment_tip +

    Tip: You can also add notes to lines in a file. Hover to the left of a line to make a note

    +
    + +
    +
    + + + +
    + + + +
    + + +
    + +
    + + + + + + + Watch thread + + + +
    +
    +
    + Thread notifications + +
    + +
    + +
    + +
    + +

    Not watching

    + You only receive notifications for this thread if you participate or are @mentioned. + + + Watch thread + +
    +
    + +
    + +
    + +

    Watching

    + Receive all notifications for this thread. + + + Unwatch thread + +
    +
    + +
    + +
    + +

    Ignoring

    + You do not receive notifications for this thread. + + + Stop ignoring thread + +
    +
    + +
    +
    +
    +
    + +

    You only receive notifications for this thread when you participate or are @mentioned.

    +
    + + + + + + + +
    +
    +
    +
    + + +
    + + + + + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    + + + +
    + + Something went wrong with that request. Please try again. + +
    + + + + + + + + diff --git a/tests/utils_tests/files/strip_tags2.txt b/tests/utils_tests/files/strip_tags2.txt new file mode 100644 index 0000000000..6b2a4033c2 --- /dev/null +++ b/tests/utils_tests/files/strip_tags2.txt @@ -0,0 +1,118 @@ +_**Prerequisite**: You are already aware of the [basics of building a HelloWorld](http://developer.android.com/training/index.html) in Android and know [how to use the APIs provided in the support library](http://developer.android.com/training/basics/fragments/support-lib.html)._ + +_The code example is available on [github](http://github.com/iontech/Fragments_Example "Fragments Example")._ +_____________________________________________________________ +Ever wanted a code snippet from an Activity to be available to other activities? Perhaps a Button or a ListView, maybe a Layout or any View/ViewGroup for that matter? Fragments let us do just that. + +Necessity is the mother of invention. +Before understanding what Fragments are and how they work, we must first realize their existence in the first place. + +The Problem +----------- +Suppose we have an Android app with two Activities- [*FirstActivity*](https://github.com/iontech/Fragments_Example/blob/master/src/main/java/com/github/iontech/fragments_example/FirstActivity.java) and [*SecondActivity*](https://github.com/iontech/Fragments_Example/blob/master/src/main/java/com/github/iontech/fragments_example/SecondActivity.java). +*FirstActivity* contains two Views, a `TextView` (*textView*) and a `Button` (*button1*); and *button1* has an `onClick()` callback that `Toast`'s a simple message "Button pressed". +*SecondActivity* contains both the Views present in *FirstActivity* and a `Button` (*button2*). + +Now we want to utilize the two layout components(Views) of *FirstActivity* in *SecondActivity*, we can go about this with two approaches: + +1. Copy and Paste the xml elements of the two Views. +2. Create a separate layout for common Views and reuse it using `` layout element. + More about this [here](http://developer.android.com/training/improving-layouts/reusing-layouts.html). + +Electing the second approach makes sense cause it enables us to make reusable layouts. Everything seems great till now. We are able to make reusable layouts and use them as many times as we want. + +Now recollect that we have an `onClick()` callback assigned to *button1*. How do we reuse the same callback functionality of *button1* across multiple Activities? `` lets us reuse layouts and not the Activity source. +This is where Fragments come into play. + +Fragments +--------- +
    ![image](http://iontech.files.wordpress.com/2013/01/androidfragmentation1-264x300.png)
    +Fragments encompass both layout resource and Java source. Hence, unlike ``, they allow us to reuse the View components along with their functionality, if needed. +Fragments were first introduced in Honeycomb(API 11), living under the `android.app` package. +**Note**: API 11 implies that Fragments have no support for devices less than Honeycomb and, for the record, as of writing this post, [more than 50% of Android devices worldwide run versions of Android below Honeycomb](http://developer.android.com/about/dashboards/index.html). Developer dissapointed? You don't have to be, cause google has been cautious enough to add the Fragment APIs to the support library. Yay! + +In the support library Fragment APIs sit in the `android.support.v4.app` package. This post assumes that your `minSdk` support is below API 11. Hence we concentrate on the Fragment APIs of the support library. + +### Diving into code + +Performing code reuse with Fragments involves three major steps: + +1. Creating reusable View components - Creating a layout for the fragment. +2. Creating reusable Java source - Writing the layout's corresponding Fragment class. +3. Employing the reusable components in Activity - Making an Activity to host this Fragment. + +#### 1. Creating reusable View components +##### Creating a layout for the Fragment +This is done precisely as we do it for our activity layouts. The layout contains a root element (ViewGroup) defining the layout, For instance in our example we use a LinearLayout and its child elements(the reusable Views) that we want to have in our fragment. + +> [fragment_common.xml](https://github.com/iontech/Fragments_Example/blob/master/res/layout/fragment_common.xml) + + + + + +