Fixed #30998 -- Added ModelChoiceIteratorValue to pass the model instance to ChoiceWidget.create_option().
This commit is contained in:
parent
5da85ea737
commit
67ea35df52
|
@ -1126,6 +1126,20 @@ class InlineForeignKeyField(Field):
|
||||||
return False
|
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:
|
class ModelChoiceIterator:
|
||||||
def __init__(self, field):
|
def __init__(self, field):
|
||||||
self.field = field
|
self.field = field
|
||||||
|
@ -1151,7 +1165,10 @@ class ModelChoiceIterator:
|
||||||
return self.field.empty_label is not None or self.queryset.exists()
|
return self.field.empty_label is not None or self.queryset.exists()
|
||||||
|
|
||||||
def choice(self, obj):
|
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):
|
class ModelChoiceField(ChoiceField):
|
||||||
|
|
|
@ -1144,7 +1144,7 @@ method::
|
||||||
|
|
||||||
Both ``ModelChoiceField`` and ``ModelMultipleChoiceField`` have an ``iterator``
|
Both ``ModelChoiceField`` and ``ModelMultipleChoiceField`` have an ``iterator``
|
||||||
attribute which specifies the class used to iterate over the queryset when
|
attribute which specifies the class used to iterate over the queryset when
|
||||||
generating choices.
|
generating choices. See :ref:`iterating-relationship-choices` for details.
|
||||||
|
|
||||||
``ModelChoiceField``
|
``ModelChoiceField``
|
||||||
--------------------
|
--------------------
|
||||||
|
@ -1285,8 +1285,73 @@ generating choices.
|
||||||
|
|
||||||
Same as :class:`ModelChoiceField.iterator`.
|
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``
|
``ModelChoiceIterator``
|
||||||
-----------------------
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. class:: ModelChoiceIterator(field)
|
.. class:: ModelChoiceIterator(field)
|
||||||
|
|
||||||
|
@ -1305,8 +1370,41 @@ generating choices.
|
||||||
|
|
||||||
.. method:: __iter__()
|
.. method:: __iter__()
|
||||||
|
|
||||||
Yield 2-tuple choices in the same format as used by
|
Yields 2-tuple choices, in the ``(value, label)`` format used by
|
||||||
:attr:`ChoiceField.choices`.
|
: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
|
Creating custom fields
|
||||||
======================
|
======================
|
||||||
|
|
|
@ -170,7 +170,12 @@ File Uploads
|
||||||
Forms
|
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
|
Generic Views
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
|
@ -260,6 +260,32 @@ class ModelChoiceFieldTests(TestCase):
|
||||||
self.assertIsInstance(field.choices, CustomModelChoiceIterator)
|
self.assertIsInstance(field.choices, CustomModelChoiceIterator)
|
||||||
|
|
||||||
def test_choice_iterator_passes_model_to_widget(self):
|
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:
|
class CustomModelChoiceValue:
|
||||||
def __init__(self, value, obj):
|
def __init__(self, value, obj):
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
Loading…
Reference in New Issue