From 59d127e45f5ed31e346162a0ac312f672c096821 Mon Sep 17 00:00:00 2001 From: Anton Baklanov Date: Wed, 17 Apr 2013 18:20:31 +0300 Subject: Fixed #20276 -- Implemented __bool__ for MergeDict MergeDict evaluates now to False if all contained dicts are empty. Thanks til for the report and the initial patch. --- tests/utils_tests/test_datastructures.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'tests') diff --git a/tests/utils_tests/test_datastructures.py b/tests/utils_tests/test_datastructures.py index 3161c04f97..91111cc2ed 100644 --- a/tests/utils_tests/test_datastructures.py +++ b/tests/utils_tests/test_datastructures.py @@ -203,6 +203,13 @@ class MergeDictTests(SimpleTestCase): ('key2', ['value2', 'value3']), ('key4', ['value5', 'value6'])]) + def test_bool_casting(self): + empty = MergeDict({}, {}, {}) + not_empty = MergeDict({}, {}, {"key": "value"}) + self.assertFalse(empty) + self.assertTrue(not_empty) + + class MultiValueDictTests(SimpleTestCase): def test_multivaluedict(self): -- cgit v1.3 From 714161c8642646f1f617436479313ca49c71b5c8 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 19 Apr 2013 10:58:29 -0700 Subject: Fix != operations on lazy objects. --- django/utils/functional.py | 1 + tests/utils_tests/test_simplelazyobject.py | 9 +++++++++ 2 files changed, 10 insertions(+) (limited to 'tests') diff --git a/django/utils/functional.py b/django/utils/functional.py index 13aec9cb82..1592828c7f 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -346,6 +346,7 @@ class SimpleLazyObject(LazyObject): # care about this (especially in equality tests) __class__ = property(new_method_proxy(operator.attrgetter("__class__"))) __eq__ = new_method_proxy(operator.eq) + __ne__ = new_method_proxy(operator.ne) __hash__ = new_method_proxy(hash) __bool__ = new_method_proxy(bool) # Python 3 __nonzero__ = __bool__ # Python 2 diff --git a/tests/utils_tests/test_simplelazyobject.py b/tests/utils_tests/test_simplelazyobject.py index 883e60aa81..f925e01eb6 100644 --- a/tests/utils_tests/test_simplelazyobject.py +++ b/tests/utils_tests/test_simplelazyobject.py @@ -152,3 +152,12 @@ class TestUtilsSimpleLazyObject(TestCase): SimpleLazyObject(None) finally: sys.settrace(old_trace_func) + + def test_not_equal(self): + lazy1 = SimpleLazyObject(lambda: 2) + lazy2 = SimpleLazyObject(lambda: 2) + lazy3 = SimpleLazyObject(lambda: 3) + self.assertEqual(lazy1, lazy2) + self.assertNotEqual(lazy1, lazy3) + self.assertTrue(lazy1 != lazy3) + self.assertFalse(lazy1 != lazy2) -- cgit v1.3 From 4769db6b5fddaed93a8d8d03d0c36f7262e9ac8b Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 26 Apr 2013 08:57:39 +0200 Subject: Fixed #20321 -- Added missing key name in MergeDict KeyError message Thanks mark.harviston et gmail.com for the report. --- django/utils/datastructures.py | 2 +- tests/utils_tests/test_datastructures.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index b3060202be..a211323320 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -26,7 +26,7 @@ class MergeDict(object): return dict_[key] except KeyError: pass - raise KeyError + raise KeyError(key) def __copy__(self): return self.__class__(*self.dicts) diff --git a/tests/utils_tests/test_datastructures.py b/tests/utils_tests/test_datastructures.py index 91111cc2ed..5829e7c2d7 100644 --- a/tests/utils_tests/test_datastructures.py +++ b/tests/utils_tests/test_datastructures.py @@ -209,6 +209,14 @@ class MergeDictTests(SimpleTestCase): self.assertFalse(empty) self.assertTrue(not_empty) + def test_key_error(self): + """ + Test that the message of KeyError contains the missing key name. + """ + d1 = MergeDict({'key1': 42}) + with six.assertRaisesRegex(self, KeyError, 'key2'): + d1['key2'] + class MultiValueDictTests(SimpleTestCase): -- cgit v1.3 From 90fe9141ded9f7005546e9a66f31fd196f60e7e4 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 28 Apr 2013 16:45:05 +0200 Subject: Fixed #18986 -- Improved error message for missing files in CachedStaticFilesStorage. Thanks zyegfryed for his work on the patch. --- .../staticfiles/management/commands/collectstatic.py | 6 ++++++ django/contrib/staticfiles/storage.py | 5 ++++- tests/staticfiles_tests/project/faulty/faulty.css | 1 + tests/staticfiles_tests/tests.py | 17 ++++++++++++++++- 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/staticfiles_tests/project/faulty/faulty.css (limited to 'tests') diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index 6116f31efc..22fdac7c76 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -116,6 +116,12 @@ class Command(NoArgsCommand): processor = self.storage.post_process(found_files, dry_run=self.dry_run) for original_path, processed_path, processed in processor: + if isinstance(processed, Exception): + self.stderr.write("Post-processing '%s' failed!" % original_path) + # Add a blank line before the traceback, otherwise it's + # too easy to miss the relevant part of the error message. + self.stderr.write("") + raise processed if processed: self.log("Post-processed '%s' as '%s'" % (original_path, processed_path), level=1) diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index f444e12c19..d085cf723f 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -251,7 +251,10 @@ class CachedFilesMixin(object): for patterns in self._patterns.values(): for pattern, template in patterns: converter = self.url_converter(name, template) - content = pattern.sub(converter, content) + try: + content = pattern.sub(converter, content) + except ValueError as exc: + yield name, None, exc if hashed_file_exists: self.delete(hashed_name) # then save the processed result diff --git a/tests/staticfiles_tests/project/faulty/faulty.css b/tests/staticfiles_tests/project/faulty/faulty.css new file mode 100644 index 0000000000..ca57c77e55 --- /dev/null +++ b/tests/staticfiles_tests/project/faulty/faulty.css @@ -0,0 +1 @@ +@import url("missing.css"); diff --git a/tests/staticfiles_tests/tests.py b/tests/staticfiles_tests/tests.py index 0391b8b018..912dcffe83 100644 --- a/tests/staticfiles_tests/tests.py +++ b/tests/staticfiles_tests/tests.py @@ -244,7 +244,7 @@ class TestCollection(CollectionTestCase, TestDefaults): class TestCollectionClear(CollectionTestCase): """ - Test the ``--clear`` option of the ``collectstatic`` managemenet command. + Test the ``--clear`` option of the ``collectstatic`` management command. """ def run_collectstatic(self, **kwargs): clear_filepath = os.path.join(settings.STATIC_ROOT, 'cleared.txt') @@ -550,6 +550,21 @@ class TestCollectionCachedStorage(BaseCollectionTestCase, self.assertNotIn(b"cached/other.css", content) self.assertIn(b"other.d41d8cd98f00.css", content) + @override_settings( + STATICFILES_DIRS=(os.path.join(TEST_ROOT, 'project', 'faulty'),), + STATICFILES_FINDERS=('django.contrib.staticfiles.finders.FileSystemFinder',), + ) + def test_post_processing_failure(self): + """ + Test that post_processing indicates the origin of the error when it + fails. Regression test for #18986. + """ + finders._finders.clear() + err = six.StringIO() + with self.assertRaises(Exception) as cm: + call_command('collectstatic', interactive=False, verbosity=0, stderr=err) + self.assertEqual("Post-processing 'faulty.css' failed!\n\n", err.getvalue()) + # we set DEBUG to False here since the template tag wouldn't work otherwise @override_settings(**dict(TEST_SETTINGS, -- cgit v1.3 From 3a4276ffc35326f20e95890b50a67aebeabc9ad0 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 28 Apr 2013 17:15:41 +0200 Subject: Tested that get_or_create raises IntegrityError. It used to raise "DatabaseError: no such savepoint" with the old transaction management. Closes #15117. --- tests/get_or_create/models.py | 4 ++++ tests/get_or_create/tests.py | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/get_or_create/models.py b/tests/get_or_create/models.py index 678f5a401c..82905de4f8 100644 --- a/tests/get_or_create/models.py +++ b/tests/get_or_create/models.py @@ -24,3 +24,7 @@ class Person(models.Model): class ManualPrimaryKeyTest(models.Model): id = models.IntegerField(primary_key=True) data = models.CharField(max_length=100) + + +class Profile(models.Model): + person = models.ForeignKey(Person, primary_key=True) diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py index 1e300fbb4d..e9cce9bbde 100644 --- a/tests/get_or_create/tests.py +++ b/tests/get_or_create/tests.py @@ -4,9 +4,9 @@ from datetime import date import traceback from django.db import IntegrityError -from django.test import TestCase +from django.test import TestCase, TransactionTestCase -from .models import Person, ManualPrimaryKeyTest +from .models import Person, ManualPrimaryKeyTest, Profile class GetOrCreateTests(TestCase): @@ -64,3 +64,16 @@ class GetOrCreateTests(TestCase): formatted_traceback = traceback.format_exc() self.assertIn('obj.save', formatted_traceback) + +class GetOrCreateTransactionTests(TransactionTestCase): + + def test_get_or_create_integrityerror(self): + # Regression test for #15117. Requires a TransactionTestCase on + # databases that delay integrity checks until the end of transactions, + # otherwise the exception is never raised. + try: + Profile.objects.get_or_create(person=Person(id=1)) + except IntegrityError: + pass + else: + self.skipTest("This backend does not support integrity checks.") -- cgit v1.3 From 780fa48f5fb81b2f0f58de95167abff84a6149aa Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Wed, 1 May 2013 16:40:49 +0200 Subject: Fixed test failures introduced in a5becad9094e5c5403b692b9a7b3a6ffaabf64a3. --- tests/admin_scripts/tests.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index baec16820e..5071977b2d 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -14,7 +14,8 @@ import subprocess import sys import codecs -from django import conf, bin, get_version +import django +from django import conf, get_version from django.conf import settings from django.core.management import BaseCommand, CommandError from django.db import connection @@ -149,8 +150,8 @@ class AdminScriptTestCase(unittest.TestCase): return out, err def run_django_admin(self, args, settings_file=None): - bin_dir = os.path.abspath(os.path.dirname(upath(bin.__file__))) - return self.run_test(os.path.join(bin_dir, 'django-admin.py'), args, settings_file) + script_dir = os.path.abspath(os.path.join(os.path.dirname(upath(django.__file__)), 'bin')) + return self.run_test(os.path.join(script_dir, 'django-admin.py'), args, settings_file) def run_manage(self, args, settings_file=None): def safe_remove(path): -- cgit v1.3 From da85c8cf326fa8858bac29f1b92da7972dd88dda Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Sun, 5 May 2013 15:27:14 +0200 Subject: Fixed a regression introduced in 9f7a01ef2bed8c0395a970286e8f87fd7d344b3b. --- tests/view_tests/tests/test_i18n.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index 5a3bdd1062..2da17dd0f5 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/tests/view_tests/tests/test_i18n.py @@ -217,5 +217,6 @@ class JavascriptI18nTests(LiveServerTestCase): def test_escaping(self): extended_apps = list(settings.INSTALLED_APPS) + ['view_tests'] with self.settings(INSTALLED_APPS=extended_apps): - response = self.client.get('%s%s' % (self.live_server_url, '/jsi18n_admin/')) + # Force a language via GET otherwise the gettext functions are a noop! + response = self.client.get('/jsi18n_admin/?language=de') self.assertContains(response, '\\x04') -- cgit v1.3 From 99a6f0e77c6e02b310687667d41df56c17b953bf Mon Sep 17 00:00:00 2001 From: Tai Lee Date: Mon, 6 May 2013 13:32:07 +1000 Subject: Fixed #20354 -- `makemessages` no longer crashes with `UnicodeDecodeError` Handle the `UnicodeDecodeError` exception, send a warning to `stdout` with the file name and location, and continue processing other files. --- django/core/management/commands/makemessages.py | 5 ++++- tests/i18n/commands/extraction.py | 22 +++++++++++++++------- tests/i18n/commands/not_utf8.sample | 1 + 3 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 tests/i18n/commands/not_utf8.sample (limited to 'tests') diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index a7e98173ac..bc171176c2 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -294,7 +294,10 @@ class Command(NoArgsCommand): os.unlink(potfile) for f in file_list: - f.process(self, potfile, self.domain, self.keep_pot) + try: + f.process(self, potfile, self.domain, self.keep_pot) + except UnicodeDecodeError: + self.stdout.write("UnicodeDecodeError: skipped file %s in %s" % (f.file, f.dirpath)) return potfile def find_files(self, root): diff --git a/tests/i18n/commands/extraction.py b/tests/i18n/commands/extraction.py index 80e4ee0110..7c482e58fb 100644 --- a/tests/i18n/commands/extraction.py +++ b/tests/i18n/commands/extraction.py @@ -30,6 +30,10 @@ class ExtractorTests(SimpleTestCase): return shutil.rmtree(dname) + def rmfile(self, filepath): + if os.path.exists(filepath): + os.remove(filepath) + def tearDown(self): os.chdir(self.test_dir) try: @@ -126,18 +130,22 @@ class BasicExtractorTests(ExtractorTests): # Check that the temporary file was cleaned up self.assertFalse(os.path.exists('./templates/template_with_error.tpl.py')) + def test_unicode_decode_error(self): + os.chdir(self.test_dir) + shutil.copyfile('./not_utf8.sample', './not_utf8.txt') + self.addCleanup(self.rmfile, os.path.join(self.test_dir, 'not_utf8.txt')) + stdout = StringIO() + management.call_command('makemessages', locale=LOCALE, stdout=stdout) + self.assertIn("UnicodeDecodeError: skipped file not_utf8.txt in .", + force_text(stdout.getvalue())) + def test_extraction_warning(self): """test xgettext warning about multiple bare interpolation placeholders""" os.chdir(self.test_dir) shutil.copyfile('./code.sample', './code_sample.py') + self.addCleanup(self.rmfile, os.path.join(self.test_dir, 'code_sample.py')) stdout = StringIO() - try: - management.call_command('makemessages', locale=LOCALE, stdout=stdout) - finally: - try: - os.remove('./code_sample.py') - except OSError: - pass + management.call_command('makemessages', locale=LOCALE, stdout=stdout) self.assertIn("code_sample.py:4", force_text(stdout.getvalue())) def test_template_message_context_extractor(self): diff --git a/tests/i18n/commands/not_utf8.sample b/tests/i18n/commands/not_utf8.sample new file mode 100644 index 0000000000..6449f52803 --- /dev/null +++ b/tests/i18n/commands/not_utf8.sample @@ -0,0 +1 @@ +Copyright (c) 2009 Øyvind Sean Kinsey, oyvind@kinsey.no \ No newline at end of file -- cgit v1.3 From 7476d96f83a004d674244aeb7a66289035427396 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 8 May 2013 15:33:02 +0200 Subject: Marked tests of BinaryFields as expected failures on MySQL and Python 3. Current ports of MySQLdb are very buggy in this area. --- docs/ref/databases.txt | 3 +++ tests/model_fields/tests.py | 6 +++++- tests/serializers_regress/tests.py | 7 ++++++- 3 files changed, 14 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index f28079ae33..2ef048216f 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -259,6 +259,9 @@ At the time of writing, the latest release of MySQLdb (1.2.4) doesn't support Python 3. In order to use MySQL under Python 3, you'll have to install an unofficial fork, such as `MySQL-for-Python-3`_. +This port is still in alpha. In particular, it doesn't support binary data, +making it impossible to use :class:`django.db.models.BinaryField`. + .. _MySQL-for-Python-3: https://github.com/clelland/MySQL-for-Python-3 Creating your database diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py index 9af8325040..035a5c2ae3 100644 --- a/tests/model_fields/tests.py +++ b/tests/model_fields/tests.py @@ -6,7 +6,7 @@ from decimal import Decimal from django import test from django import forms from django.core.exceptions import ValidationError -from django.db import models, IntegrityError +from django.db import connection, models, IntegrityError from django.db.models.fields.files import FieldFile from django.utils import six from django.utils import unittest @@ -455,6 +455,10 @@ class BinaryFieldTests(test.TestCase): # Test default value self.assertEqual(bytes(dm.short_data), b'\x08') + if connection.vendor == 'mysql' and six.PY3: + # Existing MySQL DB-API drivers fail on binary data. + test_set_and_retrieve = unittest.expectedFailure(test_set_and_retrieve) + def test_max_length(self): dm = DataModel(short_data=self.binary_data*4) self.assertRaises(ValidationError, dm.full_clean) diff --git a/tests/serializers_regress/tests.py b/tests/serializers_regress/tests.py index 72e3825d91..04b4d4c839 100644 --- a/tests/serializers_regress/tests.py +++ b/tests/serializers_regress/tests.py @@ -26,7 +26,7 @@ from django.test import TestCase from django.utils import six from django.utils.encoding import force_text from django.utils.functional import curry -from django.utils.unittest import skipUnless +from django.utils.unittest import expectedFailure, skipUnless from .models import (BinaryData, BooleanData, CharData, DateData, DateTimeData, EmailData, FileData, FilePathData, DecimalData, FloatData, IntegerData, IPAddressData, @@ -459,6 +459,11 @@ def serializerTest(format, self): for klass, count in instance_count.items(): self.assertEqual(count, klass.objects.count()) +if connection.vendor == 'mysql' and six.PY3: + # Existing MySQL DB-API drivers fail on binary data. + serializerTest = expectedFailure(serializerTest) + + def naturalKeySerializerTest(format, self): # Create all the objects defined in the test data objects = [] -- cgit v1.3 From 832b4a5722ba6b55e7b17c3bac6614ecca9aa88d Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 8 May 2013 23:12:04 +0200 Subject: Marked a test as an expected failure on MySQL and Python 3.2. This test hits a bug in current ports of MySQLdb. --- tests/model_regress/tests.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/model_regress/tests.py b/tests/model_regress/tests.py index c1e864c270..71ccb071d2 100644 --- a/tests/model_regress/tests.py +++ b/tests/model_regress/tests.py @@ -2,12 +2,14 @@ from __future__ import absolute_import, unicode_literals import datetime from operator import attrgetter +import sys from django.core.exceptions import ValidationError from django.test import TestCase, skipUnlessDBFeature from django.utils import six from django.utils import tzinfo -from django.db import router +from django.utils import unittest +from django.db import connection, router from django.db.models.sql import InsertQuery from .models import (Worker, Article, Party, Event, Department, @@ -131,6 +133,11 @@ class ModelTests(TestCase): attrgetter("when") ) + if (3,) <= sys.version_info < (3, 3) and connection.vendor == 'mysql': + # In Python < 3.3, datetime.strftime raises an exception for years + # below 1000, and existing MySQL DB-API drivers hit this problem. + test_date_lookup = unittest.expectedFailure(test_date_lookup) + def test_date_filter_null(self): # Date filtering was failing with NULL date values in SQLite # (regression test for #3501, amongst other things). -- cgit v1.3 From 1556b1c3b7a18cadf4d66d2ab6301d7fe8646ba5 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Thu, 9 May 2013 00:49:05 +0100 Subject: Removed fragile admin validation of fields on ModelForm Refs #19445 --- django/contrib/admin/options.py | 8 ++++++-- django/contrib/admin/validation.py | 9 --------- tests/admin_validation/tests.py | 8 -------- 3 files changed, 6 insertions(+), 19 deletions(-) (limited to 'tests') diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index b35f100fda..543655b357 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -13,7 +13,7 @@ from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_o from django.contrib.admin.templatetags.admin_static import static from django.contrib import messages from django.views.decorators.csrf import csrf_protect -from django.core.exceptions import PermissionDenied, ValidationError +from django.core.exceptions import PermissionDenied, ValidationError, FieldError from django.core.paginator import Paginator from django.core.urlresolvers import reverse from django.db import models, transaction, router @@ -488,7 +488,11 @@ class ModelAdmin(BaseModelAdmin): "formfield_callback": partial(self.formfield_for_dbfield, request=request), } defaults.update(kwargs) - return modelform_factory(self.model, **defaults) + try: + return modelform_factory(self.model, **defaults) + except FieldError as e: + raise FieldError('%s. Check fields/fieldsets/exclude attributes of class %s.' + % (e, self.__class__.__name__)) def get_changelist(self, request, **kwargs): """ diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index a02bb7a316..178df9d844 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -387,15 +387,6 @@ def check_formfield(cls, model, opts, label, field): except KeyError: raise ImproperlyConfigured("'%s.%s' refers to field '%s' that " "is missing from the form." % (cls.__name__, label, field)) - else: - get_form_is_overridden = hasattr(cls, 'get_form') and cls.get_form != ModelAdmin.get_form - if not get_form_is_overridden: - fields = fields_for_model(model) - try: - fields[field] - except KeyError: - raise ImproperlyConfigured("'%s.%s' refers to field '%s' that " - "is missing from the form." % (cls.__name__, label, field)) def fetch_attr(cls, model, opts, label, field): try: diff --git a/tests/admin_validation/tests.py b/tests/admin_validation/tests.py index 5b2c45f6f2..5329cc7e8c 100644 --- a/tests/admin_validation/tests.py +++ b/tests/admin_validation/tests.py @@ -16,10 +16,6 @@ class ValidFields(admin.ModelAdmin): form = SongForm fields = ['title'] -class InvalidFields(admin.ModelAdmin): - form = SongForm - fields = ['spam'] - class ValidFormFieldsets(admin.ModelAdmin): def get_form(self, request, obj=None, **kwargs): class ExtraFieldForm(SongForm): @@ -49,10 +45,6 @@ class ValidationTestCase(TestCase): # Regression test for #8027: custom ModelForms with fields/fieldsets """ validate(ValidFields, Song) - self.assertRaisesMessage(ImproperlyConfigured, - "'InvalidFields.fields' refers to field 'spam' that is missing from the form.", - validate, - InvalidFields, Song) def test_custom_get_form_with_fieldsets(self): """ -- cgit v1.3 From 2f8f2ad1d7762b2c46cdc707b4921685bcea3612 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Thu, 9 May 2013 01:45:34 +0100 Subject: Removed some failing tests missed in 1556b1c3b7a18cadf4d66d2ab6301d7fe8646ba5 --- tests/modeladmin/tests.py | 42 ------------------------------------------ 1 file changed, 42 deletions(-) (limited to 'tests') diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index e5450ab8ff..a63984a8a9 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -616,17 +616,6 @@ class ValidationTests(unittest.TestCase): ValidationTestModel, ) - class ValidationTestModelAdmin(ModelAdmin): - fieldsets = (("General", {"fields": ("non_existent_field",)}),) - - six.assertRaisesRegex(self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.fieldsets\[0\]\[1\]\['fields'\]' refers to field 'non_existent_field' that is missing from the form.", - validate, - ValidationTestModelAdmin, - ValidationTestModel, - ) - class ValidationTestModelAdmin(ModelAdmin): fieldsets = (("General", {"fields": ("name",)}),) @@ -684,22 +673,6 @@ class ValidationTests(unittest.TestCase): def test_fieldsets_with_custom_form_validation(self): - class BandAdmin(ModelAdmin): - - fieldsets = ( - ('Band', { - 'fields': ('non_existent_field',) - }), - ) - - six.assertRaisesRegex(self, - ImproperlyConfigured, - "'BandAdmin.fieldsets\[0\]\[1\]\['fields'\]' refers to field 'non_existent_field' that is missing from the form.", - validate, - BandAdmin, - Band, - ) - class BandAdmin(ModelAdmin): fieldsets = ( ('Band', { @@ -1371,21 +1344,6 @@ class ValidationTests(unittest.TestCase): ValidationTestModel, ) - class ValidationTestInline(TabularInline): - model = ValidationTestInlineModel - fields = ("non_existent_field",) - - class ValidationTestModelAdmin(ModelAdmin): - inlines = [ValidationTestInline] - - six.assertRaisesRegex(self, - ImproperlyConfigured, - "'ValidationTestInline.fields' refers to field 'non_existent_field' that is missing from the form.", - validate, - ValidationTestModelAdmin, - ValidationTestModel, - ) - def test_fk_name_validation(self): class ValidationTestInline(TabularInline): -- cgit v1.3 From 6634cb7b53bc35241e070f1896ff2fc439c19d0f Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 9 May 2013 06:55:25 -0700 Subject: Removed a test that didn't make sense; code could never be called the way the test was written. --- tests/many_to_one_regress/tests.py | 4 ---- 1 file changed, 4 deletions(-) (limited to 'tests') diff --git a/tests/many_to_one_regress/tests.py b/tests/many_to_one_regress/tests.py index a9969a7e90..035ba53bff 100644 --- a/tests/many_to_one_regress/tests.py +++ b/tests/many_to_one_regress/tests.py @@ -60,10 +60,6 @@ class ManyToOneRegressionTests(TestCase): self.assertRaises(ValueError, Child, name='xyzzy', parent=None) self.assertRaises(ValueError, Child.objects.create, name='xyzzy', parent=None) - # Trying to assign to unbound attribute raises AttributeError - six.assertRaisesRegex(self, AttributeError, "must be accessed via instance", - Child.parent.__set__, None, p) - # Creation using keyword argument should cache the related object. p = Parent.objects.get(name="Parent") c = Child(parent=p) -- cgit v1.3 From 1e37cb37cec330a6b78925e2ef5589817428d09a Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Thu, 9 May 2013 15:11:02 +0100 Subject: Further removal of static admin validation that can fail erroneously --- django/contrib/admin/validation.py | 17 ----------------- tests/admin_validation/tests.py | 2 -- tests/modeladmin/tests.py | 21 --------------------- 3 files changed, 40 deletions(-) (limited to 'tests') diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 178df9d844..8d65f96cf1 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -246,7 +246,6 @@ def validate_fields_spec(cls, model, opts, flds, label): # readonly_fields will handle the validation of such # things. continue - check_formfield(cls, model, opts, label, field) try: f = opts.get_field(field) except models.FieldDoesNotExist: @@ -302,14 +301,6 @@ def validate_base(cls, model): # exclude if cls.exclude: # default value is None check_isseq(cls, 'exclude', cls.exclude) - for field in cls.exclude: - check_formfield(cls, model, opts, 'exclude', field) - try: - f = opts.get_field(field) - except models.FieldDoesNotExist: - # If we can't find a field on the model that matches, - # it could be an extra field on the form. - continue if len(cls.exclude) > len(set(cls.exclude)): raise ImproperlyConfigured('There are duplicate field(s) in %s.exclude' % cls.__name__) @@ -380,14 +371,6 @@ def get_field(cls, model, opts, label, field): raise ImproperlyConfigured("'%s.%s' refers to field '%s' that is missing from model '%s.%s'." % (cls.__name__, label, field, model._meta.app_label, model.__name__)) -def check_formfield(cls, model, opts, label, field): - if getattr(cls.form, 'base_fields', None): - try: - cls.form.base_fields[field] - except KeyError: - raise ImproperlyConfigured("'%s.%s' refers to field '%s' that " - "is missing from the form." % (cls.__name__, label, field)) - def fetch_attr(cls, model, opts, label, field): try: return opts.get_field(field) diff --git a/tests/admin_validation/tests.py b/tests/admin_validation/tests.py index 5329cc7e8c..6ce0ee7e03 100644 --- a/tests/admin_validation/tests.py +++ b/tests/admin_validation/tests.py @@ -269,8 +269,6 @@ class ValidationTestCase(TestCase): """ class SongForm(forms.ModelForm): extra_data = forms.CharField() - class Meta: - model = Song class FieldsOnFormOnlyAdmin(admin.ModelAdmin): form = SongForm diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index a63984a8a9..bac8d30153 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -682,27 +682,6 @@ class ValidationTests(unittest.TestCase): validate(BandAdmin, Band) - class AdminBandForm(forms.ModelForm): - class Meta: - model = Band - - class BandAdmin(ModelAdmin): - form = AdminBandForm - - fieldsets = ( - ('Band', { - 'fields': ('non_existent_field',) - }), - ) - - six.assertRaisesRegex(self, - ImproperlyConfigured, - "'BandAdmin.fieldsets\[0]\[1\]\['fields'\]' refers to field 'non_existent_field' that is missing from the form.", - validate, - BandAdmin, - Band, - ) - class AdminBandForm(forms.ModelForm): delete = forms.BooleanField() -- cgit v1.3 From f026a519aea8f3ea7ca339bfbbb007e1ee0068b0 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Thu, 21 Feb 2013 21:56:55 +0000 Subject: Fixed #19733 - deprecated ModelForms without 'fields' or 'exclude', and added '__all__' shortcut This also updates all dependent functionality, including modelform_factory and modelformset_factory, and the generic views `ModelFormMixin`, `CreateView` and `UpdateView` which gain a new `fields` attribute. --- django/contrib/admin/options.py | 14 +- django/contrib/auth/forms.py | 1 + django/contrib/contenttypes/generic.py | 9 +- django/contrib/flatpages/forms.py | 1 + .../contrib/formtools/tests/wizard/test_forms.py | 4 +- .../formtools/tests/wizard/wizardtests/tests.py | 1 + django/forms/models.py | 57 ++++++- django/views/generic/edit.py | 11 +- docs/ref/class-based-views/generic-editing.txt | 2 + docs/ref/class-based-views/mixins-editing.txt | 12 ++ docs/ref/contrib/admin/index.txt | 34 ++++- docs/ref/forms/models.txt | 8 + docs/releases/1.6.txt | 52 +++++++ docs/topics/auth/customizing.txt | 1 + docs/topics/class-based-views/generic-editing.txt | 37 +++-- docs/topics/forms/modelforms.txt | 163 +++++++++++---------- tests/admin_validation/tests.py | 2 + tests/bug639/models.py | 1 + tests/foreign_object/tests.py | 1 + tests/forms_tests/tests/test_regressions.py | 1 + tests/forms_tests/tests/tests.py | 4 + tests/generic_relations/tests.py | 1 + tests/generic_views/test_edit.py | 44 +++++- tests/generic_views/test_forms.py | 1 + tests/generic_views/views.py | 9 ++ tests/i18n/forms.py | 1 + tests/inline_formsets/tests.py | 10 +- tests/model_forms/tests.py | 111 ++++++++++++-- tests/model_forms_regress/tests.py | 57 ++++++- tests/model_formsets/tests.py | 89 +++++------ tests/model_formsets_regress/tests.py | 28 ++-- tests/model_inheritance_regress/tests.py | 2 + tests/modeladmin/tests.py | 9 +- tests/timezones/forms.py | 1 + 34 files changed, 578 insertions(+), 201 deletions(-) (limited to 'tests') diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 543655b357..9bc3d55454 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -5,7 +5,7 @@ from django import forms from django.conf import settings from django.forms.formsets import all_valid, DELETION_FIELD_NAME from django.forms.models import (modelform_factory, modelformset_factory, - inlineformset_factory, BaseInlineFormSet) + inlineformset_factory, BaseInlineFormSet, modelform_defines_fields) from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets, helpers from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects, @@ -488,6 +488,10 @@ class ModelAdmin(BaseModelAdmin): "formfield_callback": partial(self.formfield_for_dbfield, request=request), } defaults.update(kwargs) + + if defaults['fields'] is None and not modelform_defines_fields(defaults['form']): + defaults['fields'] = forms.ALL_FIELDS + try: return modelform_factory(self.model, **defaults) except FieldError as e: @@ -523,6 +527,10 @@ class ModelAdmin(BaseModelAdmin): "formfield_callback": partial(self.formfield_for_dbfield, request=request), } defaults.update(kwargs) + if (defaults.get('fields') is None + and not modelform_defines_fields(defaults.get('form'))): + defaults['fields'] = forms.ALL_FIELDS + return modelform_factory(self.model, **defaults) def get_changelist_formset(self, request, **kwargs): @@ -1527,6 +1535,10 @@ class InlineModelAdmin(BaseModelAdmin): return result defaults['form'] = DeleteProtectedModelForm + + if defaults['fields'] is None and not modelform_defines_fields(defaults['form']): + defaults['fields'] = forms.ALL_FIELDS + return inlineformset_factory(self.parent_model, self.model, **defaults) def get_fieldsets(self, request, obj=None): diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 42abde2f19..e44e7a703e 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -130,6 +130,7 @@ class UserChangeForm(forms.ModelForm): class Meta: model = User + fields = '__all__' def __init__(self, *args, **kwargs): super(UserChangeForm, self).__init__(*args, **kwargs) diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index cc03f799f3..399d24aa87 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -13,8 +13,9 @@ from django.db.models import signals from django.db.models.fields.related import ForeignObject, ForeignObjectRel from django.db.models.related import PathInfo from django.db.models.sql.where import Constraint -from django.forms import ModelForm -from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance +from django.forms import ModelForm, ALL_FIELDS +from django.forms.models import (BaseModelFormSet, modelformset_factory, save_instance, + modelform_defines_fields) from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets from django.contrib.contenttypes.models import ContentType from django.utils import six @@ -480,6 +481,10 @@ class GenericInlineModelAdmin(InlineModelAdmin): "exclude": exclude } defaults.update(kwargs) + + if defaults['fields'] is None and not modelform_defines_fields(defaults['form']): + defaults['fields'] = ALL_FIELDS + return generic_inlineformset_factory(self.model, **defaults) class GenericStackedInline(GenericInlineModelAdmin): diff --git a/django/contrib/flatpages/forms.py b/django/contrib/flatpages/forms.py index a848875a9f..80938116ad 100644 --- a/django/contrib/flatpages/forms.py +++ b/django/contrib/flatpages/forms.py @@ -12,6 +12,7 @@ class FlatpageForm(forms.ModelForm): class Meta: model = FlatPage + fields = '__all__' def clean_url(self): url = self.cleaned_data['url'] diff --git a/django/contrib/formtools/tests/wizard/test_forms.py b/django/contrib/formtools/tests/wizard/test_forms.py index 14c6e6a685..7425755cf5 100644 --- a/django/contrib/formtools/tests/wizard/test_forms.py +++ b/django/contrib/formtools/tests/wizard/test_forms.py @@ -60,9 +60,11 @@ class TestModel(models.Model): class TestModelForm(forms.ModelForm): class Meta: model = TestModel + fields = '__all__' -TestModelFormSet = forms.models.modelformset_factory(TestModel, form=TestModelForm, extra=2) +TestModelFormSet = forms.models.modelformset_factory(TestModel, form=TestModelForm, extra=2, + fields='__all__') class TestWizard(WizardView): diff --git a/django/contrib/formtools/tests/wizard/wizardtests/tests.py b/django/contrib/formtools/tests/wizard/wizardtests/tests.py index 1ee5dbdc78..3c2dbc3efb 100644 --- a/django/contrib/formtools/tests/wizard/wizardtests/tests.py +++ b/django/contrib/formtools/tests/wizard/wizardtests/tests.py @@ -15,6 +15,7 @@ from django.utils._os import upath class UserForm(forms.ModelForm): class Meta: model = User + fields = '__all__' UserFormSet = forms.models.modelformset_factory(User, form=UserForm, extra=2) diff --git a/django/forms/models.py b/django/forms/models.py index 5e7797809a..af5cda8faf 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -5,6 +5,8 @@ and database field objects. from __future__ import absolute_import, unicode_literals +import warnings + from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError from django.forms.fields import Field, ChoiceField from django.forms.forms import BaseForm, get_declared_fields @@ -22,8 +24,12 @@ from django.utils.translation import ugettext_lazy as _, ugettext __all__ = ( 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 'save_instance', 'ModelChoiceField', 'ModelMultipleChoiceField', + 'ALL_FIELDS', ) +ALL_FIELDS = '__all__' + + def construct_instance(form, instance, fields=None, exclude=None): """ Constructs and returns a model instance from the bound ``form``'s @@ -211,7 +217,7 @@ class ModelFormMetaclass(type): # of ('foo',) for opt in ['fields', 'exclude']: value = getattr(opts, opt) - if isinstance(value, six.string_types): + if isinstance(value, six.string_types) and value != ALL_FIELDS: msg = ("%(model)s.Meta.%(opt)s cannot be a string. " "Did you mean to type: ('%(value)s',)?" % { 'model': new_class.__name__, @@ -222,6 +228,20 @@ class ModelFormMetaclass(type): if opts.model: # If a model is defined, extract form fields from it. + + if opts.fields is None and opts.exclude is None: + # This should be some kind of assertion error once deprecation + # cycle is complete. + warnings.warn("Creating a ModelForm without either the 'fields' attribute " + "or the 'exclude' attribute is deprecated - form %s " + "needs updating" % name, + PendingDeprecationWarning) + + if opts.fields == ALL_FIELDS: + # sentinel for fields_for_model to indicate "get the list of + # fields from the model" + opts.fields = None + fields = fields_for_model(opts.model, opts.fields, opts.exclude, opts.widgets, formfield_callback) # make sure opts.fields doesn't specify an invalid field @@ -394,7 +414,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, Returns a ModelForm containing form fields for the given model. ``fields`` is an optional list of field names. If provided, only the named - fields will be included in the returned fields. + fields will be included in the returned fields. If omitted or '__all__', + all fields will be used. ``exclude`` is an optional list of field names. If provided, the named fields will be excluded from the returned fields, even if they are listed @@ -434,6 +455,15 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, 'formfield_callback': formfield_callback } + # The ModelFormMetaclass will trigger a similar warning/error, but this will + # be difficult to debug for code that needs updating, so we produce the + # warning here too. + if (getattr(Meta, 'fields', None) is None and + getattr(Meta, 'exclude', None) is None): + warnings.warn("Calling modelform_factory without defining 'fields' or " + "'exclude' explicitly is deprecated", + PendingDeprecationWarning, stacklevel=2) + # Instatiate type(form) in order to use the same metaclass as form. return type(form)(class_name, (form,), form_class_attrs) @@ -701,6 +731,21 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None, """ Returns a FormSet class for the given Django model class. """ + # modelform_factory will produce the same warning/error, but that will be + # difficult to debug for code that needs upgrading, so we produce the + # warning here too. This logic is reproducing logic inside + # modelform_factory, but it can be removed once the deprecation cycle is + # complete, since the validation exception will produce a helpful + # stacktrace. + meta = getattr(form, 'Meta', None) + if meta is None: + meta = type(str('Meta'), (object,), {}) + if (getattr(meta, 'fields', fields) is None and + getattr(meta, 'exclude', exclude) is None): + warnings.warn("Calling modelformset_factory without defining 'fields' or " + "'exclude' explicitly is deprecated", + PendingDeprecationWarning, stacklevel=2) + form = modelform_factory(model, form=form, fields=fields, exclude=exclude, formfield_callback=formfield_callback, widgets=widgets) @@ -1091,3 +1136,11 @@ class ModelMultipleChoiceField(ModelChoiceField): initial_set = set([force_text(value) for value in self.prepare_value(initial)]) data_set = set([force_text(value) for value in data]) return data_set != initial_set + + +def modelform_defines_fields(form_class): + return (form_class is not None and ( + hasattr(form_class, '_meta') and + (form_class._meta.fields is not None or + form_class._meta.exclude is not None) + )) diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py index 5b97fc81c9..e2cc741ffb 100644 --- a/django/views/generic/edit.py +++ b/django/views/generic/edit.py @@ -1,3 +1,5 @@ +import warnings + from django.forms import models as model_forms from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponseRedirect @@ -95,7 +97,14 @@ class ModelFormMixin(FormMixin, SingleObjectMixin): # Try to get a queryset and extract the model class # from that model = self.get_queryset().model - return model_forms.modelform_factory(model) + + fields = getattr(self, 'fields', None) + if fields is None: + warnings.warn("Using ModelFormMixin (base class of %s) without " + "the 'fields' attribute is deprecated." % self.__class__.__name__, + PendingDeprecationWarning) + + return model_forms.modelform_factory(model, fields=fields) def get_form_kwargs(self): """ diff --git a/docs/ref/class-based-views/generic-editing.txt b/docs/ref/class-based-views/generic-editing.txt index 1dbb427036..555ba40cfb 100644 --- a/docs/ref/class-based-views/generic-editing.txt +++ b/docs/ref/class-based-views/generic-editing.txt @@ -110,6 +110,7 @@ CreateView class AuthorCreate(CreateView): model = Author + fields = ['name'] UpdateView ---------- @@ -152,6 +153,7 @@ UpdateView class AuthorUpdate(UpdateView): model = Author + fields = ['name'] DeleteView ---------- diff --git a/docs/ref/class-based-views/mixins-editing.txt b/docs/ref/class-based-views/mixins-editing.txt index 3f32269742..51d8628818 100644 --- a/docs/ref/class-based-views/mixins-editing.txt +++ b/docs/ref/class-based-views/mixins-editing.txt @@ -116,6 +116,18 @@ ModelFormMixin by examining ``self.object`` or :attr:`~django.views.generic.detail.SingleObjectMixin.queryset`. + .. attribute:: fields + + .. versionadded:: 1.6 + + A list of names of fields. This is interpreted the same way as the + ``Meta.fields`` attribute of :class:`~django.forms.ModelForm`. + + This is a required attribute if you are generating the form class + automatically (e.g. using ``model``). Omitting this attribute will + result in all fields being used, but this behaviour is deprecated + and will be removed in Django 1.8. + .. attribute:: success_url The URL to redirect to when the form is successfully processed. diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 43f0398566..67e498ee91 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -335,6 +335,22 @@ subclass:: For an example see the section `Adding custom validation to the admin`_. + .. admonition:: Note + + .. versionchanged:: 1.6 + + If you define the ``Meta.model`` attribute on a + :class:`~django.forms.ModelForm`, you must also define the + ``Meta.fields`` attribute (or the ``Meta.exclude`` attribute). However, + since the admin has its own way of defining fields, the ``Meta.fields`` + attribute will be ignored. + + If the ``ModelForm`` is only going to be used for the admin, the easiest + solution is to omit the ``Meta.model`` attribute, since ``ModelAdmin`` + will provide the correct model to use. Alternatively, you can set + ``fields = []`` in the ``Meta`` class to satisfy the validation on the + ``ModelForm``. + .. admonition:: Note If your ``ModelForm`` and ``ModelAdmin`` both define an ``exclude`` @@ -1283,13 +1299,24 @@ templates used by the :class:`ModelAdmin` views: on the changelist page. To use a custom form, for example:: class MyForm(forms.ModelForm): - class Meta: - model = MyModel + pass class MyModelAdmin(admin.ModelAdmin): def get_changelist_form(self, request, **kwargs): return MyForm + .. admonition:: Note + + .. versionchanged:: 1.6 + + If you define the ``Meta.model`` attribute on a + :class:`~django.forms.ModelForm`, you must also define the + ``Meta.fields`` attribute (or the ``Meta.exclude`` attribute). However, + ``ModelAdmin`` ignores this value, overriding it with the + :attr:`ModelAdmin.list_editable` attribute. The easiest solution is to + omit the ``Meta.model`` attribute, since ``ModelAdmin`` will provide the + correct model to use. + .. method:: ModelAdmin.get_changelist_formset(self, request, **kwargs) Returns a :ref:`ModelFormSet ` class for use on the @@ -1490,9 +1517,6 @@ needed. Now within your form you can add your own custom validation for any field:: class MyArticleAdminForm(forms.ModelForm): - class Meta: - model = Article - def clean_name(self): # do something that validates your data return self.cleaned_data["name"] diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt index 7e3a1470b6..9b3480758a 100644 --- a/docs/ref/forms/models.txt +++ b/docs/ref/forms/models.txt @@ -25,6 +25,14 @@ Model Form Functions See :ref:`modelforms-factory` for example usage. + .. versionchanged:: 1.6 + + You must provide the list of fields explicitly, either via keyword arguments + ``fields`` or ``exclude``, or the corresponding attributes on the form's + inner ``Meta`` class. See :ref:`modelforms-selecting-fields` for more + information. Omitting any definition of the fields to use will result in all + fields being used, but this behaviour is deprecated. + .. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False) Returns a ``FormSet`` class for the given ``model`` class. diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 611b661415..9cce36aac3 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -532,3 +532,55 @@ including it in an URLconf, simply replace:: with:: (r'^prefix/(?P\d+)/(?P.*)/$', 'django.contrib.contenttypes.views.shortcut'), + +``ModelForm`` without ``fields`` or ``exclude`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, if you wanted a :class:`~django.forms.ModelForm` to use all fields on +the model, you could simply omit the ``Meta.fields`` attribute, and all fields +would be used. + +This can lead to security problems where fields are added to the model and, +unintentionally, automatically become editable by end users. In some cases, +particular with boolean fields, it is possible for this problem to be completely +invisible. This is a form of `Mass assignment vulnerability +`_. + +For this reason, this behaviour is deprecated, and using the ``Meta.exclude`` +option is strongly discouraged. Instead, all fields that are intended for +inclusion in the form should be listed explicitly in the ``fields`` attribute. + +If this security concern really does not apply in your case, there is a shortcut +to explicitly indicate that all fields should be used - use the special value +``"__all__"`` for the fields attribute:: + + class MyModelForm(ModelForm): + class Meta: + fields = "__all__" + model = MyModel + +If you have custom ``ModelForms`` that only need to be used in the admin, there +is another option. The admin has its own methods for defining fields +(``fieldsets`` etc.), and so adding a list of fields to the ``ModelForm`` is +redundant. Instead, simply omit the ``Meta`` inner class of the ``ModelForm``, +or omit the ``Meta.model`` attribute. Since the ``ModelAdmin`` subclass knows +which model it is for, it can add the necessary attributes to derive a +functioning ``ModelForm``. This behaviour also works for earlier Django +versions. + +``UpdateView`` and ``CreateView`` without explicit fields +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The generic views :class:`~django.views.generic.edit.CreateView` and +:class:`~django.views.generic.edit.UpdateView`, and anything else derived from +:class:`~django.views.generic.edit.ModelFormMixin`, are vulnerable to the +security problem described in the section above, because they can automatically +create a ``ModelForm`` that uses all fields for a model. + +For this reason, if you use these views for editing models, you must also supply +the ``fields`` attribute, which is a list of model fields and works in the same +way as the :class:`~django.forms.ModelForm` ``Meta.fields`` attribute. Alternatively, +you can set set the ``form_class`` attribute to a ``ModelForm`` that explicitly +defines the fields to be used. Defining an ``UpdateView`` or ``CreateView`` +subclass to be used with a model but without an explicit list of fields is +deprecated. diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index a5d7d3f9a1..b53bbe8211 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -1051,6 +1051,7 @@ code would be required in the app's ``admin.py`` file:: class Meta: model = MyUser + fields = ['email', 'password', 'date_of_birth', 'is_active', 'is_admin'] def clean_password(self): # Regardless of what the user provides, return the initial value. diff --git a/docs/topics/class-based-views/generic-editing.txt b/docs/topics/class-based-views/generic-editing.txt index 66ba36fd87..86c5280159 100644 --- a/docs/topics/class-based-views/generic-editing.txt +++ b/docs/topics/class-based-views/generic-editing.txt @@ -114,9 +114,11 @@ here; we don't have to write any logic ourselves:: class AuthorCreate(CreateView): model = Author + fields = ['name'] class AuthorUpdate(UpdateView): model = Author + fields = ['name'] class AuthorDelete(DeleteView): model = Author @@ -126,6 +128,17 @@ here; we don't have to write any logic ourselves:: We have to use :func:`~django.core.urlresolvers.reverse_lazy` here, not just ``reverse`` as the urls are not loaded when the file is imported. +.. versionchanged:: 1.6 + +In Django 1.6, the ``fields`` attribute was added, which works the same way as +the ``fields`` attribute on the inner ``Meta`` class on +:class:`~django.forms.ModelForm`. + +Omitting the fields attribute will work as previously, but is deprecated and +this attribute will be required from 1.8 (unless you define the form class in +another way). + + Finally, we hook these new views into the URLconf:: # urls.py @@ -177,33 +190,17 @@ the foreign key relation to the model:: # ... -Create a custom :class:`~django.forms.ModelForm` in order to exclude the -``created_by`` field and prevent the user from editing it: - -.. code-block:: python - - # forms.py - from django import forms - from myapp.models import Author - - class AuthorForm(forms.ModelForm): - class Meta: - model = Author - exclude = ('created_by',) - -In the view, use the custom -:attr:`~django.views.generic.edit.FormMixin.form_class` and override -:meth:`~django.views.generic.edit.ModelFormMixin.form_valid()` to add the -user:: +In the view, ensure that you exclude ``created_by`` in the list of fields to +edit, and override +:meth:`~django.views.generic.edit.ModelFormMixin.form_valid()` to add the user:: # views.py from django.views.generic.edit import CreateView from myapp.models import Author - from myapp.forms import AuthorForm class AuthorCreate(CreateView): - form_class = AuthorForm model = Author + fields = ['name'] def form_valid(self, form): form.instance.created_by = self.request.user diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index eaf2bbbaf2..e58dade736 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -28,6 +28,7 @@ For example:: >>> class ArticleForm(ModelForm): ... class Meta: ... model = Article + ... fields = ['pub_date', 'headline', 'content', 'reporter'] # Creating a form to add an article. >>> form = ArticleForm() @@ -39,11 +40,13 @@ For example:: Field types ----------- -The generated ``Form`` class will have a form field for every model field. Each -model field has a corresponding default form field. For example, a -``CharField`` on a model is represented as a ``CharField`` on a form. A -model ``ManyToManyField`` is represented as a ``MultipleChoiceField``. Here is -the full list of conversions: +The generated ``Form`` class will have a form field for every model field +specified, in the order specified in the ``fields`` attribute. + +Each model field has a corresponding default form field. For example, a +``CharField`` on a model is represented as a ``CharField`` on a form. A model +``ManyToManyField`` is represented as a ``MultipleChoiceField``. Here is the +full list of conversions: =============================== ======================================== Model field Form field @@ -168,10 +171,13 @@ Consider this set of models:: class AuthorForm(ModelForm): class Meta: model = Author + fields = ['name', 'title', 'birth_date'] class BookForm(ModelForm): class Meta: model = Book + fields = ['name', 'authors'] + With these models, the ``ModelForm`` subclasses above would be roughly equivalent to this (the only difference being the ``save()`` method, which @@ -288,47 +294,66 @@ method is used to determine whether a form requires multipart file upload (and hence whether ``request.FILES`` must be passed to the form), etc. See :ref:`binding-uploaded-files` for more information. -Using a subset of fields on the form ------------------------------------- +.. _modelforms-selecting-fields: -In some cases, you may not want all the model fields to appear on the generated -form. There are three ways of telling ``ModelForm`` to use only a subset of the -model fields: +Selecting the fields to use +--------------------------- -1. Set ``editable=False`` on the model field. As a result, *any* form - created from the model via ``ModelForm`` will not include that - field. +It is strongly recommended that you explicitly set all fields that should be +edited in the form using the ``fields`` attribute. Failure to do so can easily +lead to security problems when a form unexpectedly allows a user to set certain +fields, especially when new fields are added to a model. Depending on how the +form is rendered, the problem may not even be visible on the web page. -2. Use the ``fields`` attribute of the ``ModelForm``'s inner ``Meta`` - class. This attribute, if given, should be a list of field names - to include in the form. The order in which the fields names are specified - in that list is respected when the form renders them. +The alternative approach would be to include all fields automatically, or +blacklist only some. This fundamental approach is known to be much less secure +and has led to serious exploits on major websites (e.g. `GitHub +`_). -3. Use the ``exclude`` attribute of the ``ModelForm``'s inner ``Meta`` - class. This attribute, if given, should be a list of field names - to exclude from the form. +There are, however, two shortcuts available for cases where you can guarantee +these security concerns do not apply to you: -For example, if you want a form for the ``Author`` model (defined -above) that includes only the ``name`` and ``birth_date`` fields, you would -specify ``fields`` or ``exclude`` like this:: +1. Set the ``fields`` attribute to the special value ``'__all__'`` to indicate + that all fields in the model should be used. For example:: - class PartialAuthorForm(ModelForm): - class Meta: - model = Author - fields = ('name', 'birth_date') + class AuthorForm(ModelForm): + class Meta: + model = Author + fields = '__all__' - class PartialAuthorForm(ModelForm): - class Meta: - model = Author - exclude = ('title',) +2. Set the ``exclude`` attribute of the ``ModelForm``'s inner ``Meta`` class to + a list of fields to be excluded from the form. + + For example:: + + class PartialAuthorForm(ModelForm): + class Meta: + model = Author + exclude = ['title'] + + Since the ``Author`` model has the 3 fields ``name``, ``title`` and + ``birth_date``, this will result in the fields ``name`` and ``birth_date`` + being present on the form. + +If either of these are used, the order the fields appear in the form will be the +order the fields are defined in the model, with ``ManyToManyField`` instances +appearing last. + +In addition, Django applies the following rule: if you set ``editable=False`` on +the model field, *any* form created from the model via ``ModelForm`` will not +include that field. + +.. versionchanged:: 1.6 + + Before version 1.6, the ``'__all__'`` shortcut did not exist, but omitting + the ``fields`` attribute had the same effect. Omitting both ``fields`` and + ``exclude`` is now deprecated, but will continue to work as before until + version 1.8 -Since the Author model has only 3 fields, 'name', 'title', and -'birth_date', the forms above will contain exactly the same fields. .. note:: - If you specify ``fields`` or ``exclude`` when creating a form with - ``ModelForm``, then the fields that are not in the resulting form + Any fields not included in a form by the above logic will not be set by the form's ``save()`` method. Also, if you manually add the excluded fields back to the form, they will not be initialized from the model instance. @@ -401,15 +426,19 @@ field, you could do the following:: class Meta: model = Article + fields = ['pub_date', 'headline', 'content', 'reporter'] + If you want to override a field's default label, then specify the ``label`` parameter when declaring the form field:: - >>> class ArticleForm(ModelForm): - ... pub_date = DateField(label='Publication date') - ... - ... class Meta: - ... model = Article + class ArticleForm(ModelForm): + pub_date = DateField(label='Publication date') + + class Meta: + model = Article + fields = ['pub_date', 'headline', 'content', 'reporter'] + .. note:: @@ -436,6 +465,7 @@ parameter when declaring the form field:: class Meta: model = Article + fields = ['headline', 'content'] You must ensure that the type of the form field can be used to set the contents of the corresponding model field. When they are not compatible, @@ -444,30 +474,6 @@ parameter when declaring the form field:: See the :doc:`form field documentation ` for more information on fields and their arguments. -Changing the order of fields ----------------------------- - -By default, a ``ModelForm`` will render fields in the same order that they are -defined on the model, with ``ManyToManyField`` instances appearing last. If -you want to change the order in which fields are rendered, you can use the -``fields`` attribute on the ``Meta`` class. - -The ``fields`` attribute defines the subset of model fields that will be -rendered, and the order in which they will be rendered. For example given this -model:: - - class Book(models.Model): - author = models.ForeignKey(Author) - title = models.CharField(max_length=100) - -the ``author`` field would be rendered first. If we wanted the title field -to be rendered first, we could specify the following ``ModelForm``:: - - >>> class BookForm(ModelForm): - ... class Meta: - ... model = Book - ... fields = ('title', 'author') - .. _overriding-modelform-clean-method: Overriding the clean() method @@ -550,21 +556,19 @@ definition. This may be more convenient if you do not have many customizations to make:: >>> from django.forms.models import modelform_factory - >>> BookForm = modelform_factory(Book) + >>> BookForm = modelform_factory(Book, fields=("author", "title")) This can also be used to make simple modifications to existing forms, for -example by specifying which fields should be displayed:: - - >>> Form = modelform_factory(Book, form=BookForm, fields=("author",)) - -... or which fields should be excluded:: - - >>> Form = modelform_factory(Book, form=BookForm, exclude=("title",)) - -You can also specify the widgets to be used for a given field:: +example by specifying the widgets to be used for a given field:: >>> from django.forms import Textarea - >>> Form = modelform_factory(Book, form=BookForm, widgets={"title": Textarea()}) + >>> Form = modelform_factory(Book, form=BookForm, + widgets={"title": Textarea()}) + +The fields to include can be specified using the ``fields`` and ``exclude`` +keyword arguments, or the corresponding attributes on the ``ModelForm`` inner +``Meta`` class. Please see the ``ModelForm`` :ref:`modelforms-selecting-fields` +documentation. .. _model-formsets: @@ -688,11 +692,10 @@ database. If a given instance's data didn't change in the bound data, the instance won't be saved to the database and won't be included in the return value (``instances``, in the above example). -When fields are missing from the form (for example because they have -been excluded), these fields will not be set by the ``save()`` -method. You can find more information about this restriction, which -also holds for regular ``ModelForms``, in `Using a subset of fields on -the form`_. +When fields are missing from the form (for example because they have been +excluded), these fields will not be set by the ``save()`` method. You can find +more information about this restriction, which also holds for regular +``ModelForms``, in `Selecting the fields to use`_. Pass ``commit=False`` to return the unsaved model instances:: diff --git a/tests/admin_validation/tests.py b/tests/admin_validation/tests.py index 6ce0ee7e03..16f73c6390 100644 --- a/tests/admin_validation/tests.py +++ b/tests/admin_validation/tests.py @@ -285,6 +285,8 @@ class ValidationTestCase(TestCase): extra_data = forms.CharField() class Meta: model = Song + fields = '__all__' + class FieldsOnFormOnlyAdmin(admin.ModelAdmin): form = SongForm diff --git a/tests/bug639/models.py b/tests/bug639/models.py index e641555c87..fa8e7d2c07 100644 --- a/tests/bug639/models.py +++ b/tests/bug639/models.py @@ -25,3 +25,4 @@ class Photo(models.Model): class PhotoForm(ModelForm): class Meta: model = Photo + fields = '__all__' diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index 2ca13cb786..55dd6a0f47 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -322,6 +322,7 @@ class FormsTests(TestCase): class ArticleForm(forms.ModelForm): class Meta: model = Article + fields = '__all__' def test_foreign_object_form(self): # A very crude test checking that the non-concrete fields do not get form fields. diff --git a/tests/forms_tests/tests/test_regressions.py b/tests/forms_tests/tests/test_regressions.py index be9dc8c593..74509a0f1a 100644 --- a/tests/forms_tests/tests/test_regressions.py +++ b/tests/forms_tests/tests/test_regressions.py @@ -139,6 +139,7 @@ class FormsRegressionsTestCase(TestCase): class CheeseForm(ModelForm): class Meta: model = Cheese + fields = '__all__' form = CheeseForm({ 'name': 'Brie', diff --git a/tests/forms_tests/tests/tests.py b/tests/forms_tests/tests/tests.py index 80e2cadcc3..deda4822b8 100644 --- a/tests/forms_tests/tests/tests.py +++ b/tests/forms_tests/tests/tests.py @@ -17,11 +17,13 @@ from ..models import (ChoiceOptionModel, ChoiceFieldModel, FileModel, Group, class ChoiceFieldForm(ModelForm): class Meta: model = ChoiceFieldModel + fields = '__all__' class OptionalMultiChoiceModelForm(ModelForm): class Meta: model = OptionalMultiChoiceModel + fields = '__all__' class FileForm(Form): @@ -139,6 +141,7 @@ class FormsModelTestCase(TestCase): class BoundaryForm(ModelForm): class Meta: model = BoundaryModel + fields = '__all__' f = BoundaryForm({'positive_integer': 100}) self.assertTrue(f.is_valid()) @@ -154,6 +157,7 @@ class FormsModelTestCase(TestCase): class DefaultsForm(ModelForm): class Meta: model = Defaults + fields = '__all__' self.assertEqual(DefaultsForm().fields['name'].initial, 'class default value') self.assertEqual(DefaultsForm().fields['def_date'].initial, datetime.date(1980, 1, 1)) diff --git a/tests/generic_relations/tests.py b/tests/generic_relations/tests.py index 27b25185ea..dd9dc506ca 100644 --- a/tests/generic_relations/tests.py +++ b/tests/generic_relations/tests.py @@ -247,6 +247,7 @@ class CustomWidget(forms.TextInput): class TaggedItemForm(forms.ModelForm): class Meta: model = TaggedItem + fields = '__all__' widgets = {'tag': CustomWidget} class GenericInlineFormsetTest(TestCase): diff --git a/tests/generic_views/test_edit.py b/tests/generic_views/test_edit.py index c54d632363..54eab7ffa4 100644 --- a/tests/generic_views/test_edit.py +++ b/tests/generic_views/test_edit.py @@ -1,12 +1,14 @@ from __future__ import absolute_import +import warnings + from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse from django import forms from django.test import TestCase from django.utils.unittest import expectedFailure from django.views.generic.base import View -from django.views.generic.edit import FormMixin +from django.views.generic.edit import FormMixin, CreateView, UpdateView from . import views from .models import Artist, Author @@ -34,6 +36,7 @@ class ModelFormMixinTests(TestCase): form_class = views.AuthorGetQuerySetFormView().get_form_class() self.assertEqual(form_class._meta.model, Author) + class CreateViewTests(TestCase): urls = 'generic_views.urls' @@ -112,6 +115,45 @@ class CreateViewTests(TestCase): self.assertEqual(res.status_code, 302) self.assertRedirects(res, 'http://testserver/accounts/login/?next=/edit/authors/create/restricted/') + def test_create_view_with_restricted_fields(self): + + class MyCreateView(CreateView): + model = Author + fields = ['name'] + + self.assertEqual(list(MyCreateView().get_form_class().base_fields), + ['name']) + + def test_create_view_all_fields(self): + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", PendingDeprecationWarning) + + class MyCreateView(CreateView): + model = Author + fields = '__all__' + + self.assertEqual(list(MyCreateView().get_form_class().base_fields), + ['name', 'slug']) + self.assertEqual(len(w), 0) + + + def test_create_view_without_explicit_fields(self): + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", PendingDeprecationWarning) + + class MyCreateView(CreateView): + model = Author + + # Until end of the deprecation cycle, should still create the form + # as before: + self.assertEqual(list(MyCreateView().get_form_class().base_fields), + ['name', 'slug']) + + # but with a warning: + self.assertEqual(w[0].category, PendingDeprecationWarning) + class UpdateViewTests(TestCase): urls = 'generic_views.urls' diff --git a/tests/generic_views/test_forms.py b/tests/generic_views/test_forms.py index e036ad8afc..8c118e32a6 100644 --- a/tests/generic_views/test_forms.py +++ b/tests/generic_views/test_forms.py @@ -11,6 +11,7 @@ class AuthorForm(forms.ModelForm): class Meta: model = Author + fields = ['name', 'slug'] class ContactForm(forms.Form): diff --git a/tests/generic_views/views.py b/tests/generic_views/views.py index 69bfcb32dd..aa8777e8c6 100644 --- a/tests/generic_views/views.py +++ b/tests/generic_views/views.py @@ -85,15 +85,18 @@ class ContactView(generic.FormView): class ArtistCreate(generic.CreateView): model = Artist + fields = '__all__' class NaiveAuthorCreate(generic.CreateView): queryset = Author.objects.all() + fields = '__all__' class AuthorCreate(generic.CreateView): model = Author success_url = '/list/authors/' + fields = '__all__' class SpecializedAuthorCreate(generic.CreateView): @@ -112,19 +115,23 @@ class AuthorCreateRestricted(AuthorCreate): class ArtistUpdate(generic.UpdateView): model = Artist + fields = '__all__' class NaiveAuthorUpdate(generic.UpdateView): queryset = Author.objects.all() + fields = '__all__' class AuthorUpdate(generic.UpdateView): model = Author success_url = '/list/authors/' + fields = '__all__' class OneAuthorUpdate(generic.UpdateView): success_url = '/list/authors/' + fields = '__all__' def get_object(self): return Author.objects.get(pk=1) @@ -184,6 +191,8 @@ class BookDetail(BookConfig, generic.DateDetailView): pass class AuthorGetQuerySetFormView(generic.edit.ModelFormMixin): + fields = '__all__' + def get_queryset(self): return Author.objects.all() diff --git a/tests/i18n/forms.py b/tests/i18n/forms.py index abb99f443a..6e4def9c5e 100644 --- a/tests/i18n/forms.py +++ b/tests/i18n/forms.py @@ -24,3 +24,4 @@ class CompanyForm(forms.ModelForm): class Meta: model = Company + fields = '__all__' diff --git a/tests/inline_formsets/tests.py b/tests/inline_formsets/tests.py index df682d34ef..ad8a666cb5 100644 --- a/tests/inline_formsets/tests.py +++ b/tests/inline_formsets/tests.py @@ -10,7 +10,7 @@ from .models import Poet, Poem, School, Parent, Child class DeletionTests(TestCase): def test_deletion(self): - PoemFormSet = inlineformset_factory(Poet, Poem, can_delete=True) + PoemFormSet = inlineformset_factory(Poet, Poem, can_delete=True, fields="__all__") poet = Poet.objects.create(name='test') poem = poet.poem_set.create(name='test poem') data = { @@ -32,7 +32,7 @@ class DeletionTests(TestCase): Make sure that an add form that is filled out, but marked for deletion doesn't cause validation errors. """ - PoemFormSet = inlineformset_factory(Poet, Poem, can_delete=True) + PoemFormSet = inlineformset_factory(Poet, Poem, can_delete=True, fields="__all__") poet = Poet.objects.create(name='test') data = { 'poem_set-TOTAL_FORMS': '1', @@ -60,7 +60,7 @@ class DeletionTests(TestCase): Make sure that a change form that is filled out, but marked for deletion doesn't cause validation errors. """ - PoemFormSet = inlineformset_factory(Poet, Poem, can_delete=True) + PoemFormSet = inlineformset_factory(Poet, Poem, can_delete=True, fields="__all__") poet = Poet.objects.create(name='test') poem = poet.poem_set.create(name='test poem') data = { @@ -115,8 +115,8 @@ class InlineFormsetFactoryTest(TestCase): """ These should both work without a problem. """ - inlineformset_factory(Parent, Child, fk_name='mother') - inlineformset_factory(Parent, Child, fk_name='father') + inlineformset_factory(Parent, Child, fk_name='mother', fields="__all__") + inlineformset_factory(Parent, Child, fk_name='father', fields="__all__") def test_exception_on_unspecified_foreign_key(self): """ diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index 96c3ecbdce..c5db011404 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals import datetime import os from decimal import Decimal +import warnings from django import forms from django.core.exceptions import FieldError @@ -30,19 +31,25 @@ if test_images: class ImageFileForm(forms.ModelForm): class Meta: model = ImageFile + fields = '__all__' + class OptionalImageFileForm(forms.ModelForm): class Meta: model = OptionalImageFile + fields = '__all__' + class ProductForm(forms.ModelForm): class Meta: model = Product + fields = '__all__' class PriceForm(forms.ModelForm): class Meta: model = Price + fields = '__all__' class BookForm(forms.ModelForm): @@ -66,11 +73,13 @@ class ExplicitPKForm(forms.ModelForm): class PostForm(forms.ModelForm): class Meta: model = Post + fields = '__all__' class DerivedPostForm(forms.ModelForm): class Meta: model = DerivedPost + fields = '__all__' class CustomAuthorForm(forms.ModelForm): @@ -78,61 +87,79 @@ class CustomAuthorForm(forms.ModelForm): class Meta: model = Author + fields = '__all__' class FlexDatePostForm(forms.ModelForm): class Meta: model = FlexibleDatePost + fields = '__all__' class BaseCategoryForm(forms.ModelForm): class Meta: model = Category + fields = '__all__' class ArticleForm(forms.ModelForm): class Meta: model = Article + fields = '__all__' class ArticleForm(forms.ModelForm): class Meta: model = Article + fields = '__all__' + class PartialArticleForm(forms.ModelForm): class Meta: model = Article fields = ('headline','pub_date') + class RoykoForm(forms.ModelForm): class Meta: model = Author + fields = '__all__' + class TestArticleForm(forms.ModelForm): class Meta: model = Article + fields = '__all__' + class PartialArticleFormWithSlug(forms.ModelForm): class Meta: model = Article - fields=('headline', 'slug', 'pub_date') + fields = ('headline', 'slug', 'pub_date') + class ArticleStatusForm(forms.ModelForm): class Meta: model = ArticleStatus + fields = '__all__' + class InventoryForm(forms.ModelForm): class Meta: model = Inventory + fields = '__all__' + class SelectInventoryForm(forms.Form): items = forms.ModelMultipleChoiceField(Inventory.objects.all(), to_field_name='barcode') + class CustomFieldForExclusionForm(forms.ModelForm): class Meta: model = CustomFieldForExclusionModel fields = ['name', 'markup'] + class ShortCategory(forms.ModelForm): name = forms.CharField(max_length=5) slug = forms.CharField(max_length=5) @@ -140,30 +167,44 @@ class ShortCategory(forms.ModelForm): class Meta: model = Category + fields = '__all__' + class ImprovedArticleForm(forms.ModelForm): class Meta: model = ImprovedArticle + fields = '__all__' + class ImprovedArticleWithParentLinkForm(forms.ModelForm): class Meta: model = ImprovedArticleWithParentLink + fields = '__all__' + class BetterAuthorForm(forms.ModelForm): class Meta: model = BetterAuthor + fields = '__all__' + class AuthorProfileForm(forms.ModelForm): class Meta: model = AuthorProfile + fields = '__all__' + class TextFileForm(forms.ModelForm): class Meta: model = TextFile + fields = '__all__' + class BigIntForm(forms.ModelForm): class Meta: model = BigInt + fields = '__all__' + class ModelFormWithMedia(forms.ModelForm): class Media: @@ -173,19 +214,25 @@ class ModelFormWithMedia(forms.ModelForm): } class Meta: model = TextFile + fields = '__all__' + class CommaSeparatedIntegerForm(forms.ModelForm): - class Meta: - model = CommaSeparatedInteger + class Meta: + model = CommaSeparatedInteger + fields = '__all__' + class PriceFormWithoutQuantity(forms.ModelForm): class Meta: model = Price exclude = ('quantity',) + class ColourfulItemForm(forms.ModelForm): class Meta: model = ColourfulItem + fields = '__all__' class ModelFormBaseTest(TestCase): @@ -193,6 +240,25 @@ class ModelFormBaseTest(TestCase): self.assertEqual(list(BaseCategoryForm.base_fields), ['name', 'slug', 'url']) + def test_missing_fields_attribute(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", PendingDeprecationWarning) + + class MissingFieldsForm(forms.ModelForm): + class Meta: + model = Category + + # There is some internal state in warnings module which means that + # if a warning has been seen already, the catch_warnings won't + # have recorded it. The following line therefore will not work reliably: + + # self.assertEqual(w[0].category, PendingDeprecationWarning) + + # Until end of the deprecation cycle, should still create the + # form as before: + self.assertEqual(list(MissingFieldsForm.base_fields), + ['name', 'slug', 'url']) + def test_extra_fields(self): class ExtraFields(BaseCategoryForm): some_extra_field = forms.BooleanField() @@ -206,6 +272,33 @@ class ModelFormBaseTest(TestCase): class Meta: model = Category + fields = '__all__' + + self.assertTrue(isinstance(ReplaceField.base_fields['url'], + forms.fields.BooleanField)) + + def test_replace_field_variant_2(self): + # Should have the same result as before, + # but 'fields' attribute specified differently + class ReplaceField(forms.ModelForm): + url = forms.BooleanField() + + class Meta: + model = Category + fields = ['url'] + + self.assertTrue(isinstance(ReplaceField.base_fields['url'], + forms.fields.BooleanField)) + + def test_replace_field_variant_3(self): + # Should have the same result as before, + # but 'fields' attribute specified differently + class ReplaceField(forms.ModelForm): + url = forms.BooleanField() + + class Meta: + model = Category + fields = [] # url will still appear, since it is explicit above self.assertTrue(isinstance(ReplaceField.base_fields['url'], forms.fields.BooleanField)) @@ -216,19 +309,11 @@ class ModelFormBaseTest(TestCase): class Meta: model = Author + fields = '__all__' wf = AuthorForm({'name': 'Richard Lockridge'}) self.assertTrue(wf.is_valid()) - def test_limit_fields(self): - class LimitFields(forms.ModelForm): - class Meta: - model = Category - fields = ['url'] - - self.assertEqual(list(LimitFields.base_fields), - ['url']) - def test_limit_nonexistent_field(self): expected_msg = 'Unknown field(s) (nonexistent) specified for Category' with self.assertRaisesMessage(FieldError, expected_msg): @@ -294,6 +379,7 @@ class ModelFormBaseTest(TestCase): """ class Meta: model = Article + fields = '__all__' # MixModelForm is now an Article-related thing, because MixModelForm.Meta # overrides BaseCategoryForm.Meta. @@ -348,6 +434,7 @@ class ModelFormBaseTest(TestCase): class Meta: model = Category + fields = '__all__' class SubclassMeta(SomeCategoryForm): """ We can also subclass the Meta inner class to change the fields diff --git a/tests/model_forms_regress/tests.py b/tests/model_forms_regress/tests.py index 90c907f2a6..3f15c35938 100644 --- a/tests/model_forms_regress/tests.py +++ b/tests/model_forms_regress/tests.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, unicode_literals from datetime import date +import warnings from django import forms from django.core.exceptions import FieldError, ValidationError @@ -43,9 +44,12 @@ class ModelMultipleChoiceFieldTests(TestCase): f.clean([p.pk for p in Person.objects.all()[8:9]]) self.assertTrue(self._validator_run) + class TripleForm(forms.ModelForm): class Meta: model = Triple + fields = '__all__' + class UniqueTogetherTests(TestCase): def test_multiple_field_unique_together(self): @@ -63,15 +67,18 @@ class UniqueTogetherTests(TestCase): form = TripleForm({'left': '1', 'middle': '3', 'right': '1'}) self.assertTrue(form.is_valid()) + class TripleFormWithCleanOverride(forms.ModelForm): class Meta: model = Triple + fields = '__all__' def clean(self): if not self.cleaned_data['left'] == self.cleaned_data['right']: raise forms.ValidationError('Left and right should be equal') return self.cleaned_data + class OverrideCleanTests(TestCase): def test_override_clean(self): """ @@ -84,6 +91,7 @@ class OverrideCleanTests(TestCase): # by form.full_clean(). self.assertEqual(form.instance.left, 1) + # Regression test for #12960. # Make sure the cleaned_data returned from ModelForm.clean() is applied to the # model instance. @@ -95,6 +103,8 @@ class PublicationForm(forms.ModelForm): class Meta: model = Publication + fields = '__all__' + class ModelFormCleanTest(TestCase): def test_model_form_clean_applies_to_model(self): @@ -103,9 +113,12 @@ class ModelFormCleanTest(TestCase): publication = form.save() self.assertEqual(publication.title, 'TEST') + class FPForm(forms.ModelForm): class Meta: model = FilePathModel + fields = '__all__' + class FilePathFieldTests(TestCase): def test_file_path_field_blank(self): @@ -133,7 +146,8 @@ class ManyToManyCallableInitialTests(TestCase): book3 = Publication.objects.create(title="Third Book", date_published=date(2009,1,1)) # Create a ModelForm, instantiate it, and check that the output is as expected - ModelForm = modelform_factory(Article, formfield_callback=formfield_for_dbfield) + ModelForm = modelform_factory(Article, fields="__all__", + formfield_callback=formfield_for_dbfield) form = ModelForm() self.assertHTMLEqual(form.as_ul(), """
  • Hold down "Control", or "Command" on a Mac, to select more than one.
  • """ % (book1.pk, book2.pk, book3.pk)) + class CFFForm(forms.ModelForm): class Meta: model = CustomFF + fields = '__all__' + class CustomFieldSaveTests(TestCase): def test_save(self): @@ -168,9 +185,12 @@ class ModelChoiceIteratorTests(TestCase): f = Form() self.assertEqual(len(f.fields["publications"].choices), 1) + class RealPersonForm(forms.ModelForm): class Meta: model = RealPerson + fields = '__all__' + class CustomModelFormSaveMethod(TestCase): def test_string_message(self): @@ -230,9 +250,12 @@ class TestTicket11183(TestCase): self.assertTrue(field1 is not ModelChoiceForm.base_fields['person']) self.assertTrue(field1.widget.choices.field is field1) + class HomepageForm(forms.ModelForm): class Meta: model = Homepage + fields = '__all__' + class URLFieldTests(TestCase): def test_url_on_modelform(self): @@ -274,6 +297,7 @@ class FormFieldCallbackTests(TestCase): class Meta: model = Person widgets = {'name': widget} + fields = "__all__" Form = modelform_factory(Person, form=BaseForm) self.assertTrue(Form.base_fields['name'].widget is widget) @@ -285,11 +309,11 @@ class FormFieldCallbackTests(TestCase): widget = forms.Textarea() # Without a widget should not set the widget to textarea - Form = modelform_factory(Person) + Form = modelform_factory(Person, fields="__all__") self.assertNotEqual(Form.base_fields['name'].widget.__class__, forms.Textarea) # With a widget should not set the widget to textarea - Form = modelform_factory(Person, widgets={'name':widget}) + Form = modelform_factory(Person, fields="__all__", widgets={'name':widget}) self.assertEqual(Form.base_fields['name'].widget.__class__, forms.Textarea) def test_custom_callback(self): @@ -307,6 +331,7 @@ class FormFieldCallbackTests(TestCase): class Meta: model = Person widgets = {'name': widget} + fields = "__all__" _ = modelform_factory(Person, form=BaseForm, formfield_callback=callback) @@ -317,7 +342,7 @@ class FormFieldCallbackTests(TestCase): def test_bad_callback(self): # A bad callback provided by user still gives an error - self.assertRaises(TypeError, modelform_factory, Person, + self.assertRaises(TypeError, modelform_factory, Person, fields="__all__", formfield_callback='not a function or callable') @@ -362,6 +387,8 @@ class InvalidFieldAndFactory(TestCase): class DocumentForm(forms.ModelForm): class Meta: model = Document + fields = '__all__' + class FileFieldTests(unittest.TestCase): def test_clean_false(self): @@ -425,6 +452,7 @@ class FileFieldTests(unittest.TestCase): self.assertTrue('something.txt' in rendered) self.assertTrue('myfile-clear' in rendered) + class EditionForm(forms.ModelForm): author = forms.ModelChoiceField(queryset=Person.objects.all()) publication = forms.ModelChoiceField(queryset=Publication.objects.all()) @@ -433,6 +461,8 @@ class EditionForm(forms.ModelForm): class Meta: model = Edition + fields = '__all__' + class UniqueErrorsTests(TestCase): def setUp(self): @@ -473,7 +503,7 @@ class EmptyFieldsTestCase(TestCase): def test_empty_fields_to_construct_instance(self): "No fields should be set on a model instance if construct_instance receives fields=()" - form = modelform_factory(Person)({'name': 'John Doe'}) + form = modelform_factory(Person, fields="__all__")({'name': 'John Doe'}) self.assertTrue(form.is_valid()) instance = construct_instance(form, Person(), fields=()) self.assertEqual(instance.name, '') @@ -485,10 +515,25 @@ class CustomMetaclass(ModelFormMetaclass): new.base_fields = {} return new + class CustomMetaclassForm(six.with_metaclass(CustomMetaclass, forms.ModelForm)): pass + class CustomMetaclassTestCase(TestCase): def test_modelform_factory_metaclass(self): - new_cls = modelform_factory(Person, form=CustomMetaclassForm) + new_cls = modelform_factory(Person, fields="__all__", form=CustomMetaclassForm) self.assertEqual(new_cls.base_fields, {}) + + +class TestTicket19733(TestCase): + def test_modelform_factory_without_fields(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", PendingDeprecationWarning) + # This should become an error once deprecation cycle is complete. + form = modelform_factory(Person) + self.assertEqual(w[0].category, PendingDeprecationWarning) + + def test_modelform_factory_with_all_fields(self): + form = modelform_factory(Person, fields="__all__") + self.assertEqual(form.base_fields.keys(), ["name"]) diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index 8d0c017a61..8cfdf53995 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -21,7 +21,7 @@ from .models import (Author, BetterAuthor, Book, BookWithCustomPK, class DeletionTests(TestCase): def test_deletion(self): - PoetFormSet = modelformset_factory(Poet, can_delete=True) + PoetFormSet = modelformset_factory(Poet, fields="__all__", can_delete=True) poet = Poet.objects.create(name='test') data = { 'form-TOTAL_FORMS': '1', @@ -41,7 +41,7 @@ class DeletionTests(TestCase): Make sure that an add form that is filled out, but marked for deletion doesn't cause validation errors. """ - PoetFormSet = modelformset_factory(Poet, can_delete=True) + PoetFormSet = modelformset_factory(Poet, fields="__all__", can_delete=True) poet = Poet.objects.create(name='test') # One existing untouched and two new unvalid forms data = { @@ -75,7 +75,7 @@ class DeletionTests(TestCase): Make sure that a change form that is filled out, but marked for deletion doesn't cause validation errors. """ - PoetFormSet = modelformset_factory(Poet, can_delete=True) + PoetFormSet = modelformset_factory(Poet, fields="__all__", can_delete=True) poet = Poet.objects.create(name='test') data = { 'form-TOTAL_FORMS': '1', @@ -100,7 +100,7 @@ class DeletionTests(TestCase): class ModelFormsetTest(TestCase): def test_simple_save(self): qs = Author.objects.all() - AuthorFormSet = modelformset_factory(Author, extra=3) + AuthorFormSet = modelformset_factory(Author, fields="__all__", extra=3) formset = AuthorFormSet(queryset=qs) self.assertEqual(len(formset.forms), 3) @@ -138,7 +138,7 @@ class ModelFormsetTest(TestCase): # we'll use it to display them in alphabetical order by name. qs = Author.objects.order_by('name') - AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=False) + AuthorFormSet = modelformset_factory(Author, fields="__all__", extra=1, can_delete=False) formset = AuthorFormSet(queryset=qs) self.assertEqual(len(formset.forms), 3) @@ -176,7 +176,7 @@ class ModelFormsetTest(TestCase): # marked for deletion, make sure we don't save that form. qs = Author.objects.order_by('name') - AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=True) + AuthorFormSet = modelformset_factory(Author, fields="__all__", extra=1, can_delete=True) formset = AuthorFormSet(queryset=qs) self.assertEqual(len(formset.forms), 4) @@ -256,7 +256,7 @@ class ModelFormsetTest(TestCase): author4 = Author.objects.create(name='John Steinbeck') - AuthorMeetingFormSet = modelformset_factory(AuthorMeeting, extra=1, can_delete=True) + AuthorMeetingFormSet = modelformset_factory(AuthorMeeting, fields="__all__", extra=1, can_delete=True) data = { 'form-TOTAL_FORMS': '2', # the number of forms rendered 'form-INITIAL_FORMS': '1', # the number of forms with initial data @@ -294,22 +294,22 @@ class ModelFormsetTest(TestCase): qs = Author.objects.order_by('name') - AuthorFormSet = modelformset_factory(Author, max_num=None, extra=3) + AuthorFormSet = modelformset_factory(Author, fields="__all__", max_num=None, extra=3) formset = AuthorFormSet(queryset=qs) self.assertEqual(len(formset.forms), 6) self.assertEqual(len(formset.extra_forms), 3) - AuthorFormSet = modelformset_factory(Author, max_num=4, extra=3) + AuthorFormSet = modelformset_factory(Author, fields="__all__", max_num=4, extra=3) formset = AuthorFormSet(queryset=qs) self.assertEqual(len(formset.forms), 4) self.assertEqual(len(formset.extra_forms), 1) - AuthorFormSet = modelformset_factory(Author, max_num=0, extra=3) + AuthorFormSet = modelformset_factory(Author, fields="__all__", max_num=0, extra=3) formset = AuthorFormSet(queryset=qs) self.assertEqual(len(formset.forms), 3) self.assertEqual(len(formset.extra_forms), 0) - AuthorFormSet = modelformset_factory(Author, max_num=None) + AuthorFormSet = modelformset_factory(Author, fields="__all__", max_num=None) formset = AuthorFormSet(queryset=qs) self.assertQuerysetEqual(formset.get_queryset(), [ '', @@ -317,7 +317,7 @@ class ModelFormsetTest(TestCase): '', ]) - AuthorFormSet = modelformset_factory(Author, max_num=0) + AuthorFormSet = modelformset_factory(Author, fields="__all__", max_num=0) formset = AuthorFormSet(queryset=qs) self.assertQuerysetEqual(formset.get_queryset(), [ '', @@ -325,7 +325,7 @@ class ModelFormsetTest(TestCase): '', ]) - AuthorFormSet = modelformset_factory(Author, max_num=4) + AuthorFormSet = modelformset_factory(Author, fields="__all__", max_num=4) formset = AuthorFormSet(queryset=qs) self.assertQuerysetEqual(formset.get_queryset(), [ '', @@ -343,7 +343,7 @@ class ModelFormsetTest(TestCase): author.save() return author - PoetFormSet = modelformset_factory(Poet, form=PoetForm) + PoetFormSet = modelformset_factory(Poet, fields="__all__", form=PoetForm) data = { 'form-TOTAL_FORMS': '3', # the number of forms rendered @@ -387,7 +387,7 @@ class ModelFormsetTest(TestCase): self.assertFalse("subtitle" in formset.forms[0].fields) def test_model_inheritance(self): - BetterAuthorFormSet = modelformset_factory(BetterAuthor) + BetterAuthorFormSet = modelformset_factory(BetterAuthor, fields="__all__") formset = BetterAuthorFormSet() self.assertEqual(len(formset.forms), 1) self.assertHTMLEqual(formset.forms[0].as_p(), @@ -440,7 +440,7 @@ class ModelFormsetTest(TestCase): # We can also create a formset that is tied to a parent model. This is # how the admin system's edit inline functionality works. - AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=3) + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=3, fields="__all__") author = Author.objects.create(name='Charles Baudelaire') formset = AuthorBooksFormSet(instance=author) @@ -474,7 +474,7 @@ class ModelFormsetTest(TestCase): # another one. This time though, an edit form will be available for # every existing book. - AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2) + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2, fields="__all__") author = Author.objects.get(name='Charles Baudelaire') formset = AuthorBooksFormSet(instance=author) @@ -514,7 +514,7 @@ class ModelFormsetTest(TestCase): def test_inline_formsets_save_as_new(self): # The save_as_new parameter lets you re-associate the data to a new # instance. This is used in the admin for save_as functionality. - AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2) + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2, fields="__all__") author = Author.objects.create(name='Charles Baudelaire') data = { @@ -553,7 +553,7 @@ class ModelFormsetTest(TestCase): # primary key that is not the fk to the parent object. self.maxDiff = 1024 - AuthorBooksFormSet2 = inlineformset_factory(Author, BookWithCustomPK, can_delete=False, extra=1) + AuthorBooksFormSet2 = inlineformset_factory(Author, BookWithCustomPK, can_delete=False, extra=1, fields="__all__") author = Author.objects.create(pk=1, name='Charles Baudelaire') formset = AuthorBooksFormSet2(instance=author) @@ -585,7 +585,7 @@ class ModelFormsetTest(TestCase): # Test inline formsets where the inline-edited object uses multi-table # inheritance, thus has a non AutoField yet auto-created primary key. - AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1) + AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1, fields="__all__") author = Author.objects.create(pk=1, name='Charles Baudelaire') formset = AuthorBooksFormSet3(instance=author) @@ -616,7 +616,7 @@ class ModelFormsetTest(TestCase): # Test inline formsets where the inline-edited object has a # unique_together constraint with a nullable member - AuthorBooksFormSet4 = inlineformset_factory(Author, BookWithOptionalAltEditor, can_delete=False, extra=2) + AuthorBooksFormSet4 = inlineformset_factory(Author, BookWithOptionalAltEditor, can_delete=False, extra=2, fields="__all__") author = Author.objects.create(pk=1, name='Charles Baudelaire') data = { @@ -640,7 +640,7 @@ class ModelFormsetTest(TestCase): self.assertEqual(book2.title, 'Les Fleurs du Mal') def test_inline_formsets_with_custom_save_method(self): - AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2) + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2, fields="__all__") author = Author.objects.create(pk=1, name='Charles Baudelaire') book1 = Book.objects.create(pk=1, author=author, title='Les Paradis Artificiels') book2 = Book.objects.create(pk=2, author=author, title='Les Fleurs du Mal') @@ -655,7 +655,7 @@ class ModelFormsetTest(TestCase): poem.save() return poem - PoemFormSet = inlineformset_factory(Poet, Poem, form=PoemForm) + PoemFormSet = inlineformset_factory(Poet, Poem, form=PoemForm, fields="__all__") data = { 'poem_set-TOTAL_FORMS': '3', # the number of forms rendered @@ -732,7 +732,7 @@ class ModelFormsetTest(TestCase): def test_custom_pk(self): # We need to ensure that it is displayed - CustomPrimaryKeyFormSet = modelformset_factory(CustomPrimaryKey) + CustomPrimaryKeyFormSet = modelformset_factory(CustomPrimaryKey, fields="__all__") formset = CustomPrimaryKeyFormSet() self.assertEqual(len(formset.forms), 1) self.assertHTMLEqual(formset.forms[0].as_p(), @@ -743,7 +743,7 @@ class ModelFormsetTest(TestCase): place = Place.objects.create(pk=1, name='Giordanos', city='Chicago') - FormSet = inlineformset_factory(Place, Owner, extra=2, can_delete=False) + FormSet = inlineformset_factory(Place, Owner, extra=2, can_delete=False, fields="__all__") formset = FormSet(instance=place) self.assertEqual(len(formset.forms), 2) self.assertHTMLEqual(formset.forms[0].as_p(), @@ -799,7 +799,7 @@ class ModelFormsetTest(TestCase): # Ensure a custom primary key that is a ForeignKey or OneToOneField get rendered for the user to choose. - FormSet = modelformset_factory(OwnerProfile) + FormSet = modelformset_factory(OwnerProfile, fields="__all__") formset = FormSet() self.assertHTMLEqual(formset.forms[0].as_p(), '

    ' % (band2.id, self.band.id)) class AdminConcertForm(forms.ModelForm): - class Meta: - model = Concert + pass def __init__(self, *args, **kwargs): super(AdminConcertForm, self).__init__(*args, **kwargs) @@ -685,9 +681,6 @@ class ValidationTests(unittest.TestCase): class AdminBandForm(forms.ModelForm): delete = forms.BooleanField() - class Meta: - model = Band - class BandAdmin(ModelAdmin): form = AdminBandForm diff --git a/tests/timezones/forms.py b/tests/timezones/forms.py index 3c9c31167e..45fb1d080b 100644 --- a/tests/timezones/forms.py +++ b/tests/timezones/forms.py @@ -11,3 +11,4 @@ class EventSplitForm(forms.Form): class EventModelForm(forms.ModelForm): class Meta: model = Event + fields = '__all__' -- cgit v1.3 From 9a3708cec8cf76165afd0fd46d1267d7cae772ca Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 9 May 2013 19:33:02 +0200 Subject: Fixed a test that fails under Python 3. --- tests/model_forms_regress/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/model_forms_regress/tests.py b/tests/model_forms_regress/tests.py index 3f15c35938..0e033e033f 100644 --- a/tests/model_forms_regress/tests.py +++ b/tests/model_forms_regress/tests.py @@ -536,4 +536,4 @@ class TestTicket19733(TestCase): def test_modelform_factory_with_all_fields(self): form = modelform_factory(Person, fields="__all__") - self.assertEqual(form.base_fields.keys(), ["name"]) + self.assertEqual(list(form.base_fields), ["name"]) -- cgit v1.3