diff options
| author | Jon Dufresne <jon.dufresne@gmail.com> | 2019-11-17 15:41:23 -0800 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2019-12-23 10:34:50 +0100 |
| commit | 67ea35df52f2e29bafca8881e4f356934061644e (patch) | |
| tree | 42dfda9db4b6d8db3792ee6939c06019fc55477a | |
| parent | 5da85ea73724d75e609c5ee4316e7e5be8f17810 (diff) | |
Fixed #30998 -- Added ModelChoiceIteratorValue to pass the model instance to ChoiceWidget.create_option().
| -rw-r--r-- | django/forms/models.py | 19 | ||||
| -rw-r--r-- | docs/ref/forms/fields.txt | 106 | ||||
| -rw-r--r-- | docs/releases/3.1.txt | 7 | ||||
| -rw-r--r-- | tests/model_forms/test_modelchoicefield.py | 26 |
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 |
