diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index f5d70ead9..aff448a7f 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -92,7 +92,7 @@ WagtailAdminModelForm = WagtailAdminModelFormMetaclass(str('WagtailAdminModelFor def get_form_for_model( - model, + model, form_class=WagtailAdminModelForm, fields=None, exclude=None, formsets=None, exclude_formsets=None, widgets=None ): @@ -124,7 +124,8 @@ def get_form_for_model( 'Meta': type(str('Meta'), (object,), attrs) } - return WagtailAdminModelFormMetaclass(class_name, (WagtailAdminModelForm,), form_class_attrs) + metaclass = type(form_class) + return metaclass(class_name, (form_class,), form_class_attrs) def extract_panel_definitions_from_model_class(model, exclude=None): @@ -184,19 +185,6 @@ class EditHandler(object): def html_declarations(cls): return '' - # the top-level edit handler is responsible for providing a form class that can produce forms - # acceptable to the edit handler - _form_class = None - - @classmethod - def get_form_class(cls, model): - if cls._form_class is None: - cls._form_class = get_form_for_model( - model, - fields=cls.required_fields(), - formsets=cls.required_formsets(), widgets=cls.widget_overrides()) - return cls._form_class - def __init__(self, instance=None, form=None): if not instance: raise ValueError("EditHandler did not receive an instance object") @@ -341,30 +329,62 @@ class BaseCompositeEditHandler(EditHandler): })) -class BaseTabbedInterface(BaseCompositeEditHandler): +class BaseFormEditHandler(BaseCompositeEditHandler): + """ + Base class for edit handlers that can construct a form class for all their + child edit handlers. + """ + + # The form class used as the base for constructing specific forms for this + # edit handler. Subclasses can override this attribute to provide a form + # with custom validation, for example. Custom forms must subclass + # WagtailAdminModelForm + base_form_class = WagtailAdminModelForm + + @classmethod + @lru_cache() + def get_form_class(cls, model): + """ + Construct a form class that has all the fields and formsets named in + the children of this edit handler. + """ + return get_form_for_model( + model, + form_class=cls.base_form_class, + fields=cls.required_fields(), + formsets=cls.required_formsets(), + widgets=cls.widget_overrides()) + + +class BaseTabbedInterface(BaseFormEditHandler): template = "wagtailadmin/edit_handlers/tabbed_interface.html" class TabbedInterface(object): - def __init__(self, children): + def __init__(self, children, base_form_class=BaseFormEditHandler.base_form_class): self.children = children + self.base_form_class = base_form_class def bind_to_model(self, model): return type(str('_TabbedInterface'), (BaseTabbedInterface,), { 'model': model, 'children': [child.bind_to_model(model) for child in self.children], + 'base_form_class': self.base_form_class, }) -class BaseObjectList(BaseCompositeEditHandler): +class BaseObjectList(BaseFormEditHandler): template = "wagtailadmin/edit_handlers/object_list.html" class ObjectList(object): - def __init__(self, children, heading="", classname=""): + + def __init__(self, children, heading="", classname="", + base_form_class=BaseFormEditHandler.base_form_class): self.children = children self.heading = heading self.classname = classname + self.base_form_class = base_form_class def bind_to_model(self, model): return type(str('_ObjectList'), (BaseObjectList,), { @@ -372,6 +392,7 @@ class ObjectList(object): 'children': [child.bind_to_model(model) for child in self.children], 'heading': self.heading, 'classname': self.classname, + 'base_form_class': self.base_form_class, }) diff --git a/wagtail/wagtailadmin/tests/test_edit_handlers.py b/wagtail/wagtailadmin/tests/test_edit_handlers.py index c538a8c7d..1a9bd7439 100644 --- a/wagtail/wagtailadmin/tests/test_edit_handlers.py +++ b/wagtail/wagtailadmin/tests/test_edit_handlers.py @@ -348,10 +348,11 @@ class TestPageChooserPanel(TestCase): model = PageChooserModel # a model with a foreign key to Page which we want to render as a page chooser # a PageChooserPanel class that works on PageChooserModel's 'page' field - self.MyPageChooserPanel = PageChooserPanel('page').bind_to_model(PageChooserModel) + self.EditHandler = ObjectList([PageChooserPanel('page')]).bind_to_model(PageChooserModel) + self.MyPageChooserPanel = self.EditHandler.children[0] # build a form class containing the fields that MyPageChooserPanel wants - self.PageChooserForm = self.MyPageChooserPanel.get_form_class(PageChooserModel) + self.PageChooserForm = self.EditHandler.get_form_class(PageChooserModel) # a test instance of PageChooserModel, pointing to the 'christmas' page self.christmas_page = Page.objects.get(slug='christmas') @@ -418,10 +419,13 @@ class TestPageChooserPanel(TestCase): def test_override_page_type(self): # Model has a foreign key to Page, but we specify EventPage in the PageChooserPanel # to restrict the chooser to that page type - MyPageChooserPanel = PageChooserPanel('page', 'tests.EventPage').bind_to_model(EventPageChooserModel) - PageChooserForm = MyPageChooserPanel.get_form_class(EventPageChooserModel) + MyPageObjectList = ObjectList([ + PageChooserPanel('page', 'tests.EventPage') + ]).bind_to_model(EventPageChooserModel) + MyPageChooserPanel = MyPageObjectList.children[0] + PageChooserForm = MyPageObjectList.get_form_class(EventPageChooserModel) form = PageChooserForm(instance=self.test_instance) - page_chooser_panel = self.MyPageChooserPanel(instance=self.test_instance, form=form) + page_chooser_panel = MyPageChooserPanel(instance=self.test_instance, form=form) result = page_chooser_panel.render_as_field() expected_js = 'createPageChooser("{id}", ["{model}"], {parent}, false);'.format( @@ -432,10 +436,11 @@ class TestPageChooserPanel(TestCase): def test_autodetect_page_type(self): # Model has a foreign key to EventPage, which we want to autodetect # instead of specifying the page type in PageChooserPanel - MyPageChooserPanel = PageChooserPanel('page').bind_to_model(EventPageChooserModel) - PageChooserForm = MyPageChooserPanel.get_form_class(EventPageChooserModel) + MyPageObjectList = ObjectList([PageChooserPanel('page')]).bind_to_model(EventPageChooserModel) + MyPageChooserPanel = MyPageObjectList.children[0] + PageChooserForm = MyPageObjectList.get_form_class(EventPageChooserModel) form = PageChooserForm(instance=self.test_instance) - page_chooser_panel = self.MyPageChooserPanel(instance=self.test_instance, form=form) + page_chooser_panel = MyPageChooserPanel(instance=self.test_instance, form=form) result = page_chooser_panel.render_as_field() expected_js = 'createPageChooser("{id}", ["{model}"], {parent}, false);'.format( @@ -475,8 +480,9 @@ class TestInlinePanel(TestCase, WagtailTestUtils): Check that the inline panel renders the panels set on the model when no 'panels' parameter is passed in the InlinePanel definition """ - SpeakerInlinePanel = InlinePanel('speakers', label="Speakers").bind_to_model(EventPage) - EventPageForm = SpeakerInlinePanel.get_form_class(EventPage) + SpeakerObjectList = ObjectList([InlinePanel('speakers', label="Speakers")]).bind_to_model(EventPage) + SpeakerInlinePanel = SpeakerObjectList.children[0] + EventPageForm = SpeakerObjectList.get_form_class(EventPage) # SpeakerInlinePanel should instruct the form class to include a 'speakers' formset self.assertEqual(['speakers'], list(EventPageForm.formsets.keys())) @@ -510,11 +516,14 @@ class TestInlinePanel(TestCase, WagtailTestUtils): Check that inline panel renders the panels listed in the InlinePanel definition where one is specified """ - SpeakerInlinePanel = InlinePanel('speakers', label="Speakers", panels=[ - FieldPanel('first_name', widget=forms.Textarea), - ImageChooserPanel('image'), + SpeakerObjectList = ObjectList([ + InlinePanel('speakers', label="Speakers", panels=[ + FieldPanel('first_name', widget=forms.Textarea), + ImageChooserPanel('image'), + ]), ]).bind_to_model(EventPage) - EventPageForm = SpeakerInlinePanel.get_form_class(EventPage) + SpeakerInlinePanel = SpeakerObjectList.children[0] + EventPageForm = SpeakerObjectList.get_form_class(EventPage) # SpeakerInlinePanel should instruct the form class to include a 'speakers' formset self.assertEqual(['speakers'], list(EventPageForm.formsets.keys()))