mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[1.7.x] Fixed #17642 -- Added min_num support to modelformsets, inlines, and the admin.
Thanks Stephen Burrows for work on the patch as well.
This commit is contained in:
		
				
					committed by
					
						 Tim Graham
						Tim Graham
					
				
			
			
				
	
			
			
			
						parent
						
							93d5b0d5b6
						
					
				
				
					commit
					2914f66983
				
			| @@ -850,6 +850,7 @@ class InlineModelAdminChecks(BaseModelAdminChecks): | ||||
|         errors.extend(self._check_exclude_of_parent_model(cls, parent_model)) | ||||
|         errors.extend(self._check_extra(cls)) | ||||
|         errors.extend(self._check_max_num(cls)) | ||||
|         errors.extend(self._check_min_num(cls)) | ||||
|         errors.extend(self._check_formset(cls)) | ||||
|         return errors | ||||
|  | ||||
| @@ -909,12 +910,22 @@ class InlineModelAdminChecks(BaseModelAdminChecks): | ||||
|         else: | ||||
|             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): | ||||
|         """ Check formset is a subclass of BaseModelFormSet. """ | ||||
|  | ||||
|         if not issubclass(cls.formset, BaseModelFormSet): | ||||
|             return must_inherit_from(parent='BaseModelFormSet', option='formset', | ||||
|                                      obj=cls, id='admin.E205') | ||||
|                                      obj=cls, id='admin.E206') | ||||
|         else: | ||||
|             return [] | ||||
|  | ||||
|   | ||||
| @@ -1707,6 +1707,7 @@ class InlineModelAdmin(BaseModelAdmin): | ||||
|     fk_name = None | ||||
|     formset = BaseInlineFormSet | ||||
|     extra = 3 | ||||
|     min_num = None | ||||
|     max_num = None | ||||
|     template = None | ||||
|     verbose_name = None | ||||
| @@ -1739,6 +1740,10 @@ class InlineModelAdmin(BaseModelAdmin): | ||||
|         """Hook for customizing the number of extra inline forms.""" | ||||
|         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): | ||||
|         """Hook for customizing the max number of extra inline forms.""" | ||||
|         return self.max_num | ||||
| @@ -1770,6 +1775,7 @@ class InlineModelAdmin(BaseModelAdmin): | ||||
|             "exclude": exclude, | ||||
|             "formfield_callback": partial(self.formfield_for_dbfield, request=request), | ||||
|             "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), | ||||
|             "can_delete": can_delete, | ||||
|         } | ||||
|   | ||||
| @@ -119,6 +119,7 @@ class GenericInlineModelAdmin(InlineModelAdmin): | ||||
|             "can_delete": can_delete, | ||||
|             "can_order": False, | ||||
|             "fields": fields, | ||||
|             "min_num": self.min_num, | ||||
|             "max_num": self.max_num, | ||||
|             "exclude": exclude | ||||
|         } | ||||
|   | ||||
| @@ -56,9 +56,9 @@ def generic_inlineformset_factory(model, form=ModelForm, | ||||
|                                   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): | ||||
|                                   max_num=None, formfield_callback=None, | ||||
|                                   validate_max=False, for_concrete_model=True, | ||||
|                                   min_num=None, validate_min=False): | ||||
|     """ | ||||
|     Returns a ``GenericInlineFormSet`` for the given kwargs. | ||||
|  | ||||
| @@ -81,7 +81,8 @@ def generic_inlineformset_factory(model, form=ModelForm, | ||||
|                                    formset=formset, | ||||
|                                    extra=extra, can_delete=can_delete, can_order=can_order, | ||||
|                                    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_fk_field = fk_field | ||||
|     FormSet.for_concrete_model = for_concrete_model | ||||
|   | ||||
| @@ -810,7 +810,8 @@ def 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): | ||||
|                          labels=None, help_texts=None, error_messages=None, | ||||
|                          min_num=None, validate_min=False): | ||||
|     """ | ||||
|     Returns a FormSet class for the given Django model class. | ||||
|     """ | ||||
| @@ -833,9 +834,9 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None, | ||||
|                              formfield_callback=formfield_callback, | ||||
|                              widgets=widgets, localized_fields=localized_fields, | ||||
|                              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, | ||||
|                               validate_max=validate_max) | ||||
|                               validate_min=validate_min, validate_max=validate_max) | ||||
|     FormSet.model = model | ||||
|     return FormSet | ||||
|  | ||||
| @@ -979,7 +980,8 @@ def inlineformset_factory(parent_model, model, form=ModelForm, | ||||
|                           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): | ||||
|                           labels=None, help_texts=None, error_messages=None, | ||||
|                           min_num=None, validate_min=False): | ||||
|     """ | ||||
|     Returns an ``InlineFormSet`` for the given kwargs. | ||||
|  | ||||
| @@ -999,8 +1001,10 @@ def inlineformset_factory(parent_model, model, form=ModelForm, | ||||
|         'can_order': can_order, | ||||
|         'fields': fields, | ||||
|         'exclude': exclude, | ||||
|         'min_num': min_num, | ||||
|         'max_num': max_num, | ||||
|         'widgets': widgets, | ||||
|         'validate_min': validate_min, | ||||
|         'validate_max': validate_max, | ||||
|         'localized_fields': localized_fields, | ||||
|         'labels': labels, | ||||
|   | ||||
| @@ -204,7 +204,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.E203**: The value of ``extra`` 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 | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|   | ||||
| @@ -1994,6 +1994,16 @@ The ``InlineModelAdmin`` class adds: | ||||
|     :meth:`InlineModelAdmin.get_max_num` also allows you to customize the | ||||
|     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 | ||||
|  | ||||
|     By default, Django's admin uses a select-box interface (<select>) for | ||||
| @@ -2074,6 +2084,16 @@ The ``InlineModelAdmin`` class adds: | ||||
|                     return max_num - 5 | ||||
|                 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 | ||||
| --------------------------------------------------------------------------- | ||||
|   | ||||
| @@ -500,7 +500,7 @@ The :mod:`django.contrib.contenttypes.forms` module provides: | ||||
|  | ||||
|         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 | ||||
|     :func:`~django.forms.models.modelformset_factory`. | ||||
| @@ -521,6 +521,10 @@ The :mod:`django.contrib.contenttypes.forms` module provides: | ||||
|  | ||||
|         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 | ||||
|  | ||||
|   | ||||
| @@ -45,7 +45,7 @@ Model Form Functions | ||||
|     The ``localized_fields``, ``labels``, ``help_texts``, and | ||||
|     ``error_messages`` parameters were added. | ||||
|  | ||||
| .. 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. | ||||
|  | ||||
| @@ -66,7 +66,7 @@ Model Form Functions | ||||
|         The ``widgets``, ``validate_max``, ``localized_fields``, ``labels``, | ||||
|         ``help_texts``, and ``error_messages`` parameters were added. | ||||
|  | ||||
| .. 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 | ||||
|     defaults of ``formset=``:class:`~django.forms.models.BaseInlineFormSet`, | ||||
|   | ||||
| @@ -1,13 +1,14 @@ | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| from django.contrib.admin import TabularInline, ModelAdmin | ||||
| from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase | ||||
| from django.contrib.admin.helpers import InlineAdminForm | ||||
| from django.contrib.auth.models import User, Permission | ||||
| 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 | ||||
| from .admin import InnerInline | ||||
| from .admin import InnerInline, site as admin_site | ||||
| from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person, | ||||
|     OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile, | ||||
|     ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, | ||||
| @@ -28,6 +29,7 @@ class TestInline(TestCase): | ||||
|  | ||||
|         result = self.client.login(username='super', password='secret') | ||||
|         self.assertEqual(result, True) | ||||
|         self.factory = RequestFactory() | ||||
|  | ||||
|     def tearDown(self): | ||||
|         self.client.logout() | ||||
| @@ -221,6 +223,62 @@ class TestInline(TestCase): | ||||
|         self.assertContains(response, max_forms_input % 2) | ||||
|         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): | ||||
|         response = self.client.get('/admin/admin_inlines/author/add/') | ||||
|         self.assertContains(response, | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| from django.contrib import admin | ||||
| from django.contrib.contenttypes.admin import GenericTabularInline | ||||
|  | ||||
| from .models import (Media, PhoneNumber, Episode, EpisodeExtra, Contact, | ||||
|     Category, EpisodePermanent, EpisodeMaxNum) | ||||
| from .models import (Media, PhoneNumber, Episode, Contact, | ||||
|     Category, EpisodePermanent) | ||||
|  | ||||
|  | ||||
| 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): | ||||
|     model = PhoneNumber | ||||
|  | ||||
| @@ -39,8 +28,6 @@ class MediaPermanentInline(GenericTabularInline): | ||||
|  | ||||
|  | ||||
| site.register(Episode, EpisodeAdmin) | ||||
| site.register(EpisodeExtra, inlines=[MediaExtraInline]) | ||||
| site.register(EpisodeMaxNum, inlines=[MediaMaxNumInline]) | ||||
| site.register(Contact, inlines=[PhoneNumberInline]) | ||||
| site.register(Category) | ||||
| site.register(EpisodePermanent, inlines=[MediaPermanentInline]) | ||||
|   | ||||
| @@ -27,26 +27,6 @@ class Media(models.Model): | ||||
|     def __str__(self): | ||||
|         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 | ||||
|   | ||||
| @@ -4,16 +4,16 @@ import warnings | ||||
|  | ||||
| from django.contrib import admin | ||||
| 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.forms import generic_inlineformset_factory | ||||
| from django.forms.formsets import DEFAULT_MAX_NUM | ||||
| from django.forms.models import ModelForm | ||||
| from django.test import TestCase, override_settings | ||||
| from django.test import TestCase, override_settings, RequestFactory | ||||
|  | ||||
| # local test models | ||||
| from .admin import MediaInline, MediaPermanentInline | ||||
| from .models import (Episode, EpisodeExtra, EpisodeMaxNum, Media, | ||||
|     EpisodePermanent, Category) | ||||
| from .admin import MediaInline, MediaPermanentInline, site as admin_site | ||||
| from .models import Episode, Media, EpisodePermanent, Category | ||||
|  | ||||
|  | ||||
| @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',), | ||||
| @@ -136,6 +136,7 @@ class GenericInlineAdminParametersTest(TestCase): | ||||
|  | ||||
|     def setUp(self): | ||||
|         self.client.login(username='super', password='secret') | ||||
|         self.factory = RequestFactory() | ||||
|  | ||||
|     def tearDown(self): | ||||
|         self.client.logout() | ||||
| @@ -165,9 +166,18 @@ class GenericInlineAdminParametersTest(TestCase): | ||||
|         """ | ||||
|         With extra=0, there should be one form. | ||||
|         """ | ||||
|         e = self._create_object(EpisodeExtra) | ||||
|         response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/episodeextra/%s/' % e.pk) | ||||
|         formset = response.context['inline_admin_formsets'][0].formset | ||||
|         class ExtraInline(GenericTabularInline): | ||||
|             model = Media | ||||
|             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.initial_form_count(), 1) | ||||
|  | ||||
| @@ -175,12 +185,43 @@ class GenericInlineAdminParametersTest(TestCase): | ||||
|         """ | ||||
|         With extra=5 and max_num=2, there should be only 2 forms. | ||||
|         """ | ||||
|         e = self._create_object(EpisodeMaxNum) | ||||
|         response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/episodemaxnum/%s/' % e.pk) | ||||
|         formset = response.context['inline_admin_formsets'][0].formset | ||||
|         class MaxNumInline(GenericTabularInline): | ||||
|             model = Media | ||||
|             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.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',)) | ||||
| class GenericInlineAdminWithUniqueTogetherTest(TestCase): | ||||
|   | ||||
| @@ -366,6 +366,33 @@ class ModelFormsetTest(TestCase): | ||||
|             '<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): | ||||
|         class PoetForm(forms.ModelForm): | ||||
|             def save(self, commit=True): | ||||
|   | ||||
| @@ -1444,6 +1444,33 @@ class MaxNumCheckTests(CheckTestCase): | ||||
|         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): | ||||
|  | ||||
|     def test_invalid_type(self): | ||||
| @@ -1460,7 +1487,7 @@ class FormsetCheckTests(CheckTestCase): | ||||
|         self.assertIsInvalid( | ||||
|             ValidationTestModelAdmin, ValidationTestModel, | ||||
|             "The value of 'formset' must inherit from 'BaseModelFormSet'.", | ||||
|             'admin.E205', | ||||
|             'admin.E206', | ||||
|             invalid_obj=ValidationTestInline) | ||||
|  | ||||
|     def test_valid_case(self): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user