diff --git a/django/forms/forms.py b/django/forms/forms.py index 2d293b2ace1..16fc5b00f24 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -31,6 +31,8 @@ def pretty_name(name): return '' return name.replace('_', ' ').capitalize() +UNSET = object() + def get_declared_fields(bases, attrs, with_base_fields=True): """ @@ -134,6 +136,7 @@ class BaseForm(object): # Instances should always modify self.fields; they should not modify # self.base_fields. self.fields = copy.deepcopy(self.base_fields) + self._bound_fields_cache = {} def __str__(self): return self.as_table() @@ -161,7 +164,9 @@ class BaseForm(object): except KeyError: raise KeyError( "Key %r not found in '%s'" % (name, self.__class__.__name__)) - return BoundField(self, field, name) + if name not in self._bound_fields_cache: + self._bound_fields_cache[name] = BoundField(self, field, name) + return self._bound_fields_cache[name] @property def errors(self): @@ -527,6 +532,7 @@ class BoundField(object): else: self.label = self.field.label self.help_text = field.help_text or '' + self._initial_value = UNSET def __str__(self): """Renders this field as an HTML widget.""" @@ -621,12 +627,16 @@ class BoundField(object): if not self.form.is_bound: 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 getattr(self.field.widget, 'supports_microseconds', True)): - data = data.replace(microsecond=0) + if self._initial_value is not UNSET: + data = self._initial_value + else: + 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 getattr(self.field.widget, 'supports_microseconds', True)): + data = data.replace(microsecond=0) + self._initial_value = data else: data = self.field.bound_data( self.data, self.form.initial.get(self.name, self.field.initial) diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 33bccbb53b4..82a9dd92e42 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import copy import datetime import json +import uuid from django.core.exceptions import NON_FIELD_ERRORS from django.core.files.uploadedfile import SimpleUploadedFile @@ -1376,6 +1377,20 @@ class FormsTestCase(TestCase): self.assertEqual(bound['password'].value(), 'foo') self.assertEqual(unbound['password'].value(), None) + def test_boundfield_initial_called_once(self): + """ + Multiple calls to BoundField().value() in an unbound form should return + the same result each time (#24391). + """ + class MyForm(Form): + name = CharField(max_length=10, initial=uuid.uuid4) + + form = MyForm() + name = form['name'] + self.assertEqual(name.value(), name.value()) + # BoundField is also cached + self.assertIs(form['name'], name) + def test_boundfield_rendering(self): """ Python 2 issue: Test that rendering a BoundField with bytestring content