summaryrefslogtreecommitdiff
path: root/tests/model_forms
diff options
context:
space:
mode:
authorFrançois Freitag <mail@franek.fr>2018-02-24 19:38:23 -0800
committerTim Graham <timograham@gmail.com>2018-03-01 14:05:35 -0500
commit06172d7bc29185b7d37fbf62ec3cfdcfcafb4856 (patch)
treeb0d4b8790a52a2fdd4d1e32d01b056d96a816c7b /tests/model_forms
parent94a180402c9e52bca2e911b6427c88f436e95449 (diff)
downloaddjango-06172d7bc29185b7d37fbf62ec3cfdcfcafb4856.tar.gz
Consolidated ModelChoiceField tests.
Diffstat (limited to 'tests/model_forms')
-rw-r--r--tests/model_forms/test_modelchoicefield.py275
-rw-r--r--tests/model_forms/tests.py260
2 files changed, 277 insertions, 258 deletions
diff --git a/tests/model_forms/test_modelchoicefield.py b/tests/model_forms/test_modelchoicefield.py
new file mode 100644
index 0000000000..057eb0d25b
--- /dev/null
+++ b/tests/model_forms/test_modelchoicefield.py
@@ -0,0 +1,275 @@
+import datetime
+
+from django import forms
+from django.core.validators import ValidationError
+from django.forms.models import ModelChoiceIterator
+from django.forms.widgets import CheckboxSelectMultiple
+from django.template import Context, Template
+from django.test import TestCase
+
+from .models import Article, Author, Book, Category, 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), [
+ ('', '---------'),
+ (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_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')
+ self.assertEqual(list(f.choices), [
+ ('', '---------'),
+ (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), [
+ ('', '---------'),
+ (self.c1.pk, 'Entertainment'),
+ (self.c2.pk, 'A test'),
+ ])
+
+ # Overriding label_from_instance() to print custom labels.
+ f.queryset = Category.objects.all()
+ f.label_from_instance = lambda obj: 'category ' + str(obj)
+ self.assertEqual(list(f.choices), [
+ ('', '---------'),
+ (self.c1.pk, 'category Entertainment'),
+ (self.c2.pk, 'category A test'),
+ (self.c3.pk, 'category Third'),
+ ])
+
+ 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_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 CustomModelChoiceValue:
+ def __init__(self, value, obj):
+ self.value = value
+ self.obj = obj
+
+ def __str__(self):
+ return str(self.value)
+
+ class CustomModelChoiceIterator(ModelChoiceIterator):
+ def choice(self, obj):
+ value, label = super().choice(obj)
+ return CustomModelChoiceValue(value, obj), label
+
+ 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=None, attrs=None)
+ # Modify the HTML based on the object being rendered.
+ c = value.obj
+ option['attrs']['data-slug'] = c.slug
+ return option
+
+ class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField):
+ iterator = CustomModelChoiceIterator
+ 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_choices_not_fetched_when_not_rendering(self):
+ with self.assertNumQueries(1):
+ field = forms.ModelChoiceField(Category.objects.order_by('-name'))
+ self.assertEqual('Entertainment', field.clean(self.c1.pk).name)
+
+ def test_queryset_manager(self):
+ f = forms.ModelChoiceField(Category.objects)
+ self.assertEqual(list(f.choices), [
+ ('', '---------'),
+ (self.c1.pk, 'Entertainment'),
+ (self.c2.pk, 'A test'),
+ (self.c3.pk, 'Third'),
+ ])
+
+ def test_num_queries(self):
+ """
+ Widgets that render multiple subwidgets shouldn't make more than one
+ database query.
+ """
+ categories = Category.objects.all()
+
+ class CategoriesForm(forms.Form):
+ radio = forms.ModelChoiceField(queryset=categories, widget=forms.RadioSelect)
+ checkbox = forms.ModelMultipleChoiceField(queryset=categories, widget=forms.CheckboxSelectMultiple)
+
+ template = Template(
+ '{% for widget in form.checkbox %}{{ widget }}{% endfor %}'
+ '{% for widget in form.radio %}{{ widget }}{% endfor %}'
+ )
+ with self.assertNumQueries(2):
+ template.render(Context({'form': CategoriesForm()}))
diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py
index 9b991d209d..fced7403ba 100644
--- a/tests/model_forms/tests.py
+++ b/tests/model_forms/tests.py
@@ -12,10 +12,9 @@ from django.core.validators import ValidationError
from django.db import connection, models
from django.db.models.query import EmptyQuerySet
from django.forms.models import (
- ModelChoiceIterator, ModelFormMetaclass, construct_instance,
- fields_for_model, model_to_dict, modelform_factory,
+ ModelFormMetaclass, construct_instance, fields_for_model, model_to_dict,
+ modelform_factory,
)
-from django.forms.widgets import CheckboxSelectMultiple
from django.template import Context, Template
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
@@ -1557,261 +1556,6 @@ class ModelFormBasicTests(TestCase):
obj.full_clean()
-class ModelChoiceFieldTests(TestCase):
- def setUp(self):
- self.c1 = Category.objects.create(
- name="Entertainment", slug="entertainment", url="entertainment")
- self.c2 = Category.objects.create(
- name="It's a test", slug="its-test", url="test")
- self.c3 = Category.objects.create(
- name="Third", slug="third-test", url="third")
-
- # ModelChoiceField ############################################################
- def test_modelchoicefield(self):
- f = forms.ModelChoiceField(Category.objects.all())
- self.assertEqual(list(f.choices), [
- ('', '---------'),
- (self.c1.pk, 'Entertainment'),
- (self.c2.pk, "It's 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 (#22808).
- with self.assertRaises(ValidationError):
- f.clean([['fail']])
- with self.assertRaises(ValidationError):
- f.clean([{'foo': 'bar'}])
-
- self.assertEqual(f.clean(self.c2.id).name, "It's 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 time of instantiation.
- 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 time of instantiation.
- 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_modelchoicefield_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')
- self.assertEqual(list(f.choices), [
- ('', '---------'),
- (self.c1.pk, 'Entertainment'),
- (self.c2.pk, "It's a test")])
- self.assertEqual(f.clean(self.c2.id).name, "It's a test")
- with self.assertRaises(ValidationError):
- f.clean(self.c3.id)
-
- # check that we can safely iterate choices repeatedly
- gen_one = list(f.choices)
- gen_two = f.choices
- self.assertEqual(gen_one[2], (self.c2.pk, "It's a test"))
- self.assertEqual(list(gen_two), [
- ('', '---------'),
- (self.c1.pk, 'Entertainment'),
- (self.c2.pk, "It's a test")])
-
- # check that we can override the label_from_instance method to print custom labels (#4620)
- f.queryset = Category.objects.all()
- f.label_from_instance = lambda obj: "category " + str(obj)
- self.assertEqual(list(f.choices), [
- ('', '---------'),
- (self.c1.pk, 'category Entertainment'),
- (self.c2.pk, "category It's a test"),
- (self.c3.pk, 'category Third')])
-
- def test_modelchoicefield_11183(self):
- """
- Regression test for ticket #11183.
- """
- 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 correctly,
- # without affecting other forms, the following must hold:
- self.assertIsNot(field1, ModelChoiceForm.base_fields['category'])
- self.assertIs(field1.widget.choices.field, field1)
-
- def test_modelchoicefield_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_modelchoicefield_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_modelchoicefield_22745(self):
- """
- #22745 -- Make sure that 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_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_modelchoicefield_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_modelchoicefield_iterator_pass_model_to_widget(self):
- class CustomModelChoiceValue:
- def __init__(self, value, obj):
- self.value = value
- self.obj = obj
-
- def __str__(self):
- return str(self.value)
-
- class CustomModelChoiceIterator(ModelChoiceIterator):
- def choice(self, obj):
- value, label = super().choice(obj)
- return CustomModelChoiceValue(value, obj), label
-
- 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=None, attrs=None)
- # Modify the HTML based on the object being rendered.
- c = value.obj
- option['attrs']['data-slug'] = c.slug
- return option
-
- class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField):
- iterator = CustomModelChoiceIterator
- 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="its-test">It&#39;s 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_modelchoicefield_num_queries(self):
- """
- Widgets that render multiple subwidgets shouldn't make more than one
- database query.
- """
- categories = Category.objects.all()
-
- class CategoriesForm(forms.Form):
- radio = forms.ModelChoiceField(queryset=categories, widget=forms.RadioSelect)
- checkbox = forms.ModelMultipleChoiceField(queryset=categories, widget=forms.CheckboxSelectMultiple)
-
- template = Template("""
- {% for widget in form.checkbox %}{{ widget }}{% endfor %}
- {% for widget in form.radio %}{{ widget }}{% endfor %}
- """)
-
- with self.assertNumQueries(2):
- template.render(Context({'form': CategoriesForm()}))
-
-
class ModelMultipleChoiceFieldTests(TestCase):
def setUp(self):
self.c1 = Category.objects.create(