From 92d507752ff818801bafeb804ac7c25c2f2f89ea Mon Sep 17 00:00:00 2001 From: Tim Heap Date: Thu, 29 Oct 2015 10:20:20 +1100 Subject: [PATCH] Allow overriding base form class for EditHandlers EditHandlers that can produce forms can now specify which base form class to use. This defaults to WagtailAdminModelForm, which was the hard coded base class previously. Only TabbedInterface and ObjectList EditHandlers now support generating a form. Other EditHandlers generating forms was not used anywhere except in the tests. --- wagtail/wagtailadmin/edit_handlers.py | 59 +++++++++++++------ .../wagtailadmin/tests/test_edit_handlers.py | 37 +++++++----- 2 files changed, 63 insertions(+), 33 deletions(-) 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()))