mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #30998 -- Added ModelChoiceIteratorValue to pass the model instance to ChoiceWidget.create_option().
This commit is contained in:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							5da85ea737
						
					
				
				
					commit
					67ea35df52
				
			| @@ -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): | ||||
|   | ||||
| @@ -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 | ||||
| ====================== | ||||
|   | ||||
| @@ -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 | ||||
| ~~~~~~~~~~~~~ | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user