diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py index a9b4c5e063..e68744dbdc 100644 --- a/django/forms/boundfield.py +++ b/django/forms/boundfield.py @@ -129,12 +129,9 @@ class BoundField(object): Returns the value for this BoundField, using the initial value if the form is not bound or the data otherwise. """ - if not self.form.is_bound: - data = self.initial - else: - data = self.field.bound_data( - self.data, self.form.initial.get(self.name, self.field.initial) - ) + data = self.initial + if self.form.is_bound: + data = self.field.bound_data(self.data, data) return self.field.prepare_value(data) def label_tag(self, contents=None, attrs=None, label_suffix=None): @@ -218,14 +215,12 @@ class BoundField(object): @cached_property def initial(self): - data = self.form.initial.get(self.name, self.field.initial) - if callable(data): - data = data() - # If this is an auto-generated default date, nix the microseconds - # for standardized handling. See #22502. - if (isinstance(data, (datetime.datetime, datetime.time)) and - not self.field.widget.supports_microseconds): - data = data.replace(microsecond=0) + data = self.form.get_initial_for_field(self.field, self.name) + # If this is an auto-generated default date, nix the microseconds for + # standardized handling. See #22502. + if (isinstance(data, (datetime.datetime, datetime.time)) and + not self.field.widget.supports_microseconds): + data = data.replace(microsecond=0) return data def build_widget_attrs(self, attrs, widget=None): diff --git a/django/forms/forms.py b/django/forms/forms.py index 25e081e324..17d4598f4c 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -377,12 +377,12 @@ class BaseForm(object): # Each widget type knows how to retrieve its own data, because some # widgets split data over several HTML fields. if field.disabled: - value = self.initial.get(name, field.initial) + value = self.get_initial_for_field(field, name) else: value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) try: if isinstance(field, FileField): - initial = self.initial.get(name, field.initial) + initial = self.get_initial_for_field(field, name) value = field.clean(value, initial) else: value = field.clean(value) @@ -431,9 +431,9 @@ class BaseForm(object): prefixed_name = self.add_prefix(name) data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name) if not field.show_hidden_initial: - initial_value = self.initial.get(name, field.initial) - if callable(initial_value): - initial_value = initial_value() + # Use the BoundField's initial as this is the value passed to + # the widget. + initial_value = self[name].initial else: initial_prefixed_name = self.add_initial_prefix(name) hidden_widget = field.hidden_widget() @@ -482,6 +482,16 @@ class BaseForm(object): """ return [field for field in self if not field.is_hidden] + def get_initial_for_field(self, field, field_name): + """ + Return initial data for field on form. Use initial data from the form + or the field, in that order. Evaluate callable values. + """ + value = self.initial.get(field_name, field.initial) + if callable(value): + value = value() + return value + class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)): "A collection of Fields, plus their associated data." diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 094be3a565..8849ce284d 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -248,6 +248,14 @@ precedence:: Url: Comment: +.. method:: Form.get_initial_for_field(field, field_name) + +.. versionadded:: 1.11 + +Use :meth:`~Form.get_initial_for_field()` to retrieve initial data for a form +field. It retrieves data from :attr:`Form.initial` and :attr:`Field.initial`, +in that order, and evaluates any callable initial values. + Checking which form data has changed ==================================== diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index c8fc03c2b6..41baeb40dd 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -187,6 +187,10 @@ Forms * The new :attr:`CharField.empty_value ` attribute allows specifying the Python value to use to represent "empty". +* The new :meth:`Form.get_initial_for_field() + ` method returns initial data for a + form field. + Generic Views ~~~~~~~~~~~~~ diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 0e0ea9f1c2..183caa354c 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -1904,6 +1904,21 @@ Password: """ ) + def test_get_initial_for_field(self): + class PersonForm(Form): + first_name = CharField(initial='John') + last_name = CharField(initial='Doe') + age = IntegerField() + occupation = CharField(initial=lambda: 'Unknown') + + form = PersonForm(initial={'first_name': 'Jane'}) + self.assertEqual(form.get_initial_for_field(form.fields['age'], 'age'), None) + self.assertEqual(form.get_initial_for_field(form.fields['last_name'], 'last_name'), 'Doe') + # Form.initial overrides Field.initial. + self.assertEqual(form.get_initial_for_field(form.fields['first_name'], 'first_name'), 'Jane') + # Callables are evaluated. + self.assertEqual(form.get_initial_for_field(form.fields['occupation'], 'occupation'), 'Unknown') + def test_changed_data(self): class Person(Form): first_name = CharField(initial='Hans') @@ -1960,6 +1975,19 @@ Password: # BoundField is also cached self.assertIs(form['name'], name) + def test_boundfield_value_disabled_callable_initial(self): + class PersonForm(Form): + name = CharField(initial=lambda: 'John Doe', disabled=True) + + # Without form data. + form = PersonForm() + self.assertEqual(form['name'].value(), 'John Doe') + + # With form data. As the field is disabled, the value should not be + # affected by the form data. + form = PersonForm({}) + self.assertEqual(form['name'].value(), 'John Doe') + def test_boundfield_rendering(self): """ Python 2 issue: Test that rendering a BoundField with bytestring content @@ -2021,6 +2049,23 @@ Password: self.assertEqual(unbound['hi_without_microsec'].value(), now_no_ms) self.assertEqual(unbound['ti_without_microsec'].value(), now_no_ms) + def test_datetime_clean_initial_callable_disabled(self): + now = datetime.datetime(2006, 10, 25, 14, 30, 45, 123456) + + class DateTimeForm(forms.Form): + dt = DateTimeField(initial=lambda: now, disabled=True) + + form = DateTimeForm({}) + self.assertEqual(form.errors, {}) + self.assertEqual(form.cleaned_data, {'dt': now}) + + def test_datetime_changed_data_callable_with_microseconds(self): + class DateTimeForm(forms.Form): + dt = DateTimeField(initial=lambda: datetime.datetime(2006, 10, 25, 14, 30, 45, 123456), disabled=True) + + form = DateTimeForm({'dt': '2006-10-25 14:30:45'}) + self.assertEqual(form.changed_data, []) + def test_help_text(self): # You can specify descriptive text for a field by using the 'help_text' argument) class UserRegistration(Form): @@ -2369,6 +2414,14 @@ Password: 'File1:', ) + def test_filefield_initial_callable(self): + class FileForm(forms.Form): + file1 = forms.FileField(initial=lambda: 'resume.txt') + + f = FileForm({}) + self.assertEqual(f.errors, {}) + self.assertEqual(f.cleaned_data['file1'], 'resume.txt') + def test_basic_processing_in_view(self): class UserRegistration(Form): username = CharField(max_length=10)