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

This commit is contained in:
Jon Dufresne 2016-08-18 17:55:47 -07:00 committed by GitHub
parent 13857b45ca
commit f5c6d3c8d9
5 changed files with 89 additions and 19 deletions

View File

@ -129,12 +129,9 @@ class BoundField(object):
Returns the value for this BoundField, using the initial value if Returns the value for this BoundField, using the initial value if
the form is not bound or the data otherwise. the form is not bound or the data otherwise.
""" """
if not self.form.is_bound: data = self.initial
data = self.initial if self.form.is_bound:
else: data = self.field.bound_data(self.data, data)
data = self.field.bound_data(
self.data, self.form.initial.get(self.name, self.field.initial)
)
return self.field.prepare_value(data) return self.field.prepare_value(data)
def label_tag(self, contents=None, attrs=None, label_suffix=None): def label_tag(self, contents=None, attrs=None, label_suffix=None):
@ -218,14 +215,12 @@ class BoundField(object):
@cached_property @cached_property
def initial(self): def initial(self):
data = self.form.initial.get(self.name, self.field.initial) data = self.form.get_initial_for_field(self.field, self.name)
if callable(data): # If this is an auto-generated default date, nix the microseconds for
data = data() # standardized handling. See #22502.
# If this is an auto-generated default date, nix the microseconds if (isinstance(data, (datetime.datetime, datetime.time)) and
# for standardized handling. See #22502. not self.field.widget.supports_microseconds):
if (isinstance(data, (datetime.datetime, datetime.time)) and data = data.replace(microsecond=0)
not self.field.widget.supports_microseconds):
data = data.replace(microsecond=0)
return data return data
def build_widget_attrs(self, attrs, widget=None): def build_widget_attrs(self, attrs, widget=None):

View File

@ -377,12 +377,12 @@ class BaseForm(object):
# Each widget type knows how to retrieve its own data, because some # Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields. # widgets split data over several HTML fields.
if field.disabled: if field.disabled:
value = self.initial.get(name, field.initial) value = self.get_initial_for_field(field, name)
else: else:
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
try: try:
if isinstance(field, FileField): if isinstance(field, FileField):
initial = self.initial.get(name, field.initial) initial = self.get_initial_for_field(field, name)
value = field.clean(value, initial) value = field.clean(value, initial)
else: else:
value = field.clean(value) value = field.clean(value)
@ -431,9 +431,9 @@ class BaseForm(object):
prefixed_name = self.add_prefix(name) prefixed_name = self.add_prefix(name)
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name) data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
if not field.show_hidden_initial: if not field.show_hidden_initial:
initial_value = self.initial.get(name, field.initial) # Use the BoundField's initial as this is the value passed to
if callable(initial_value): # the widget.
initial_value = initial_value() initial_value = self[name].initial
else: else:
initial_prefixed_name = self.add_initial_prefix(name) initial_prefixed_name = self.add_initial_prefix(name)
hidden_widget = field.hidden_widget() hidden_widget = field.hidden_widget()
@ -482,6 +482,16 @@ class BaseForm(object):
""" """
return [field for field in self if not field.is_hidden] 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)): class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)):
"A collection of Fields, plus their associated data." "A collection of Fields, plus their associated data."

View File

@ -248,6 +248,14 @@ precedence::
<tr><th>Url:</th><td><input type="url" name="url" required /></td></tr> <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> <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 Checking which form data has changed
==================================== ====================================

View File

@ -187,6 +187,10 @@ Forms
* The new :attr:`CharField.empty_value <django.forms.CharField.empty_value>` * The new :attr:`CharField.empty_value <django.forms.CharField.empty_value>`
attribute allows specifying the Python value to use to represent "empty". 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 Generic Views
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -1904,6 +1904,21 @@ Password: <input type="password" name="password" required /></li>
</select></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): def test_changed_data(self):
class Person(Form): class Person(Form):
first_name = CharField(initial='Hans') first_name = CharField(initial='Hans')
@ -1960,6 +1975,19 @@ Password: <input type="password" name="password" required /></li>
# BoundField is also cached # BoundField is also cached
self.assertIs(form['name'], name) 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): def test_boundfield_rendering(self):
""" """
Python 2 issue: Test that rendering a BoundField with bytestring content 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['hi_without_microsec'].value(), now_no_ms)
self.assertEqual(unbound['ti_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): def test_help_text(self):
# You can specify descriptive text for a field by using the 'help_text' argument) # You can specify descriptive text for a field by using the 'help_text' argument)
class UserRegistration(Form): 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>', '<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): def test_basic_processing_in_view(self):
class UserRegistration(Form): class UserRegistration(Form):
username = CharField(max_length=10) username = CharField(max_length=10)