From 125a749a9ab785757dd898e2f88bfb8fd3f65e11 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Thu, 20 Mar 2025 23:02:27 +0000 Subject: [PATCH] Defer validation of min_num constraints on InlinePanel when saving drafts --- wagtail/admin/forms/models.py | 13 ++- wagtail/admin/tests/pages/test_create_page.py | 84 +++++++++++++++++++ .../migrations/0049_promotionalpage.py | 35 ++++++++ wagtail/test/testapp/models.py | 6 ++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 wagtail/test/testapp/migrations/0049_promotionalpage.py diff --git a/wagtail/admin/forms/models.py b/wagtail/admin/forms/models.py index 02f171b3b2..80e3380057 100644 --- a/wagtail/admin/forms/models.py +++ b/wagtail/admin/forms/models.py @@ -135,10 +135,11 @@ class WagtailAdminModelForm( # keep hold of the `for_user` kwarg as well as passing it on to PermissionedForm self.for_user = kwargs.get("for_user") self.deferred_required_fields = [] + self.deferred_formset_min_nums = {} super().__init__(*args, **kwargs) def defer_required_fields(self): - if self.deferred_required_fields: + if self.deferred_required_fields or self.deferred_formset_min_nums: # defer_required_fields has already been called return @@ -150,14 +151,20 @@ class WagtailAdminModelForm( except KeyError: pass - for formset in self.formsets.values(): + for name, formset in self.formsets.items(): for form in formset: form.defer_required_fields() + if formset.min_num is not None: + self.deferred_formset_min_nums[name] = formset.min_num + formset.min_num = 0 def restore_required_fields(self): - for formset in self.formsets.values(): + for name, formset in self.formsets.items(): for form in formset: form.restore_required_fields() + if name in self.deferred_formset_min_nums: + formset.min_num = self.deferred_formset_min_nums[name] + self.deferred_formset_min_nums = {} for field_name in self.deferred_required_fields: self.fields[field_name].required = True diff --git a/wagtail/admin/tests/pages/test_create_page.py b/wagtail/admin/tests/pages/test_create_page.py index 45bc2bb231..ed8c5b979b 100644 --- a/wagtail/admin/tests/pages/test_create_page.py +++ b/wagtail/admin/tests/pages/test_create_page.py @@ -18,6 +18,7 @@ from wagtail.models import ( ) from wagtail.signals import page_published from wagtail.test.testapp.models import ( + Advert, BusinessChild, BusinessIndex, BusinessSubIndex, @@ -903,6 +904,89 @@ class TestPageCreation(WagtailTestUtils, TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, "This field is required.") + def test_can_create_page_with_less_than_min_inline_panels(self): + # min_num constraints on inline panels should not be enforced when saving as draft + post_data = nested_form_data( + { + "title": "Promotional page", + "slug": "promotional-page", + "advert_placements": inline_formset([]), + } + ) + response = self.client.post( + reverse( + "wagtailadmin_pages:add", + args=("tests", "promotionalpage", self.root_page.id), + ), + post_data, + ) + # Find the page and check it + page = Page.objects.get( + path__startswith=self.root_page.path, slug="promotional-page" + ).specific + + # Should be redirected to edit page + self.assertRedirects( + response, reverse("wagtailadmin_pages:edit", args=(page.id,)) + ) + + self.assertEqual(page.advert_placements.count(), 0) + self.assertFalse(page.live) + + def test_cannot_publish_page_with_less_than_min_inline_panels(self): + # min_num constraints on inline panels should not be enforced when saving as draft + post_data = nested_form_data( + { + "title": "Promotional page", + "slug": "promotional-page", + "advert_placements": inline_formset([]), + "action-publish": "Publish", + } + ) + response = self.client.post( + reverse( + "wagtailadmin_pages:add", + args=("tests", "promotionalpage", self.root_page.id), + ), + post_data, + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Please submit at least 1 form.") + + def test_can_publish_page_with_enough_inline_panels(self): + advert = Advert.objects.create(text="Red Bull gives you wings") + # min_num constraints on inline panels should not be enforced when saving as draft + post_data = nested_form_data( + { + "title": "Promotional page", + "slug": "promotional-page", + "advert_placements": inline_formset( + [ + { + "advert": advert.id, + "colour": "red", + }, + ] + ), + "action-publish": "Publish", + } + ) + response = self.client.post( + reverse( + "wagtailadmin_pages:add", + args=("tests", "promotionalpage", self.root_page.id), + ), + post_data, + ) + self.assertRedirects( + response, reverse("wagtailadmin_explore", args=(self.root_page.id,)) + ) + page = Page.objects.get( + path__startswith=self.root_page.path, slug="promotional-page" + ).specific + self.assertTrue(page.live) + self.assertEqual(page.advert_placements.count(), 1) + def test_create_simplepage_scheduled(self): go_live_at = timezone.now() + datetime.timedelta(days=1) expire_at = timezone.now() + datetime.timedelta(days=2) diff --git a/wagtail/test/testapp/migrations/0049_promotionalpage.py b/wagtail/test/testapp/migrations/0049_promotionalpage.py new file mode 100644 index 0000000000..2eb5aa2946 --- /dev/null +++ b/wagtail/test/testapp/migrations/0049_promotionalpage.py @@ -0,0 +1,35 @@ +# Generated by Django 5.0.12 on 2025-03-20 22:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("tests", "0048_requireddatepage"), + ("wagtailcore", "0094_alter_page_locale"), + ] + + operations = [ + migrations.CreateModel( + name="PromotionalPage", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("wagtailcore.page",), + ), + ] diff --git a/wagtail/test/testapp/models.py b/wagtail/test/testapp/models.py index 07f7879b24..741873d4d1 100644 --- a/wagtail/test/testapp/models.py +++ b/wagtail/test/testapp/models.py @@ -1396,6 +1396,12 @@ class StandardIndex(Page): promote_panels = [] +class PromotionalPage(Page): + content_panels = Page.content_panels + [ + InlinePanel("advert_placements", label="Adverts", min_num=1), + ] + + class StandardChild(Page): pass