Fixed #17642 -- Added min_num support to modelformsets, inlines, and the admin.

Thanks Stephen Burrows for work on the patch as well.

Forwardport of 2914f66983 from stable/1.7.x
This commit is contained in:
Anders Steinlein 2014-03-05 21:19:40 +01:00 committed by Tim Graham
parent 860d31ac7a
commit 4ef10f245a
15 changed files with 229 additions and 61 deletions

View File

@ -850,6 +850,7 @@ class InlineModelAdminChecks(BaseModelAdminChecks):
errors.extend(self._check_exclude_of_parent_model(cls, parent_model)) errors.extend(self._check_exclude_of_parent_model(cls, parent_model))
errors.extend(self._check_extra(cls)) errors.extend(self._check_extra(cls))
errors.extend(self._check_max_num(cls)) errors.extend(self._check_max_num(cls))
errors.extend(self._check_min_num(cls))
errors.extend(self._check_formset(cls)) errors.extend(self._check_formset(cls))
return errors return errors
@ -909,12 +910,22 @@ class InlineModelAdminChecks(BaseModelAdminChecks):
else: else:
return [] return []
def _check_min_num(self, cls):
""" Check that min_num is an integer. """
if cls.min_num is None:
return []
elif not isinstance(cls.min_num, int):
return must_be('an integer', option='min_num', obj=cls, id='admin.E205')
else:
return []
def _check_formset(self, cls): def _check_formset(self, cls):
""" Check formset is a subclass of BaseModelFormSet. """ """ Check formset is a subclass of BaseModelFormSet. """
if not issubclass(cls.formset, BaseModelFormSet): if not issubclass(cls.formset, BaseModelFormSet):
return must_inherit_from(parent='BaseModelFormSet', option='formset', return must_inherit_from(parent='BaseModelFormSet', option='formset',
obj=cls, id='admin.E205') obj=cls, id='admin.E206')
else: else:
return [] return []

View File

@ -1706,6 +1706,7 @@ class InlineModelAdmin(BaseModelAdmin):
fk_name = None fk_name = None
formset = BaseInlineFormSet formset = BaseInlineFormSet
extra = 3 extra = 3
min_num = None
max_num = None max_num = None
template = None template = None
verbose_name = None verbose_name = None
@ -1738,6 +1739,10 @@ class InlineModelAdmin(BaseModelAdmin):
"""Hook for customizing the number of extra inline forms.""" """Hook for customizing the number of extra inline forms."""
return self.extra return self.extra
def get_min_num(self, request, obj=None, **kwargs):
"""Hook for customizing the min number of inline forms."""
return self.min_num
def get_max_num(self, request, obj=None, **kwargs): def get_max_num(self, request, obj=None, **kwargs):
"""Hook for customizing the max number of extra inline forms.""" """Hook for customizing the max number of extra inline forms."""
return self.max_num return self.max_num
@ -1769,6 +1774,7 @@ class InlineModelAdmin(BaseModelAdmin):
"exclude": exclude, "exclude": exclude,
"formfield_callback": partial(self.formfield_for_dbfield, request=request), "formfield_callback": partial(self.formfield_for_dbfield, request=request),
"extra": self.get_extra(request, obj, **kwargs), "extra": self.get_extra(request, obj, **kwargs),
"min_num": self.get_min_num(request, obj, **kwargs),
"max_num": self.get_max_num(request, obj, **kwargs), "max_num": self.get_max_num(request, obj, **kwargs),
"can_delete": can_delete, "can_delete": can_delete,
} }

View File

@ -119,6 +119,7 @@ class GenericInlineModelAdmin(InlineModelAdmin):
"can_delete": can_delete, "can_delete": can_delete,
"can_order": False, "can_order": False,
"fields": fields, "fields": fields,
"min_num": self.min_num,
"max_num": self.max_num, "max_num": self.max_num,
"exclude": exclude "exclude": exclude
} }

View File

@ -56,9 +56,9 @@ def generic_inlineformset_factory(model, form=ModelForm,
ct_field="content_type", fk_field="object_id", ct_field="content_type", fk_field="object_id",
fields=None, exclude=None, fields=None, exclude=None,
extra=3, can_order=False, can_delete=True, extra=3, can_order=False, can_delete=True,
max_num=None, max_num=None, formfield_callback=None,
formfield_callback=None, validate_max=False, validate_max=False, for_concrete_model=True,
for_concrete_model=True): min_num=None, validate_min=False):
""" """
Returns a ``GenericInlineFormSet`` for the given kwargs. Returns a ``GenericInlineFormSet`` for the given kwargs.
@ -81,7 +81,8 @@ def generic_inlineformset_factory(model, form=ModelForm,
formset=formset, formset=formset,
extra=extra, can_delete=can_delete, can_order=can_order, extra=extra, can_delete=can_delete, can_order=can_order,
fields=fields, exclude=exclude, max_num=max_num, fields=fields, exclude=exclude, max_num=max_num,
validate_max=validate_max) validate_max=validate_max, min_num=min_num,
validate_min=validate_min)
FormSet.ct_field = ct_field FormSet.ct_field = ct_field
FormSet.ct_fk_field = fk_field FormSet.ct_fk_field = fk_field
FormSet.for_concrete_model = for_concrete_model FormSet.for_concrete_model = for_concrete_model

View File

@ -805,7 +805,8 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
formset=BaseModelFormSet, extra=1, can_delete=False, formset=BaseModelFormSet, extra=1, can_delete=False,
can_order=False, max_num=None, fields=None, exclude=None, can_order=False, max_num=None, fields=None, exclude=None,
widgets=None, validate_max=False, localized_fields=None, widgets=None, validate_max=False, localized_fields=None,
labels=None, help_texts=None, error_messages=None): labels=None, help_texts=None, error_messages=None,
min_num=None, validate_min=False):
""" """
Returns a FormSet class for the given Django model class. Returns a FormSet class for the given Django model class.
""" """
@ -823,9 +824,9 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
formfield_callback=formfield_callback, formfield_callback=formfield_callback,
widgets=widgets, localized_fields=localized_fields, widgets=widgets, localized_fields=localized_fields,
labels=labels, help_texts=help_texts, error_messages=error_messages) labels=labels, help_texts=help_texts, error_messages=error_messages)
FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, FormSet = formset_factory(form, formset, extra=extra, min_num=min_num, max_num=max_num,
can_order=can_order, can_delete=can_delete, can_order=can_order, can_delete=can_delete,
validate_max=validate_max) validate_min=validate_min, validate_max=validate_max)
FormSet.model = model FormSet.model = model
return FormSet return FormSet
@ -969,7 +970,8 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
fields=None, exclude=None, extra=3, can_order=False, fields=None, exclude=None, extra=3, can_order=False,
can_delete=True, max_num=None, formfield_callback=None, can_delete=True, max_num=None, formfield_callback=None,
widgets=None, validate_max=False, localized_fields=None, widgets=None, validate_max=False, localized_fields=None,
labels=None, help_texts=None, error_messages=None): labels=None, help_texts=None, error_messages=None,
min_num=None, validate_min=False):
""" """
Returns an ``InlineFormSet`` for the given kwargs. Returns an ``InlineFormSet`` for the given kwargs.
@ -989,8 +991,10 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
'can_order': can_order, 'can_order': can_order,
'fields': fields, 'fields': fields,
'exclude': exclude, 'exclude': exclude,
'min_num': min_num,
'max_num': max_num, 'max_num': max_num,
'widgets': widgets, 'widgets': widgets,
'validate_min': validate_min,
'validate_max': validate_max, 'validate_max': validate_max,
'localized_fields': localized_fields, 'localized_fields': localized_fields,
'labels': labels, 'labels': labels,

View File

@ -205,7 +205,8 @@ inline on a :class:`~django.contrib.admin.ModelAdmin`.
* **admin.E202**: ``<model>`` has no ForeignKey to ``<parent model>``./``<model>`` has more than one ForeignKey to ``<parent model>``. * **admin.E202**: ``<model>`` has no ForeignKey to ``<parent model>``./``<model>`` has more than one ForeignKey to ``<parent model>``.
* **admin.E203**: The value of ``extra`` must be an integer. * **admin.E203**: The value of ``extra`` must be an integer.
* **admin.E204**: The value of ``max_num`` must be an integer. * **admin.E204**: The value of ``max_num`` must be an integer.
* **admin.E205**: The value of ``formset`` must inherit from ``BaseModelFormSet``. * **admin.E205**: The value of ``min_num`` must be an integer.
* **admin.E206**: The value of ``formset`` must inherit from ``BaseModelFormSet``.
GenericInlineModelAdmin GenericInlineModelAdmin
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -1966,6 +1966,16 @@ The ``InlineModelAdmin`` class adds:
:meth:`InlineModelAdmin.get_max_num` also allows you to customize the :meth:`InlineModelAdmin.get_max_num` also allows you to customize the
maximum number of extra forms. maximum number of extra forms.
.. attribute:: InlineModelAdmin.min_num
.. versionadded:: 1.7
This controls the minimum number of forms to show in the inline.
See :func:`~django.forms.models.modelformset_factory` for more information.
:meth:`InlineModelAdmin.get_min_num` also allows you to customize the
minimum number of displayed forms.
.. attribute:: InlineModelAdmin.raw_id_fields .. attribute:: InlineModelAdmin.raw_id_fields
By default, Django's admin uses a select-box interface (<select>) for By default, Django's admin uses a select-box interface (<select>) for
@ -2042,6 +2052,16 @@ The ``InlineModelAdmin`` class adds:
return max_num - 5 return max_num - 5
return max_num return max_num
.. method:: InlineModelAdmin.get_min_num(request, obj=None, **kwargs)
.. versionadded:: 1.7
Returns the minimum number of inline forms to use. By default,
returns the :attr:`InlineModelAdmin.min_num` attribute.
Override this method to programmatically determine the minimum number of
inline forms. For example, this may be based on the model instance
(passed as the keyword argument ``obj``).
Working with a model with two or more foreign keys to the same parent model Working with a model with two or more foreign keys to the same parent model
--------------------------------------------------------------------------- ---------------------------------------------------------------------------

View File

@ -495,7 +495,7 @@ The :mod:`django.contrib.contenttypes.forms` module provides:
This class used to be defined in ``django.contrib.contenttypes.generic``. This class used to be defined in ``django.contrib.contenttypes.generic``.
.. function:: generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False, for_concrete_model=True) .. function:: generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False, for_concrete_model=True, min_num=None, validate_min=False)
Returns a ``GenericInlineFormSet`` using Returns a ``GenericInlineFormSet`` using
:func:`~django.forms.models.modelformset_factory`. :func:`~django.forms.models.modelformset_factory`.
@ -514,6 +514,10 @@ The :mod:`django.contrib.contenttypes.forms` module provides:
This function used to be defined in ``django.contrib.contenttypes.generic``. This function used to be defined in ``django.contrib.contenttypes.generic``.
.. versionchanged:: 1.7
``min_num`` and ``validate_min`` were added.
.. module:: django.contrib.contenttypes.admin .. module:: django.contrib.contenttypes.admin

View File

@ -45,7 +45,7 @@ Model Form Functions
Previously, omitting the list of fields was allowed and resulted in Previously, omitting the list of fields was allowed and resulted in
a form with all fields of the model. a form with all fields of the model.
.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None) .. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False)
Returns a ``FormSet`` class for the given ``model`` class. Returns a ``FormSet`` class for the given ``model`` class.
@ -61,7 +61,7 @@ Model Form Functions
See :ref:`model-formsets` for example usage. See :ref:`model-formsets` for example usage.
.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None) .. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False)
Returns an ``InlineFormSet`` using :func:`modelformset_factory` with Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
defaults of ``formset=``:class:`~django.forms.models.BaseInlineFormSet`, defaults of ``formset=``:class:`~django.forms.models.BaseInlineFormSet`,

View File

@ -1,13 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.admin import TabularInline, ModelAdmin
from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
from django.contrib.admin.helpers import InlineAdminForm from django.contrib.admin.helpers import InlineAdminForm
from django.contrib.auth.models import User, Permission from django.contrib.auth.models import User, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.test import TestCase, override_settings from django.test import TestCase, override_settings, RequestFactory
# local test models # local test models
from .admin import InnerInline from .admin import InnerInline, site as admin_site
from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person, from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile, OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile,
ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2,
@ -28,6 +29,7 @@ class TestInline(TestCase):
result = self.client.login(username='super', password='secret') result = self.client.login(username='super', password='secret')
self.assertEqual(result, True) self.assertEqual(result, True)
self.factory = RequestFactory()
def tearDown(self): def tearDown(self):
self.client.logout() self.client.logout()
@ -221,6 +223,62 @@ class TestInline(TestCase):
self.assertContains(response, max_forms_input % 2) self.assertContains(response, max_forms_input % 2)
self.assertContains(response, total_forms_hidden) self.assertContains(response, total_forms_hidden)
def test_min_num(self):
"""
Ensure that min_num and extra determine number of forms.
"""
class MinNumInline(TabularInline):
model = BinaryTree
min_num = 2
extra = 3
modeladmin = ModelAdmin(BinaryTree, admin_site)
modeladmin.inlines = [MinNumInline]
min_forms = '<input id="id_binarytree_set-MIN_NUM_FORMS" name="binarytree_set-MIN_NUM_FORMS" type="hidden" value="2" />'
total_forms = '<input id="id_binarytree_set-TOTAL_FORMS" name="binarytree_set-TOTAL_FORMS" type="hidden" value="5" />'
request = self.factory.get('/admin/admin_inlines/binarytree/add/')
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request)
self.assertContains(response, min_forms)
self.assertContains(response, total_forms)
def test_custom_min_num(self):
"""
Ensure that get_min_num is called and used correctly.
See #22628 - this will change when that's fixed.
"""
bt_head = BinaryTree.objects.create(name="Tree Head")
BinaryTree.objects.create(name="First Child", parent=bt_head)
class MinNumInline(TabularInline):
model = BinaryTree
extra = 3
def get_min_num(self, request, obj=None, **kwargs):
if obj:
return 5
return 2
modeladmin = ModelAdmin(BinaryTree, admin_site)
modeladmin.inlines = [MinNumInline]
min_forms = '<input id="id_binarytree_set-MIN_NUM_FORMS" name="binarytree_set-MIN_NUM_FORMS" type="hidden" value="%d" />'
total_forms = '<input id="id_binarytree_set-TOTAL_FORMS" name="binarytree_set-TOTAL_FORMS" type="hidden" value="%d" />'
request = self.factory.get('/admin/admin_inlines/binarytree/add/')
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request)
self.assertContains(response, min_forms % 2)
self.assertContains(response, total_forms % 5)
request = self.factory.get("/admin/admin_inlines/binarytree/%d/" % bt_head.id)
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request, object_id=str(bt_head.id))
self.assertContains(response, min_forms % 5)
self.assertContains(response, total_forms % 9)
def test_inline_nonauto_noneditable_pk(self): def test_inline_nonauto_noneditable_pk(self):
response = self.client.get('/admin/admin_inlines/author/add/') response = self.client.get('/admin/admin_inlines/author/add/')
self.assertContains(response, self.assertContains(response,

View File

@ -1,8 +1,8 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.contenttypes.admin import GenericTabularInline from django.contrib.contenttypes.admin import GenericTabularInline
from .models import (Media, PhoneNumber, Episode, EpisodeExtra, Contact, from .models import (Media, PhoneNumber, Episode, Contact,
Category, EpisodePermanent, EpisodeMaxNum) Category, EpisodePermanent)
site = admin.AdminSite(name="admin") site = admin.AdminSite(name="admin")
@ -18,17 +18,6 @@ class EpisodeAdmin(admin.ModelAdmin):
] ]
class MediaExtraInline(GenericTabularInline):
model = Media
extra = 0
class MediaMaxNumInline(GenericTabularInline):
model = Media
extra = 5
max_num = 2
class PhoneNumberInline(GenericTabularInline): class PhoneNumberInline(GenericTabularInline):
model = PhoneNumber model = PhoneNumber
@ -39,8 +28,6 @@ class MediaPermanentInline(GenericTabularInline):
site.register(Episode, EpisodeAdmin) site.register(Episode, EpisodeAdmin)
site.register(EpisodeExtra, inlines=[MediaExtraInline])
site.register(EpisodeMaxNum, inlines=[MediaMaxNumInline])
site.register(Contact, inlines=[PhoneNumberInline]) site.register(Contact, inlines=[PhoneNumberInline])
site.register(Category) site.register(Category)
site.register(EpisodePermanent, inlines=[MediaPermanentInline]) site.register(EpisodePermanent, inlines=[MediaPermanentInline])

View File

@ -27,26 +27,6 @@ class Media(models.Model):
def __str__(self): def __str__(self):
return self.url return self.url
#
# These models let us test the different GenericInline settings at
# different urls in the admin site.
#
#
# Generic inline with extra = 0
#
class EpisodeExtra(Episode):
pass
#
# Generic inline with extra and max_num
#
class EpisodeMaxNum(Episode):
pass
# #
# Generic inline with unique_together # Generic inline with unique_together

View File

@ -4,17 +4,17 @@ import warnings
from django.contrib import admin from django.contrib import admin
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import User
from django.contrib.contenttypes.admin import GenericTabularInline from django.contrib.contenttypes.admin import GenericTabularInline
from django.contrib.contenttypes.forms import generic_inlineformset_factory from django.contrib.contenttypes.forms import generic_inlineformset_factory
from django.forms.formsets import DEFAULT_MAX_NUM from django.forms.formsets import DEFAULT_MAX_NUM
from django.forms.models import ModelForm from django.forms.models import ModelForm
from django.test import TestCase, override_settings from django.test import TestCase, override_settings, RequestFactory
from django.utils.deprecation import RemovedInDjango19Warning from django.utils.deprecation import RemovedInDjango19Warning
# local test models # local test models
from .admin import MediaInline, MediaPermanentInline from .admin import MediaInline, MediaPermanentInline, site as admin_site
from .models import (Episode, EpisodeExtra, EpisodeMaxNum, Media, from .models import Episode, Media, EpisodePermanent, Category
EpisodePermanent, Category)
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',), @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
@ -137,6 +137,7 @@ class GenericInlineAdminParametersTest(TestCase):
def setUp(self): def setUp(self):
self.client.login(username='super', password='secret') self.client.login(username='super', password='secret')
self.factory = RequestFactory()
def tearDown(self): def tearDown(self):
self.client.logout() self.client.logout()
@ -166,9 +167,18 @@ class GenericInlineAdminParametersTest(TestCase):
""" """
With extra=0, there should be one form. With extra=0, there should be one form.
""" """
e = self._create_object(EpisodeExtra) class ExtraInline(GenericTabularInline):
response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/episodeextra/%s/' % e.pk) model = Media
formset = response.context['inline_admin_formsets'][0].formset extra = 0
modeladmin = admin.ModelAdmin(Episode, admin_site)
modeladmin.inlines = [ExtraInline]
e = self._create_object(Episode)
request = self.factory.get('/generic_inline_admin/admin/generic_inline_admin/episode/%s/' % e.pk)
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request, object_id=str(e.pk))
formset = response.context_data['inline_admin_formsets'][0].formset
self.assertEqual(formset.total_form_count(), 1) self.assertEqual(formset.total_form_count(), 1)
self.assertEqual(formset.initial_form_count(), 1) self.assertEqual(formset.initial_form_count(), 1)
@ -176,12 +186,43 @@ class GenericInlineAdminParametersTest(TestCase):
""" """
With extra=5 and max_num=2, there should be only 2 forms. With extra=5 and max_num=2, there should be only 2 forms.
""" """
e = self._create_object(EpisodeMaxNum) class MaxNumInline(GenericTabularInline):
response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/episodemaxnum/%s/' % e.pk) model = Media
formset = response.context['inline_admin_formsets'][0].formset extra = 5
max_num = 2
modeladmin = admin.ModelAdmin(Episode, admin_site)
modeladmin.inlines = [MaxNumInline]
e = self._create_object(Episode)
request = self.factory.get('/generic_inline_admin/admin/generic_inline_admin/episode/%s/' % e.pk)
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request, object_id=str(e.pk))
formset = response.context_data['inline_admin_formsets'][0].formset
self.assertEqual(formset.total_form_count(), 2) self.assertEqual(formset.total_form_count(), 2)
self.assertEqual(formset.initial_form_count(), 1) self.assertEqual(formset.initial_form_count(), 1)
def testMinNumParam(self):
"""
With extra=3 and min_num=2, there should be six forms.
See #22628 - this will change when that's fixed.
"""
class MinNumInline(GenericTabularInline):
model = Media
extra = 3
min_num = 2
modeladmin = admin.ModelAdmin(Episode, admin_site)
modeladmin.inlines = [MinNumInline]
e = self._create_object(Episode)
request = self.factory.get('/generic_inline_admin/admin/generic_inline_admin/episode/%s/' % e.pk)
request.user = User(username='super', is_superuser=True)
response = modeladmin.changeform_view(request, object_id=str(e.pk))
formset = response.context_data['inline_admin_formsets'][0].formset
self.assertEqual(formset.total_form_count(), 6)
self.assertEqual(formset.initial_form_count(), 1)
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',), @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
ROOT_URLCONF="generic_inline_admin.urls") ROOT_URLCONF="generic_inline_admin.urls")

View File

@ -376,6 +376,33 @@ class ModelFormsetTest(TestCase):
'<Author: Walt Whitman>', '<Author: Walt Whitman>',
]) ])
def test_min_num(self):
# Test the behavior of min_num with model formsets. It should be
# added to extra.
qs = Author.objects.none()
AuthorFormSet = modelformset_factory(Author, fields="__all__", extra=0)
formset = AuthorFormSet(queryset=qs)
self.assertEqual(len(formset.forms), 0)
AuthorFormSet = modelformset_factory(Author, fields="__all__", min_num=1, extra=0)
formset = AuthorFormSet(queryset=qs)
self.assertEqual(len(formset.forms), 1)
AuthorFormSet = modelformset_factory(Author, fields="__all__", min_num=1, extra=1)
formset = AuthorFormSet(queryset=qs)
self.assertEqual(len(formset.forms), 2)
def test_min_num_with_existing(self):
# Test the behavior of min_num with existing objects.
# See #22628 - this will change when that's fixed.
Author.objects.create(name='Charles Baudelaire')
qs = Author.objects.all()
AuthorFormSet = modelformset_factory(Author, fields="__all__", extra=0, min_num=1)
formset = AuthorFormSet(queryset=qs)
self.assertEqual(len(formset.forms), 2)
def test_custom_save_method(self): def test_custom_save_method(self):
class PoetForm(forms.ModelForm): class PoetForm(forms.ModelForm):
def save(self, commit=True): def save(self, commit=True):

View File

@ -1444,6 +1444,33 @@ class MaxNumCheckTests(CheckTestCase):
self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel)
class MinNumCheckTests(CheckTestCase):
def test_not_integer(self):
class ValidationTestInline(TabularInline):
model = ValidationTestInlineModel
min_num = "hello"
class ValidationTestModelAdmin(ModelAdmin):
inlines = [ValidationTestInline]
self.assertIsInvalid(
ValidationTestModelAdmin, ValidationTestModel,
"The value of 'min_num' must be an integer.",
'admin.E205',
invalid_obj=ValidationTestInline)
def test_valid_case(self):
class ValidationTestInline(TabularInline):
model = ValidationTestInlineModel
min_num = 2
class ValidationTestModelAdmin(ModelAdmin):
inlines = [ValidationTestInline]
self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel)
class FormsetCheckTests(CheckTestCase): class FormsetCheckTests(CheckTestCase):
def test_invalid_type(self): def test_invalid_type(self):
@ -1460,7 +1487,7 @@ class FormsetCheckTests(CheckTestCase):
self.assertIsInvalid( self.assertIsInvalid(
ValidationTestModelAdmin, ValidationTestModel, ValidationTestModelAdmin, ValidationTestModel,
"The value of 'formset' must inherit from 'BaseModelFormSet'.", "The value of 'formset' must inherit from 'BaseModelFormSet'.",
'admin.E205', 'admin.E206',
invalid_obj=ValidationTestInline) invalid_obj=ValidationTestInline)
def test_valid_case(self): def test_valid_case(self):