Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #27039 -- Fixed fallback to model field default in model forms. #7068

Merged
merged 1 commit into from Aug 24, 2016
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -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

This comment has been minimized.

Copy link
@RobertAKARobin

RobertAKARobin May 31, 2019

Contributor

@timgraham This caused me a bit of a debugging headache. I expected that if I did something like this:

def clean(self):
    self.cleaned_data['some_field'] = 'some_value'

...that would be sufficient to set what was ultimately saved to the database. But I found that some_field was always being saved with its default value. It took me a while to figure out why, from looking at the Django source. Now I'm doing this, with success:

def __init__(self, *args, **kwargs):
    super(MyForm, self).__init__(*args, **kwargs)
    self.data = self.data.copy()
    self.data['some_field'] = 'some_value'

I'm just curious what the reasoning is behind the rule you implemented here?

This comment has been minimized.

Copy link
@timgraham

timgraham May 31, 2019

Author Member

Your expectation seems reasonable. If there's a way to fix that while keeping backwards compatibility, feel free to offer a patch.

This comment has been minimized.

Copy link
@RobertAKARobin

RobertAKARobin May 31, 2019

Contributor

Do Django's automated tests ensure backward compatibility? If not I probably don't have enough familiarity with Django to do so. Glad I'm not taking crazy pills, though. :)

# 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):
@@ -481,6 +481,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
@@ -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`).
@@ -332,6 +332,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
@@ -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):
@@ -565,6 +565,42 @@ class Meta:
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:
@@ -774,7 +810,10 @@ def test_abstract_inherited_unique(self):
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.'])
@@ -2491,7 +2530,7 @@ def test_callable_field_default(self):
class PublicationDefaultsForm(forms.ModelForm):
class Meta:
model = PublicationDefaults
fields = '__all__'
fields = ('title', 'date_published', 'mode', 'category')

self.maxDiff = 2000
form = PublicationDefaultsForm()
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.