summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Graham <timograham@gmail.com>2016-08-11 15:18:48 -0400
committerTim Graham <timograham@gmail.com>2016-08-24 19:37:04 -0400
commit325dd0befea3012c42eefa061f509fbdf1b6a8aa (patch)
tree87d026fd736c1f8985cb1882e6a42e4bf7e76776
parentc4ee93128f1880c3993cf07619ff4e0524920a8e (diff)
[1.10.x] Fixed #27039 -- Fixed empty data fallback to model field default in model forms.
Backport of 4bc6b939944183533ae74791d21282e613f63a96 from master
-rw-r--r--django/forms/models.py5
-rw-r--r--django/forms/widgets.py4
-rw-r--r--docs/releases/1.10.1.txt3
-rw-r--r--docs/topics/forms/modelforms.txt15
-rw-r--r--tests/model_forms/models.py1
-rw-r--r--tests/model_forms/tests.py43
6 files changed, 69 insertions, 2 deletions
diff --git a/django/forms/models.py b/django/forms/models.py
index 15e251abe7..b3aab63124 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -52,6 +52,11 @@ def construct_instance(form, instance, fields=None, exclude=None):
continue
if exclude and f.name in exclude:
continue
+ # Leave defaults for fields that aren't in POST data, except for
+ # checkbox inputs because they don't appear in POST data if not checked.
+ if (f.has_default() and f.name not in form.data and
+ not getattr(form[f.name].field.widget, 'dont_use_model_field_default_for_empty_data', False)):
+ continue
# Defer saving file-type fields until after the other fields, so a
# callable upload_to can use the values from other fields.
if isinstance(f, models.FileField):
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 5eb6ed089b..735207aa80 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -480,6 +480,10 @@ def boolean_check(v):
class CheckboxInput(Widget):
+ # Don't use model field defaults for fields that aren't in POST data,
+ # because checkboxes don't appear in POST data if not checked.
+ dont_use_model_field_default_for_empty_data = True
+
def __init__(self, attrs=None, check_test=None):
super(CheckboxInput, self).__init__(attrs)
# check_test is a callable that takes a value and returns True
diff --git a/docs/releases/1.10.1.txt b/docs/releases/1.10.1.txt
index 2fd860527a..719f536b29 100644
--- a/docs/releases/1.10.1.txt
+++ b/docs/releases/1.10.1.txt
@@ -79,3 +79,6 @@ Bugfixes
* Reallowed subclassing ``UserCreationForm`` without ``USERNAME_FIELD`` in
``Meta.fields`` (:ticket:`27111`).
+
+* Fixed a regression in model forms where model fields with a ``default`` that
+ didn't appear in POST data no longer used the ``default`` (:ticket:`27039`).
diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
index df6bd6bd38..e2b57fe410 100644
--- a/docs/topics/forms/modelforms.txt
+++ b/docs/topics/forms/modelforms.txt
@@ -330,6 +330,21 @@ Note that if the form :ref:`hasn't been validated
``form.errors``. A ``ValueError`` will be raised if the data in the form
doesn't validate -- i.e., if ``form.errors`` evaluates to ``True``.
+If an optional field doesn't appear in the form's data, the resulting model
+instance uses the model field :attr:`~django.db.models.Field.default`, if
+there is one, for that field. This behavior doesn't apply to fields that use
+:class:`~django.forms.CheckboxInput` (or any custom widget with
+``dont_use_model_field_default_for_empty_data=True``) since an unchecked
+checkbox doesn't appear in the data of an HTML form submission. Use a custom
+form field or widget if you're designing an API and want the default fallback
+for a ``BooleanField``.
+
+.. versionchanged:: 1.10.1
+
+ Older versions don't have the exception for
+ :class:`~django.forms.CheckboxInput` which means that unchecked checkboxes
+ receive a value of ``True`` if that's the model field default.
+
This ``save()`` method accepts an optional ``commit`` keyword argument, which
accepts either ``True`` or ``False``. If you call ``save()`` with
``commit=False``, then it will return an object that hasn't yet been saved to
diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py
index 2c3ff26cbe..dad245ba78 100644
--- a/tests/model_forms/models.py
+++ b/tests/model_forms/models.py
@@ -122,6 +122,7 @@ class PublicationDefaults(models.Model):
date_published = models.DateField(default=datetime.date.today)
mode = models.CharField(max_length=2, choices=MODE_CHOICES, default=default_mode)
category = models.IntegerField(choices=CATEGORY_CHOICES, default=default_category)
+ active = models.BooleanField(default=True)
class Author(models.Model):
diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py
index f03c80fb6b..9d3d440fdb 100644
--- a/tests/model_forms/tests.py
+++ b/tests/model_forms/tests.py
@@ -549,6 +549,42 @@ class ModelFormBaseTest(TestCase):
self.assertEqual(list(OrderFields2.base_fields),
['slug', 'name'])
+ def test_default_populated_on_optional_field(self):
+ class PubForm(forms.ModelForm):
+ mode = forms.CharField(max_length=255, required=False)
+
+ class Meta:
+ model = PublicationDefaults
+ fields = ('mode',)
+
+ # Empty data uses the model field default.
+ mf1 = PubForm({})
+ self.assertEqual(mf1.errors, {})
+ m1 = mf1.save(commit=False)
+ self.assertEqual(m1.mode, 'di')
+ self.assertEqual(m1._meta.get_field('mode').get_default(), 'di')
+
+ # Blank data doesn't use the model field default.
+ mf2 = PubForm({'mode': ''})
+ self.assertEqual(mf2.errors, {})
+ m2 = mf2.save(commit=False)
+ self.assertEqual(m2.mode, '')
+
+ def test_default_not_populated_on_optional_checkbox_input(self):
+ class PubForm(forms.ModelForm):
+ class Meta:
+ model = PublicationDefaults
+ fields = ('active',)
+
+ # Empty data doesn't use the model default because CheckboxInput
+ # doesn't have a value in HTML form submission.
+ mf1 = PubForm({})
+ self.assertEqual(mf1.errors, {})
+ m1 = mf1.save(commit=False)
+ self.assertIs(m1.active, False)
+ self.assertIsInstance(mf1.fields['active'].widget, forms.CheckboxInput)
+ self.assertIs(m1._meta.get_field('active').get_default(), True)
+
class FieldOverridesByFormMetaForm(forms.ModelForm):
class Meta:
@@ -758,7 +794,10 @@ class UniqueTest(TestCase):
title = 'Boss'
isbn = '12345'
DerivedBook.objects.create(title=title, author=self.writer, isbn=isbn)
- form = DerivedBookForm({'title': 'Other', 'author': self.writer.pk, 'isbn': isbn})
+ form = DerivedBookForm({
+ 'title': 'Other', 'author': self.writer.pk, 'isbn': isbn,
+ 'suffix1': '1', 'suffix2': '2',
+ })
self.assertFalse(form.is_valid())
self.assertEqual(len(form.errors), 1)
self.assertEqual(form.errors['isbn'], ['Derived book with this Isbn already exists.'])
@@ -2443,7 +2482,7 @@ class OtherModelFormTests(TestCase):
class PublicationDefaultsForm(forms.ModelForm):
class Meta:
model = PublicationDefaults
- fields = '__all__'
+ fields = ('title', 'date_published', 'mode', 'category')
self.maxDiff = 2000
form = PublicationDefaultsForm()