diff options
Diffstat (limited to 'tests/model_forms_regress/tests.py')
-rw-r--r-- | tests/model_forms_regress/tests.py | 494 |
1 files changed, 494 insertions, 0 deletions
diff --git a/tests/model_forms_regress/tests.py b/tests/model_forms_regress/tests.py new file mode 100644 index 0000000000..90c907f2a6 --- /dev/null +++ b/tests/model_forms_regress/tests.py @@ -0,0 +1,494 @@ +from __future__ import absolute_import, unicode_literals + +from datetime import date + +from django import forms +from django.core.exceptions import FieldError, ValidationError +from django.core.files.uploadedfile import SimpleUploadedFile +from django.forms.models import (modelform_factory, ModelChoiceField, + fields_for_model, construct_instance, ModelFormMetaclass) +from django.utils import six +from django.utils import unittest +from django.test import TestCase + +from .models import (Person, RealPerson, Triple, FilePathModel, Article, + Publication, CustomFF, Author, Author1, Homepage, Document, Edition) + + +class ModelMultipleChoiceFieldTests(TestCase): + def test_model_multiple_choice_number_of_queries(self): + """ + Test that ModelMultipleChoiceField does O(1) queries instead of + O(n) (#10156). + """ + persons = [Person.objects.create(name="Person %s" % i) for i in range(30)] + + f = forms.ModelMultipleChoiceField(queryset=Person.objects.all()) + self.assertNumQueries(1, f.clean, [p.pk for p in persons[1:11:2]]) + + def test_model_multiple_choice_run_validators(self): + """ + Test that ModelMultipleChoiceField run given validators (#14144). + """ + for i in range(30): + Person.objects.create(name="Person %s" % i) + + self._validator_run = False + def my_validator(value): + self._validator_run = True + + f = forms.ModelMultipleChoiceField(queryset=Person.objects.all(), + validators=[my_validator]) + + f.clean([p.pk for p in Person.objects.all()[8:9]]) + self.assertTrue(self._validator_run) + +class TripleForm(forms.ModelForm): + class Meta: + model = Triple + +class UniqueTogetherTests(TestCase): + def test_multiple_field_unique_together(self): + """ + When the same field is involved in multiple unique_together + constraints, we need to make sure we don't remove the data for it + before doing all the validation checking (not just failing after + the first one). + """ + Triple.objects.create(left=1, middle=2, right=3) + + form = TripleForm({'left': '1', 'middle': '2', 'right': '3'}) + self.assertFalse(form.is_valid()) + + form = TripleForm({'left': '1', 'middle': '3', 'right': '1'}) + self.assertTrue(form.is_valid()) + +class TripleFormWithCleanOverride(forms.ModelForm): + class Meta: + model = Triple + + def clean(self): + if not self.cleaned_data['left'] == self.cleaned_data['right']: + raise forms.ValidationError('Left and right should be equal') + return self.cleaned_data + +class OverrideCleanTests(TestCase): + def test_override_clean(self): + """ + Regression for #12596: Calling super from ModelForm.clean() should be + optional. + """ + form = TripleFormWithCleanOverride({'left': 1, 'middle': 2, 'right': 1}) + self.assertTrue(form.is_valid()) + # form.instance.left will be None if the instance was not constructed + # by form.full_clean(). + self.assertEqual(form.instance.left, 1) + +# Regression test for #12960. +# Make sure the cleaned_data returned from ModelForm.clean() is applied to the +# model instance. + +class PublicationForm(forms.ModelForm): + def clean(self): + self.cleaned_data['title'] = self.cleaned_data['title'].upper() + return self.cleaned_data + + class Meta: + model = Publication + +class ModelFormCleanTest(TestCase): + def test_model_form_clean_applies_to_model(self): + data = {'title': 'test', 'date_published': '2010-2-25'} + form = PublicationForm(data) + publication = form.save() + self.assertEqual(publication.title, 'TEST') + +class FPForm(forms.ModelForm): + class Meta: + model = FilePathModel + +class FilePathFieldTests(TestCase): + def test_file_path_field_blank(self): + """ + Regression test for #8842: FilePathField(blank=True) + """ + form = FPForm() + names = [p[1] for p in form['path'].field.choices] + names.sort() + self.assertEqual(names, ['---------', '__init__.py', 'models.py', 'tests.py']) + +class ManyToManyCallableInitialTests(TestCase): + def test_callable(self): + "Regression for #10349: A callable can be provided as the initial value for an m2m field" + + # Set up a callable initial value + def formfield_for_dbfield(db_field, **kwargs): + if db_field.name == 'publications': + kwargs['initial'] = lambda: Publication.objects.all().order_by('date_published')[:2] + return db_field.formfield(**kwargs) + + # Set up some Publications to use as data + book1 = Publication.objects.create(title="First Book", date_published=date(2007,1,1)) + book2 = Publication.objects.create(title="Second Book", date_published=date(2008,1,1)) + book3 = Publication.objects.create(title="Third Book", date_published=date(2009,1,1)) + + # Create a ModelForm, instantiate it, and check that the output is as expected + ModelForm = modelform_factory(Article, formfield_callback=formfield_for_dbfield) + form = ModelForm() + self.assertHTMLEqual(form.as_ul(), """<li><label for="id_headline">Headline:</label> <input id="id_headline" type="text" name="headline" maxlength="100" /></li> +<li><label for="id_publications">Publications:</label> <select multiple="multiple" name="publications" id="id_publications"> +<option value="%d" selected="selected">First Book</option> +<option value="%d" selected="selected">Second Book</option> +<option value="%d">Third Book</option> +</select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></li>""" + % (book1.pk, book2.pk, book3.pk)) + +class CFFForm(forms.ModelForm): + class Meta: + model = CustomFF + +class CustomFieldSaveTests(TestCase): + def test_save(self): + "Regression for #11149: save_form_data should be called only once" + + # It's enough that the form saves without error -- the custom save routine will + # generate an AssertionError if it is called more than once during save. + form = CFFForm(data = {'f': None}) + form.save() + +class ModelChoiceIteratorTests(TestCase): + def test_len(self): + class Form(forms.ModelForm): + class Meta: + model = Article + fields = ["publications"] + + Publication.objects.create(title="Pravda", + date_published=date(1991, 8, 22)) + f = Form() + self.assertEqual(len(f.fields["publications"].choices), 1) + +class RealPersonForm(forms.ModelForm): + class Meta: + model = RealPerson + +class CustomModelFormSaveMethod(TestCase): + def test_string_message(self): + data = {'name': 'anonymous'} + form = RealPersonForm(data) + self.assertEqual(form.is_valid(), False) + self.assertEqual(form.errors['__all__'], ['Please specify a real name.']) + +class ModelClassTests(TestCase): + def test_no_model_class(self): + class NoModelModelForm(forms.ModelForm): + pass + self.assertRaises(ValueError, NoModelModelForm) + +class OneToOneFieldTests(TestCase): + def test_assignment_of_none(self): + class AuthorForm(forms.ModelForm): + class Meta: + model = Author + fields = ['publication', 'full_name'] + + publication = Publication.objects.create(title="Pravda", + date_published=date(1991, 8, 22)) + author = Author.objects.create(publication=publication, full_name='John Doe') + form = AuthorForm({'publication':'', 'full_name':'John Doe'}, instance=author) + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['publication'], None) + author = form.save() + # author object returned from form still retains original publication object + # that's why we need to retreive it from database again + new_author = Author.objects.get(pk=author.pk) + self.assertEqual(new_author.publication, None) + + def test_assignment_of_none_null_false(self): + class AuthorForm(forms.ModelForm): + class Meta: + model = Author1 + fields = ['publication', 'full_name'] + + publication = Publication.objects.create(title="Pravda", + date_published=date(1991, 8, 22)) + author = Author1.objects.create(publication=publication, full_name='John Doe') + form = AuthorForm({'publication':'', 'full_name':'John Doe'}, instance=author) + self.assertTrue(not form.is_valid()) + + +class ModelChoiceForm(forms.Form): + person = ModelChoiceField(Person.objects.all()) + + +class TestTicket11183(TestCase): + def test_11183(self): + form1 = ModelChoiceForm() + field1 = form1.fields['person'] + # To allow the widget to change the queryset of field1.widget.choices correctly, + # without affecting other forms, the following must hold: + self.assertTrue(field1 is not ModelChoiceForm.base_fields['person']) + self.assertTrue(field1.widget.choices.field is field1) + +class HomepageForm(forms.ModelForm): + class Meta: + model = Homepage + +class URLFieldTests(TestCase): + def test_url_on_modelform(self): + "Check basic URL field validation on model forms" + self.assertFalse(HomepageForm({'url': 'foo'}).is_valid()) + self.assertFalse(HomepageForm({'url': 'http://'}).is_valid()) + self.assertFalse(HomepageForm({'url': 'http://example'}).is_valid()) + self.assertFalse(HomepageForm({'url': 'http://example.'}).is_valid()) + self.assertFalse(HomepageForm({'url': 'http://com.'}).is_valid()) + + self.assertTrue(HomepageForm({'url': 'http://localhost'}).is_valid()) + self.assertTrue(HomepageForm({'url': 'http://example.com'}).is_valid()) + self.assertTrue(HomepageForm({'url': 'http://www.example.com'}).is_valid()) + self.assertTrue(HomepageForm({'url': 'http://www.example.com:8000'}).is_valid()) + self.assertTrue(HomepageForm({'url': 'http://www.example.com/test'}).is_valid()) + self.assertTrue(HomepageForm({'url': 'http://www.example.com:8000/test'}).is_valid()) + self.assertTrue(HomepageForm({'url': 'http://example.com/foo/bar'}).is_valid()) + + def test_http_prefixing(self): + "If the http:// prefix is omitted on form input, the field adds it again. (Refs #13613)" + form = HomepageForm({'url': 'example.com'}) + form.is_valid() + # self.assertTrue(form.is_valid()) + # self.assertEqual(form.cleaned_data['url'], 'http://example.com/') + + form = HomepageForm({'url': 'example.com/test'}) + form.is_valid() + # self.assertTrue(form.is_valid()) + # self.assertEqual(form.cleaned_data['url'], 'http://example.com/test') + + +class FormFieldCallbackTests(TestCase): + + def test_baseform_with_widgets_in_meta(self): + """Regression for #13095: Using base forms with widgets defined in Meta should not raise errors.""" + widget = forms.Textarea() + + class BaseForm(forms.ModelForm): + class Meta: + model = Person + widgets = {'name': widget} + + Form = modelform_factory(Person, form=BaseForm) + self.assertTrue(Form.base_fields['name'].widget is widget) + + def test_factory_with_widget_argument(self): + """ Regression for #15315: modelform_factory should accept widgets + argument + """ + widget = forms.Textarea() + + # Without a widget should not set the widget to textarea + Form = modelform_factory(Person) + self.assertNotEqual(Form.base_fields['name'].widget.__class__, forms.Textarea) + + # With a widget should not set the widget to textarea + Form = modelform_factory(Person, widgets={'name':widget}) + self.assertEqual(Form.base_fields['name'].widget.__class__, forms.Textarea) + + def test_custom_callback(self): + """Test that a custom formfield_callback is used if provided""" + + callback_args = [] + + def callback(db_field, **kwargs): + callback_args.append((db_field, kwargs)) + return db_field.formfield(**kwargs) + + widget = forms.Textarea() + + class BaseForm(forms.ModelForm): + class Meta: + model = Person + widgets = {'name': widget} + + _ = modelform_factory(Person, form=BaseForm, + formfield_callback=callback) + id_field, name_field = Person._meta.fields + + self.assertEqual(callback_args, + [(id_field, {}), (name_field, {'widget': widget})]) + + def test_bad_callback(self): + # A bad callback provided by user still gives an error + self.assertRaises(TypeError, modelform_factory, Person, + formfield_callback='not a function or callable') + + +class InvalidFieldAndFactory(TestCase): + """ Tests for #11905 """ + + def test_extra_field_model_form(self): + try: + class ExtraPersonForm(forms.ModelForm): + """ ModelForm with an extra field """ + + age = forms.IntegerField() + + class Meta: + model = Person + fields = ('name', 'no-field') + except FieldError as e: + # Make sure the exception contains some reference to the + # field responsible for the problem. + self.assertTrue('no-field' in e.args[0]) + else: + self.fail('Invalid "no-field" field not caught') + + def test_extra_declared_field_model_form(self): + try: + class ExtraPersonForm(forms.ModelForm): + """ ModelForm with an extra field """ + + age = forms.IntegerField() + + class Meta: + model = Person + fields = ('name', 'age') + except FieldError: + self.fail('Declarative field raised FieldError incorrectly') + + def test_extra_field_modelform_factory(self): + self.assertRaises(FieldError, modelform_factory, + Person, fields=['no-field', 'name']) + + +class DocumentForm(forms.ModelForm): + class Meta: + model = Document + +class FileFieldTests(unittest.TestCase): + def test_clean_false(self): + """ + If the ``clean`` method on a non-required FileField receives False as + the data (meaning clear the field value), it returns False, regardless + of the value of ``initial``. + + """ + f = forms.FileField(required=False) + self.assertEqual(f.clean(False), False) + self.assertEqual(f.clean(False, 'initial'), False) + + def test_clean_false_required(self): + """ + If the ``clean`` method on a required FileField receives False as the + data, it has the same effect as None: initial is returned if non-empty, + otherwise the validation catches the lack of a required value. + + """ + f = forms.FileField(required=True) + self.assertEqual(f.clean(False, 'initial'), 'initial') + self.assertRaises(ValidationError, f.clean, False) + + def test_full_clear(self): + """ + Integration happy-path test that a model FileField can actually be set + and cleared via a ModelForm. + + """ + form = DocumentForm() + self.assertTrue('name="myfile"' in six.text_type(form)) + self.assertTrue('myfile-clear' not in six.text_type(form)) + form = DocumentForm(files={'myfile': SimpleUploadedFile('something.txt', b'content')}) + self.assertTrue(form.is_valid()) + doc = form.save(commit=False) + self.assertEqual(doc.myfile.name, 'something.txt') + form = DocumentForm(instance=doc) + self.assertTrue('myfile-clear' in six.text_type(form)) + form = DocumentForm(instance=doc, data={'myfile-clear': 'true'}) + doc = form.save(commit=False) + self.assertEqual(bool(doc.myfile), False) + + def test_clear_and_file_contradiction(self): + """ + If the user submits a new file upload AND checks the clear checkbox, + they get a validation error, and the bound redisplay of the form still + includes the current file and the clear checkbox. + + """ + form = DocumentForm(files={'myfile': SimpleUploadedFile('something.txt', b'content')}) + self.assertTrue(form.is_valid()) + doc = form.save(commit=False) + form = DocumentForm(instance=doc, + files={'myfile': SimpleUploadedFile('something.txt', b'content')}, + data={'myfile-clear': 'true'}) + self.assertTrue(not form.is_valid()) + self.assertEqual(form.errors['myfile'], + ['Please either submit a file or check the clear checkbox, not both.']) + rendered = six.text_type(form) + self.assertTrue('something.txt' in rendered) + self.assertTrue('myfile-clear' in rendered) + +class EditionForm(forms.ModelForm): + author = forms.ModelChoiceField(queryset=Person.objects.all()) + publication = forms.ModelChoiceField(queryset=Publication.objects.all()) + edition = forms.IntegerField() + isbn = forms.CharField(max_length=13) + + class Meta: + model = Edition + +class UniqueErrorsTests(TestCase): + def setUp(self): + self.author1 = Person.objects.create(name='Author #1') + self.author2 = Person.objects.create(name='Author #2') + self.pub1 = Publication.objects.create(title='Pub #1', date_published=date(2000, 10, 31)) + self.pub2 = Publication.objects.create(title='Pub #2', date_published=date(2004, 1, 5)) + form = EditionForm(data={'author': self.author1.pk, 'publication': self.pub1.pk, 'edition': 1, 'isbn': '9783161484100'}) + form.save() + + def test_unique_error_message(self): + form = EditionForm(data={'author': self.author1.pk, 'publication': self.pub2.pk, 'edition': 1, 'isbn': '9783161484100'}) + self.assertEqual(form.errors, {'isbn': ['Edition with this Isbn already exists.']}) + + def test_unique_together_error_message(self): + form = EditionForm(data={'author': self.author1.pk, 'publication': self.pub1.pk, 'edition': 2, 'isbn': '9783161489999'}) + self.assertEqual(form.errors, {'__all__': ['Edition with this Author and Publication already exists.']}) + form = EditionForm(data={'author': self.author2.pk, 'publication': self.pub1.pk, 'edition': 1, 'isbn': '9783161487777'}) + self.assertEqual(form.errors, {'__all__': ['Edition with this Publication and Edition already exists.']}) + + +class EmptyFieldsTestCase(TestCase): + "Tests for fields=() cases as reported in #14119" + class EmptyPersonForm(forms.ModelForm): + class Meta: + model = Person + fields = () + + def test_empty_fields_to_fields_for_model(self): + "An argument of fields=() to fields_for_model should return an empty dictionary" + field_dict = fields_for_model(Person, fields=()) + self.assertEqual(len(field_dict), 0) + + def test_empty_fields_on_modelform(self): + "No fields on a ModelForm should actually result in no fields" + form = self.EmptyPersonForm() + self.assertEqual(len(form.fields), 0) + + def test_empty_fields_to_construct_instance(self): + "No fields should be set on a model instance if construct_instance receives fields=()" + form = modelform_factory(Person)({'name': 'John Doe'}) + self.assertTrue(form.is_valid()) + instance = construct_instance(form, Person(), fields=()) + self.assertEqual(instance.name, '') + + +class CustomMetaclass(ModelFormMetaclass): + def __new__(cls, name, bases, attrs): + new = super(CustomMetaclass, cls).__new__(cls, name, bases, attrs) + new.base_fields = {} + return new + +class CustomMetaclassForm(six.with_metaclass(CustomMetaclass, forms.ModelForm)): + pass + +class CustomMetaclassTestCase(TestCase): + def test_modelform_factory_metaclass(self): + new_cls = modelform_factory(Person, form=CustomMetaclassForm) + self.assertEqual(new_cls.base_fields, {}) |