Browse Source

Fixed #27068 -- Unified form field initial data retrieval.

Jon Dufresne 8 years ago
parent
commit
f5c6d3c8d9

+ 9 - 14
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):

+ 15 - 5
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."

+ 8 - 0
docs/ref/forms/api.txt

@@ -248,6 +248,14 @@ precedence::
     <tr><th>Url:</th><td><input type="url" name="url" required /></td></tr>
     <tr><th>Comment:</th><td><input type="text" name="comment" required /></td></tr>
 
+.. 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
 ====================================
 

+ 4 - 0
docs/releases/1.11.txt

@@ -187,6 +187,10 @@ Forms
 * The new :attr:`CharField.empty_value <django.forms.CharField.empty_value>`
   attribute allows specifying the Python value to use to represent "empty".
 
+* The new :meth:`Form.get_initial_for_field()
+  <django.forms.Form.get_initial_for_field>` method returns initial data for a
+  form field.
+
 Generic Views
 ~~~~~~~~~~~~~
 

+ 53 - 0
tests/forms_tests/tests/test_forms.py

@@ -1904,6 +1904,21 @@ Password: <input type="password" name="password" required /></li>
 </select></li>"""
         )
 
+    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: <input type="password" name="password" required /></li>
         # 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: <input type="password" name="password" required /></li>
         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: <input type="password" name="password" required />
             '<tr><th>File1:</th><td><input type="file" name="file1" /></td></tr>',
         )
 
+    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)