diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index b4fa54a..1b75600 100644
a
|
b
|
class InlineModelAdmin(BaseModelAdmin):
|
1706 | 1706 | fk_name = None |
1707 | 1707 | formset = BaseInlineFormSet |
1708 | 1708 | extra = 3 |
| 1709 | min_num = None |
1709 | 1710 | max_num = None |
1710 | 1711 | template = None |
1711 | 1712 | verbose_name = None |
… |
… |
class InlineModelAdmin(BaseModelAdmin):
|
1738 | 1739 | """Hook for customizing the number of extra inline forms.""" |
1739 | 1740 | return self.extra |
1740 | 1741 | |
| 1742 | def get_min_num(self, request, obj=None, **kwargs): |
| 1743 | """Hook for customizing the min number of inline forms.""" |
| 1744 | return self.min_num |
| 1745 | |
1741 | 1746 | def get_max_num(self, request, obj=None, **kwargs): |
1742 | 1747 | """Hook for customizing the max number of extra inline forms.""" |
1743 | 1748 | return self.max_num |
… |
… |
class InlineModelAdmin(BaseModelAdmin):
|
1769 | 1774 | "exclude": exclude, |
1770 | 1775 | "formfield_callback": partial(self.formfield_for_dbfield, request=request), |
1771 | 1776 | "extra": self.get_extra(request, obj, **kwargs), |
| 1777 | "min_num": self.get_min_num(request, obj, **kwargs), |
1772 | 1778 | "max_num": self.get_max_num(request, obj, **kwargs), |
1773 | 1779 | "can_delete": can_delete, |
1774 | 1780 | } |
diff --git a/django/contrib/contenttypes/admin.py b/django/contrib/contenttypes/admin.py
index c67fbca..9fa3af6 100644
a
|
b
|
class GenericInlineModelAdmin(InlineModelAdmin):
|
41 | 41 | "can_delete": can_delete, |
42 | 42 | "can_order": False, |
43 | 43 | "fields": fields, |
| 44 | "min_num": self.min_num, |
44 | 45 | "max_num": self.max_num, |
45 | 46 | "exclude": exclude |
46 | 47 | } |
diff --git a/django/contrib/contenttypes/forms.py b/django/contrib/contenttypes/forms.py
index 33df752..f962793 100644
a
|
b
|
def generic_inlineformset_factory(model, form=ModelForm,
|
56 | 56 | ct_field="content_type", fk_field="object_id", |
57 | 57 | fields=None, exclude=None, |
58 | 58 | extra=3, can_order=False, can_delete=True, |
59 | | max_num=None, |
60 | | formfield_callback=None, validate_max=False, |
61 | | for_concrete_model=True): |
| 59 | max_num=None, validate_max=False, |
| 60 | min_num=None, validate_min=False, |
| 61 | formfield_callback=None, for_concrete_model=True): |
62 | 62 | """ |
63 | 63 | Returns a ``GenericInlineFormSet`` for the given kwargs. |
64 | 64 | |
… |
… |
def generic_inlineformset_factory(model, form=ModelForm,
|
81 | 81 | formset=formset, |
82 | 82 | extra=extra, can_delete=can_delete, can_order=can_order, |
83 | 83 | fields=fields, exclude=exclude, max_num=max_num, |
84 | | validate_max=validate_max) |
| 84 | validate_max=validate_max, min_num=min_num, |
| 85 | validate_min=validate_min) |
85 | 86 | FormSet.ct_field = ct_field |
86 | 87 | FormSet.ct_fk_field = fk_field |
87 | 88 | FormSet.for_concrete_model = for_concrete_model |
diff --git a/django/forms/models.py b/django/forms/models.py
index a0b47e6..b0a3c36 100644
a
|
b
|
class BaseModelFormSet(BaseFormSet):
|
806 | 806 | |
807 | 807 | def modelformset_factory(model, form=ModelForm, formfield_callback=None, |
808 | 808 | formset=BaseModelFormSet, extra=1, can_delete=False, |
809 | | can_order=False, max_num=None, fields=None, exclude=None, |
810 | | widgets=None, validate_max=False, localized_fields=None, |
| 809 | can_order=False, min_num=None, max_num=None, fields=None,exclude=None, |
| 810 | widgets=None, validate_min=False, validate_max=False, localized_fields=None, |
811 | 811 | labels=None, help_texts=None, error_messages=None): |
812 | 812 | """ |
813 | 813 | Returns a FormSet class for the given Django model class. |
… |
… |
def modelformset_factory(model, form=ModelForm, formfield_callback=None,
|
831 | 831 | formfield_callback=formfield_callback, |
832 | 832 | widgets=widgets, localized_fields=localized_fields, |
833 | 833 | labels=labels, help_texts=help_texts, error_messages=error_messages) |
834 | | FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, |
| 834 | FormSet = formset_factory(form, formset, extra=extra, min_num=min_num, max_num=max_num, |
835 | 835 | can_order=can_order, can_delete=can_delete, |
836 | | validate_max=validate_max) |
| 836 | validate_min=validate_min, validate_max=validate_max) |
837 | 837 | FormSet.model = model |
838 | 838 | return FormSet |
839 | 839 | |
… |
… |
def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False):
|
975 | 975 | def inlineformset_factory(parent_model, model, form=ModelForm, |
976 | 976 | formset=BaseInlineFormSet, fk_name=None, |
977 | 977 | fields=None, exclude=None, extra=3, can_order=False, |
978 | | can_delete=True, max_num=None, formfield_callback=None, |
979 | | widgets=None, validate_max=False, localized_fields=None, |
| 978 | can_delete=True, min_num=None, max_num=None, formfield_callback=None, |
| 979 | widgets=None, validate_min=False, validate_max=False, localized_fields=None, |
980 | 980 | labels=None, help_texts=None, error_messages=None): |
981 | 981 | """ |
982 | 982 | Returns an ``InlineFormSet`` for the given kwargs. |
… |
… |
def inlineformset_factory(parent_model, model, form=ModelForm,
|
997 | 997 | 'can_order': can_order, |
998 | 998 | 'fields': fields, |
999 | 999 | 'exclude': exclude, |
| 1000 | 'min_num': min_num, |
1000 | 1001 | 'max_num': max_num, |
1001 | 1002 | 'widgets': widgets, |
| 1003 | 'validate_min': validate_min, |
1002 | 1004 | 'validate_max': validate_max, |
1003 | 1005 | 'localized_fields': localized_fields, |
1004 | 1006 | 'labels': labels, |
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index e160926..63c1fa5 100644
a
|
b
|
The ``InlineModelAdmin`` class adds:
|
1993 | 1993 | :meth:`InlineModelAdmin.get_max_num` also allows you to customize the |
1994 | 1994 | maximum number of extra forms. |
1995 | 1995 | |
| 1996 | .. _ref-contrib-admin-inline-min-num: |
| 1997 | |
| 1998 | .. attribute:: InlineModelAdmin.min_num |
| 1999 | |
| 2000 | This controls the minimum number of forms to show in the inline. |
| 2001 | See :ref:`model-formsets-min-num` for more information. |
| 2002 | |
| 2003 | .. versionadded:: 1.7 |
| 2004 | |
| 2005 | :meth:`InlineModelAdmin.get_min_num` also allows you to customize the |
| 2006 | maximum number of extra forms. |
| 2007 | |
1996 | 2008 | .. attribute:: InlineModelAdmin.raw_id_fields |
1997 | 2009 | |
1998 | 2010 | By default, Django's admin uses a select-box interface (<select>) for |
… |
… |
The ``InlineModelAdmin`` class adds:
|
2073 | 2085 | return max_num - 5 |
2074 | 2086 | return max_num |
2075 | 2087 | |
| 2088 | .. method:: InlineModelAdmin.get_min_num(request, obj=None, **kwargs) |
| 2089 | |
| 2090 | .. versionadded:: 1.7 |
| 2091 | |
| 2092 | Returns the minimum number of inline forms to use. By default, |
| 2093 | returns the :attr:`InlineModelAdmin.min_num` attribute. |
| 2094 | |
| 2095 | Override this method to programmatically determine the minimum number of |
| 2096 | inline forms. For example, this may be based on the model instance |
| 2097 | (passed as the keyword argument ``obj``). |
2076 | 2098 | |
2077 | 2099 | Working with a model with two or more foreign keys to the same parent model |
2078 | 2100 | --------------------------------------------------------------------------- |
diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py
index b46a045..a9ea346 100644
a
|
b
|
from .models import (
|
8 | 8 | Inner4Tabular, NonAutoPKBook, Novel, ParentModelWithCustomPk, Poll, |
9 | 9 | Profile, ProfileCollection, Question, ReadOnlyInline, ShoppingWeakness, |
10 | 10 | Sighting, SomeChildModel, SomeParentModel, SottoCapo, Title, |
11 | | TitleCollection, |
| 11 | TitleCollection, Holder5, Inner5 |
12 | 12 | ) |
13 | 13 | |
14 | 14 | site = admin.AdminSite(name="admin") |
… |
… |
class BinaryTreeAdmin(admin.TabularInline):
|
171 | 171 | return max_num |
172 | 172 | |
173 | 173 | |
| 174 | # inline and admin for testing min_num |
| 175 | |
| 176 | class InnerMinNumInline(admin.TabularInline): |
| 177 | model = Inner5 |
| 178 | min_num = 2 |
| 179 | extra = 2 |
| 180 | |
| 181 | |
| 182 | class HolderMinNumAdmin(admin.ModelAdmin): |
| 183 | inlines = [InnerMinNumInline] |
| 184 | |
| 185 | |
174 | 186 | # admin for #19524 |
175 | 187 | class SightingInline(admin.TabularInline): |
176 | 188 | model = Sighting |
… |
… |
site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2In
|
212 | 224 | site.register(BinaryTree, inlines=[BinaryTreeAdmin]) |
213 | 225 | site.register(ExtraTerrestrial, inlines=[SightingInline]) |
214 | 226 | site.register(SomeParentModel, inlines=[SomeChildModelInline]) |
| 227 | site.register(Holder5, HolderMinNumAdmin) |
diff --git a/tests/admin_inlines/models.py b/tests/admin_inlines/models.py
index 0ad59a0..b824728 100644
a
|
b
|
class Inner4Tabular(models.Model):
|
114 | 114 | dummy = models.IntegerField(help_text="Awesome tabular help text is awesome.") |
115 | 115 | holder = models.ForeignKey(Holder4) |
116 | 116 | |
| 117 | |
| 118 | # Models for min_num |
| 119 | |
| 120 | class Holder5(models.Model): |
| 121 | dummy = models.IntegerField() |
| 122 | |
| 123 | |
| 124 | class Inner5(models.Model): |
| 125 | dummy = models.IntegerField() |
| 126 | holder = models.ForeignKey(Holder5) |
| 127 | |
| 128 | |
117 | 129 | # Models for #12749 |
118 | 130 | |
119 | 131 | |
diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py
index 2f3fdfd..feb4225 100644
a
|
b
|
class TestInline(TestCase):
|
221 | 221 | self.assertContains(response, max_forms_input % 2) |
222 | 222 | self.assertContains(response, total_forms_hidden) |
223 | 223 | |
| 224 | def test_min_num(self): |
| 225 | """ |
| 226 | Ensure that min_num + extra dermine number of inlines. |
| 227 | """ |
| 228 | min_forms = '<input id="id_inner5_set-MIN_NUM_FORMS" name="inner5_set-MIN_NUM_FORMS" type="hidden" value="2" />' |
| 229 | total_forms = '<input id="id_inner5_set-TOTAL_FORMS" name="inner5_set-TOTAL_FORMS" type="hidden" value="4" />' |
| 230 | |
| 231 | response = self.client.get('/admin/admin_inlines/holder5/add/') |
| 232 | self.assertContains(response, min_forms) |
| 233 | self.assertContains(response, total_forms) |
| 234 | |
224 | 235 | def test_inline_nonauto_noneditable_pk(self): |
225 | 236 | response = self.client.get('/admin/admin_inlines/author/add/') |
226 | 237 | self.assertContains(response, |