import datetime from django import forms from django.core.exceptions import ValidationError from django.db.models.utils import get_blank_choice_label from django.forms.models import ModelChoiceIterator, ModelChoiceIteratorValue from django.forms.widgets import CheckboxSelectMultiple from django.template import Context, Template from django.test import TestCase from .models import Article, Author, Book, Category, ExplicitPK, Writer class ModelChoiceFieldTests(TestCase): @classmethod def setUpTestData(cls): cls.c1 = Category.objects.create( name="Entertainment", slug="entertainment", url="entertainment" ) cls.c2 = Category.objects.create(name="A test", slug="test", url="test") cls.c3 = Category.objects.create(name="Third", slug="third-test", url="third") def test_basics(self): f = forms.ModelChoiceField(Category.objects.all()) self.assertEqual( list(f.choices), [ ("", get_blank_choice_label()), (self.c1.pk, "Entertainment"), (self.c2.pk, "A test"), (self.c3.pk, "Third"), ], ) with self.assertRaises(ValidationError): f.clean("") with self.assertRaises(ValidationError): f.clean(None) with self.assertRaises(ValidationError): f.clean(0) # Invalid types that require TypeError to be caught. with self.assertRaises(ValidationError): f.clean([["fail"]]) with self.assertRaises(ValidationError): f.clean([{"foo": "bar"}]) self.assertEqual(f.clean(self.c2.id).name, "A test") self.assertEqual(f.clean(self.c3.id).name, "Third") # Add a Category object *after* the ModelChoiceField has already been # instantiated. This proves clean() checks the database during clean() # rather than caching it at instantiation time. c4 = Category.objects.create(name="Fourth", url="4th") self.assertEqual(f.clean(c4.id).name, "Fourth") # Delete a Category object *after* the ModelChoiceField has already # been instantiated. This proves clean() checks the database during # clean() rather than caching it at instantiation time. Category.objects.get(url="4th").delete() msg = ( "['Select a valid choice. That choice is not one of the available " "choices.']" ) with self.assertRaisesMessage(ValidationError, msg): f.clean(c4.id) def test_clean_model_instance(self): f = forms.ModelChoiceField(Category.objects.all()) self.assertEqual(f.clean(self.c1), self.c1) # An instance of incorrect model. msg = ( "['Select a valid choice. That choice is not one of the available " "choices.']" ) with self.assertRaisesMessage(ValidationError, msg): f.clean(Book.objects.create()) def test_clean_to_field_name(self): f = forms.ModelChoiceField(Category.objects.all(), to_field_name="slug") self.assertEqual(f.clean(self.c1.slug), self.c1) self.assertEqual(f.clean(self.c1), self.c1) def test_model_choice_null_characters(self): f = forms.ModelChoiceField(queryset=ExplicitPK.objects.all()) msg = "Null characters are not allowed." with self.assertRaisesMessage(ValidationError, msg): f.clean("\x00something") def test_choices(self): f = forms.ModelChoiceField( Category.objects.filter(pk=self.c1.id), required=False ) self.assertIsNone(f.clean("")) self.assertEqual(f.clean(str(self.c1.id)).name, "Entertainment") with self.assertRaises(ValidationError): f.clean("100") # len() can be called on choices. self.assertEqual(len(f.choices), 2) # queryset can be changed after the field is created. f.queryset = Category.objects.exclude(name="Third").order_by("pk") self.assertEqual( list(f.choices), [ ("", get_blank_choice_label()), (self.c1.pk, "Entertainment"), (self.c2.pk, "A test"), ], ) self.assertEqual(f.clean(self.c2.id).name, "A test") with self.assertRaises(ValidationError): f.clean(self.c3.id) # Choices can be iterated repeatedly. gen_one = list(f.choices) gen_two = f.choices self.assertEqual(gen_one[2], (self.c2.pk, "A test")) self.assertEqual( list(gen_two), [ ("", get_blank_choice_label()), (self.c1.pk, "Entertainment"), (self.c2.pk, "A test"), ], ) # Overriding label_from_instance() to print custom labels. f.queryset = Category.objects.order_by("pk") f.label_from_instance = lambda obj: "category " + str(obj) self.assertEqual( list(f.choices), [ ("", get_blank_choice_label()), (self.c1.pk, "category Entertainment"), (self.c2.pk, "category A test"), (self.c3.pk, "category Third"), ], ) def test_choices_freshness(self): f = forms.ModelChoiceField(Category.objects.order_by("pk")) self.assertEqual(len(f.choices), 4) self.assertEqual( list(f.choices), [ ("", get_blank_choice_label()), (self.c1.pk, "Entertainment"), (self.c2.pk, "A test"), (self.c3.pk, "Third"), ], ) c4 = Category.objects.create(name="Fourth", slug="4th", url="4th") self.assertEqual(len(f.choices), 5) self.assertEqual( list(f.choices), [ ("", get_blank_choice_label()), (self.c1.pk, "Entertainment"), (self.c2.pk, "A test"), (self.c3.pk, "Third"), (c4.pk, "Fourth"), ], ) def test_choices_bool(self): f = forms.ModelChoiceField(Category.objects.all(), empty_label=None) self.assertIs(bool(f.choices), True) Category.objects.all().delete() self.assertIs(bool(f.choices), False) def test_choices_bool_empty_label(self): f = forms.ModelChoiceField(Category.objects.all(), empty_label="--------") Category.objects.all().delete() self.assertIs(bool(f.choices), True) def test_choices_radio_blank(self): blank_choice = [("", get_blank_choice_label())] choices = [ (self.c1.pk, "Entertainment"), (self.c2.pk, "A test"), (self.c3.pk, "Third"), ] categories = Category.objects.order_by("pk") for widget in [forms.RadioSelect, forms.RadioSelect()]: for blank in [True, False]: with self.subTest(widget=widget, blank=blank): f = forms.ModelChoiceField( categories, widget=widget, blank=blank, ) self.assertEqual( list(f.choices), (blank_choice + choices if blank else choices), ) def test_deepcopies_widget(self): class ModelChoiceForm(forms.Form): category = forms.ModelChoiceField(Category.objects.all()) form1 = ModelChoiceForm() field1 = form1.fields["category"] # To allow the widget to change the queryset of field1.widget.choices # without affecting other forms, the following must hold (#11183): self.assertIsNot(field1, ModelChoiceForm.base_fields["category"]) self.assertIs(field1.widget.choices.field, field1) def test_result_cache_not_shared(self): class ModelChoiceForm(forms.Form): category = forms.ModelChoiceField(Category.objects.all()) form1 = ModelChoiceForm() self.assertCountEqual( form1.fields["category"].queryset, [self.c1, self.c2, self.c3] ) form2 = ModelChoiceForm() self.assertIsNone(form2.fields["category"].queryset._result_cache) def test_queryset_none(self): class ModelChoiceForm(forms.Form): category = forms.ModelChoiceField(queryset=None) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["category"].queryset = Category.objects.filter( slug__contains="test" ) form = ModelChoiceForm() self.assertCountEqual(form.fields["category"].queryset, [self.c2, self.c3]) def test_no_extra_query_when_accessing_attrs(self): """ ModelChoiceField with RadioSelect widget doesn't produce unnecessary db queries when accessing its BoundField's attrs. """ class ModelChoiceForm(forms.Form): category = forms.ModelChoiceField( Category.objects.all(), widget=forms.RadioSelect ) form = ModelChoiceForm() field = form["category"] # BoundField template = Template("{{ field.name }}{{ field }}{{ field.help_text }}") with self.assertNumQueries(1): template.render(Context({"field": field})) def test_disabled_modelchoicefield(self): class ModelChoiceForm(forms.ModelForm): author = forms.ModelChoiceField(Author.objects.all(), disabled=True) class Meta: model = Book fields = ["author"] book = Book.objects.create(author=Writer.objects.create(name="Test writer")) form = ModelChoiceForm({}, instance=book) self.assertEqual( form.errors["author"], ["Select a valid choice. That choice is not one of the available choices."], ) def test_disabled_modelchoicefield_has_changed(self): field = forms.ModelChoiceField(Author.objects.all(), disabled=True) self.assertIs(field.has_changed("x", "y"), False) def test_disabled_modelchoicefield_initial_model_instance(self): class ModelChoiceForm(forms.Form): categories = forms.ModelChoiceField( Category.objects.all(), disabled=True, initial=self.c1, ) self.assertTrue(ModelChoiceForm(data={"categories": self.c1.pk}).is_valid()) def test_disabled_multiplemodelchoicefield(self): class ArticleForm(forms.ModelForm): categories = forms.ModelMultipleChoiceField( Category.objects.all(), required=False ) class Meta: model = Article fields = ["categories"] category1 = Category.objects.create(name="cat1") category2 = Category.objects.create(name="cat2") article = Article.objects.create( pub_date=datetime.date(1988, 1, 4), writer=Writer.objects.create(name="Test writer"), ) article.categories.set([category1.pk]) form = ArticleForm(data={"categories": [category2.pk]}, instance=article) self.assertEqual(form.errors, {}) self.assertEqual( [x.pk for x in form.cleaned_data["categories"]], [category2.pk] ) # Disabled fields use the value from `instance` rather than `data`. form = ArticleForm(data={"categories": [category2.pk]}, instance=article) form.fields["categories"].disabled = True self.assertEqual(form.errors, {}) self.assertEqual( [x.pk for x in form.cleaned_data["categories"]], [category1.pk] ) def test_disabled_modelmultiplechoicefield_has_changed(self): field = forms.ModelMultipleChoiceField(Author.objects.all(), disabled=True) self.assertIs(field.has_changed("x", "y"), False) def test_overridable_choice_iterator(self): """ Iterator defaults to ModelChoiceIterator and can be overridden with the iterator attribute on a ModelChoiceField subclass. """ field = forms.ModelChoiceField(Category.objects.all()) self.assertIsInstance(field.choices, ModelChoiceIterator) class CustomModelChoiceIterator(ModelChoiceIterator): pass class CustomModelChoiceField(forms.ModelChoiceField): iterator = CustomModelChoiceIterator field = CustomModelChoiceField(Category.objects.all()) 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.order_by("pk")) self.assertHTMLEqual( field.widget.render("name", []), ( "