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']),
'
Model.clean() error messages.
'
)
+ 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"])