mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[5.0.x] Fixed #34899 -- Added blank choice to forms' callable choices lazily.
Backport of 171f91d9ef from main
			
			
This commit is contained in:
		| @@ -16,6 +16,7 @@ from django.db.models.constants import LOOKUP_SEP | ||||
| from django.db.models.query_utils import DeferredAttribute, RegisterLookupMixin | ||||
| from django.utils import timezone | ||||
| from django.utils.choices import ( | ||||
|     BlankChoiceIterator, | ||||
|     CallableChoiceIterator, | ||||
|     flatten_choices, | ||||
|     normalize_choices, | ||||
| @@ -1055,14 +1056,9 @@ class Field(RegisterLookupMixin): | ||||
|         as <select> choices for this field. | ||||
|         """ | ||||
|         if self.choices is not None: | ||||
|             choices = list(self.choices) | ||||
|             if include_blank: | ||||
|                 blank_defined = any( | ||||
|                     choice in ("", None) for choice, _ in self.flatchoices | ||||
|                 ) | ||||
|                 if not blank_defined: | ||||
|                     choices = blank_choice + choices | ||||
|             return choices | ||||
|                 return BlankChoiceIterator(self.choices, blank_choice) | ||||
|             return self.choices | ||||
|         rel_model = self.remote_field.model | ||||
|         limit_choices_to = limit_choices_to or self.get_limit_choices_to() | ||||
|         choice_func = operator.attrgetter( | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| from collections.abc import Callable, Iterable, Iterator, Mapping | ||||
| from itertools import islice, zip_longest | ||||
| from itertools import islice, tee, zip_longest | ||||
|  | ||||
| from django.utils.functional import Promise | ||||
|  | ||||
| __all__ = [ | ||||
|     "BaseChoiceIterator", | ||||
|     "BlankChoiceIterator", | ||||
|     "CallableChoiceIterator", | ||||
|     "flatten_choices", | ||||
|     "normalize_choices", | ||||
| @@ -34,6 +35,20 @@ class BaseChoiceIterator: | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class BlankChoiceIterator(BaseChoiceIterator): | ||||
|     """Iterator to lazily inject a blank choice.""" | ||||
|  | ||||
|     def __init__(self, choices, blank_choice): | ||||
|         self.choices = choices | ||||
|         self.blank_choice = blank_choice | ||||
|  | ||||
|     def __iter__(self): | ||||
|         choices, other = tee(self.choices) | ||||
|         if not any(value in ("", None) for value, _ in flatten_choices(other)): | ||||
|             yield from self.blank_choice | ||||
|         yield from choices | ||||
|  | ||||
|  | ||||
| class CallableChoiceIterator(BaseChoiceIterator): | ||||
|     """Iterator to lazily normalize choices generated by a callable.""" | ||||
|  | ||||
|   | ||||
| @@ -23,6 +23,7 @@ from django.forms.models import ( | ||||
| from django.template import Context, Template | ||||
| from django.test import SimpleTestCase, TestCase, ignore_warnings, skipUnlessDBFeature | ||||
| from django.test.utils import isolate_apps | ||||
| from django.utils.choices import BlankChoiceIterator | ||||
| from django.utils.deprecation import RemovedInDjango60Warning | ||||
|  | ||||
| from .models import ( | ||||
| @@ -2012,6 +2013,38 @@ class ModelFormBasicTests(TestCase): | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|     @isolate_apps("model_forms") | ||||
|     def test_callable_choices_are_lazy(self): | ||||
|         call_count = 0 | ||||
|  | ||||
|         def get_animal_choices(): | ||||
|             nonlocal call_count | ||||
|             call_count += 1 | ||||
|             return [("LION", "Lion"), ("ZEBRA", "Zebra")] | ||||
|  | ||||
|         class ZooKeeper(models.Model): | ||||
|             animal = models.CharField( | ||||
|                 blank=True, | ||||
|                 choices=get_animal_choices, | ||||
|                 max_length=5, | ||||
|             ) | ||||
|  | ||||
|         class ZooKeeperForm(forms.ModelForm): | ||||
|             class Meta: | ||||
|                 model = ZooKeeper | ||||
|                 fields = ["animal"] | ||||
|  | ||||
|         self.assertEqual(call_count, 0) | ||||
|         form = ZooKeeperForm() | ||||
|         self.assertEqual(call_count, 0) | ||||
|         self.assertIsInstance(form.fields["animal"].choices, BlankChoiceIterator) | ||||
|         self.assertEqual(call_count, 0) | ||||
|         self.assertEqual( | ||||
|             form.fields["animal"].choices, | ||||
|             models.BLANK_CHOICE_DASH + [("LION", "Lion"), ("ZEBRA", "Zebra")], | ||||
|         ) | ||||
|         self.assertEqual(call_count, 1) | ||||
|  | ||||
|     def test_recleaning_model_form_instance(self): | ||||
|         """ | ||||
|         Re-cleaning an instance that was added via a ModelForm shouldn't raise | ||||
|   | ||||
		Reference in New Issue
	
	Block a user