Fixed #32920 -- Changed BaseForm to access its values through bound fields.

This commit is contained in:
Chris Jerdonek 2021-07-13 05:22:26 -04:00 committed by Carlton Gibson
parent 84400d2e9d
commit 90a33ab2ce
2 changed files with 54 additions and 20 deletions

View File

@ -143,7 +143,13 @@ class BaseForm:
'fields': ';'.join(self.fields), 'fields': ';'.join(self.fields),
} }
def _bound_items(self):
"""Yield (name, bf) pairs, where bf is a BoundField object."""
for name in self.fields:
yield name, self[name]
def __iter__(self): def __iter__(self):
"""Yield the form's fields as BoundField objects."""
for name in self.fields: for name in self.fields:
yield self[name] yield self[name]
@ -206,9 +212,9 @@ class BaseForm:
top_errors = self.non_field_errors().copy() top_errors = self.non_field_errors().copy()
output, hidden_fields = [], [] output, hidden_fields = [], []
for name, field in self.fields.items(): for name, bf in self._bound_items():
field = bf.field
html_class_attr = '' html_class_attr = ''
bf = self[name]
bf_errors = self.error_class(bf.errors) bf_errors = self.error_class(bf.errors)
if bf.is_hidden: if bf.is_hidden:
if bf_errors: if bf_errors:
@ -387,15 +393,12 @@ class BaseForm:
self._post_clean() self._post_clean()
def _clean_fields(self): def _clean_fields(self):
for name, field in self.fields.items(): for name, bf in self._bound_items():
if field.disabled: field = bf.field
value = self.get_initial_for_field(field, name) value = bf.initial if field.disabled else bf.data
else:
value = self._field_data_value(field, self.add_prefix(name))
try: try:
if isinstance(field, FileField): if isinstance(field, FileField):
initial = self.get_initial_for_field(field, name) value = field.clean(value, bf.initial)
value = field.clean(value, initial)
else: else:
value = field.clean(value) value = field.clean(value)
self.cleaned_data[name] = value self.cleaned_data[name] = value
@ -437,24 +440,23 @@ class BaseForm:
@cached_property @cached_property
def changed_data(self): def changed_data(self):
data = [] data = []
for name, field in self.fields.items(): for name, bf in self._bound_items():
data_value = self._field_data_value(field, self.add_prefix(name)) field = bf.field
if not field.show_hidden_initial: if not field.show_hidden_initial:
# Use the BoundField's initial as this is the value passed to # Use the BoundField's initial as this is the value passed to
# the widget. # the widget.
initial_value = self[name].initial initial_value = bf.initial
else: else:
initial_prefixed_name = self.add_initial_prefix(name)
hidden_widget = field.hidden_widget() hidden_widget = field.hidden_widget()
try: try:
initial_value = field.to_python( initial_value = field.to_python(
self._widget_data_value(hidden_widget, initial_prefixed_name) self._widget_data_value(hidden_widget, bf.html_initial_name)
) )
except ValidationError: except ValidationError:
# Always assume data has changed if validation fails. # Always assume data has changed if validation fails.
data.append(name) data.append(name)
continue continue
if field.has_changed(initial_value, data_value): if field.has_changed(initial_value, bf.data):
data.append(name) data.append(name)
return data return data

View File

@ -2112,15 +2112,47 @@ 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): def get_datetime_form_with_callable_initial(self, disabled, microseconds=0):
now = datetime.datetime(2006, 10, 25, 14, 30, 45, 123456) class FakeTime:
def __init__(self):
self.elapsed_seconds = 0
def now(self):
self.elapsed_seconds += 1
return datetime.datetime(
2006, 10, 25, 14, 30, 45 + self.elapsed_seconds,
microseconds,
)
class DateTimeForm(forms.Form): class DateTimeForm(forms.Form):
dt = DateTimeField(initial=lambda: now, disabled=True) dt = DateTimeField(initial=FakeTime().now, disabled=disabled)
form = DateTimeForm({}) return DateTimeForm({})
def test_datetime_clean_disabled_callable_initial_microseconds(self):
"""
Cleaning a form with a disabled DateTimeField and callable initial
removes microseconds.
"""
form = self.get_datetime_form_with_callable_initial(
disabled=True, microseconds=123456,
)
self.assertEqual(form.errors, {}) self.assertEqual(form.errors, {})
self.assertEqual(form.cleaned_data, {'dt': now}) self.assertEqual(form.cleaned_data, {
'dt': datetime.datetime(2006, 10, 25, 14, 30, 46),
})
def test_datetime_clean_disabled_callable_initial_bound_field(self):
"""
The cleaned value for a form with a disabled DateTimeField and callable
initial matches the bound field's cached initial value.
"""
form = self.get_datetime_form_with_callable_initial(disabled=True)
self.assertEqual(form.errors, {})
cleaned = form.cleaned_data['dt']
self.assertEqual(cleaned, datetime.datetime(2006, 10, 25, 14, 30, 46))
bf = form['dt']
self.assertEqual(cleaned, bf.initial)
def test_datetime_changed_data_callable_with_microseconds(self): def test_datetime_changed_data_callable_with_microseconds(self):
class DateTimeForm(forms.Form): class DateTimeForm(forms.Form):