From 90a33ab2ceddef7f2cdd11612f77ea9296cc7fb9 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 13 Jul 2021 05:22:26 -0400 Subject: [PATCH] Fixed #32920 -- Changed BaseForm to access its values through bound fields. --- django/forms/forms.py | 32 ++++++++++---------- tests/forms_tests/tests/test_forms.py | 42 +++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/django/forms/forms.py b/django/forms/forms.py index d14037bfe9..40ed71ed5c 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -143,7 +143,13 @@ class BaseForm: '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): + """Yield the form's fields as BoundField objects.""" for name in self.fields: yield self[name] @@ -206,9 +212,9 @@ class BaseForm: top_errors = self.non_field_errors().copy() output, hidden_fields = [], [] - for name, field in self.fields.items(): + for name, bf in self._bound_items(): + field = bf.field html_class_attr = '' - bf = self[name] bf_errors = self.error_class(bf.errors) if bf.is_hidden: if bf_errors: @@ -387,15 +393,12 @@ class BaseForm: self._post_clean() def _clean_fields(self): - for name, field in self.fields.items(): - if field.disabled: - value = self.get_initial_for_field(field, name) - else: - value = self._field_data_value(field, self.add_prefix(name)) + for name, bf in self._bound_items(): + field = bf.field + value = bf.initial if field.disabled else bf.data try: if isinstance(field, FileField): - initial = self.get_initial_for_field(field, name) - value = field.clean(value, initial) + value = field.clean(value, bf.initial) else: value = field.clean(value) self.cleaned_data[name] = value @@ -437,24 +440,23 @@ class BaseForm: @cached_property def changed_data(self): data = [] - for name, field in self.fields.items(): - data_value = self._field_data_value(field, self.add_prefix(name)) + for name, bf in self._bound_items(): + field = bf.field if not field.show_hidden_initial: # Use the BoundField's initial as this is the value passed to # the widget. - initial_value = self[name].initial + initial_value = bf.initial else: - initial_prefixed_name = self.add_initial_prefix(name) hidden_widget = field.hidden_widget() try: 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: # Always assume data has changed if validation fails. data.append(name) continue - if field.has_changed(initial_value, data_value): + if field.has_changed(initial_value, bf.data): data.append(name) return data diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index e1567f12ce..d1615f21f8 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -2112,15 +2112,47 @@ 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) + def get_datetime_form_with_callable_initial(self, disabled, microseconds=0): + 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): - 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.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): class DateTimeForm(forms.Form):