From 666a2ad22ff868c214bf251d1171148d943cfc82 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 14 Mar 2014 15:18:08 +0100 Subject: [PATCH] Merged model_forms_regress with model_forms tests --- tests/model_forms/models.py | 59 +++ tests/model_forms/tests.py | 685 +++++++++++++++++++++++--- tests/model_forms_regress/__init__.py | 0 tests/model_forms_regress/models.py | 90 ---- tests/model_forms_regress/tests.py | 604 ----------------------- 5 files changed, 665 insertions(+), 773 deletions(-) delete mode 100644 tests/model_forms_regress/__init__.py delete mode 100644 tests/model_forms_regress/models.py delete mode 100644 tests/model_forms_regress/tests.py diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index 63cde3f594..59b18dc39d 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -18,6 +18,7 @@ from django.core.files.storage import FileSystemStorage from django.db import models from django.utils import six from django.utils.encoding import python_2_unicode_compatible +from django.utils._os import upath temp_storage_dir = tempfile.mkdtemp(dir=os.environ['DJANGO_TEST_TEMP_DIR']) @@ -36,6 +37,10 @@ ARTICLE_STATUS_CHAR = ( ) +class Person(models.Model): + name = models.CharField(max_length=100) + + @python_2_unicode_compatible class Category(models.Model): name = models.CharField(max_length=20) @@ -92,6 +97,25 @@ class BetterWriter(Writer): score = models.IntegerField() +@python_2_unicode_compatible +class Publication(models.Model): + title = models.CharField(max_length=30) + date_published = models.DateField() + + def __str__(self): + return self.title + + +class Author(models.Model): + publication = models.OneToOneField(Publication, null=True, blank=True) + full_name = models.CharField(max_length=255) + + +class Author1(models.Model): + publication = models.OneToOneField(Publication, null=False) + full_name = models.CharField(max_length=255) + + @python_2_unicode_compatible class WriterProfile(models.Model): writer = models.OneToOneField(Writer, primary_key=True) @@ -101,6 +125,10 @@ class WriterProfile(models.Model): return "%s is %s" % (self.writer, self.age) +class Document(models.Model): + myfile = models.FileField(upload_to='unused', blank=True) + + @python_2_unicode_compatible class TextFile(models.Model): description = models.CharField(max_length=20) @@ -109,6 +137,22 @@ class TextFile(models.Model): def __str__(self): return self.description + +class CustomFileField(models.FileField): + def save_form_data(self, instance, data): + been_here = getattr(self, 'been_saved', False) + assert not been_here, "save_form_data called more than once" + setattr(self, 'been_saved', True) + + +class CustomFF(models.Model): + f = CustomFileField(upload_to='unused', blank=True) + + +class FilePathModel(models.Model): + path = models.FilePathField(path=os.path.dirname(upath(__file__)), match=".*\.py$", blank=True) + + try: from django.utils.image import Image # NOQA: detect if Pillow is installed @@ -161,6 +205,10 @@ class CommaSeparatedInteger(models.Model): return self.field +class Homepage(models.Model): + url = models.URLField() + + @python_2_unicode_compatible class Product(models.Model): slug = models.SlugField(unique=True) @@ -181,6 +229,15 @@ class Price(models.Model): unique_together = (('price', 'quantity'),) +class Triple(models.Model): + left = models.IntegerField() + middle = models.IntegerField() + right = models.IntegerField() + + class Meta: + unique_together = (('left', 'middle'), ('middle', 'right')) + + class ArticleStatus(models.Model): status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True) @@ -329,6 +386,8 @@ class CustomErrorMessage(models.Model): def clean(self): if self.name1 == 'FORBIDDEN_VALUE': raise ValidationError({'name1': [ValidationError('Model.clean() error messages.')]}) + elif self.name1 == 'GLOBAL_ERROR': + raise ValidationError("Global error message.") def today_callable_dict(): diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index 8e63da33ff..3a3ea894d0 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -12,19 +12,20 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.core.validators import ValidationError from django.db import connection from django.db.models.query import EmptyQuerySet -from django.forms.models import model_to_dict +from django.forms.models import (construct_instance, fields_for_model, + model_to_dict, modelform_factory, ModelFormMetaclass) from django.test import TestCase, skipUnlessDBFeature from django.utils.deprecation import RemovedInDjango18Warning from django.utils._os import upath from django.utils import six -from .models import (Article, ArticleStatus, BetterWriter, BigInt, Book, - Category, CommaSeparatedInteger, CustomFieldForExclusionModel, DerivedBook, - DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle, - ImprovedArticleWithParentLink, Inventory, Post, Price, - Product, TextFile, Writer, WriterProfile, Colour, ColourfulItem, - ArticleStatusNote, DateTimePost, CustomErrorMessage, test_images, - StumpJoke, Character) +from .models import (Article, ArticleStatus, Author, Author1, BetterWriter, BigInt, Book, + Category, CommaSeparatedInteger, CustomFF, CustomFieldForExclusionModel, + DerivedBook, DerivedPost, Document, ExplicitPK, FilePathModel, FlexibleDatePost, Homepage, + ImprovedArticle, ImprovedArticleWithParentLink, Inventory, Person, Post, Price, + Product, Publication, TextFile, Triple, Writer, WriterProfile, + Colour, ColourfulItem, ArticleStatusNote, DateTimePost, CustomErrorMessage, + test_images, StumpJoke, Character) if test_images: from .models import ImageFile, OptionalImageFile @@ -154,18 +155,6 @@ class ModelFormWithMedia(forms.ModelForm): fields = '__all__' -class CommaSeparatedIntegerForm(forms.ModelForm): - class Meta: - model = CommaSeparatedInteger - fields = '__all__' - - -class PriceFormWithoutQuantity(forms.ModelForm): - class Meta: - model = Price - exclude = ('quantity',) - - class CustomErrorMessageForm(forms.ModelForm): name1 = forms.CharField(error_messages={'invalid': 'Form custom error message.'}) @@ -179,6 +168,39 @@ class ModelFormBaseTest(TestCase): self.assertEqual(list(BaseCategoryForm.base_fields), ['name', 'slug', 'url']) + def test_no_model_class(self): + class NoModelModelForm(forms.ModelForm): + pass + self.assertRaises(ValueError, NoModelModelForm) + + 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. + """ + class EmptyPersonForm(forms.ModelForm): + class Meta: + model = Person + fields = () + + form = 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, fields="__all__")({'name': 'John Doe'}) + self.assertTrue(form.is_valid()) + instance = construct_instance(form, Person(), fields=()) + self.assertEqual(instance.name, '') + def test_missing_fields_attribute(self): with warnings.catch_warnings(record=True): warnings.simplefilter("always", RemovedInDjango18Warning) @@ -205,6 +227,38 @@ class ModelFormBaseTest(TestCase): self.assertEqual(list(ExtraFields.base_fields), ['name', 'slug', 'url', 'some_extra_field']) + 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']) + def test_replace_field(self): class ReplaceField(forms.ModelForm): url = forms.BooleanField() @@ -295,6 +349,36 @@ class ModelFormBaseTest(TestCase): model = Category exclude = ('url') # note the missing comma + def test_exclude_and_validation(self): + # This Price instance generated by this form is not valid because the quantity + # field is required, but the form is valid because the field is excluded from + # the form. This is for backwards compatibility. + class PriceFormWithoutQuantity(forms.ModelForm): + class Meta: + model = Price + exclude = ('quantity',) + + form = PriceFormWithoutQuantity({'price': '6.00'}) + self.assertTrue(form.is_valid()) + price = form.save(commit=False) + with self.assertRaises(ValidationError): + price.full_clean() + + # The form should not validate fields that it doesn't contain even if they are + # specified using 'fields', not 'exclude'. + class PriceFormWithoutQuantity(forms.ModelForm): + class Meta: + model = Price + fields = ('price',) + form = PriceFormWithoutQuantity({'price': '6.00'}) + self.assertTrue(form.is_valid()) + + # The form should still have an instance of a model that is not complete and + # not saved into a DB yet. + self.assertEqual(form.instance.price, Decimal('6.00')) + self.assertIsNone(form.instance.quantity) + self.assertIsNone(form.instance.pk) + def test_confused_form(self): class ConfusedForm(forms.ModelForm): """ Using 'fields' *and* 'exclude'. Not sure why you'd want to do @@ -438,12 +522,6 @@ class FieldOverridesByFormMetaForm(forms.ModelForm): } -class StumpJokeForm(forms.ModelForm): - class Meta: - model = StumpJoke - fields = '__all__' - - class TestFieldOverridesByFormMeta(TestCase): def test_widget_overrides(self): form = FieldOverridesByFormMetaForm() @@ -535,8 +613,10 @@ class ValidationTest(TestCase): assert form.is_valid() -# unique/unique_together validation class UniqueTest(TestCase): + """ + unique/unique_together validation. + """ def setUp(self): self.writer = Writer.objects.create(name='Mike Royko') @@ -560,6 +640,26 @@ class UniqueTest(TestCase): self.assertEqual(len(form.errors), 1) self.assertEqual(form.errors['__all__'], ['Price with this Price and Quantity already exists.']) + 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). + """ + class TripleForm(forms.ModelForm): + class Meta: + model = Triple + fields = '__all__' + + 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()) + @skipUnlessDBFeature('ignores_nulls_in_unique_constraints') def test_unique_null(self): title = 'I May Be Wrong But I Doubt It' @@ -962,6 +1062,31 @@ class ModelFormBasicTests(TestCase): test_art = Article.objects.get(id=art_id_1) self.assertEqual(test_art.headline, 'Test headline') + def test_m2m_initial_callable(self): + """ + Regression for #10349: A callable can be provided as the initial value for an m2m field + """ + self.maxDiff = 1200 + self.create_basic_data() + + # Set up a callable initial value + def formfield_for_dbfield(db_field, **kwargs): + if db_field.name == 'categories': + kwargs['initial'] = lambda: Category.objects.all().order_by('name')[:2] + return db_field.formfield(**kwargs) + + # Create a ModelForm, instantiate it, and check that the output is as expected + ModelForm = modelform_factory(Article, fields=['headline', 'categories'], + formfield_callback=formfield_for_dbfield) + form = ModelForm() + self.assertHTMLEqual(form.as_ul(), """
  • +
  • Hold down "Control", or "Command" on a Mac, to select more than one.
  • """ + % (self.c1.pk, self.c2.pk, self.c3.pk)) + def test_basic_creation(self): self.assertEqual(Category.objects.count(), 0) f = BaseCategoryForm({'name': 'Entertainment', @@ -1225,8 +1350,8 @@ class ModelFormBasicTests(TestCase): ''' % (self.w_woodward.pk, w_bernstein.pk, self.w_royko.pk, self.c1.pk, self.c2.pk, self.c3.pk, c4.pk)) -class ModelFieldsTests(TestCase): - def create_categories(self): +class ModelChoiceFieldTests(TestCase): + def setUp(self): self.c1 = Category.objects.create( name="Entertainment", slug="entertainment", url="entertainment") self.c2 = Category.objects.create( @@ -1236,7 +1361,6 @@ class ModelFieldsTests(TestCase): # ModelChoiceField ############################################################ def test_modelchoicefield(self): - self.create_categories() f = forms.ModelChoiceField(Category.objects.all()) self.assertEqual(list(f.choices), [ ('', '---------'), @@ -1266,13 +1390,15 @@ class ModelFieldsTests(TestCase): f.clean(c4.id) def test_modelchoicefield_choices(self): - self.create_categories() 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), [ @@ -1301,9 +1427,31 @@ class ModelFieldsTests(TestCase): (self.c2.pk, "category It's a test"), (self.c3.pk, 'category Third')]) - # ModelMultipleChoiceField #################################################### - def test_modelmultiplechoicefield(self): - self.create_categories() + 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.assertTrue(field1 is not ModelChoiceForm.base_fields['category']) + self.assertTrue(field1.widget.choices.field is field1) + + +class ModelMultipleChoiceFieldTests(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") + + def test_model_multiple_choice_field(self): f = forms.ModelMultipleChoiceField(Category.objects.all()) self.assertEqual(list(f.choices), [ (self.c1.pk, 'Entertainment'), @@ -1345,8 +1493,7 @@ class ModelFieldsTests(TestCase): with self.assertRaises(ValidationError): f.clean([c6.id]) - def test_modelmultiplechoicefield_required_false(self): - self.create_categories() + def test_model_multiple_choice_required_false(self): f = forms.ModelMultipleChoiceField(Category.objects.all(), required=False) self.assertIsInstance(f.clean([]), EmptyQuerySet) self.assertIsInstance(f.clean(()), EmptyQuerySet) @@ -1375,7 +1522,59 @@ class ModelFieldsTests(TestCase): (self.c2.pk, "multicategory It's a test"), (self.c3.pk, 'multicategory Third')]) - # OneToOneField ############################################################### + def test_model_multiple_choice_number_of_queries(self): + """ + Test that ModelMultipleChoiceField does O(1) queries instead of + O(n) (#10156). + """ + persons = [Writer.objects.create(name="Person %s" % i) for i in range(30)] + + f = forms.ModelMultipleChoiceField(queryset=Writer.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): + Writer.objects.create(name="Person %s" % i) + + self._validator_run = False + + def my_validator(value): + self._validator_run = True + + f = forms.ModelMultipleChoiceField(queryset=Writer.objects.all(), + validators=[my_validator]) + + f.clean([p.pk for p in Writer.objects.all()[8:9]]) + self.assertTrue(self._validator_run) + + def test_model_multiple_choice_show_hidden_initial(self): + """ + Test support of show_hidden_initial by ModelMultipleChoiceField. + """ + class WriterForm(forms.Form): + persons = forms.ModelMultipleChoiceField(show_hidden_initial=True, + queryset=Writer.objects.all()) + + person1 = Writer.objects.create(name="Person 1") + person2 = Writer.objects.create(name="Person 2") + + form = WriterForm(initial={'persons': [person1, person2]}, + data={'initial-persons': [str(person1.pk), str(person2.pk)], + 'persons': [str(person1.pk), str(person2.pk)]}) + self.assertTrue(form.is_valid()) + self.assertFalse(form.has_changed()) + + form = WriterForm(initial={'persons': [person1, person2]}, + data={'initial-persons': [str(person1.pk), str(person2.pk)], + 'persons': [str(person2.pk)]}) + self.assertTrue(form.is_valid()) + self.assertTrue(form.has_changed()) + + +class ModelOneToOneFieldTests(TestCase): def test_modelform_onetoonefield(self): class ImprovedArticleForm(forms.ModelForm): class Meta: @@ -1440,7 +1639,106 @@ class ModelFieldsTests(TestCase):

    ''' % (self.w_woodward.pk, self.w_royko.pk)) - def test_file_field(self): + 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=datetime.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 retrieve 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=datetime.date(1991, 8, 22)) + author = Author1.objects.create(publication=publication, full_name='John Doe') + form = AuthorForm({'publication': '', 'full_name': 'John Doe'}, instance=author) + self.assertFalse(form.is_valid()) + + +class FileAndImageFieldTests(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. + """ + class DocumentForm(forms.ModelForm): + class Meta: + model = Document + fields = '__all__' + + 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. + """ + class DocumentForm(forms.ModelForm): + class Meta: + model = Document + fields = '__all__' + + 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) + + def test_file_field_data(self): # Test conditions when files is either not given or empty. f = TextFileForm(data={'description': 'Assistance'}) self.assertFalse(f.is_valid()) @@ -1497,7 +1795,7 @@ class ModelFieldsTests(TestCase): instance.file.delete() instance.delete() - def test_onetoonefield_required_false(self): + def test_filefield_required_false(self): # Test the non-required FileField f = TextFileForm(data={'description': 'Assistance'}) f.fields['file'].required = False @@ -1526,17 +1824,33 @@ class ModelFieldsTests(TestCase): instance.file.delete() instance.delete() - def test_big_integer_field(self): - bif = BigIntForm({'biggie': '-9223372036854775808'}) - self.assertTrue(bif.is_valid()) - bif = BigIntForm({'biggie': '-9223372036854775809'}) - self.assertFalse(bif.is_valid()) - self.assertEqual(bif.errors, {'biggie': ['Ensure this value is greater than or equal to -9223372036854775808.']}) - bif = BigIntForm({'biggie': '9223372036854775807'}) - self.assertTrue(bif.is_valid()) - bif = BigIntForm({'biggie': '9223372036854775808'}) - self.assertFalse(bif.is_valid()) - self.assertEqual(bif.errors, {'biggie': ['Ensure this value is less than or equal to 9223372036854775807.']}) + def test_custom_file_field_save(self): + """ + Regression for #11149: save_form_data should be called only once + """ + class CFFForm(forms.ModelForm): + class Meta: + model = CustomFF + fields = '__all__' + + # 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() + + def test_file_path_field_blank(self): + """ + Regression test for #8842: FilePathField(blank=True) + """ + class FPForm(forms.ModelForm): + class Meta: + model = FilePathModel + fields = '__all__' + + form = FPForm() + names = [p[1] for p in form['path'].field.choices] + names.sort() + self.assertEqual(names, ['---------', '__init__.py', 'models.py', 'tests.py']) @skipUnless(test_images, "Pillow/PIL not installed") def test_image_field(self): @@ -1674,12 +1988,24 @@ class ModelFieldsTests(TestCase): self.assertEqual(instance.image.name, 'foo/test4.png') instance.delete() - def test_media_on_modelform(self): - # Similar to a regular Form class you can define custom media to be used on - # the ModelForm. - f = ModelFormWithMedia() - self.assertHTMLEqual(six.text_type(f.media), ''' -''') +class ModelOtherFieldTests(TestCase): + def test_big_integer_field(self): + bif = BigIntForm({'biggie': '-9223372036854775808'}) + self.assertTrue(bif.is_valid()) + bif = BigIntForm({'biggie': '-9223372036854775809'}) + self.assertFalse(bif.is_valid()) + self.assertEqual(bif.errors, {'biggie': ['Ensure this value is greater than or equal to -9223372036854775808.']}) + bif = BigIntForm({'biggie': '9223372036854775807'}) + self.assertTrue(bif.is_valid()) + bif = BigIntForm({'biggie': '9223372036854775808'}) + self.assertFalse(bif.is_valid()) + self.assertEqual(bif.errors, {'biggie': ['Ensure this value is less than or equal to 9223372036854775807.']}) + + def test_comma_separated_integer_field(self): + class CommaSeparatedIntegerForm(forms.ModelForm): + class Meta: + model = CommaSeparatedInteger + fields = '__all__' f = CommaSeparatedIntegerForm({'field': '1,2,3'}) self.assertTrue(f.is_valid()) @@ -1700,31 +2026,54 @@ class ModelFieldsTests(TestCase): self.assertTrue(f.is_valid()) self.assertEqual(f.cleaned_data, {'field': '1'}) - # This Price instance generated by this form is not valid because the quantity - # field is required, but the form is valid because the field is excluded from - # the form. This is for backwards compatibility. - - form = PriceFormWithoutQuantity({'price': '6.00'}) - self.assertTrue(form.is_valid()) - price = form.save(commit=False) - with self.assertRaises(ValidationError): - price.full_clean() - - # The form should not validate fields that it doesn't contain even if they are - # specified using 'fields', not 'exclude'. + def test_url_on_modelform(self): + "Check basic URL field validation on model forms" + class HomepageForm(forms.ModelForm): class Meta: - model = Price - fields = ('price',) - form = PriceFormWithoutQuantity({'price': '6.00'}) + model = Homepage + fields = '__all__' + + 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) + """ + class HomepageForm(forms.ModelForm): + class Meta: + model = Homepage + fields = '__all__' + + form = HomepageForm({'url': 'example.com'}) self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['url'], 'http://example.com/') - # The form should still have an instance of a model that is not complete and - # not saved into a DB yet. + form = HomepageForm({'url': 'example.com/test'}) + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['url'], 'http://example.com/test') - self.assertEqual(form.instance.price, Decimal('6.00')) - self.assertIsNone(form.instance.quantity) - self.assertIsNone(form.instance.pk) +class OtherModelFormTests(TestCase): + def test_media_on_modelform(self): + # Similar to a regular Form class you can define custom media to be used on + # the ModelForm. + f = ModelFormWithMedia() + self.assertHTMLEqual(six.text_type(f.media), ''' +''') + + def test_choices_type(self): # Choices on CharField and IntegerField f = ArticleForm() with self.assertRaises(ValidationError): @@ -1805,6 +2154,8 @@ class ModelFieldsTests(TestCase): Hold down "Control", or "Command" on a Mac, to select more than one.

    """ % {'blue_pk': colour.pk}) + +class ModelFormCustomErrorTests(TestCase): def test_custom_error_messages(self): data = {'name1': '@#$!!**@#$', 'name2': '@#$!!**@#$'} errors = CustomErrorMessageForm(data).errors @@ -1819,11 +2170,58 @@ class ModelFieldsTests(TestCase): def test_model_clean_error_messages(self): data = {'name1': 'FORBIDDEN_VALUE', 'name2': 'ABC'} - errors = CustomErrorMessageForm(data).errors + form = CustomErrorMessageForm(data) + self.assertFalse(form.is_valid()) self.assertHTMLEqual( - str(errors['name1']), + str(form.errors['name1']), '' ) + data = {'name1': 'GLOBAL_ERROR', 'name2': 'ABC'} + form = CustomErrorMessageForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['__all__'], ['Global error message.']) + + +class CustomCleanTests(TestCase): + def test_override_clean(self): + """ + Regression for #12596: Calling super from ModelForm.clean() should be + optional. + """ + class TripleFormWithCleanOverride(forms.ModelForm): + class Meta: + model = Triple + fields = '__all__' + + 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 + + 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) + + def test_model_form_clean_applies_to_model(self): + """ + Regression test for #12960. Make sure the cleaned_data returned from + ModelForm.clean() is applied to the model instance. + """ + class CategoryForm(forms.ModelForm): + class Meta: + model = Category + fields = '__all__' + + def clean(self): + self.cleaned_data['name'] = self.cleaned_data['name'].upper() + return self.cleaned_data + + data = {'name': 'Test', 'slug': 'test', 'url': '/test'} + form = CategoryForm(data) + category = form.save() + self.assertEqual(category.name, 'TEST') class M2mHelpTextTest(TestCase): @@ -1892,6 +2290,12 @@ class ModelFormInheritanceTests(TestCase): self.assertEqual(list(type(str('NewForm'), (ModelForm, Form), {'age': None})().fields.keys()), ['name']) +class StumpJokeForm(forms.ModelForm): + class Meta: + model = StumpJoke + fields = '__all__' + + class LimitChoicesToTest(TestCase): """ Tests the functionality of ``limit_choices_to``. @@ -1921,3 +2325,126 @@ class LimitChoicesToTest(TestCase): stumpjokeform = StumpJokeForm() self.assertIn(self.threepwood, stumpjokeform.fields['has_fooled_today'].queryset) self.assertNotIn(self.marley, stumpjokeform.fields['has_fooled_today'].queryset) + + +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} + fields = "__all__" + + 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, fields="__all__") + self.assertNotEqual(Form.base_fields['name'].widget.__class__, forms.Textarea) + + # With a widget should not set the widget to textarea + Form = modelform_factory(Person, fields="__all__", widgets={'name': widget}) + self.assertEqual(Form.base_fields['name'].widget.__class__, forms.Textarea) + + def test_modelform_factory_without_fields(self): + """ Regression for #19733 """ + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", RemovedInDjango18Warning) + # This should become an error once deprecation cycle is complete. + modelform_factory(Person) + self.assertEqual(w[0].category, RemovedInDjango18Warning) + + def test_modelform_factory_with_all_fields(self): + """ Regression for #19733 """ + form = modelform_factory(Person, fields="__all__") + self.assertEqual(list(form.base_fields), ["name"]) + + 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} + fields = "__all__" + + 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, fields="__all__", + formfield_callback='not a function or callable') + + +class LocalizedModelFormTest(TestCase): + def test_model_form_applies_localize_to_some_fields(self): + class PartiallyLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = ('left', 'right',) + fields = '__all__' + + f = PartiallyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) + self.assertTrue(f.is_valid()) + self.assertTrue(f.fields['left'].localize) + self.assertFalse(f.fields['middle'].localize) + self.assertTrue(f.fields['right'].localize) + + def test_model_form_applies_localize_to_all_fields(self): + class FullyLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = '__all__' + fields = '__all__' + + f = FullyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) + self.assertTrue(f.is_valid()) + self.assertTrue(f.fields['left'].localize) + self.assertTrue(f.fields['middle'].localize) + self.assertTrue(f.fields['right'].localize) + + def test_model_form_refuses_arbitrary_string(self): + with self.assertRaises(TypeError): + class BrokenLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = "foo" + + +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, fields="__all__", form=CustomMetaclassForm) + self.assertEqual(new_cls.base_fields, {}) diff --git a/tests/model_forms_regress/__init__.py b/tests/model_forms_regress/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/model_forms_regress/models.py b/tests/model_forms_regress/models.py deleted file mode 100644 index 396bd1eaa4..0000000000 --- a/tests/model_forms_regress/models.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import unicode_literals - -import os - -from django.core.exceptions import ValidationError -from django.db import models -from django.utils.encoding import python_2_unicode_compatible -from django.utils._os import upath - - -class Person(models.Model): - name = models.CharField(max_length=100) - - -class Triple(models.Model): - left = models.IntegerField() - middle = models.IntegerField() - right = models.IntegerField() - - class Meta: - unique_together = (('left', 'middle'), ('middle', 'right')) - - -class FilePathModel(models.Model): - path = models.FilePathField(path=os.path.dirname(upath(__file__)), match=".*\.py$", blank=True) - - -@python_2_unicode_compatible -class Publication(models.Model): - title = models.CharField(max_length=30) - date_published = models.DateField() - - def __str__(self): - return self.title - - -@python_2_unicode_compatible -class Article(models.Model): - headline = models.CharField(max_length=100) - publications = models.ManyToManyField(Publication) - - def __str__(self): - return self.headline - - -class CustomFileField(models.FileField): - def save_form_data(self, instance, data): - been_here = getattr(self, 'been_saved', False) - assert not been_here, "save_form_data called more than once" - setattr(self, 'been_saved', True) - - -class CustomFF(models.Model): - f = CustomFileField(upload_to='unused', blank=True) - - -class RealPerson(models.Model): - name = models.CharField(max_length=100) - - def clean(self): - if self.name.lower() == 'anonymous': - raise ValidationError("Please specify a real name.") - - -class Author(models.Model): - publication = models.OneToOneField(Publication, null=True, blank=True) - full_name = models.CharField(max_length=255) - - -class Author1(models.Model): - publication = models.OneToOneField(Publication, null=False) - full_name = models.CharField(max_length=255) - - -class Homepage(models.Model): - url = models.URLField() - - -class Document(models.Model): - myfile = models.FileField(upload_to='unused', blank=True) - - -class Edition(models.Model): - author = models.ForeignKey(Person) - publication = models.ForeignKey(Publication) - edition = models.IntegerField() - isbn = models.CharField(max_length=13, unique=True) - - class Meta: - unique_together = (('author', 'publication'), ('publication', 'edition'),) diff --git a/tests/model_forms_regress/tests.py b/tests/model_forms_regress/tests.py deleted file mode 100644 index 2e6bdc4b31..0000000000 --- a/tests/model_forms_regress/tests.py +++ /dev/null @@ -1,604 +0,0 @@ -from __future__ import unicode_literals - -from datetime import date -import unittest -import warnings - -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.test import TestCase -from django.utils import six -from django.utils.deprecation import RemovedInDjango18Warning - -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) - - def test_model_multiple_choice_show_hidden_initial(self): - """ - Test support of show_hidden_initial by ModelMultipleChoiceField. - """ - class PersonForm(forms.Form): - persons = forms.ModelMultipleChoiceField(show_hidden_initial=True, - queryset=Person.objects.all()) - - person1 = Person.objects.create(name="Person 1") - person2 = Person.objects.create(name="Person 2") - - form = PersonForm(initial={'persons': [person1, person2]}, - data={'initial-persons': [str(person1.pk), str(person2.pk)], - 'persons': [str(person1.pk), str(person2.pk)]}) - self.assertTrue(form.is_valid()) - self.assertFalse(form.has_changed()) - - form = PersonForm(initial={'persons': [person1, person2]}, - data={'initial-persons': [str(person1.pk), str(person2.pk)], - 'persons': [str(person2.pk)]}) - self.assertTrue(form.is_valid()) - self.assertTrue(form.has_changed()) - - -class TripleForm(forms.ModelForm): - class Meta: - model = Triple - fields = '__all__' - - -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 - fields = '__all__' - - 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) - - -class PartiallyLocalizedTripleForm(forms.ModelForm): - class Meta: - model = Triple - localized_fields = ('left', 'right',) - fields = '__all__' - - -class FullyLocalizedTripleForm(forms.ModelForm): - class Meta: - model = Triple - localized_fields = '__all__' - fields = '__all__' - - -class LocalizedModelFormTest(TestCase): - def test_model_form_applies_localize_to_some_fields(self): - f = PartiallyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) - self.assertTrue(f.is_valid()) - self.assertTrue(f.fields['left'].localize) - self.assertFalse(f.fields['middle'].localize) - self.assertTrue(f.fields['right'].localize) - - def test_model_form_applies_localize_to_all_fields(self): - f = FullyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) - self.assertTrue(f.is_valid()) - self.assertTrue(f.fields['left'].localize) - self.assertTrue(f.fields['middle'].localize) - self.assertTrue(f.fields['right'].localize) - - def test_model_form_refuses_arbitrary_string(self): - with self.assertRaises(TypeError): - class BrokenLocalizedTripleForm(forms.ModelForm): - class Meta: - model = Triple - localized_fields = "foo" - - -# 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 - fields = '__all__' - - -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 - fields = '__all__' - - -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, fields="__all__", - formfield_callback=formfield_for_dbfield) - form = ModelForm() - self.assertHTMLEqual(form.as_ul(), """
  • -
  • Hold down "Control", or "Command" on a Mac, to select more than one.
  • """ - % (book1.pk, book2.pk, book3.pk)) - - -class CFFForm(forms.ModelForm): - class Meta: - model = CustomFF - fields = '__all__' - - -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 - fields = '__all__' - - -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 retrieve 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 - fields = '__all__' - - -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} - fields = "__all__" - - 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, fields="__all__") - self.assertNotEqual(Form.base_fields['name'].widget.__class__, forms.Textarea) - - # With a widget should not set the widget to textarea - Form = modelform_factory(Person, fields="__all__", 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} - fields = "__all__" - - 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, fields="__all__", - 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 - fields = '__all__' - - -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 - fields = '__all__' - - -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, fields="__all__")({'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, fields="__all__", form=CustomMetaclassForm) - self.assertEqual(new_cls.base_fields, {}) - - -class TestTicket19733(TestCase): - def test_modelform_factory_without_fields(self): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always", RemovedInDjango18Warning) - # This should become an error once deprecation cycle is complete. - modelform_factory(Person) - self.assertEqual(w[0].category, RemovedInDjango18Warning) - - def test_modelform_factory_with_all_fields(self): - form = modelform_factory(Person, fields="__all__") - self.assertEqual(list(form.base_fields), ["name"])