Fixed #27068 -- Unified form field initial data retrieval.
This commit is contained in:
parent
13857b45ca
commit
f5c6d3c8d9
|
@ -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):
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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
|
||||||
====================================
|
====================================
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue