diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ef8d8d446..ff9acdbc9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -18,6 +18,7 @@ Changelog * Add StreamFieldPanel to available panel types in documentation (Dan Swain) * Add {{ block.super }} example to ModelAdmin customisation in documentation (Dan Swain) * Add ability to filter image index by a tag (Benedikt Willi) + * Add formal support for nested InlinePanels (Matt Westcott) * Fix: Rename documents listing column 'uploaded' to 'created' (LB (Ben Johnston)) * Fix: Submenu items longer then the page height are no longer broken by the submenu footer (Igor van Spengen) * Fix: Unbundle the l18n library as it was bundled to avoid installation errors which have been resolved (Matt Westcott) diff --git a/docs/releases/2.8.rst b/docs/releases/2.8.rst index 00db4f51c..ed1b52cfc 100644 --- a/docs/releases/2.8.rst +++ b/docs/releases/2.8.rst @@ -27,6 +27,7 @@ Other features * Add StreamFieldPanel to available panel types in documentation (Dan Swain) * Add {{ block.super }} example to ModelAdmin customisation in documentation (Dan Swain) * Add ability to filter image index by a tag (Benedikt Willi) + * Add formal support for nested InlinePanels (Matt Westcott) Bug fixes diff --git a/wagtail/admin/forms/models.py b/wagtail/admin/forms/models.py index 16a1be8bb..1d192b953 100644 --- a/wagtail/admin/forms/models.py +++ b/wagtail/admin/forms/models.py @@ -66,6 +66,10 @@ class WagtailAdminModelFormMetaclass(ClusterFormMetaclass): new_class = super(WagtailAdminModelFormMetaclass, cls).__new__(cls, name, bases, attrs) return new_class + @classmethod + def child_form(cls): + return WagtailAdminModelForm + class WagtailAdminModelForm(ClusterForm, metaclass=WagtailAdminModelFormMetaclass): @property diff --git a/wagtail/admin/tests/pages/test_edit_page.py b/wagtail/admin/tests/pages/test_edit_page.py index 1ca08ab8a..8cdab8231 100644 --- a/wagtail/admin/tests/pages/test_edit_page.py +++ b/wagtail/admin/tests/pages/test_edit_page.py @@ -19,6 +19,7 @@ from wagtail.tests.testapp.models import ( EVENT_AUDIENCE_CHOICES, Advert, AdvertPlacement, EventCategory, EventPage, EventPageCarouselItem, FilePage, ManyToManyBlogPage, SimplePage, SingleEventPage, StandardIndex, TaggedPage) from wagtail.tests.utils import WagtailTestUtils +from wagtail.tests.utils.form_data import inline_formset, nested_form_data class TestPageEdit(TestCase, WagtailTestUtils): @@ -1632,3 +1633,92 @@ class TestValidationErrorMessages(TestCase, WagtailTestUtils): self.assertContains(response, """

This field is required.

""", count=1, html=True) # Error on title shown in the header message self.assertContains(response, "
  • Title: This field is required.
  • ", count=1) + + +class TestNestedInlinePanel(TestCase, WagtailTestUtils): + fixtures = ['test.json'] + + def setUp(self): + self.events_index = Page.objects.get(url_path='/home/events/') + self.christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + self.speaker = self.christmas_page.speakers.first() + self.speaker.awards.create( + name="Beard Of The Year", date_awarded=datetime.date(1997, 12, 25) + ) + self.speaker.save() + self.user = self.login() + + def test_get_edit_form(self): + response = self.client.get( + reverse('wagtailadmin_pages:edit', args=(self.christmas_page.id, )) + ) + self.assertEqual(response.status_code, 200) + self.assertContains( + response, + """""", + count=1, html=True + ) + + # there should be no "extra" forms, as the nested formset should respect the extra_form_count=0 set on WagtailAdminModelForm + self.assertContains( + response, + """""", + count=1, html=True + ) + self.assertContains( + response, + """""", + count=0, html=True + ) + + # date field should use AdminDatePicker + self.assertContains( + response, + """""", + count=1, html=True + ) + + def test_post_edit(self): + post_data = nested_form_data({ + 'title': "Christmas", + 'date_from': "2017-12-25", + 'date_to': "2017-12-25", + 'slug': "christmas", + 'audience': "public", + 'location': "The North Pole", + 'cost': "Free", + 'carousel_items': inline_formset([]), + 'speakers': inline_formset([ + { + 'id': self.speaker.id, + 'first_name': "Jeff", + 'last_name': "Christmas", + 'awards': inline_formset([ + { + 'id': self.speaker.awards.first().id, + 'name': "Beard Of The Century", + 'date_awarded': "1997-12-25", + }, + { + 'name': "Bobsleigh Olympic gold medallist", + 'date_awarded': "2018-02-01", + }, + ], initial=1) + }, + ], initial=1), + 'related_links': inline_formset([]), + 'head_counts': inline_formset([]), + 'action-publish': "Publish", + }) + response = self.client.post( + reverse('wagtailadmin_pages:edit', args=(self.christmas_page.id, )), + post_data + ) + self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.events_index.id, ))) + + new_christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + self.assertEqual(new_christmas_page.speakers.first().first_name, "Jeff") + awards = new_christmas_page.speakers.first().awards.all() + self.assertEqual(len(awards), 2) + self.assertEqual(awards[0].name, "Beard Of The Century") + self.assertEqual(awards[1].name, "Bobsleigh Olympic gold medallist") diff --git a/wagtail/admin/tests/test_edit_handlers.py b/wagtail/admin/tests/test_edit_handlers.py index 1d6fa34f3..8de3b1889 100644 --- a/wagtail/admin/tests/test_edit_handlers.py +++ b/wagtail/admin/tests/test_edit_handlers.py @@ -220,7 +220,7 @@ class TestExtractPanelDefinitionsFromModelClass(TestCase): def test_can_extract_panel_property(self): # A class with a 'panels' property defined should return that list result = extract_panel_definitions_from_model_class(EventPageSpeaker) - self.assertEqual(len(result), 4) + self.assertEqual(len(result), 5) self.assertTrue(any([isinstance(panel, ImageChooserPanel) for panel in result])) def test_exclude(self): diff --git a/wagtail/tests/testapp/migrations/0043_eventpagespeakeraward.py b/wagtail/tests/testapp/migrations/0043_eventpagespeakeraward.py new file mode 100644 index 000000000..63004bfdb --- /dev/null +++ b/wagtail/tests/testapp/migrations/0043_eventpagespeakeraward.py @@ -0,0 +1,29 @@ +# Generated by Django 2.1.11 on 2019-09-06 15:11 + +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('tests', '0042_simplechildpage_simpleparentpage'), + ] + + operations = [ + migrations.CreateModel( + name='EventPageSpeakerAward', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), + ('name', models.CharField(max_length=255, verbose_name='Award name')), + ('date_awarded', models.DateField(blank=True, null=True)), + ('speaker', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='awards', to='tests.EventPageSpeaker')), + ], + options={ + 'ordering': ['sort_order'], + 'abstract': False, + }, + ), + ] diff --git a/wagtail/tests/testapp/models.py b/wagtail/tests/testapp/models.py index cf2330066..0f0466573 100644 --- a/wagtail/tests/testapp/models.py +++ b/wagtail/tests/testapp/models.py @@ -198,7 +198,18 @@ class EventPageRelatedLink(Orderable, RelatedLink): page = ParentalKey('tests.EventPage', related_name='related_links', on_delete=models.CASCADE) -class EventPageSpeaker(Orderable, LinkFields): +class EventPageSpeakerAward(Orderable, models.Model): + speaker = ParentalKey('tests.EventPageSpeaker', related_name='awards', on_delete=models.CASCADE) + name = models.CharField("Award name", max_length=255) + date_awarded = models.DateField(null=True, blank=True) + + panels = [ + FieldPanel('name'), + FieldPanel('date_awarded'), + ] + + +class EventPageSpeaker(Orderable, LinkFields, ClusterableModel): page = ParentalKey('tests.EventPage', related_name='speakers', related_query_name='speaker', on_delete=models.CASCADE) first_name = models.CharField("Name", max_length=255, blank=True) last_name = models.CharField("Surname", max_length=255, blank=True) @@ -219,6 +230,7 @@ class EventPageSpeaker(Orderable, LinkFields): FieldPanel('last_name'), ImageChooserPanel('image'), MultiFieldPanel(LinkFields.panels, "Link"), + InlinePanel('awards', label="Awards"), ]