summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJon Dufresne <jon.dufresne@gmail.com>2019-11-17 15:41:23 -0800
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2019-12-23 10:34:50 +0100
commit67ea35df52f2e29bafca8881e4f356934061644e (patch)
tree42dfda9db4b6d8db3792ee6939c06019fc55477a
parent5da85ea73724d75e609c5ee4316e7e5be8f17810 (diff)
Fixed #30998 -- Added ModelChoiceIteratorValue to pass the model instance to ChoiceWidget.create_option().
-rw-r--r--django/forms/models.py19
-rw-r--r--docs/ref/forms/fields.txt106
-rw-r--r--docs/releases/3.1.txt7
-rw-r--r--tests/model_forms/test_modelchoicefield.py26
4 files changed, 152 insertions, 6 deletions
diff --git a/django/forms/models.py b/django/forms/models.py
index 0684199db5..d05931ab6e 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -1126,6 +1126,20 @@ class InlineForeignKeyField(Field):
return False
+class ModelChoiceIteratorValue:
+ def __init__(self, value, instance):
+ self.value = value
+ self.instance = instance
+
+ def __str__(self):
+ return str(self.value)
+
+ def __eq__(self, other):
+ if isinstance(other, ModelChoiceIteratorValue):
+ other = other.value
+ return self.value == other
+
+
class ModelChoiceIterator:
def __init__(self, field):
self.field = field
@@ -1151,7 +1165,10 @@ class ModelChoiceIterator:
return self.field.empty_label is not None or self.queryset.exists()
def choice(self, obj):
- return (self.field.prepare_value(obj), self.field.label_from_instance(obj))
+ return (
+ ModelChoiceIteratorValue(self.field.prepare_value(obj), obj),
+ self.field.label_from_instance(obj),
+ )
class ModelChoiceField(ChoiceField):
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
index 0a2e13c85d..a74980a845 100644
--- a/docs/ref/forms/fields.txt
+++ b/docs/ref/forms/fields.txt
@@ -1144,7 +1144,7 @@ method::
Both ``ModelChoiceField`` and ``ModelMultipleChoiceField`` have an ``iterator``
attribute which specifies the class used to iterate over the queryset when
-generating choices.
+generating choices. See :ref:`iterating-relationship-choices` for details.
``ModelChoiceField``
--------------------
@@ -1285,8 +1285,73 @@ generating choices.
Same as :class:`ModelChoiceField.iterator`.
+.. _iterating-relationship-choices:
+
+Iterating relationship choices
+------------------------------
+
+By default, :class:`ModelChoiceField` and :class:`ModelMultipleChoiceField` use
+:class:`ModelChoiceIterator` to generate their field ``choices``.
+
+When iterated, ``ModelChoiceIterator`` yields 2-tuple choices containing
+:class:`ModelChoiceIteratorValue` instances as the first ``value`` element in
+each choice. ``ModelChoiceIteratorValue`` wraps the choice value whilst
+maintaining a reference to the source model instance that can be used in custom
+widget implementations, for example, to add `data-* attributes`_ to
+``<option>`` elements.
+
+.. _`data-* attributes`: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*
+
+For example, consider the following models::
+
+ from django.db import models
+
+ class Topping(models.Model):
+ name = models.CharField(max_length=100)
+ price = models.DecimalField(decimal_places=2, max_digits=6)
+
+ def __str__(self):
+ return self.name
+
+ class Pizza(models.Model):
+ topping = models.ForeignKey(Topping, on_delete=models.CASCADE)
+
+You can use a :class:`~django.forms.Select` widget subclass to include
+the value of ``Topping.price`` as the HTML attribute ``data-price`` for each
+``<option>`` element::
+
+ from django import forms
+
+ class ToppingSelect(forms.Select):
+ def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
+ option = super().create_option(name, value, label, selected, index, subindex, attrs)
+ if value:
+ option['attrs']['data-price'] = value.instance.price
+ return option
+
+ class PizzaForm(forms.ModelForm):
+ class Meta:
+ model = Pizza
+ fields = ['topping']
+ widgets = {'topping': ToppingSelect}
+
+This will render the ``Pizza.topping`` select as:
+
+.. code-block:: html
+
+ <select id="id_topping" name="topping" required>
+ <option value="" selected>---------</option>
+ <option value="1" data-price="1.50">mushrooms</option>
+ <option value="2" data-price="1.25">onions</option>
+ <option value="3" data-price="1.75">peppers</option>
+ <option value="4" data-price="2.00">pineapple</option>
+ </select>
+
+For more advanced usage you may subclass ``ModelChoiceIterator`` in order to
+customize the yielded 2-tuple choices.
+
``ModelChoiceIterator``
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
.. class:: ModelChoiceIterator(field)
@@ -1305,8 +1370,41 @@ generating choices.
.. method:: __iter__()
- Yield 2-tuple choices in the same format as used by
- :attr:`ChoiceField.choices`.
+ Yields 2-tuple choices, in the ``(value, label)`` format used by
+ :attr:`ChoiceField.choices`. The first ``value`` element is a
+ :class:`ModelChoiceIteratorValue` instance.
+
+ .. versionchanged:: 3.1
+
+ In older versions, the first ``value`` element in the choice tuple
+ is the ``field`` value itself, rather than a
+ ``ModelChoiceIteratorValue`` instance.
+
+``ModelChoiceIteratorValue``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: ModelChoiceIteratorValue(value, instance)
+
+ .. versionadded:: 3.1
+
+ Two arguments are required:
+
+ .. attribute:: value
+
+ The value of the choice. This value is used to render the ``value``
+ attribute of an HTML ``<option>`` element.
+
+ .. attribute:: instance
+
+ The model instance from the queryset. The instance can be accessed in
+ custom ``ChoiceWidget.create_option()`` implementations to adjust the
+ rendered HTML.
+
+ ``ModelChoiceIteratorValue`` has the following method:
+
+ .. method:: __str__()
+
+ Return ``value`` as a string to be rendered in HTML.
Creating custom fields
======================
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index 8a4d9f5326..16d475b45b 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -170,7 +170,12 @@ File Uploads
Forms
~~~~~
-* ...
+* :class:`~django.forms.ModelChoiceIterator`, used by
+ :class:`~django.forms.ModelChoiceField` and
+ :class:`~django.forms.ModelMultipleChoiceField`, now uses
+ :class:`~django.forms.ModelChoiceIteratorValue` that can be used by widgets
+ to access model instances. See :ref:`iterating-relationship-choices` for
+ details.
Generic Views
~~~~~~~~~~~~~
diff --git a/tests/model_forms/test_modelchoicefield.py b/tests/model_forms/test_modelchoicefield.py
index cecc727267..4a2ef30b90 100644
--- a/tests/model_forms/test_modelchoicefield.py
+++ b/tests/model_forms/test_modelchoicefield.py
@@ -260,6 +260,32 @@ class ModelChoiceFieldTests(TestCase):
self.assertIsInstance(field.choices, CustomModelChoiceIterator)
def test_choice_iterator_passes_model_to_widget(self):
+ class CustomCheckboxSelectMultiple(CheckboxSelectMultiple):
+ def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
+ option = super().create_option(name, value, label, selected, index, subindex, attrs)
+ # Modify the HTML based on the object being rendered.
+ c = value.instance
+ option['attrs']['data-slug'] = c.slug
+ return option
+
+ class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField):
+ widget = CustomCheckboxSelectMultiple
+
+ field = CustomModelMultipleChoiceField(Category.objects.all())
+ self.assertHTMLEqual(
+ field.widget.render('name', []), (
+ '<ul>'
+ '<li><label><input type="checkbox" name="name" value="%d" '
+ 'data-slug="entertainment">Entertainment</label></li>'
+ '<li><label><input type="checkbox" name="name" value="%d" '
+ 'data-slug="test">A test</label></li>'
+ '<li><label><input type="checkbox" name="name" value="%d" '
+ 'data-slug="third-test">Third</label></li>'
+ '</ul>'
+ ) % (self.c1.pk, self.c2.pk, self.c3.pk),
+ )
+
+ def test_custom_choice_iterator_passes_model_to_widget(self):
class CustomModelChoiceValue:
def __init__(self, value, obj):
self.value = value