From 18e79f1425fa87f2f38df25c65203ec4d311f499 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 20 Jun 2013 18:55:27 +0800 Subject: Fixed #20486 -- Ensure that file_move_safe raises an error if the destination already exists. Thanks to kux for the report, and Russ Webber for the patch. --- tests/files/tests.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'tests') diff --git a/tests/files/tests.py b/tests/files/tests.py index cd2d15acdb..f1e3d5b14b 100644 --- a/tests/files/tests.py +++ b/tests/files/tests.py @@ -1,11 +1,13 @@ from __future__ import absolute_import +import os import gzip import shutil import tempfile from django.core.cache import cache from django.core.files import File +from django.core.files.move import file_move_safe from django.core.files.base import ContentFile from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase @@ -146,3 +148,15 @@ class FileTests(unittest.TestCase): file = SimpleUploadedFile("mode_test.txt", b"content") self.assertFalse(hasattr(file, 'mode')) g = gzip.GzipFile(fileobj=file) + + +class FileMoveSafeTests(unittest.TestCase): + def test_file_move_overwrite(self): + handle_a, self.file_a = tempfile.mkstemp(dir=os.environ['DJANGO_TEST_TEMP_DIR']) + handle_b, self.file_b = tempfile.mkstemp(dir=os.environ['DJANGO_TEST_TEMP_DIR']) + + # file_move_safe should raise an IOError exception if destination file exists and allow_overwrite is False + self.assertRaises(IOError, lambda: file_move_safe(self.file_a, self.file_b, allow_overwrite=False)) + + # should allow it and continue on if allow_overwrite is True + self.assertIsNone(file_move_safe(self.file_a, self.file_b, allow_overwrite=True)) -- cgit v1.3 From 04628e2016641bfa657333d6ee1f5b371c17f62c Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Wed, 19 Jun 2013 23:42:23 -0400 Subject: Fixed #20630 -- Removed `maxlength` attribute from `NumberInput`. This attribute is only allowed on inputs of type "text", "search", "url", "tel", "email", or "password". Thanks to yoyoma for the report and @bmispelon for the review. --- django/forms/fields.py | 10 ++-------- tests/forms_tests/tests/test_fields.py | 4 ++-- tests/model_formsets/tests.py | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) (limited to 'tests') diff --git a/django/forms/fields.py b/django/forms/fields.py index 52bcf9485c..c4bc3fa88c 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -370,14 +370,8 @@ class DecimalField(IntegerField): def widget_attrs(self, widget): attrs = super(DecimalField, self).widget_attrs(widget) - if isinstance(widget, NumberInput): - if self.max_digits is not None: - max_length = self.max_digits + 1 # for the sign - if self.decimal_places is None or self.decimal_places > 0: - max_length += 1 # for the dot - attrs['maxlength'] = max_length - if self.decimal_places: - attrs['step'] = '0.%s1' % ('0' * (self.decimal_places-1)) + if isinstance(widget, NumberInput) and self.decimal_places: + attrs['step'] = '0.%s1' % ('0' * (self.decimal_places - 1)) return attrs diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py index 47c637befa..f7d83503f4 100644 --- a/tests/forms_tests/tests/test_fields.py +++ b/tests/forms_tests/tests/test_fields.py @@ -296,7 +296,7 @@ class FieldsTests(SimpleTestCase): def test_decimalfield_1(self): f = DecimalField(max_digits=4, decimal_places=2) - self.assertWidgetRendersTo(f, '') + self.assertWidgetRendersTo(f, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) self.assertEqual(f.clean('1'), Decimal("1")) @@ -342,7 +342,7 @@ class FieldsTests(SimpleTestCase): def test_decimalfield_3(self): f = DecimalField(max_digits=4, decimal_places=2, max_value=Decimal('1.5'), min_value=Decimal('0.5')) - self.assertWidgetRendersTo(f, '') + self.assertWidgetRendersTo(f, '') self.assertRaisesMessage(ValidationError, "'Ensure this value is less than or equal to 1.5.'", f.clean, '1.6') self.assertRaisesMessage(ValidationError, "'Ensure this value is greater than or equal to 0.5.'", f.clean, '0.4') self.assertEqual(f.clean('1.5'), Decimal("1.5")) diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index 03cd3b0159..62870c7462 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -559,7 +559,7 @@ class ModelFormsetTest(TestCase): formset = AuthorBooksFormSet2(instance=author) self.assertEqual(len(formset.forms), 1) self.assertHTMLEqual(formset.forms[0].as_p(), - '

\n' + '

\n' '

') data = { -- cgit v1.3 From 552a90b44456f24de228a6c7cd916c040787cf22 Mon Sep 17 00:00:00 2001 From: Oliver Beattie Date: Fri, 19 Apr 2013 10:44:47 +0100 Subject: Fixed #20290 -- Allow override_settings to be nested Refactored override_settings to store the underlying settings._wrapped value seen at runtime, not instantiation time. --- AUTHORS | 1 + django/test/utils.py | 3 ++- tests/servers/tests.py | 5 +++++ tests/settings_tests/tests.py | 27 ++++++++++++++++++++++++++- 4 files changed, 34 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/AUTHORS b/AUTHORS index 9413a51884..3eb0a68be9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -97,6 +97,7 @@ answer newbie questions, and generally made Django that much better: Ned Batchelder batiste@dosimple.ch Batman + Oliver Beattie Brian Beck Shannon -jj Behrens Esdras Beleza diff --git a/django/test/utils.py b/django/test/utils.py index be586c75a6..fc4558875e 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -207,7 +207,6 @@ class override_settings(object): """ def __init__(self, **kwargs): self.options = kwargs - self.wrapped = settings._wrapped def __enter__(self): self.enable() @@ -246,6 +245,7 @@ class override_settings(object): override = UserSettingsHolder(settings._wrapped) for key, new_value in self.options.items(): setattr(override, key, new_value) + self.wrapped = settings._wrapped settings._wrapped = override for key, new_value in self.options.items(): setting_changed.send(sender=settings._wrapped.__class__, @@ -253,6 +253,7 @@ class override_settings(object): def disable(self): settings._wrapped = self.wrapped + del self.wrapped for key in self.options: new_value = getattr(settings, key, None) setting_changed.send(sender=settings._wrapped.__class__, diff --git a/tests/servers/tests.py b/tests/servers/tests.py index 3377793d68..be74afe820 100644 --- a/tests/servers/tests.py +++ b/tests/servers/tests.py @@ -95,6 +95,11 @@ class LiveServerAddress(LiveServerBase): else: del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] + @classmethod + def tearDownClass(cls): + # skip it, as setUpClass doesn't call its parent either + pass + @classmethod def raises_exception(cls, address, exception): os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = address diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index f8d66bb2b7..625c4f31df 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -9,17 +9,19 @@ from django.test.utils import override_settings from django.utils import unittest, six -@override_settings(TEST='override') +@override_settings(TEST='override', TEST_OUTER='outer') class FullyDecoratedTranTestCase(TransactionTestCase): available_apps = [] def test_override(self): self.assertEqual(settings.TEST, 'override') + self.assertEqual(settings.TEST_OUTER, 'outer') @override_settings(TEST='override2') def test_method_override(self): self.assertEqual(settings.TEST, 'override2') + self.assertEqual(settings.TEST_OUTER, 'outer') def test_decorated_testcase_name(self): self.assertEqual(FullyDecoratedTranTestCase.__name__, 'FullyDecoratedTranTestCase') @@ -168,6 +170,29 @@ class SettingsTests(TestCase): self.assertRaises(AttributeError, getattr, settings, 'USE_I18N') self.assertEqual(settings.USE_I18N, previous_i18n) + def test_override_settings_nested(self): + """ + Test that override_settings uses the actual _wrapped attribute at + runtime, not when it was instantiated. + """ + + self.assertRaises(AttributeError, getattr, settings, 'TEST') + self.assertRaises(AttributeError, getattr, settings, 'TEST2') + + inner = override_settings(TEST2='override') + with override_settings(TEST='override'): + self.assertEqual('override', settings.TEST) + with inner: + self.assertEqual('override', settings.TEST) + self.assertEqual('override', settings.TEST2) + # inner's __exit__ should have restored the settings of the outer + # context manager, not those when the class was instantiated + self.assertEqual('override', settings.TEST) + self.assertRaises(AttributeError, getattr, settings, 'TEST2') + + self.assertRaises(AttributeError, getattr, settings, 'TEST') + self.assertRaises(AttributeError, getattr, settings, 'TEST2') + def test_allowed_include_roots_string(self): """ ALLOWED_INCLUDE_ROOTS is not allowed to be incorrectly set to a string -- cgit v1.3 From ef79582e8630cb3c119caed52130c9671188addd Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 22 Jun 2013 09:25:14 +0200 Subject: Fixed 17478 -- Allowed queryset overriding in BaseModelFormSet init BaseModelFormSet.forms is now a cached property instead of being populated in the __init__ method. This behaviour also matches an example in the documentation. Thanks Thomasz Swiderski for the report and Simon Charette for the review. --- django/forms/formsets.py | 15 ++++++++------- tests/forms_tests/tests/test_formsets.py | 3 ++- tests/model_formsets/tests.py | 19 ++++++++++++++++++- 3 files changed, 28 insertions(+), 9 deletions(-) (limited to 'tests') diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 9c2e88bee7..cb3126e6d7 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -6,6 +6,7 @@ from django.forms.fields import IntegerField, BooleanField from django.forms.util import ErrorList from django.forms.widgets import HiddenInput from django.utils.encoding import python_2_unicode_compatible +from django.utils.functional import cached_property from django.utils.safestring import mark_safe from django.utils import six from django.utils.six.moves import xrange @@ -55,8 +56,6 @@ class BaseFormSet(object): self.error_class = error_class self._errors = None self._non_form_errors = None - # construct the forms in the formset - self._construct_forms() def __str__(self): return self.as_table() @@ -125,12 +124,14 @@ class BaseFormSet(object): initial_forms = len(self.initial) if self.initial else 0 return initial_forms - def _construct_forms(self): - # instantiate all the forms and put them in self.forms - self.forms = [] + @cached_property + def forms(self): + """ + Instantiate forms at first property access. + """ # DoS protection is included in total_form_count() - for i in xrange(self.total_form_count()): - self.forms.append(self._construct_form(i)) + forms = [self._construct_form(i) for i in xrange(self.total_form_count())] + return forms def _construct_form(self, i, **kwargs): """ diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index b26017bc78..41577e6049 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -1072,7 +1072,8 @@ ArticleFormSet = formset_factory(ArticleForm) class TestIsBoundBehavior(TestCase): def test_no_data_raises_validation_error(self): - self.assertRaises(ValidationError, ArticleFormSet, {}) + with self.assertRaises(ValidationError): + ArticleFormSet({}).is_valid() def test_with_management_data_attrs_work_fine(self): data = { diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index 62870c7462..43509c471f 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -8,7 +8,7 @@ from decimal import Decimal from django import forms from django.db import models from django.forms.models import (_get_foreign_key, inlineformset_factory, - modelformset_factory) + modelformset_factory, BaseModelFormSet) from django.test import TestCase, skipUnlessDBFeature from django.utils import six @@ -386,6 +386,23 @@ class ModelFormsetTest(TestCase): formset = PostFormSet() self.assertFalse("subtitle" in formset.forms[0].fields) + def test_custom_queryset_init(self): + """ + Test that a queryset can be overriden in the __init__ method. + https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#changing-the-queryset + """ + author1 = Author.objects.create(name='Charles Baudelaire') + author2 = Author.objects.create(name='Paul Verlaine') + + class BaseAuthorFormSet(BaseModelFormSet): + def __init__(self, *args, **kwargs): + super(BaseAuthorFormSet, self).__init__(*args, **kwargs) + self.queryset = Author.objects.filter(name__startswith='Charles') + + AuthorFormSet = modelformset_factory(Author, formset=BaseAuthorFormSet) + formset = AuthorFormSet() + self.assertEqual(len(formset.get_queryset()), 1) + def test_model_inheritance(self): BetterAuthorFormSet = modelformset_factory(BetterAuthor, fields="__all__") formset = BetterAuthorFormSet() -- cgit v1.3 From ef37b23050637da643b47b1ee744702d4d603f4c Mon Sep 17 00:00:00 2001 From: Gilberto Gonçalves Date: Sat, 22 Jun 2013 12:12:43 +0100 Subject: Fixed #18872 -- Added prefix to FormMixin Thanks @ibustama for the initial patch and dragonsnaker for opening the report. --- AUTHORS | 1 + django/views/generic/edit.py | 13 ++++++++++++- docs/ref/class-based-views/mixins-editing.txt | 4 ++++ docs/releases/1.6.txt | 3 +++ tests/generic_views/test_edit.py | 21 ++++++++++++++++++++- 5 files changed, 40 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/AUTHORS b/AUTHORS index 3eb0a68be9..e4803dbe9a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -249,6 +249,7 @@ answer newbie questions, and generally made Django that much better: martin.glueck@gmail.com Ben Godfrey GomoX + Gil Gonçalves Guilherme Mesquita Gondim Mario Gonzalez David Gouldin diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py index b31d7a218f..193071efc5 100644 --- a/django/views/generic/edit.py +++ b/django/views/generic/edit.py @@ -17,6 +17,7 @@ class FormMixin(ContextMixin): initial = {} form_class = None success_url = None + prefix = None def get_initial(self): """ @@ -24,6 +25,12 @@ class FormMixin(ContextMixin): """ return self.initial.copy() + def get_prefix(self): + """ + Returns the prefix to use for forms on this view + """ + return self.prefix + def get_form_class(self): """ Returns the form class to use in this view @@ -40,7 +47,11 @@ class FormMixin(ContextMixin): """ Returns the keyword arguments for instantiating the form. """ - kwargs = {'initial': self.get_initial()} + kwargs = { + 'initial': self.get_initial(), + 'prefix': self.get_prefix(), + } + if self.request.method in ('POST', 'PUT'): kwargs.update({ 'data': self.request.POST, diff --git a/docs/ref/class-based-views/mixins-editing.txt b/docs/ref/class-based-views/mixins-editing.txt index 48d363b3b2..a0160610d2 100644 --- a/docs/ref/class-based-views/mixins-editing.txt +++ b/docs/ref/class-based-views/mixins-editing.txt @@ -35,6 +35,10 @@ FormMixin The URL to redirect to when the form is successfully processed. + .. attribute:: prefix + + Sets the :attr:`~django.forms.Form.prefix` for the generated form. + .. method:: get_initial() Retrieve initial data for the form. By default, returns a copy of diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 16e0b94a9d..95bfedc74e 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -731,6 +731,9 @@ Miscellaneous of the admin views. You should update your custom templates if they use the previous parameter name. +* Added :attr:`~django.views.generic.edit.FormMixin.prefix` to allow you to + customize the prefix on the form. + Features deprecated in 1.6 ========================== diff --git a/tests/generic_views/test_edit.py b/tests/generic_views/test_edit.py index 435e48ba99..84d18ebcb2 100644 --- a/tests/generic_views/test_edit.py +++ b/tests/generic_views/test_edit.py @@ -7,8 +7,9 @@ from django.core.urlresolvers import reverse from django import forms from django.test import TestCase from django.utils.unittest import expectedFailure +from django.test.client import RequestFactory from django.views.generic.base import View -from django.views.generic.edit import FormMixin, CreateView, UpdateView +from django.views.generic.edit import FormMixin, CreateView from . import views from .models import Artist, Author @@ -22,6 +23,24 @@ class FormMixinTests(TestCase): initial_2 = FormMixin().get_initial() self.assertNotEqual(initial_1, initial_2) + def test_get_prefix(self): + """ Test prefix can be set (see #18872) """ + test_string = 'test' + + rf = RequestFactory() + get_request = rf.get('/') + + class TestFormMixin(FormMixin): + request = get_request + + default_kwargs = TestFormMixin().get_form_kwargs() + self.assertEqual(None, default_kwargs.get('prefix')) + + set_mixin = TestFormMixin() + set_mixin.prefix = test_string + set_kwargs = set_mixin.get_form_kwargs() + self.assertEqual(test_string, set_kwargs.get('prefix')) + class BasicFormTests(TestCase): urls = 'generic_views.urls' -- cgit v1.3 From 680b512fc1509ca05cf56fada30f6b833735362a Mon Sep 17 00:00:00 2001 From: Gilberto Gonçalves Date: Sat, 22 Jun 2013 14:05:12 +0100 Subject: Fixed #20587 -- Made convert_values handle None values --- django/db/backends/sqlite3/base.py | 3 +++ tests/backends/tests.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index a2d925ac3f..18c4029649 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -250,6 +250,9 @@ class DatabaseOperations(BaseDatabaseOperations): and gets dates and datetimes wrong. For consistency with other backends, coerce when required. """ + if value is None: + return None + internal_type = field.get_internal_type() if internal_type == 'DecimalField': return util.typecast_decimal(field.format_number(value)) diff --git a/tests/backends/tests.py b/tests/backends/tests.py index b6e3e9e36f..c6cad56ec1 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -11,9 +11,12 @@ from django.core.management.color import no_style from django.db import (connection, connections, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError, transaction) from django.db.backends.signals import connection_created +from django.db.backends.sqlite3.base import DatabaseOperations from django.db.backends.postgresql_psycopg2 import version as pg_version from django.db.backends.util import format_number from django.db.models import Sum, Avg, Variance, StdDev +from django.db.models.fields import (AutoField, DateField, DateTimeField, + DecimalField, IntegerField, TimeField) from django.db.utils import ConnectionHandler from django.test import (TestCase, skipUnlessDBFeature, skipIfDBFeature, TransactionTestCase) @@ -402,7 +405,7 @@ class EscapingChecksDebug(EscapingChecks): pass -class SqlliteAggregationTests(TestCase): +class SqliteAggregationTests(TestCase): """ #19360: Raise NotImplementedError when aggregating on date/time fields. """ @@ -418,6 +421,38 @@ class SqlliteAggregationTests(TestCase): models.Item.objects.all().aggregate, aggregate('last_modified')) +class SqliteChecks(TestCase): + + @unittest.skipUnless(connection.vendor == 'sqlite', + "No need to do SQLite checks") + def test_convert_values_to_handle_null_value(self): + database_operations = DatabaseOperations(connection) + self.assertEqual( + None, + database_operations.convert_values(None, AutoField(primary_key=True)) + ) + self.assertEqual( + None, + database_operations.convert_values(None, DateField()) + ) + self.assertEqual( + None, + database_operations.convert_values(None, DateTimeField()) + ) + self.assertEqual( + None, + database_operations.convert_values(None, DecimalField()) + ) + self.assertEqual( + None, + database_operations.convert_values(None, IntegerField()) + ) + self.assertEqual( + None, + database_operations.convert_values(None, TimeField()) + ) + + class BackendTestCase(TestCase): def create_squares_with_executemany(self, args): -- cgit v1.3 From 1559f84d8b5c2d4d088a27fc0edf02f01a1d65f0 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 22 Jun 2013 18:39:14 -0300 Subject: Fixed #20311 -- Make sure makemessages doesn't create duplicate Plural-Forms .po file headers. Thanks naktinis for the report and initial patch. --- django/core/management/commands/makemessages.py | 6 +++--- tests/i18n/commands/__init__.py | 6 +++++- tests/i18n/commands/extraction.py | 19 +++++++++++++++++++ tests/i18n/commands/templates/plural.djtpl | 6 ++++++ 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 tests/i18n/commands/templates/plural.djtpl (limited to 'tests') diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 060def5d5a..695cad419b 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -402,11 +402,11 @@ class Command(NoArgsCommand): if self.verbosity > 1: self.stdout.write("copying plural forms: %s\n" % m.group('value')) lines = [] - seen = False + found = False for line in msgs.split('\n'): - if not line and not seen: + if not found and (not line or plural_forms_re.search(line)): line = '%s\n' % m.group('value') - seen = True + found = True lines.append(line) msgs = '\n'.join(lines) break diff --git a/tests/i18n/commands/__init__.py b/tests/i18n/commands/__init__.py index ffd439140e..76249ca34d 100644 --- a/tests/i18n/commands/__init__.py +++ b/tests/i18n/commands/__init__.py @@ -1,4 +1,4 @@ -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext as _, ungettext # Translators: This comment should be extracted dummy1 = _("This is a translatable string.") @@ -6,3 +6,7 @@ dummy1 = _("This is a translatable string.") # This comment should not be extracted dummy2 = _("This is another translatable string.") +# This file has a literal with plural forms. When processed first, makemessages +# shouldn't create a .po file with duplicate `Plural-Forms` headers +number = 3 +dummuy3 = ungettext("%(number)s Foo", "%(number)s Foos", number) % {'number': number} diff --git a/tests/i18n/commands/extraction.py b/tests/i18n/commands/extraction.py index 8696ae453b..dd1dce2ed0 100644 --- a/tests/i18n/commands/extraction.py +++ b/tests/i18n/commands/extraction.py @@ -329,6 +329,15 @@ class SymlinkExtractorTests(ExtractorTests): class CopyPluralFormsExtractorTests(ExtractorTests): + PO_FILE_ES = 'locale/es/LC_MESSAGES/django.po' + + def tearDown(self): + os.chdir(self.test_dir) + try: + self._rmrf('locale/es') + except OSError: + pass + os.chdir(self._cwd) def test_copy_plural_forms(self): os.chdir(self.test_dir) @@ -338,6 +347,16 @@ class CopyPluralFormsExtractorTests(ExtractorTests): po_contents = force_text(fp.read()) self.assertTrue('Plural-Forms: nplurals=2; plural=(n != 1)' in po_contents) + def test_override_plural_forms(self): + """Ticket #20311.""" + os.chdir(self.test_dir) + management.call_command('makemessages', locale='es', extensions=['djtpl'], verbosity=0) + self.assertTrue(os.path.exists(self.PO_FILE_ES)) + with open(self.PO_FILE_ES, 'r') as fp: + po_contents = force_text(fp.read()) + found = re.findall(r'^(?P"Plural-Forms.+?\\n")\s*$', po_contents, re.MULTILINE | re.DOTALL) + self.assertEqual(1, len(found)) + class NoWrapExtractorTests(ExtractorTests): diff --git a/tests/i18n/commands/templates/plural.djtpl b/tests/i18n/commands/templates/plural.djtpl new file mode 100644 index 0000000000..b2f2a18750 --- /dev/null +++ b/tests/i18n/commands/templates/plural.djtpl @@ -0,0 +1,6 @@ +{% load i18n %} +{% comment %} +This file has a literal with plural forms. When processed first, makemessages +shouldn't create a .po file with duplicate `Plural-Forms` headers +{% endcomment %} +{% blocktrans count number=3 %}{{ number }} Bar{% plural %}{{ number }} Bars{% endblocktrans %} -- cgit v1.3 From 0346563939396fb89dec8df31f82eaefaaeb8616 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 25 Jun 2013 09:37:54 +0800 Subject: Fixed #20653 -- Renamed checksetup management command. This is to allow future compatibility with work that is ongoing in the 2013 GSoC. --- django/core/checks/__init__.py | 0 django/core/checks/compatibility/__init__.py | 0 django/core/checks/compatibility/base.py | 39 +++++++++ django/core/checks/compatibility/django_1_6_0.py | 37 ++++++++ django/core/compat_checks/__init__.py | 0 django/core/compat_checks/base.py | 39 --------- django/core/compat_checks/django_1_6_0.py | 37 -------- django/core/management/commands/check.py | 14 +++ django/core/management/commands/checksetup.py | 14 --- docs/releases/1.6.txt | 6 +- tests/check/__init__.py | 0 tests/check/models.py | 1 + tests/check/tests.py | 107 +++++++++++++++++++++++ tests/compat_checks/__init__.py | 0 tests/compat_checks/models.py | 1 - tests/compat_checks/tests.py | 107 ----------------------- 16 files changed, 201 insertions(+), 201 deletions(-) create mode 100644 django/core/checks/__init__.py create mode 100644 django/core/checks/compatibility/__init__.py create mode 100644 django/core/checks/compatibility/base.py create mode 100644 django/core/checks/compatibility/django_1_6_0.py delete mode 100644 django/core/compat_checks/__init__.py delete mode 100644 django/core/compat_checks/base.py delete mode 100644 django/core/compat_checks/django_1_6_0.py create mode 100644 django/core/management/commands/check.py delete mode 100644 django/core/management/commands/checksetup.py create mode 100644 tests/check/__init__.py create mode 100644 tests/check/models.py create mode 100644 tests/check/tests.py delete mode 100644 tests/compat_checks/__init__.py delete mode 100644 tests/compat_checks/models.py delete mode 100644 tests/compat_checks/tests.py (limited to 'tests') diff --git a/django/core/checks/__init__.py b/django/core/checks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/core/checks/compatibility/__init__.py b/django/core/checks/compatibility/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/core/checks/compatibility/base.py b/django/core/checks/compatibility/base.py new file mode 100644 index 0000000000..7fe52d2af9 --- /dev/null +++ b/django/core/checks/compatibility/base.py @@ -0,0 +1,39 @@ +from __future__ import unicode_literals +import warnings + +from django.core.checks.compatibility import django_1_6_0 + + +COMPAT_CHECKS = [ + # Add new modules at the top, so we keep things in descending order. + # After two-three minor releases, old versions should get dropped. + django_1_6_0, +] + + +def check_compatibility(): + """ + Runs through compatibility checks to warn the user with an existing install + about changes in an up-to-date Django. + + Modules should be located in ``django.core.compat_checks`` (typically one + per release of Django) & must have a ``run_checks`` function that runs + all the checks. + + Returns a list of informational messages about incompatibilities. + """ + messages = [] + + for check_module in COMPAT_CHECKS: + check = getattr(check_module, 'run_checks', None) + + if check is None: + warnings.warn( + "The '%s' module lacks a " % check_module.__name__ + + "'run_checks' method, which is needed to verify compatibility." + ) + continue + + messages.extend(check()) + + return messages diff --git a/django/core/checks/compatibility/django_1_6_0.py b/django/core/checks/compatibility/django_1_6_0.py new file mode 100644 index 0000000000..1998c5ba77 --- /dev/null +++ b/django/core/checks/compatibility/django_1_6_0.py @@ -0,0 +1,37 @@ +from __future__ import unicode_literals + + +def check_test_runner(): + """ + Checks if the user has *not* overridden the ``TEST_RUNNER`` setting & + warns them about the default behavior changes. + + If the user has overridden that setting, we presume they know what they're + doing & avoid generating a message. + """ + from django.conf import settings + new_default = 'django.test.runner.DiscoverRunner' + test_runner_setting = getattr(settings, 'TEST_RUNNER', new_default) + + if test_runner_setting == new_default: + message = [ + "You have not explicitly set 'TEST_RUNNER'. In Django 1.6,", + "there is a new test runner ('%s')" % new_default, + "by default. You should ensure your tests are still all", + "running & behaving as expected. See", + "https://docs.djangoproject.com/en/dev/releases/1.6/#discovery-of-tests-in-any-test-module", + "for more information.", + ] + return ' '.join(message) + + +def run_checks(): + """ + Required by the ``check`` management command, this returns a list of + messages from all the relevant check functions for this version of Django. + """ + checks = [ + check_test_runner() + ] + # Filter out the ``None`` or empty strings. + return [output for output in checks if output] diff --git a/django/core/compat_checks/__init__.py b/django/core/compat_checks/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/django/core/compat_checks/base.py b/django/core/compat_checks/base.py deleted file mode 100644 index e54b50f287..0000000000 --- a/django/core/compat_checks/base.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import unicode_literals -import warnings - -from django.core.compat_checks import django_1_6_0 - - -COMPAT_CHECKS = [ - # Add new modules at the top, so we keep things in descending order. - # After two-three minor releases, old versions should get dropped. - django_1_6_0, -] - - -def check_compatibility(): - """ - Runs through compatibility checks to warn the user with an existing install - about changes in an up-to-date Django. - - Modules should be located in ``django.core.compat_checks`` (typically one - per release of Django) & must have a ``run_checks`` function that runs - all the checks. - - Returns a list of informational messages about incompatibilities. - """ - messages = [] - - for check_module in COMPAT_CHECKS: - check = getattr(check_module, 'run_checks', None) - - if check is None: - warnings.warn( - "The '%s' module lacks a " % check_module.__name__ + - "'run_checks' method, which is needed to verify compatibility." - ) - continue - - messages.extend(check()) - - return messages diff --git a/django/core/compat_checks/django_1_6_0.py b/django/core/compat_checks/django_1_6_0.py deleted file mode 100644 index bb0dabedac..0000000000 --- a/django/core/compat_checks/django_1_6_0.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import unicode_literals - - -def check_test_runner(): - """ - Checks if the user has *not* overridden the ``TEST_RUNNER`` setting & - warns them about the default behavior changes. - - If the user has overridden that setting, we presume they know what they're - doing & avoid generating a message. - """ - from django.conf import settings - new_default = 'django.test.runner.DiscoverRunner' - test_runner_setting = getattr(settings, 'TEST_RUNNER', new_default) - - if test_runner_setting == new_default: - message = [ - "You have not explicitly set 'TEST_RUNNER'. In Django 1.6,", - "there is a new test runner ('%s')" % new_default, - "by default. You should ensure your tests are still all", - "running & behaving as expected. See", - "https://docs.djangoproject.com/en/dev/releases/1.6/#discovery-of-tests-in-any-test-module", - "for more information.", - ] - return ' '.join(message) - - -def run_checks(): - """ - Required by the ``checksetup`` management command, this returns a list of - messages from all the relevant check functions for this version of Django. - """ - checks = [ - check_test_runner() - ] - # Filter out the ``None`` or empty strings. - return [output for output in checks if output] diff --git a/django/core/management/commands/check.py b/django/core/management/commands/check.py new file mode 100644 index 0000000000..05f48c82bc --- /dev/null +++ b/django/core/management/commands/check.py @@ -0,0 +1,14 @@ +from __future__ import unicode_literals +import warnings + +from django.core.checks.compatibility.base import check_compatibility +from django.core.management.base import NoArgsCommand + + +class Command(NoArgsCommand): + help = "Checks your configuration's compatibility with this version " + \ + "of Django." + + def handle_noargs(self, **options): + for message in check_compatibility(): + warnings.warn(message) diff --git a/django/core/management/commands/checksetup.py b/django/core/management/commands/checksetup.py deleted file mode 100644 index d37e826757..0000000000 --- a/django/core/management/commands/checksetup.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import unicode_literals -import warnings - -from django.core.compat_checks.base import check_compatibility -from django.core.management.base import NoArgsCommand - - -class Command(NoArgsCommand): - help = "Checks your configuration's compatibility with this version " + \ - "of Django." - - def handle_noargs(self, **options): - for message in check_compatibility(): - warnings.warn(message) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 95bfedc74e..086c10c389 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -121,10 +121,10 @@ GeoDjango now provides :ref:`form fields and widgets ` for its geo-specialized fields. They are OpenLayers-based by default, but they can be customized to use any other JS framework. -``checksetup`` management command added for verifying compatibility -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``check`` management command added for verifying compatibility +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A ``checksetup`` management command was added, enabling you to verify if your +A ``check`` management command was added, enabling you to verify if your current configuration (currently oriented at settings) is compatible with the current version of Django. diff --git a/tests/check/__init__.py b/tests/check/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/check/models.py b/tests/check/models.py new file mode 100644 index 0000000000..78a10abba6 --- /dev/null +++ b/tests/check/models.py @@ -0,0 +1 @@ +# Stubby. diff --git a/tests/check/tests.py b/tests/check/tests.py new file mode 100644 index 0000000000..98495e38ae --- /dev/null +++ b/tests/check/tests.py @@ -0,0 +1,107 @@ +from django.core.checks.compatibility import base +from django.core.checks.compatibility import django_1_6_0 +from django.core.management.commands import check +from django.core.management import call_command +from django.test import TestCase + + +class StubCheckModule(object): + # Has no ``run_checks`` attribute & will trigger a warning. + __name__ = 'StubCheckModule' + + +class FakeWarnings(object): + def __init__(self): + self._warnings = [] + + def warn(self, message): + self._warnings.append(message) + + +class CompatChecksTestCase(TestCase): + def setUp(self): + super(CompatChecksTestCase, self).setUp() + + # We're going to override the list of checks to perform for test + # consistency in the future. + self.old_compat_checks = base.COMPAT_CHECKS + base.COMPAT_CHECKS = [ + django_1_6_0, + ] + + def tearDown(self): + # Restore what's supposed to be in ``COMPAT_CHECKS``. + base.COMPAT_CHECKS = self.old_compat_checks + super(CompatChecksTestCase, self).tearDown() + + def test_check_test_runner_new_default(self): + with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): + result = django_1_6_0.check_test_runner() + self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in result) + + def test_check_test_runner_overridden(self): + with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): + self.assertEqual(django_1_6_0.check_test_runner(), None) + + def test_run_checks_new_default(self): + with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): + result = django_1_6_0.run_checks() + self.assertEqual(len(result), 1) + self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in result[0]) + + def test_run_checks_overridden(self): + with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): + self.assertEqual(len(django_1_6_0.run_checks()), 0) + + def test_check_compatibility(self): + with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): + result = base.check_compatibility() + self.assertEqual(len(result), 1) + self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in result[0]) + + with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): + self.assertEqual(len(base.check_compatibility()), 0) + + def test_check_compatibility_warning(self): + # First, we're patching over the ``COMPAT_CHECKS`` with a stub which + # will trigger the warning. + base.COMPAT_CHECKS = [ + StubCheckModule(), + ] + + # Next, we unfortunately have to patch out ``warnings``. + old_warnings = base.warnings + base.warnings = FakeWarnings() + + self.assertEqual(len(base.warnings._warnings), 0) + + with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): + self.assertEqual(len(base.check_compatibility()), 0) + + self.assertEqual(len(base.warnings._warnings), 1) + self.assertTrue("The 'StubCheckModule' module lacks a 'run_checks'" in base.warnings._warnings[0]) + + # Restore the ``warnings``. + base.warnings = old_warnings + + def test_management_command(self): + # Again, we unfortunately have to patch out ``warnings``. Different + old_warnings = check.warnings + check.warnings = FakeWarnings() + + self.assertEqual(len(check.warnings._warnings), 0) + + # Should not produce any warnings. + with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): + call_command('check') + + self.assertEqual(len(check.warnings._warnings), 0) + + with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): + call_command('check') + + self.assertEqual(len(check.warnings._warnings), 1) + self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in check.warnings._warnings[0]) + + # Restore the ``warnings``. + base.warnings = old_warnings diff --git a/tests/compat_checks/__init__.py b/tests/compat_checks/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/compat_checks/models.py b/tests/compat_checks/models.py deleted file mode 100644 index 78a10abba6..0000000000 --- a/tests/compat_checks/models.py +++ /dev/null @@ -1 +0,0 @@ -# Stubby. diff --git a/tests/compat_checks/tests.py b/tests/compat_checks/tests.py deleted file mode 100644 index 879988c905..0000000000 --- a/tests/compat_checks/tests.py +++ /dev/null @@ -1,107 +0,0 @@ -from django.core.compat_checks import base -from django.core.compat_checks import django_1_6_0 -from django.core.management.commands import checksetup -from django.core.management import call_command -from django.test import TestCase - - -class StubCheckModule(object): - # Has no ``run_checks`` attribute & will trigger a warning. - __name__ = 'StubCheckModule' - - -class FakeWarnings(object): - def __init__(self): - self._warnings = [] - - def warn(self, message): - self._warnings.append(message) - - -class CompatChecksTestCase(TestCase): - def setUp(self): - super(CompatChecksTestCase, self).setUp() - - # We're going to override the list of checks to perform for test - # consistency in the future. - self.old_compat_checks = base.COMPAT_CHECKS - base.COMPAT_CHECKS = [ - django_1_6_0, - ] - - def tearDown(self): - # Restore what's supposed to be in ``COMPAT_CHECKS``. - base.COMPAT_CHECKS = self.old_compat_checks - super(CompatChecksTestCase, self).tearDown() - - def test_check_test_runner_new_default(self): - with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): - result = django_1_6_0.check_test_runner() - self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in result) - - def test_check_test_runner_overridden(self): - with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - self.assertEqual(django_1_6_0.check_test_runner(), None) - - def test_run_checks_new_default(self): - with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): - result = django_1_6_0.run_checks() - self.assertEqual(len(result), 1) - self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in result[0]) - - def test_run_checks_overridden(self): - with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - self.assertEqual(len(django_1_6_0.run_checks()), 0) - - def test_check_compatibility(self): - with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): - result = base.check_compatibility() - self.assertEqual(len(result), 1) - self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in result[0]) - - with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - self.assertEqual(len(base.check_compatibility()), 0) - - def test_check_compatibility_warning(self): - # First, we're patching over the ``COMPAT_CHECKS`` with a stub which - # will trigger the warning. - base.COMPAT_CHECKS = [ - StubCheckModule(), - ] - - # Next, we unfortunately have to patch out ``warnings``. - old_warnings = base.warnings - base.warnings = FakeWarnings() - - self.assertEqual(len(base.warnings._warnings), 0) - - with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - self.assertEqual(len(base.check_compatibility()), 0) - - self.assertEqual(len(base.warnings._warnings), 1) - self.assertTrue("The 'StubCheckModule' module lacks a 'run_checks'" in base.warnings._warnings[0]) - - # Restore the ``warnings``. - base.warnings = old_warnings - - def test_management_command(self): - # Again, we unfortunately have to patch out ``warnings``. Different - old_warnings = checksetup.warnings - checksetup.warnings = FakeWarnings() - - self.assertEqual(len(checksetup.warnings._warnings), 0) - - # Should not produce any warnings. - with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - call_command('checksetup') - - self.assertEqual(len(checksetup.warnings._warnings), 0) - - with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): - call_command('checksetup') - - self.assertEqual(len(checksetup.warnings._warnings), 1) - self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in checksetup.warnings._warnings[0]) - - # Restore the ``warnings``. - base.warnings = old_warnings -- cgit v1.3 From f819bef3dc935851f71e4ad84592eee8664a48f3 Mon Sep 17 00:00:00 2001 From: Javier Mansilla Date: Tue, 26 Feb 2013 00:27:52 -0300 Subject: Fixed #19773 - Added admin/popup_response.html template. Thanks jimmylam@ for the suggestion. --- django/contrib/admin/options.py | 11 +++++------ django/contrib/admin/templates/admin/popup_response.html | 9 +++++++++ tests/admin_views/models.py | 2 +- tests/admin_views/tests.py | 13 ++++++++++++- 4 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 django/contrib/admin/templates/admin/popup_response.html (limited to 'tests') diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index e9f4a43185..ce10cf72ba 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -24,7 +24,7 @@ from django.db.models.constants import LOOKUP_SEP from django.db.models.related import RelatedObject from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist from django.db.models.sql.constants import QUERY_TERMS -from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.http import Http404, HttpResponseRedirect from django.http.response import HttpResponseBase from django.shortcuts import get_object_or_404 from django.template.response import SimpleTemplateResponse, TemplateResponse @@ -911,11 +911,10 @@ class ModelAdmin(BaseModelAdmin): # Here, we distinguish between different save types by checking for # the presence of keys in request.POST. if IS_POPUP_VAR in request.POST: - return HttpResponse( - '' - '' % \ - # escape() calls force_text. - (escape(pk_value), escapejs(obj))) + return SimpleTemplateResponse('admin/popup_response.html', { + 'pk_value': escape(pk_value), + 'obj': escapejs(obj) + }) elif "_continue" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict diff --git a/django/contrib/admin/templates/admin/popup_response.html b/django/contrib/admin/templates/admin/popup_response.html new file mode 100644 index 0000000000..44833b2f93 --- /dev/null +++ b/django/contrib/admin/templates/admin/popup_response.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py index 5cc6f6251a..5ec4fbb544 100644 --- a/tests/admin_views/models.py +++ b/tests/admin_views/models.py @@ -137,7 +137,7 @@ class Thing(models.Model): class Actor(models.Model): name = models.CharField(max_length=50) age = models.IntegerField() - title = models.CharField(max_length=50, null=True) + title = models.CharField(max_length=50, null=True, blank=True) def __str__(self): return self.name diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 528c728069..064979801e 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -2554,6 +2554,17 @@ action) '/test_admin/admin/admin_views/subscriber/?%s' % IS_POPUP_VAR) self.assertEqual(response.context["action_form"], None) + def test_popup_template_response(self): + """ + Success on popups shall be rendered from template in order to allow + easy customization. + """ + response = self.client.post( + '/test_admin/admin/admin_views/actor/add/?%s=1' % IS_POPUP_VAR, + {'name': 'Troy McClure', 'age': '55', IS_POPUP_VAR: '1'}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.template_name, 'admin/popup_response.html') + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class TestCustomChangeList(TestCase): @@ -4275,7 +4286,7 @@ class AdminKeepChangeListFiltersTests(TestCase): # Get the `add_view`. response = self.client.get(self.get_add_url()) self.assertEqual(response.status_code, 200) - + # Check the form action. form_action = """
""" % self.get_preserved_filters_querystring() self.assertContains(response, form_action, count=1) -- cgit v1.3 From ec371ace004203100d24a74edafc16534dd5d5a9 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Tue, 25 Jun 2013 20:28:35 +0200 Subject: Fixed #20650 -- Fixed {% filter %} incorrectly accepting 'escape' as argument Thanks to grzesiof for the report and to loic84 and Alex Gaynor for the review. --- django/template/base.py | 1 + django/template/defaulttags.py | 5 +++-- tests/template_tests/tests.py | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/django/template/base.py b/django/template/base.py index dc6b0c366c..364b428070 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -1101,6 +1101,7 @@ class Library(object): # for decorators that need it e.g. stringfilter if hasattr(filter_func, "_decorated_function"): setattr(filter_func._decorated_function, attr, value) + filter_func._filter_name = name return filter_func else: raise InvalidTemplateLibrary("Unsupported arguments to " diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 04e7a37d8e..959de3dea1 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -665,8 +665,9 @@ def do_filter(parser, token): _, rest = token.contents.split(None, 1) filter_expr = parser.compile_filter("var|%s" % (rest)) for func, unused in filter_expr.filters: - if getattr(func, '_decorated_function', func).__name__ in ('escape', 'safe'): - raise TemplateSyntaxError('"filter %s" is not permitted. Use the "autoescape" tag instead.' % func.__name__) + filter_name = getattr(func, '_filter_name', None) + if filter_name in ('escape', 'safe'): + raise TemplateSyntaxError('"filter %s" is not permitted. Use the "autoescape" tag instead.' % filter_name) nodelist = parser.parse(('endfilter',)) parser.delete_first_token() return FilterNode(filter_expr, nodelist) diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 206c648398..76712a09a6 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -854,6 +854,10 @@ class TemplateTests(TransRealMixin, TestCase): 'filter02': ('{% filter upper %}django{% endfilter %}', {}, 'DJANGO'), 'filter03': ('{% filter upper|lower %}django{% endfilter %}', {}, 'django'), 'filter04': ('{% filter cut:remove %}djangospam{% endfilter %}', {'remove': 'spam'}, 'django'), + 'filter05': ('{% filter safe %}fail{% endfilter %}', {}, template.TemplateSyntaxError), + 'filter05bis': ('{% filter upper|safe %}fail{% endfilter %}', {}, template.TemplateSyntaxError), + 'filter06': ('{% filter escape %}fail{% endfilter %}', {}, template.TemplateSyntaxError), + 'filter06bis': ('{% filter upper|escape %}fail{% endfilter %}', {}, template.TemplateSyntaxError), ### FIRSTOF TAG ########################################################### 'firstof01': ('{% firstof a b c %}', {'a':0,'b':0,'c':0}, ''), -- cgit v1.3 From 273dc550a4eccdad958541f456332312b3369504 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 26 Jun 2013 01:07:18 -0700 Subject: Fixed #20462 -- null/non-string regex lookups are now consistent Thanks to noirbizarre for the report and initial patch. --- AUTHORS | 2 ++ django/db/backends/postgresql_psycopg2/operations.py | 2 +- django/db/backends/sqlite3/base.py | 2 +- tests/lookup/tests.py | 15 +++++++++++++++ 4 files changed, 19 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/AUTHORS b/AUTHORS index 5fcdf833f0..ba1f4036e9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -152,6 +152,7 @@ answer newbie questions, and generally made Django that much better: Antonis Christofides Michal Chruszcz Can Burak Çilingir + Andrew Clark Ian Clelland Travis Cline Russell Cloran @@ -273,6 +274,7 @@ answer newbie questions, and generally made Django that much better: Brian Harring Brant Harris Ronny Haryanto + Axel Haustant Hawkeye Kent Hauser Joe Heck diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py index f96757da8b..c5aab84693 100644 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ b/django/db/backends/postgresql_psycopg2/operations.py @@ -69,7 +69,7 @@ class DatabaseOperations(BaseDatabaseOperations): # Cast text lookups to text to allow things like filter(x__contains=4) if lookup_type in ('iexact', 'contains', 'icontains', 'startswith', - 'istartswith', 'endswith', 'iendswith'): + 'istartswith', 'endswith', 'iendswith', 'regex', 'iregex'): lookup = "%s::text" # Use UPPER(x) for case-insensitive lookups; it's faster. diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 18c4029649..b9caf8b99f 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -522,4 +522,4 @@ def _sqlite_format_dtdelta(dt, conn, days, secs, usecs): return str(dt) def _sqlite_regexp(re_pattern, re_string): - return bool(re.search(re_pattern, re_string)) + return bool(re.search(re_pattern, str(re_string))) if re_string is not None else False diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py index de7105f92d..fe8a5fac64 100644 --- a/tests/lookup/tests.py +++ b/tests/lookup/tests.py @@ -610,6 +610,21 @@ class LookupTests(TestCase): self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'b(.).*b\1'), ['', '', '']) + def test_regex_null(self): + """ + Ensure that a regex lookup does not fail on null/None values + """ + Season.objects.create(year=2012, gt=None) + self.assertQuerysetEqual(Season.objects.filter(gt__regex=r'^$'), []) + + def test_regex_non_string(self): + """ + Ensure that a regex lookup does not fail on non-string fields + """ + Season.objects.create(year=2013, gt=444) + self.assertQuerysetEqual(Season.objects.filter(gt__regex=r'^444$'), + ['']) + def test_nonfield_lookups(self): """ Ensure that a lookup query containing non-fields raises the proper -- cgit v1.3 From a9ea7d8c708c8265cccc17f23c62b2268d9e94f8 Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Wed, 26 Jun 2013 21:30:58 +0700 Subject: Fixed #20462 - Replaced the str() cast introduced in 273dc55 by force_text() --- django/db/backends/sqlite3/base.py | 3 ++- tests/lookup/tests.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index b9caf8b99f..324adfd97b 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -19,6 +19,7 @@ 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.encoding import force_text from django.utils.functional import cached_property from django.utils.safestring import SafeBytes from django.utils import six @@ -522,4 +523,4 @@ def _sqlite_format_dtdelta(dt, conn, days, secs, usecs): return str(dt) def _sqlite_regexp(re_pattern, re_string): - return bool(re.search(re_pattern, str(re_string))) if re_string is not None else False + return bool(re.search(re_pattern, force_text(re_string))) if re_string is not None else False diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py index fe8a5fac64..ee9c5afe1d 100644 --- a/tests/lookup/tests.py +++ b/tests/lookup/tests.py @@ -625,6 +625,13 @@ class LookupTests(TestCase): self.assertQuerysetEqual(Season.objects.filter(gt__regex=r'^444$'), ['']) + def test_regex_non_ascii(self): + """ + Ensure that a regex lookup does not trip on non-ascii characters. + """ + Player.objects.create(name='\u2660') + Player.objects.get(name__regex='\u2660') + def test_nonfield_lookups(self): """ Ensure that a lookup query containing non-fields raises the proper -- cgit v1.3 From 99b467f272da91b8894dc90d793d8d2c40b78d8c Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Thu, 27 Jun 2013 14:44:21 +0100 Subject: Add related_query_name to ForeignKey/M2M. Refs #20244 --- django/db/models/fields/related.py | 19 ++++++++++++------- tests/foreign_object/models.py | 8 ++++++++ tests/foreign_object/tests.py | 21 ++++++++++++++++++++- 3 files changed, 40 insertions(+), 8 deletions(-) (limited to 'tests') diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 80249817e2..0367d24fe8 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -139,7 +139,7 @@ class RelatedField(Field): # 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.model_name + return self.rel.related_query_name or self.rel.related_name or self.opts.model_name class RenameRelatedObjectDescriptorMethods(RenameMethodsBase): @@ -826,7 +826,7 @@ class ReverseManyRelatedObjectsDescriptor(object): class ForeignObjectRel(object): def __init__(self, field, to, related_name=None, limit_choices_to=None, - parent_link=False, on_delete=None): + parent_link=False, on_delete=None, related_query_name=None): try: to._meta except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT @@ -835,6 +835,7 @@ class ForeignObjectRel(object): self.field = field self.to = to self.related_name = related_name + self.related_query_name = related_query_name self.limit_choices_to = {} if limit_choices_to is None else limit_choices_to self.multiple = True self.parent_link = parent_link @@ -862,10 +863,10 @@ class ForeignObjectRel(object): class ManyToOneRel(ForeignObjectRel): def __init__(self, field, to, field_name, related_name=None, limit_choices_to=None, - parent_link=False, on_delete=None): + parent_link=False, on_delete=None, related_query_name=None): super(ManyToOneRel, self).__init__( field, to, related_name=related_name, limit_choices_to=limit_choices_to, - parent_link=parent_link, on_delete=on_delete) + parent_link=parent_link, on_delete=on_delete, related_query_name=related_query_name) self.field_name = field_name def get_related_field(self): @@ -885,21 +886,22 @@ class ManyToOneRel(ForeignObjectRel): class OneToOneRel(ManyToOneRel): def __init__(self, field, to, field_name, related_name=None, limit_choices_to=None, - parent_link=False, on_delete=None): + parent_link=False, on_delete=None, related_query_name=None): super(OneToOneRel, self).__init__(field, to, field_name, related_name=related_name, limit_choices_to=limit_choices_to, - parent_link=parent_link, on_delete=on_delete + parent_link=parent_link, on_delete=on_delete, related_query_name=related_query_name, ) self.multiple = False class ManyToManyRel(object): def __init__(self, to, related_name=None, limit_choices_to=None, - symmetrical=True, through=None, db_constraint=True): + symmetrical=True, through=None, db_constraint=True, related_query_name=None): if through and not db_constraint: raise ValueError("Can't supply a through model and db_constraint=False") self.to = to self.related_name = related_name + self.related_query_name = related_query_name if limit_choices_to is None: limit_choices_to = {} self.limit_choices_to = limit_choices_to @@ -933,6 +935,7 @@ class ForeignObject(RelatedField): kwargs['rel'] = ForeignObjectRel( self, to, related_name=kwargs.pop('related_name', None), + related_query_name=kwargs.pop('related_query_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), parent_link=kwargs.pop('parent_link', False), on_delete=kwargs.pop('on_delete', CASCADE), @@ -1141,6 +1144,7 @@ class ForeignKey(ForeignObject): kwargs['rel'] = rel_class( self, to, to_field, related_name=kwargs.pop('related_name', None), + related_query_name=kwargs.pop('related_query_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), parent_link=kwargs.pop('parent_link', False), on_delete=kwargs.pop('on_delete', CASCADE), @@ -1340,6 +1344,7 @@ class ManyToManyField(RelatedField): kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['rel'] = ManyToManyRel(to, related_name=kwargs.pop('related_name', None), + related_query_name=kwargs.pop('related_query_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT), through=kwargs.pop('through', None), diff --git a/tests/foreign_object/models.py b/tests/foreign_object/models.py index 2d02b5624c..eee8091a15 100644 --- a/tests/foreign_object/models.py +++ b/tests/foreign_object/models.py @@ -150,3 +150,11 @@ class ArticleTranslation(models.Model): class Meta: unique_together = ('article', 'lang') ordering = ('active_translation__title',) + +class ArticleTag(models.Model): + article = models.ForeignKey(Article, related_name="tags", related_query_name="tag") + name = models.CharField(max_length=255) + +class ArticleIdea(models.Model): + articles = models.ManyToManyField(Article, related_name="ideas", related_query_name="idea_things") + name = models.CharField(max_length=255) diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index 69636ee49b..670fc94dc5 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -1,9 +1,10 @@ import datetime from operator import attrgetter -from .models import Country, Person, Group, Membership, Friendship, Article, ArticleTranslation +from .models import Country, Person, Group, Membership, Friendship, Article, ArticleTranslation, ArticleTag, ArticleIdea from django.test import TestCase from django.utils.translation import activate +from django.core.exceptions import FieldError from django import forms class MultiColumnFKTests(TestCase): @@ -321,6 +322,24 @@ class MultiColumnFKTests(TestCase): with self.assertRaisesMessage(Article.DoesNotExist, 'ArticleTranslation has no article'): referrer.article + def test_foreign_key_related_query_name(self): + a1 = Article.objects.create(pub_date=datetime.date.today()) + ArticleTag.objects.create(article=a1, name="foo") + self.assertEqual(Article.objects.filter(tag__name="foo").count(), 1) + self.assertEqual(Article.objects.filter(tag__name="bar").count(), 0) + with self.assertRaises(FieldError): + Article.objects.filter(tags__name="foo") + + def test_many_to_many_related_query_name(self): + a1 = Article.objects.create(pub_date=datetime.date.today()) + i1 = ArticleIdea.objects.create(name="idea1") + a1.ideas.add(i1) + self.assertEqual(Article.objects.filter(idea_things__name="idea1").count(), 1) + self.assertEqual(Article.objects.filter(idea_things__name="idea2").count(), 0) + with self.assertRaises(FieldError): + Article.objects.filter(ideas__name="idea1") + + class FormsTests(TestCase): # ForeignObjects should not have any form fields, currently the user needs # to manually deal with the foreignobject relation. -- cgit v1.3 From c1284c3d3c6131a9d0ded9601ae0feb9a2e81a65 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 27 Jun 2013 22:19:54 +0200 Subject: Fixed #20571 -- Added an API to control connection.needs_rollback. This is useful: - to force a rollback on the exit of an atomic block without having to raise and catch an exception; - to prevent a rollback after handling an exception manually. --- django/db/backends/__init__.py | 9 +++++++++ django/db/transaction.py | 20 ++++++++++++++++++++ docs/topics/db/transactions.txt | 21 +++++++++++++++++++++ tests/transactions/tests.py | 26 ++++++++++++++++++++++++-- 4 files changed, 74 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index fa3cc5ac02..1a74232704 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -330,6 +330,15 @@ class BaseDatabaseWrapper(object): self._set_autocommit(autocommit) self.autocommit = autocommit + def set_rollback(self, rollback): + """ + Set or unset the "needs rollback" flag -- for *advanced use* only. + """ + if not self.in_atomic_block: + raise TransactionManagementError( + "needs_rollback doesn't work outside of an 'atomic' block.") + self.needs_rollback = rollback + def validate_no_atomic_block(self): """ Raise an error if an atomic block is active. diff --git a/django/db/transaction.py b/django/db/transaction.py index f770f2efa7..95b9ae165e 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -171,6 +171,26 @@ def clean_savepoints(using=None): """ get_connection(using).clean_savepoints() +def get_rollback(using=None): + """ + Gets the "needs rollback" flag -- for *advanced use* only. + """ + return get_connection(using).needs_rollback + +def set_rollback(rollback, using=None): + """ + Sets or unsets the "needs rollback" flag -- for *advanced use* only. + + When `rollback` is `True`, it triggers a rollback when exiting the + innermost enclosing atomic block that has `savepoint=True` (that's the + default). Use this to force a rollback without raising an exception. + + When `rollback` is `False`, it prevents such a rollback. Use this only + after rolling back to a known-good state! Otherwise, you break the atomic + block and data corruption may occur. + """ + return get_connection(using).set_rollback(rollback) + ################################# # Decorators / context managers # ################################# diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index e9a626f56b..903579cc38 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -389,6 +389,27 @@ The following example demonstrates the use of savepoints:: transaction.savepoint_rollback(sid) # open transaction now contains only a.save() +.. versionadded:: 1.6 + +Savepoints may be used to recover from a database error by performing a partial +rollback. If you're doing this inside an :func:`atomic` block, the entire block +will still be rolled back, because it doesn't know you've handled the situation +at a lower level! To prevent this, you can control the rollback behavior with +the following functions. + +.. function:: get_rollback(using=None) + +.. function:: set_rollback(rollback, using=None) + +Setting the rollback flag to ``True`` forces a rollback when exiting the +innermost atomic block. This may be useful to trigger a rollback without +raising an exception. + +Setting it to ``False`` prevents such a rollback. Before doing that, make sure +you've rolled back the transaction to a known-good savepoint within the current +atomic block! Otherwise you're breaking atomicity and data corruption may +occur. + Database-specific notes ======================= diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index 24b7615d6f..756fa40abd 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -1,9 +1,8 @@ from __future__ import absolute_import import sys -import warnings -from django.db import connection, transaction, IntegrityError +from django.db import connection, transaction, DatabaseError, IntegrityError from django.test import TransactionTestCase, skipUnlessDBFeature from django.test.utils import IgnorePendingDeprecationWarningsMixin from django.utils import six @@ -188,6 +187,29 @@ class AtomicTests(TransactionTestCase): raise Exception("Oops, that's his first name") self.assertQuerysetEqual(Reporter.objects.all(), []) + def test_force_rollback(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + # atomic block shouldn't rollback, but force it. + self.assertFalse(transaction.get_rollback()) + transaction.set_rollback(True) + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_prevent_rollback(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + sid = transaction.savepoint() + # trigger a database error inside an inner atomic without savepoint + with self.assertRaises(DatabaseError): + with transaction.atomic(savepoint=False): + connection.cursor().execute( + "SELECT no_such_col FROM transactions_reporter") + transaction.savepoint_rollback(sid) + # atomic block should rollback, but prevent it, as we just did it. + self.assertTrue(transaction.get_rollback()) + transaction.set_rollback(False) + self.assertQuerysetEqual(Reporter.objects.all(), ['']) + class AtomicInsideTransactionTests(AtomicTests): """All basic tests for atomic should also pass within an existing transaction.""" -- cgit v1.3 From 534ced5aadf964eca4cf29a689dc70185f582772 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 27 Jun 2013 19:39:20 -0400 Subject: Fixed #20664 -- Fixed a bug with raw_id_fields on Python 3. Thanks jefftriplett for the report. --- django/contrib/admin/templatetags/admin_list.py | 6 +++--- tests/admin_views/tests.py | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index e81b13cda4..8596dfb825 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -11,7 +11,7 @@ from django.contrib.admin.templatetags.admin_static import static from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils import formats -from django.utils.html import format_html +from django.utils.html import escapejs, format_html from django.utils.safestring import mark_safe from django.utils.text import capfirst from django.utils.translation import ugettext as _ @@ -226,12 +226,12 @@ def items_for_result(cl, result, form): else: attr = pk value = result.serializable_value(attr) - result_id = repr(force_text(value))[1:] + result_id = escapejs(value) yield format_html('<{0}{1}>{4}', table_tag, row_class, url, - format_html(' onclick="opener.dismissRelatedLookupPopup(window, {0}); return false;"', result_id) + format_html(' onclick="opener.dismissRelatedLookupPopup(window, '{0}'); return false;"', result_id) if cl.is_popup else '', result_repr, table_tag) diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 064979801e..a695d50ea2 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -583,6 +583,14 @@ class AdminViewBasicTest(TestCase): response = self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27") self.assertEqual(response.status_code, 200) + def test_popup_dismiss_related(self): + """ + Regression test for ticket 20664 - ensure the pk is properly quoted. + """ + actor = Actor.objects.create(name="Palin", age=27) + response = self.client.get("/test_admin/admin/admin_views/actor/?%s" % IS_POPUP_VAR) + self.assertContains(response, "opener.dismissRelatedLookupPopup(window, '%s')" % actor.pk) + def test_hide_change_password(self): """ Tests if the "change password" link in the admin is hidden if the User -- cgit v1.3 From 7c0b72a826a3637b30a57553ac83f6b913c33134 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 27 Jun 2013 20:13:42 -0400 Subject: Prevented running some admin_view tests twice. --- tests/admin_views/tests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index a695d50ea2..235fe0cb54 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -57,7 +57,7 @@ for a staff account. Note that both fields may be case-sensitive." @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class AdminViewBasicTest(TestCase): +class AdminViewBasicTestCase(TestCase): fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml', 'admin-views-books.xml'] @@ -92,6 +92,7 @@ class AdminViewBasicTest(TestCase): failing_msg ) +class AdminViewBasicTest(AdminViewBasicTestCase): def testTrailingSlashRequired(self): """ If you leave off the trailing slash, app should redirect and add it. @@ -761,7 +762,7 @@ class SaveAsTests(TestCase): self.assertEqual(response.context['form_url'], '/test_admin/admin/admin_views/person/add/') -class CustomModelAdminTest(AdminViewBasicTest): +class CustomModelAdminTest(AdminViewBasicTestCase): urls = "admin_views.urls" urlbit = "admin2" -- cgit v1.3 From d097417025e71286ad5bbde6e0a79caacabbbd64 Mon Sep 17 00:00:00 2001 From: Shai Berger Date: Fri, 28 Jun 2013 06:15:03 +0300 Subject: Support 'pyformat' style parameters in raw queries, Refs #10070 Add support for Oracle, fix an issue with the repr of RawQuerySet, add tests and documentations. Also added a 'supports_paramstyle_pyformat' database feature, True by default, False for SQLite. Thanks Donald Stufft for review of documentation. --- django/db/backends/__init__.py | 5 +++ django/db/backends/oracle/base.py | 66 ++++++++++++++++++++++++-------------- django/db/backends/sqlite3/base.py | 1 + django/db/models/query.py | 5 ++- docs/ref/databases.txt | 8 +++++ docs/releases/1.6.txt | 6 ++++ docs/topics/db/sql.txt | 25 ++++++++++++--- tests/backends/tests.py | 44 +++++++++++++++++++++++-- tests/raw_query/tests.py | 21 ++++++++++-- 9 files changed, 147 insertions(+), 34 deletions(-) (limited to 'tests') diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 1a74232704..9abb9a9637 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -613,6 +613,11 @@ class BaseDatabaseFeatures(object): # when autocommit is disabled? http://bugs.python.org/issue8145#msg109965 autocommits_when_autocommit_is_off = False + # Does the backend support 'pyformat' style ("... %(name)s ...", {'name': value}) + # parameter passing? Note this can be provided by the backend even if not + # supported by the Python driver + supports_paramstyle_pyformat = True + def __init__(self, connection): self.connection = connection diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 3f39a15aa7..5e2b763f52 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -757,20 +757,37 @@ class FormatStylePlaceholderCursor(object): self.cursor.arraysize = 100 def _format_params(self, params): - return tuple([OracleParam(p, self, True) for p in params]) + try: + return dict((k,OracleParam(v, self, True)) for k,v in params.items()) + except AttributeError: + return tuple([OracleParam(p, self, True) for p in params]) def _guess_input_sizes(self, params_list): - sizes = [None] * len(params_list[0]) - for params in params_list: - for i, value in enumerate(params): - if value.input_size: - sizes[i] = value.input_size - self.setinputsizes(*sizes) + # Try dict handling; if that fails, treat as sequence + if hasattr(params_list[0], 'keys'): + sizes = {} + for params in params_list: + for k, value in params.items(): + if value.input_size: + sizes[k] = value.input_size + self.setinputsizes(**sizes) + else: + # It's not a list of dicts; it's a list of sequences + sizes = [None] * len(params_list[0]) + for params in params_list: + for i, value in enumerate(params): + if value.input_size: + sizes[i] = value.input_size + self.setinputsizes(*sizes) def _param_generator(self, params): - return [p.force_bytes for p in params] + # Try dict handling; if that fails, treat as sequence + if hasattr(params, 'items'): + return dict((k, v.force_bytes) for k,v in params.items()) + else: + return [p.force_bytes for p in params] - def execute(self, query, params=None): + def _fix_for_params(self, query, params): # cx_Oracle wants no trailing ';' for SQL statements. For PL/SQL, it # it does want a trailing ';' but not a trailing '/'. However, these # characters must be included in the original query in case the query @@ -780,10 +797,18 @@ class FormatStylePlaceholderCursor(object): if params is None: params = [] query = convert_unicode(query, self.charset) + elif hasattr(params, 'keys'): + # Handle params as dict + args = dict((k, ":%s"%k) for k in params.keys()) + query = convert_unicode(query % args, self.charset) else: - params = self._format_params(params) + # Handle params as sequence args = [(':arg%d' % i) for i in range(len(params))] query = convert_unicode(query % tuple(args), self.charset) + return query, self._format_params(params) + + def execute(self, query, params=None): + query, params = self._fix_for_params(query, params) self._guess_input_sizes([params]) try: return self.cursor.execute(query, self._param_generator(params)) @@ -794,22 +819,15 @@ class FormatStylePlaceholderCursor(object): raise def executemany(self, query, params=None): - # cx_Oracle doesn't support iterators, convert them to lists - if params is not None and not isinstance(params, (list, tuple)): - params = list(params) - try: - args = [(':arg%d' % i) for i in range(len(params[0]))] - except (IndexError, TypeError): + if not params: # No params given, nothing to do return None - # cx_Oracle wants no trailing ';' for SQL statements. For PL/SQL, it - # it does want a trailing ';' but not a trailing '/'. However, these - # characters must be included in the original query in case the query - # is being passed to SQL*Plus. - if query.endswith(';') or query.endswith('/'): - query = query[:-1] - query = convert_unicode(query % tuple(args), self.charset) - formatted = [self._format_params(i) for i in params] + # uniform treatment for sequences and iterables + params_iter = iter(params) + query, firstparams = self._fix_for_params(query, next(params_iter)) + # we build a list of formatted params; as we're going to traverse it + # more than once, we can't make it lazy by using a generator + formatted = [firstparams]+[self._format_params(p) for p in params_iter] self._guess_input_sizes(formatted) try: return self.cursor.executemany(query, diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 324adfd97b..92dbf354ae 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -101,6 +101,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): has_bulk_insert = True can_combine_inserts_with_and_without_auto_increment_pk = False autocommits_when_autocommit_is_off = True + supports_paramstyle_pyformat = False @cached_property def uses_savepoints(self): diff --git a/django/db/models/query.py b/django/db/models/query.py index b0ce25f5b5..27a87a3f65 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1445,7 +1445,10 @@ class RawQuerySet(object): yield instance def __repr__(self): - return "" % (self.raw_query % tuple(self.params)) + text = self.raw_query + if self.params: + text = text % (self.params if hasattr(self.params, 'keys') else tuple(self.params)) + return "" % text def __getitem__(self, k): return list(self)[k] diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index a648ac1709..4e5f136e2e 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -623,6 +623,14 @@ If you're getting this error, you can solve it by: SQLite does not support the ``SELECT ... FOR UPDATE`` syntax. Calling it will have no effect. +"pyformat" parameter style in raw queries not supported +------------------------------------------------------- + +For most backends, raw queries (``Manager.raw()`` or ``cursor.execute()``) +can use the "pyformat" parameter style, where placeholders in the query +are given as ``'%(name)s'`` and the parameters are passed as a dictionary +rather than a list. SQLite does not support this. + .. _sqlite-connection-queries: Parameters not quoted in ``connection.queries`` diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 2c1fffd8cd..1fd98e1271 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -337,6 +337,12 @@ Minor features default) to allow customizing the :attr:`~django.forms.Form.prefix` of the form. +* Raw queries (``Manager.raw()`` or ``cursor.execute()``) can now use the + "pyformat" parameter style, where placeholders in the query are given as + ``'%(name)s'`` and the parameters are passed as a dictionary rather than + a list (except on SQLite). This has long been possible (but not officially + supported) on MySQL and PostgreSQL, and is now also available on Oracle. + Backwards incompatible changes in 1.6 ===================================== diff --git a/docs/topics/db/sql.txt b/docs/topics/db/sql.txt index 2ec31a4988..7437d51d28 100644 --- a/docs/topics/db/sql.txt +++ b/docs/topics/db/sql.txt @@ -166,9 +166,17 @@ argument to ``raw()``:: >>> lname = 'Doe' >>> Person.objects.raw('SELECT * FROM myapp_person WHERE last_name = %s', [lname]) -``params`` is a list of parameters. You'll use ``%s`` placeholders in the -query string (regardless of your database engine); they'll be replaced with -parameters from the ``params`` list. +``params`` is a list or dictionary of parameters. You'll use ``%s`` +placeholders in the query string for a list, or ``%(key)s`` +placeholders for a dictionary (where ``key`` is replaced by a +dictionary key, of course), regardless of your database engine. Such +placeholders will be replaced with parameters from the ``params`` +argument. + +.. note:: Dictionary params not supported with SQLite + + Dictionary params are not supported with the SQLite backend; with + this backend, you must pass parameters as a list. .. warning:: @@ -181,14 +189,21 @@ parameters from the ``params`` list. **Don't.** - Using the ``params`` list completely protects you from `SQL injection + Using the ``params`` argument completely protects you from `SQL injection attacks`__, a common exploit where attackers inject arbitrary SQL into your database. If you use string interpolation, sooner or later you'll fall victim to SQL injection. As long as you remember to always use the - ``params`` list you'll be protected. + ``params`` argument you'll be protected. __ http://en.wikipedia.org/wiki/SQL_injection +.. versionchanged:: 1.6 + + In Django 1.5 and earlier, you could pass parameters as dictionaries + when using PostgreSQL or MySQL, although this wasn't documented. Now + you can also do this whem using Oracle, and it is officially supported. + + .. _executing-custom-sql: Executing custom SQL directly diff --git a/tests/backends/tests.py b/tests/backends/tests.py index c6cad56ec1..c1a26df7fc 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -456,13 +456,24 @@ class SqliteChecks(TestCase): class BackendTestCase(TestCase): def create_squares_with_executemany(self, args): + self.create_squares(args, 'format', True) + + def create_squares(self, args, paramstyle, multiple): cursor = connection.cursor() opts = models.Square._meta tbl = connection.introspection.table_name_converter(opts.db_table) f1 = connection.ops.quote_name(opts.get_field('root').column) f2 = connection.ops.quote_name(opts.get_field('square').column) - query = 'INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' % (tbl, f1, f2) - cursor.executemany(query, args) + if paramstyle=='format': + query = 'INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' % (tbl, f1, f2) + elif paramstyle=='pyformat': + query = 'INSERT INTO %s (%s, %s) VALUES (%%(root)s, %%(square)s)' % (tbl, f1, f2) + else: + raise ValueError("unsupported paramstyle in test") + if multiple: + cursor.executemany(query, args) + else: + cursor.execute(query, args) def test_cursor_executemany(self): #4896: Test cursor.executemany @@ -491,6 +502,35 @@ class BackendTestCase(TestCase): self.create_squares_with_executemany(args) self.assertEqual(models.Square.objects.count(), 9) + @skipUnlessDBFeature('supports_paramstyle_pyformat') + def test_cursor_execute_with_pyformat(self): + #10070: Support pyformat style passing of paramters + args = {'root': 3, 'square': 9} + self.create_squares(args, 'pyformat', multiple=False) + self.assertEqual(models.Square.objects.count(), 1) + + @skipUnlessDBFeature('supports_paramstyle_pyformat') + def test_cursor_executemany_with_pyformat(self): + #10070: Support pyformat style passing of paramters + args = [{'root': i, 'square': i**2} for i in range(-5, 6)] + self.create_squares(args, 'pyformat', multiple=True) + self.assertEqual(models.Square.objects.count(), 11) + for i in range(-5, 6): + square = models.Square.objects.get(root=i) + self.assertEqual(square.square, i**2) + + @skipUnlessDBFeature('supports_paramstyle_pyformat') + def test_cursor_executemany_with_pyformat_iterator(self): + args = iter({'root': i, 'square': i**2} for i in range(-3, 2)) + self.create_squares(args, 'pyformat', multiple=True) + self.assertEqual(models.Square.objects.count(), 5) + + args = iter({'root': i, 'square': i**2} for i in range(3, 7)) + with override_settings(DEBUG=True): + # same test for DebugCursorWrapper + self.create_squares(args, 'pyformat', multiple=True) + 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 diff --git a/tests/raw_query/tests.py b/tests/raw_query/tests.py index e404c8b065..7242b8309b 100644 --- a/tests/raw_query/tests.py +++ b/tests/raw_query/tests.py @@ -3,7 +3,7 @@ from __future__ import absolute_import from datetime import date from django.db.models.query_utils import InvalidQuery -from django.test import TestCase +from django.test import TestCase, skipUnlessDBFeature from .models import Author, Book, Coffee, Reviewer, FriendlyAuthor @@ -123,10 +123,27 @@ class RawQueryTests(TestCase): query = "SELECT * FROM raw_query_author WHERE first_name = %s" author = Author.objects.all()[2] params = [author.first_name] - results = list(Author.objects.raw(query, params=params)) + qset = Author.objects.raw(query, params=params) + results = list(qset) self.assertProcessed(Author, results, [author]) self.assertNoAnnotations(results) self.assertEqual(len(results), 1) + self.assertIsInstance(repr(qset), str) + + @skipUnlessDBFeature('supports_paramstyle_pyformat') + def testPyformatParams(self): + """ + Test passing optional query parameters + """ + query = "SELECT * FROM raw_query_author WHERE first_name = %(first)s" + author = Author.objects.all()[2] + params = {'first': author.first_name} + qset = Author.objects.raw(query, params=params) + results = list(qset) + self.assertProcessed(Author, results, [author]) + self.assertNoAnnotations(results) + self.assertEqual(len(results), 1) + self.assertIsInstance(repr(qset), str) def testManyToMany(self): """ -- cgit v1.3 From 48dd1e63bbb93479666208535a56f8c7c4aeab3a Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Fri, 28 Jun 2013 17:26:05 +0100 Subject: Ported over Field.deconstruct() from my schema alteration branch. This is to help other ongoing branches which would benefit from this functionality. --- django/db/models/fields/__init__.py | 188 +++++++++++++++++++++++- django/db/models/fields/files.py | 19 +++ django/db/models/fields/related.py | 42 ++++++ tests/field_deconstruction/__init__.py | 0 tests/field_deconstruction/models.py | 0 tests/field_deconstruction/tests.py | 253 +++++++++++++++++++++++++++++++++ 6 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 tests/field_deconstruction/__init__.py create mode 100644 tests/field_deconstruction/models.py create mode 100644 tests/field_deconstruction/tests.py (limited to 'tests') diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index c0dce2e58e..a12b150cf6 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -99,7 +99,8 @@ class Field(object): db_tablespace=None, auto_created=False, validators=[], error_messages=None): self.name = name - self.verbose_name = verbose_name + self.verbose_name = verbose_name # May be set by set_attributes_from_name + self._verbose_name = verbose_name # Store original for deconstruction self.primary_key = primary_key self.max_length, self._unique = max_length, unique self.blank, self.null = blank, null @@ -128,14 +129,99 @@ class Field(object): self.creation_counter = Field.creation_counter Field.creation_counter += 1 + self._validators = validators # Store for deconstruction later self.validators = self.default_validators + validators messages = {} for c in reversed(self.__class__.__mro__): messages.update(getattr(c, 'default_error_messages', {})) messages.update(error_messages or {}) + self._error_messages = error_messages # Store for deconstruction later self.error_messages = messages + def deconstruct(self): + """ + Returns enough information to recreate the field as a 4-tuple: + + * The name of the field on the model, if contribute_to_class has been run + * The import path of the field, including the class: django.db.models.IntegerField + This should be the most portable version, so less specific may be better. + * A list of positional arguments + * A dict of keyword arguments + + Note that the positional or keyword arguments must contain values of the + following types (including inner values of collection types): + + * None, bool, str, unicode, int, long, float, complex, set, frozenset, list, tuple, dict + * UUID + * datetime.datetime (naive), datetime.date + * top-level classes, top-level functions - will be referenced by their full import path + * Storage instances - these have their own deconstruct() method + + This is because the values here must be serialised into a text format + (possibly new Python code, possibly JSON) and these are the only types + with encoding handlers defined. + + There's no need to return the exact way the field was instantiated this time, + just ensure that the resulting field is the same - prefer keyword arguments + over positional ones, and omit parameters with their default values. + """ + # Short-form way of fetching all the default parameters + keywords = {} + possibles = { + "verbose_name": None, + "primary_key": False, + "max_length": None, + "unique": False, + "blank": False, + "null": False, + "db_index": False, + "default": NOT_PROVIDED, + "editable": True, + "serialize": True, + "unique_for_date": None, + "unique_for_month": None, + "unique_for_year": None, + "choices": [], + "help_text": '', + "db_column": None, + "db_tablespace": settings.DEFAULT_INDEX_TABLESPACE, + "auto_created": False, + "validators": [], + "error_messages": None, + } + attr_overrides = { + "unique": "_unique", + "choices": "_choices", + "error_messages": "_error_messages", + "validators": "_validators", + "verbose_name": "_verbose_name", + } + equals_comparison = set(["choices", "validators", "db_tablespace"]) + for name, default in possibles.items(): + value = getattr(self, attr_overrides.get(name, name)) + if name in equals_comparison: + if value != default: + keywords[name] = value + else: + if value is not default: + keywords[name] = value + # Work out path - we shorten it for known Django core fields + path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__) + if path.startswith("django.db.models.fields.related"): + path = path.replace("django.db.models.fields.related", "django.db.models") + if path.startswith("django.db.models.fields.files"): + path = path.replace("django.db.models.fields.files", "django.db.models") + if path.startswith("django.db.models.fields"): + path = path.replace("django.db.models.fields", "django.db.models") + # Return basic info - other fields should override this. + return ( + self.name, + path, + [], + keywords, + ) + def __eq__(self, other): # Needed for @total_ordering if isinstance(other, Field): @@ -566,6 +652,7 @@ class Field(object): return '<%s: %s>' % (path, name) return '<%s>' % path + class AutoField(Field): description = _("Integer") @@ -580,6 +667,12 @@ class AutoField(Field): kwargs['blank'] = True Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(AutoField, self).deconstruct() + del kwargs['blank'] + kwargs['primary_key'] = True + return name, path, args, kwargs + def get_internal_type(self): return "AutoField" @@ -630,6 +723,11 @@ class BooleanField(Field): kwargs['blank'] = True Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(BooleanField, self).deconstruct() + del kwargs['blank'] + return name, path, args, kwargs + def get_internal_type(self): return "BooleanField" @@ -733,6 +831,18 @@ class DateField(Field): kwargs['blank'] = True Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(DateField, self).deconstruct() + if self.auto_now: + kwargs['auto_now'] = True + del kwargs['editable'] + del kwargs['blank'] + if self.auto_now_add: + kwargs['auto_now_add'] = True + del kwargs['editable'] + del kwargs['blank'] + return name, path, args, kwargs + def get_internal_type(self): return "DateField" @@ -927,6 +1037,14 @@ class DecimalField(Field): self.max_digits, self.decimal_places = max_digits, decimal_places Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(DecimalField, self).deconstruct() + if self.max_digits: + kwargs['max_digits'] = self.max_digits + if self.decimal_places: + kwargs['decimal_places'] = self.decimal_places + return name, path, args, kwargs + def get_internal_type(self): return "DecimalField" @@ -989,6 +1107,12 @@ class EmailField(CharField): kwargs['max_length'] = kwargs.get('max_length', 75) CharField.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(EmailField, self).deconstruct() + # We do not exclude max_length if it matches default as we want to change + # the default in future. + return name, path, args, kwargs + def formfield(self, **kwargs): # As with CharField, this will cause email validation to be performed # twice. @@ -1008,6 +1132,22 @@ class FilePathField(Field): kwargs['max_length'] = kwargs.get('max_length', 100) Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(FilePathField, self).deconstruct() + if self.path != '': + kwargs['path'] = self.path + if self.match is not None: + kwargs['match'] = self.match + if self.recursive is not False: + kwargs['recursive'] = self.recursive + if self.allow_files is not True: + kwargs['allow_files'] = self.allow_files + if self.allow_folders is not False: + kwargs['allow_folders'] = self.allow_folders + if kwargs.get("max_length", None) == 100: + del kwargs["max_length"] + return name, path, args, kwargs + def formfield(self, **kwargs): defaults = { 'path': self.path, @@ -1115,6 +1255,11 @@ class IPAddressField(Field): kwargs['max_length'] = 15 Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(IPAddressField, self).deconstruct() + del kwargs['max_length'] + return name, path, args, kwargs + def get_internal_type(self): return "IPAddressField" @@ -1131,12 +1276,23 @@ class GenericIPAddressField(Field): def __init__(self, verbose_name=None, name=None, protocol='both', unpack_ipv4=False, *args, **kwargs): self.unpack_ipv4 = unpack_ipv4 + self.protocol = protocol self.default_validators, invalid_error_message = \ validators.ip_address_validators(protocol, unpack_ipv4) self.default_error_messages['invalid'] = invalid_error_message kwargs['max_length'] = 39 Field.__init__(self, verbose_name, name, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(GenericIPAddressField, self).deconstruct() + if self.unpack_ipv4 is not False: + kwargs['unpack_ipv4'] = self.unpack_ipv4 + if self.protocol != "both": + kwargs['protocol'] = self.protocol + if kwargs.get("max_length", None) == 39: + del kwargs['max_length'] + return name, path, args, kwargs + def get_internal_type(self): return "GenericIPAddressField" @@ -1177,6 +1333,12 @@ class NullBooleanField(Field): kwargs['blank'] = True Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(NullBooleanField, self).deconstruct() + del kwargs['null'] + del kwargs['blank'] + return name, path, args, kwargs + def get_internal_type(self): return "NullBooleanField" @@ -1254,6 +1416,16 @@ class SlugField(CharField): kwargs['db_index'] = True super(SlugField, self).__init__(*args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(SlugField, self).deconstruct() + if kwargs.get("max_length", None) == 50: + del kwargs['max_length'] + if self.db_index is False: + kwargs['db_index'] = False + else: + del kwargs['db_index'] + return name, path, args, kwargs + def get_internal_type(self): return "SlugField" @@ -1302,6 +1474,14 @@ class TimeField(Field): kwargs['blank'] = True Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(TimeField, self).deconstruct() + if self.auto_now is not False: + kwargs["auto_now"] = self.auto_now + if self.auto_now_add is not False: + kwargs["auto_now_add"] = self.auto_now_add + return name, path, args, kwargs + def get_internal_type(self): return "TimeField" @@ -1367,6 +1547,12 @@ class URLField(CharField): kwargs['max_length'] = kwargs.get('max_length', 200) CharField.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(URLField, self).deconstruct() + if kwargs.get("max_length", None) == 200: + del kwargs['max_length'] + return name, path, args, kwargs + def formfield(self, **kwargs): # As with CharField, this will cause URL validation to be performed # twice. diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index e631f177e9..0a913e908b 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -227,6 +227,17 @@ class FileField(Field): kwargs['max_length'] = kwargs.get('max_length', 100) super(FileField, self).__init__(verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(FileField, self).deconstruct() + if kwargs.get("max_length", None) != 100: + kwargs["max_length"] = 100 + else: + del kwargs["max_length"] + kwargs['upload_to'] = self.upload_to + if self.storage is not default_storage: + kwargs['storage'] = self.storage + return name, path, args, kwargs + def get_internal_type(self): return "FileField" @@ -326,6 +337,14 @@ class ImageField(FileField): self.width_field, self.height_field = width_field, height_field super(ImageField, self).__init__(verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(ImageField, self).deconstruct() + if self.width_field: + kwargs['width_field'] = self.width_field + if self.height_field: + kwargs['height_field'] = self.height_field + return name, path, args, kwargs + def contribute_to_class(self, cls, name): super(ImageField, self).contribute_to_class(cls, name) # Attach update_dimension_fields so that dimension fields declared diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 0367d24fe8..c708bff49f 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1151,6 +1151,27 @@ class ForeignKey(ForeignObject): ) super(ForeignKey, self).__init__(to, ['self'], [to_field], **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(ForeignKey, self).deconstruct() + # Handle the simpler arguments + if self.db_index: + del kwargs['db_index'] + else: + kwargs['db_index'] = False + if self.db_constraint is not True: + kwargs['db_constraint'] = self.db_constraint + if self.rel.on_delete is not CASCADE: + kwargs['on_delete'] = self.rel.on_delete + # Rel needs more work. + rel = self.rel + if self.rel.field_name: + kwargs['to_field'] = self.rel.field_name + if isinstance(self.rel.to, basestring): + kwargs['to'] = self.rel.to + else: + kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name) + return name, path, args, kwargs + @property def related_field(self): return self.foreign_related_fields[0] @@ -1268,6 +1289,12 @@ class OneToOneField(ForeignKey): kwargs['unique'] = True super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(OneToOneField, self).deconstruct() + if "unique" in kwargs: + del kwargs['unique'] + return name, path, args, kwargs + def contribute_to_related_class(self, cls, related): setattr(cls, related.get_accessor_name(), SingleRelatedObjectDescriptor(related)) @@ -1357,6 +1384,21 @@ class ManyToManyField(RelatedField): super(ManyToManyField, self).__init__(**kwargs) + def deconstruct(self): + name, path, args, kwargs = super(ManyToManyField, self).deconstruct() + # Handle the simpler arguments + if self.rel.db_constraint is not True: + kwargs['db_constraint'] = self.db_constraint + if "help_text" in kwargs: + del kwargs['help_text'] + # Rel needs more work. + rel = self.rel + if isinstance(self.rel.to, basestring): + kwargs['to'] = self.rel.to + else: + kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name) + return name, path, args, kwargs + def _get_path_info(self, direct=False): """ Called by both direct an indirect m2m traversal. diff --git a/tests/field_deconstruction/__init__.py b/tests/field_deconstruction/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/field_deconstruction/models.py b/tests/field_deconstruction/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/field_deconstruction/tests.py b/tests/field_deconstruction/tests.py new file mode 100644 index 0000000000..4ccf4d048c --- /dev/null +++ b/tests/field_deconstruction/tests.py @@ -0,0 +1,253 @@ +from django.test import TestCase +from django.db import models + + +class FieldDeconstructionTests(TestCase): + """ + Tests the deconstruct() method on all core fields. + """ + + def test_name(self): + """ + Tests the outputting of the correct name if assigned one. + """ + # First try using a "normal" field + field = models.CharField(max_length=65) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(name, None) + field.set_attributes_from_name("is_awesome_test") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(name, "is_awesome_test") + # Now try with a ForeignKey + field = models.ForeignKey("some_fake.ModelName") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(name, None) + field.set_attributes_from_name("author") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(name, "author") + + def test_auto_field(self): + field = models.AutoField(primary_key=True) + field.set_attributes_from_name("id") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.AutoField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"primary_key": True}) + + def test_big_integer_field(self): + field = models.BigIntegerField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.BigIntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_boolean_field(self): + field = models.BooleanField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.BooleanField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + field = models.BooleanField(default=True) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.BooleanField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"default": True}) + + def test_char_field(self): + field = models.CharField(max_length=65) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.CharField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_length": 65}) + field = models.CharField(max_length=65, null=True, blank=True) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.CharField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_length": 65, "null": True, "blank": True}) + + def test_csi_field(self): + field = models.CommaSeparatedIntegerField(max_length=100) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.CommaSeparatedIntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_length": 100}) + + def test_date_field(self): + field = models.DateField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.DateField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + field = models.DateField(auto_now=True) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.DateField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"auto_now": True}) + + def test_datetime_field(self): + field = models.DateTimeField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.DateTimeField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + field = models.DateTimeField(auto_now_add=True) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.DateTimeField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"auto_now_add": True}) + + def test_decimal_field(self): + field = models.DecimalField(max_digits=5, decimal_places=2) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.DecimalField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_digits": 5, "decimal_places": 2}) + + def test_email_field(self): + field = models.EmailField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.EmailField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_length": 75}) + field = models.EmailField(max_length=255) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.EmailField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"max_length": 255}) + + def test_file_field(self): + field = models.FileField(upload_to="foo/bar") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.FileField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"upload_to": "foo/bar"}) + + def test_file_path_field(self): + field = models.FilePathField(match=".*\.txt$") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.FilePathField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"match": ".*\.txt$"}) + field = models.FilePathField(recursive=True, allow_folders=True) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.FilePathField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"recursive": True, "allow_folders": True}) + + def test_float_field(self): + field = models.FloatField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.FloatField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_foreign_key(self): + field = models.ForeignKey("auth.User") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.ForeignKey") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"to": "auth.User"}) + field = models.ForeignKey("something.Else") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.ForeignKey") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"to": "something.Else"}) + field = models.ForeignKey("auth.User", on_delete=models.SET_NULL) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.ForeignKey") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"to": "auth.User", "on_delete": models.SET_NULL}) + + def test_image_field(self): + field = models.ImageField(upload_to="foo/barness", width_field="width", height_field="height") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.ImageField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"upload_to": "foo/barness", "width_field": "width", "height_field": "height"}) + + def test_integer_field(self): + field = models.IntegerField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.IntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_ip_address_field(self): + field = models.IPAddressField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.IPAddressField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_generic_ip_address_field(self): + field = models.GenericIPAddressField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.GenericIPAddressField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + field = models.GenericIPAddressField(protocol="IPv6") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.GenericIPAddressField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"protocol": "IPv6"}) + + def test_many_to_many_field(self): + field = models.ManyToManyField("auth.User") + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.ManyToManyField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"to": "auth.User"}) + + def test_null_boolean_field(self): + field = models.NullBooleanField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.NullBooleanField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_positive_integer_field(self): + field = models.PositiveIntegerField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.PositiveIntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_positive_small_integer_field(self): + field = models.PositiveSmallIntegerField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.PositiveSmallIntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_slug_field(self): + field = models.SlugField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.SlugField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + field = models.SlugField(db_index=False) + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.SlugField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {"db_index": False}) + + def test_small_integer_field(self): + field = models.SmallIntegerField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.SmallIntegerField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_text_field(self): + field = models.TextField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.TextField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) + + def test_url_field(self): + field = models.URLField() + name, path, args, kwargs = field.deconstruct() + self.assertEqual(path, "django.db.models.URLField") + self.assertEqual(args, []) + self.assertEqual(kwargs, {}) -- cgit v1.3