diff --git a/django/forms/fields.py b/django/forms/fields.py index ea93c306c5..762d59c1b9 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -204,6 +204,14 @@ class Field(six.with_metaclass(RenameFieldMethods, object)): data_value = data if data is not None else '' return initial_value != data_value + def get_bound_field(self, form, field_name): + """ + Return a BoundField instance that will be used when accessing the form + field in a template. + """ + from django.forms.forms import BoundField + return BoundField(form, self, field_name) + def __deepcopy__(self, memo): result = copy.copy(self) memo[id(self)] = result diff --git a/django/forms/forms.py b/django/forms/forms.py index 7db2465fda..07415f8fe1 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -152,7 +152,7 @@ class BaseForm(object): raise KeyError( "Key %r not found in '%s'" % (name, self.__class__.__name__)) if name not in self._bound_fields_cache: - self._bound_fields_cache[name] = BoundField(self, field, name) + self._bound_fields_cache[name] = field.get_bound_field(self, name) return self._bound_fields_cache[name] @property diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 16e8443361..ca9e4d868f 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -932,6 +932,48 @@ and using the template above, would render something like: +Customizing ``BoundField`` +-------------------------- + +.. versionadded:: 1.9 + +If you need to access some additional information about a form field in a +template and using a subclass of :class:`~django.forms.Field` isn't +sufficient, consider also customizing :class:`~django.forms.BoundField`. + +A custom form field can override ``get_bound_field()``: + +.. method:: Field.get_bound_field(form, field_name) + + Takes an instance of :class:`~django.forms.Form` and the name of the field. + The return value will be used when accessing the field in a template. Most + likely it will be an instance of a subclass of + :class:`~django.forms.BoundField`. + +If you have a ``GPSCoordinatesField``, for example, and want to be able to +access additional information about the coordinates in a template, this could +be implemented as follows:: + + class GPSCoordinatesBoundField(BoundField): + @property + def country(self): + """ + Return the country the coordinates lie in or None if it can't be + determined. + """ + value = self.value() + if value: + return get_country_from_coordinates(value) + else: + return None + + class GPSCoordinatesField(Field): + def get_bound_field(self, form, field_name): + return GPSCoordinatesBoundField(form, self, field_name) + +Now you can access the country in a template with +``{{ form.coordinates.country }}``. + .. _binding-uploaded-files: Binding uploaded files to a form diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 88c35829c0..ac7c6400ae 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -1239,3 +1239,6 @@ custom ``Field`` classes. To do this, just create a subclass of ``clean()`` method and that its ``__init__()`` method accept the core arguments mentioned above (``required``, ``label``, ``initial``, ``widget``, ``help_text``). + +You can also customize how a field will be accessed by overriding +:meth:`~django.forms.Field.get_bound_field()`. diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index 73b2cffb8c..d5ed37737f 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -364,6 +364,9 @@ Forms * Form fields now support the :attr:`~django.forms.Field.disabled` argument, allowing the field widget to be displayed disabled by browsers. +* It's now possible to customize bound fields by overriding a field's + :meth:`~django.forms.Field.get_bound_field()` method. + Generic Views ^^^^^^^^^^^^^ diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index ac96d775a2..d3618b1a8c 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -1903,6 +1903,17 @@ Password: f = SampleForm(data={'name': 'bar'}) self.assertIsInstance(force_text(f['name']), SafeData) + def test_custom_boundfield(self): + class CustomField(CharField): + def get_bound_field(self, form, name): + return (form, name) + + class SampleForm(Form): + name = CustomField() + + f = SampleForm() + self.assertEqual(f['name'], (f, 'name')) + def test_initial_datetime_values(self): now = datetime.datetime.now() # Nix microseconds (since they should be ignored). #22502