From 4ef10f245ada0c7d5ae8dc31eebffa63790d40fb Mon Sep 17 00:00:00 2001 From: Anders Steinlein Date: Wed, 5 Mar 2014 21:19:40 +0100 Subject: Fixed #17642 -- Added min_num support to modelformsets, inlines, and the admin. Thanks Stephen Burrows for work on the patch as well. Forwardport of 2914f66983a92fcae55673c517dd8d01e8c238c4 from stable/1.7.x --- tests/admin_inlines/tests.py | 62 ++++++++++++++++++++++++++++++++++-- tests/generic_inline_admin/admin.py | 17 ++-------- tests/generic_inline_admin/models.py | 20 ------------ tests/generic_inline_admin/tests.py | 61 +++++++++++++++++++++++++++++------ tests/model_formsets/tests.py | 27 ++++++++++++++++ tests/modeladmin/tests.py | 29 ++++++++++++++++- 6 files changed, 168 insertions(+), 48 deletions(-) (limited to 'tests') diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index ff34180a5b..7025158aab 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -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 = '' + total_forms = '' + + 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 = '' + total_forms = '' + + 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, diff --git a/tests/generic_inline_admin/admin.py b/tests/generic_inline_admin/admin.py index d0ce4c70ef..46f6cf3e7f 100644 --- a/tests/generic_inline_admin/admin.py +++ b/tests/generic_inline_admin/admin.py @@ -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]) diff --git a/tests/generic_inline_admin/models.py b/tests/generic_inline_admin/models.py index 0ae3c4b052..059aadc5ba 100644 --- a/tests/generic_inline_admin/models.py +++ b/tests/generic_inline_admin/models.py @@ -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 diff --git a/tests/generic_inline_admin/tests.py b/tests/generic_inline_admin/tests.py index 30513041d6..ed2411ffdf 100644 --- a/tests/generic_inline_admin/tests.py +++ b/tests/generic_inline_admin/tests.py @@ -4,17 +4,17 @@ 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 from django.utils.deprecation import RemovedInDjango19Warning # 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',), @@ -137,6 +137,7 @@ class GenericInlineAdminParametersTest(TestCase): def setUp(self): self.client.login(username='super', password='secret') + self.factory = RequestFactory() def tearDown(self): self.client.logout() @@ -166,9 +167,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) @@ -176,12 +186,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',), ROOT_URLCONF="generic_inline_admin.urls") diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index b5e410738e..ed74dff2ed 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -376,6 +376,33 @@ class ModelFormsetTest(TestCase): '', ]) + 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): diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 9cf54c8b9f..1c3a21d374 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -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): -- cgit v1.3