Add formal support for nested InlinePanels

Fixes #1952 and #5511
This commit is contained in:
Matt Westcott 2019-09-06 16:32:34 +01:00 committed by LB
parent 233e7f5189
commit 4001516a27
7 changed files with 139 additions and 2 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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, """<p class="error-message"><span>This field is required.</span></p>""", count=1, html=True)
# Error on title shown in the header message
self.assertContains(response, "<li>Title: This field is required.</li>", 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,
"""<input type="text" name="speakers-0-awards-0-name" value="Beard Of The Year" maxlength="255" id="id_speakers-0-awards-0-name">""",
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,
"""<input type="hidden" name="speakers-0-awards-TOTAL_FORMS" value="1" id="id_speakers-0-awards-TOTAL_FORMS">""",
count=1, html=True
)
self.assertContains(
response,
"""<input type="text" name="speakers-0-awards-1-name" value="" maxlength="255" id="id_speakers-0-awards-1-name">""",
count=0, html=True
)
# date field should use AdminDatePicker
self.assertContains(
response,
"""<input type="text" name="speakers-0-awards-0-date_awarded" value="1997-12-25" autocomplete="new-date" id="id_speakers-0-awards-0-date_awarded">""",
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")

View file

@ -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):

View file

@ -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,
},
),
]

View file

@ -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"),
]