Browse Source

Simplifies edit handlers by removing redundant classes.

This also allows to provide some missing arguments to panels like PageChooserPanel.
Matt Westcott 7 years ago
parent
commit
5fc191b116

+ 2 - 0
CHANGELOG.txt

@@ -37,6 +37,7 @@ Changelog
  * Added `WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS` setting to determine whether superusers are included in moderation email notifications (Bruno Alla)
  * Added a basic Dockerfile to the project template (Tom Dyson)
  * StreamField blocks now allow custom `get_template` methods for overriding templates in instances (Christopher Bledsoe)
+ * Simplified edit handler API (Florent Osmont, Bertrand Bordage)
  * Fix: Do not remove stopwords when generating slugs from non-ASCII titles, to avoid issues with incorrect word boundaries (Sævar Öfjörð Magnússon)
  * Fix: The PostgreSQL search backend now preserves ordering of the `QuerySet` when searching with `order_by_relevance=False` (Bertrand Bordage)
  * Fix: Using `modeladmin_register` as a decorator no longer replaces the decorated class with `None` (Tim Heap)
@@ -64,6 +65,7 @@ Changelog
  * Fix: Style of the page unlock button was broken (Bertrand Bordage)
  * Fix: Admin search no longer floods browser history (Bertrand Bordage)
  * Fix: Version comparison now handles custom primary keys on inline models correctly (LB (Ben Johnston))
+ * Fixed error when inserting chooser panels into FieldRowPanel (Florent Osmont, Bertrand Bordage)
 
 
 1.13.1 (17.11.2017)

+ 1 - 0
CONTRIBUTORS.rst

@@ -268,6 +268,7 @@ Contributors
 * misraX
 * Bruno Alla
 * Christopher Bledsoe (The Motley Fool)
+* Florent Osmont
 
 Translators
 ===========

+ 2 - 0
docs/releases/2.0.rst

@@ -54,6 +54,7 @@ Other features
  * Added ``WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS`` setting to determine whether superusers are included in moderation email notifications (Bruno Alla)
  * Added a basic Dockerfile to the project template (Tom Dyson)
  * StreamField blocks now allow custom ``get_template`` methods for overriding templates in instances (Christopher Bledsoe)
+ * Simplified edit handler API (Florent Osmont, Bertrand Bordage)
 
 
 Bug fixes
@@ -87,6 +88,7 @@ Bug fixes
  * Style of the page unlock button was broken (Bertrand Bordage)
  * Admin search no longer floods browser history (Bertrand Bordage)
  * Version comparison now handles custom primary keys on inline models correctly (LB (Ben Johnston))
+ * Fixed error when inserting chooser panels into FieldRowPanel (Florent Osmont, Bertrand Bordage)
 
 
 Upgrade considerations

+ 2 - 2
wagtail/admin/checks.py

@@ -60,9 +60,9 @@ def get_form_class_check(app_configs, **kwargs):
 
     for cls in get_page_models():
         edit_handler = cls.get_edit_handler()
-        if not issubclass(edit_handler.get_form_class(cls), WagtailAdminPageForm):
+        if not issubclass(edit_handler.get_form_class(), WagtailAdminPageForm):
             errors.append(Error(
-                "{cls}.get_edit_handler().get_form_class({cls}) does not extend WagtailAdminPageForm".format(
+                "{cls}.get_edit_handler().get_form_class() does not extend WagtailAdminPageForm".format(
                     cls=cls.__name__),
                 hint="Ensure that the EditHandler for {cls} creates a subclass of WagtailAdminPageForm".format(
                     cls=cls.__name__),

+ 300 - 365
wagtail/admin/edit_handlers.py

@@ -1,4 +1,3 @@
-import math
 import re
 
 from django import forms
@@ -6,6 +5,7 @@ from django.core.exceptions import ImproperlyConfigured
 from django.db.models.fields import FieldDoesNotExist
 from django.forms.models import fields_for_model
 from django.template.loader import render_to_string
+from django.utils.encoding import force_text
 from django.utils.functional import curry
 from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext_lazy
@@ -91,43 +91,74 @@ class EditHandler:
     the EditHandler API
     """
 
+    def __init__(self, heading='', classname='', help_text=''):
+        self.heading = heading
+        self.classname = classname
+        self.help_text = help_text
+
+    def clone(self):
+        return self.__class__(
+            heading=self.heading,
+            classname=self.classname,
+            help_text=self.help_text,
+        )
+
     # return list of widget overrides that this EditHandler wants to be in place
     # on the form it receives
-    @classmethod
-    def widget_overrides(cls):
+    def widget_overrides(self):
         return {}
 
     # return list of fields that this EditHandler expects to find on the form
-    @classmethod
-    def required_fields(cls):
+    def required_fields(self):
         return []
 
     # return a dict of formsets that this EditHandler requires to be present
     # as children of the ClusterForm; the dict is a mapping from relation name
     # to parameters to be passed as part of get_form_for_model's 'formsets' kwarg
-    @classmethod
-    def required_formsets(cls):
+    def required_formsets(self):
         return {}
 
     # return any HTML that needs to be output on the edit page once per edit handler definition.
     # Typically this will be used to define snippets of HTML within <script type="text/x-template"></script> blocks
     # for Javascript code to work with.
-    @classmethod
-    def html_declarations(cls):
+    def html_declarations(self):
         return ''
 
-    def __init__(self, instance=None, form=None):
+    def bind_to_model(self, model):
+        new = self.clone()
+        new.model = model
+        new.on_model_bound()
+        return new
+
+    def on_model_bound(self):
+        pass
+
+    def bind_to_instance(self, instance=None, form=None):
+        new = self.bind_to_model(self.model)
+
         if not instance:
             raise ValueError("EditHandler did not receive an instance object")
-        self.instance = instance
+        new.instance = instance
 
         if not form:
             raise ValueError("EditHandler did not receive a form object")
-        self.form = form
+        new.form = form
+
+        new.on_instance_bound()
 
-    # Heading / help text to display to the user
-    heading = ""
-    help_text = ""
+        return new
+
+    def on_instance_bound(self):
+        pass
+
+    def __repr__(self):
+        class_name = self.__class__.__name__
+        try:
+            bound_to = force_text(getattr(self, 'instance',
+                                          getattr(self, 'model')))
+        except AttributeError:
+            return '<%s>' % class_name
+        return '<%s bound to %s>' % (class_name, bound_to)
 
     def classes(self):
         """
@@ -135,15 +166,9 @@ class EditHandler:
         Subclasses of EditHandler should override this, invoking super().classes() to
         append more classes specific to the situation.
         """
-
-        classes = []
-
-        try:
-            classes.append(self.classname)
-        except AttributeError:
-            pass
-
-        return classes
+        if self.classname:
+            return [self.classname]
+        return []
 
     def field_type(self):
         """
@@ -199,8 +224,7 @@ class EditHandler:
         """
         return mark_safe(self.render_as_object() + self.render_missing_fields())
 
-    @classmethod
-    def get_comparison(cls):
+    def get_comparison(self):
         return []
 
 
@@ -209,71 +233,70 @@ class BaseCompositeEditHandler(EditHandler):
     Abstract class for EditHandlers that manage a set of sub-EditHandlers.
     Concrete subclasses must attach a 'children' property
     """
-    _widget_overrides = None
-
-    @classmethod
-    def widget_overrides(cls):
-        if cls._widget_overrides is None:
-            # build a collated version of all its children's widget lists
-            widgets = {}
-            for handler_class in cls.children:
-                widgets.update(handler_class.widget_overrides())
-            cls._widget_overrides = widgets
-
-        return cls._widget_overrides
-
-    _required_fields = None
-
-    @classmethod
-    def required_fields(cls):
-        if cls._required_fields is None:
-            fields = []
-            for handler_class in cls.children:
-                fields.extend(handler_class.required_fields())
-            cls._required_fields = fields
-
-        return cls._required_fields
-
-    _required_formsets = None
-
-    @classmethod
-    def required_formsets(cls):
-        if cls._required_formsets is None:
-            formsets = {}
-            for handler_class in cls.children:
-                formsets.update(handler_class.required_formsets())
-            cls._required_formsets = formsets
-
-        return cls._required_formsets
 
-    @classmethod
-    def html_declarations(cls):
-        return mark_safe(''.join([c.html_declarations() for c in cls.children]))
-
-    def __init__(self, instance=None, form=None):
-        super().__init__(instance=instance, form=form)
+    def __init__(self, children=(), *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.children = children
 
-        self.children = []
-        for child in self.__class__.children:
-            if not getattr(child, "children", None) and getattr(child, "field_name", None):
+    def clone(self):
+        return self.__class__(
+            children=self.children,
+            heading=self.heading,
+            classname=self.classname,
+            help_text=self.help_text,
+        )
+
+    def widget_overrides(self):
+        # build a collated version of all its children's widget lists
+        widgets = {}
+        for handler_class in self.children:
+            widgets.update(handler_class.widget_overrides())
+        widget_overrides = widgets
+
+        return widget_overrides
+
+    def required_fields(self):
+        fields = []
+        for handler in self.children:
+            fields.extend(handler.required_fields())
+        return fields
+
+    def required_formsets(self):
+        formsets = {}
+        for handler_class in self.children:
+            formsets.update(handler_class.required_formsets())
+        return formsets
+
+    def html_declarations(self):
+        return mark_safe(''.join([c.html_declarations() for c in self.children]))
+
+    def on_model_bound(self):
+        self.children = [child.bind_to_model(self.model)
+                         for child in self.children]
+
+    def on_instance_bound(self):
+        children = []
+        for child in self.children:
+            if isinstance(child, FieldPanel):
                 if self.form._meta.exclude:
                     if child.field_name in self.form._meta.exclude:
                         continue
                 if self.form._meta.fields:
                     if child.field_name not in self.form._meta.fields:
                         continue
-            self.children.append(child(instance=self.instance, form=self.form))
+            children.append(child.bind_to_instance(instance=self.instance,
+                                                   form=self.form))
+        self.children = children
 
     def render(self):
         return mark_safe(render_to_string(self.template, {
             'self': self
         }))
 
-    @classmethod
-    def get_comparison(cls):
+    def get_comparison(self):
         comparators = []
 
-        for child in cls.children:
+        for child in self.children:
             comparators.extend(child.get_comparison())
 
         return comparators
@@ -291,137 +314,94 @@ class BaseFormEditHandler(BaseCompositeEditHandler):
     # WagtailAdminModelForm
     base_form_class = None
 
-    _form_class = None
-
-    @classmethod
-    def get_form_class(cls, model):
+    def get_form_class(self):
         """
         Construct a form class that has all the fields and formsets named in
         the children of this edit handler.
         """
-        if cls._form_class is None:
-            # If a custom form class was passed to the EditHandler, use it.
-            # Otherwise, use the base_form_class from the model.
-            # If that is not defined, use WagtailAdminModelForm.
-            model_form_class = getattr(model, 'base_form_class', WagtailAdminModelForm)
-            base_form_class = cls.base_form_class or model_form_class
-
-            cls._form_class = get_form_for_model(
-                model,
-                form_class=base_form_class,
-                fields=cls.required_fields(),
-                formsets=cls.required_formsets(),
-                widgets=cls.widget_overrides())
-        return cls._form_class
-
-
-class BaseTabbedInterface(BaseFormEditHandler):
+        if not hasattr(self, 'model'):
+            raise AttributeError(
+                '%s is not bound to a model yet. Use `.bind_to_model(model)` '
+                'before using this method.' % self.__class__.__name__)
+        # If a custom form class was passed to the EditHandler, use it.
+        # Otherwise, use the base_form_class from the model.
+        # If that is not defined, use WagtailAdminModelForm.
+        model_form_class = getattr(self.model, 'base_form_class',
+                                   WagtailAdminModelForm)
+        base_form_class = self.base_form_class or model_form_class
+
+        return get_form_for_model(
+            self.model,
+            form_class=base_form_class,
+            fields=self.required_fields(),
+            formsets=self.required_formsets(),
+            widgets=self.widget_overrides())
+
+
+class TabbedInterface(BaseFormEditHandler):
     template = "wagtailadmin/edit_handlers/tabbed_interface.html"
 
+    def __init__(self, *args, **kwargs):
+        self.base_form_class = kwargs.pop('base_form_class', None)
+        super().__init__(*args, **kwargs)
 
-class TabbedInterface:
-    def __init__(self, children, base_form_class=None):
-        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,
-        })
+    def clone(self):
+        new = super().clone()
+        new.base_form_class = self.base_form_class
+        return new
 
 
-class BaseObjectList(BaseFormEditHandler):
+class ObjectList(TabbedInterface):
     template = "wagtailadmin/edit_handlers/object_list.html"
 
 
-class ObjectList:
-    def __init__(self, children, heading="", classname="",
-                 base_form_class=None):
-        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,), {
-            'model': model,
-            'children': [child.bind_to_model(model) for child in self.children],
-            'heading': self.heading,
-            'classname': self.classname,
-            'base_form_class': self.base_form_class,
-        })
-
-
-class BaseFieldRowPanel(BaseCompositeEditHandler):
+class FieldRowPanel(BaseCompositeEditHandler):
     template = "wagtailadmin/edit_handlers/field_row_panel.html"
 
+    def on_instance_bound(self):
+        super().on_instance_bound()
 
-class FieldRowPanel:
-    def __init__(self, children, classname=""):
-        self.children = children
-        self.classname = classname
-
-    def bind_to_model(self, model):
-        col_count = " col" + str(int(math.floor(12 / len(self.children))))
-
+        col_count = ' col%s' % (12 // len(self.children))
         # If child panel doesn't have a col# class then append default based on
         # number of columns
         for child in self.children:
             if not re.search(r'\bcol\d+\b', child.classname):
                 child.classname += col_count
 
-        return type(str('_FieldRowPanel'), (BaseFieldRowPanel,), {
-            'model': model,
-            'children': [child.bind_to_model(model) for child in self.children],
-            'classname': self.classname,
-        })
-
 
-class BaseMultiFieldPanel(BaseCompositeEditHandler):
+class MultiFieldPanel(BaseCompositeEditHandler):
     template = "wagtailadmin/edit_handlers/multi_field_panel.html"
 
     def classes(self):
         classes = super().classes()
         classes.append("multi-field")
-
         return classes
 
 
-class MultiFieldPanel:
-    def __init__(self, children, heading="", classname=""):
-        self.children = children
-        self.heading = heading
-        self.classname = classname
-
-    def bind_to_model(self, model):
-        return type(str('_MultiFieldPanel'), (BaseMultiFieldPanel,), {
-            'model': model,
-            'children': [child.bind_to_model(model) for child in self.children],
-            'heading': self.heading,
-            'classname': self.classname,
-        })
-
-
-class BaseFieldPanel(EditHandler):
-
+class FieldPanel(EditHandler):
     TEMPLATE_VAR = 'field_panel'
 
-    @classmethod
-    def widget_overrides(cls):
-        """check if a specific widget has been defined for this field"""
-        if hasattr(cls, 'widget'):
-            return {cls.field_name: cls.widget}
-        else:
-            return {}
+    def __init__(self, field_name, *args, **kwargs):
+        widget = kwargs.pop('widget', None)
+        if widget is not None:
+            self.widget = widget
+        super().__init__(*args, **kwargs)
+        self.field_name = field_name
 
-    def __init__(self, instance=None, form=None):
-        super().__init__(instance=instance, form=form)
-        self.bound_field = self.form[self.field_name]
+    def clone(self):
+        return self.__class__(
+            field_name=self.field_name,
+            widget=self.widget if hasattr(self, 'widget') else None,
+            heading=self.heading,
+            classname=self.classname,
+            help_text=self.help_text
+        )
 
-        self.heading = self.bound_field.label
-        self.help_text = self.bound_field.help_text
+    def widget_overrides(self):
+        """check if a specific widget has been defined for this field"""
+        if hasattr(self, 'widget'):
+            return {self.field_name: self.widget}
+        return {}
 
     def classes(self):
         classes = super().classes()
@@ -453,25 +433,22 @@ class BaseFieldPanel(EditHandler):
     field_template = "wagtailadmin/edit_handlers/field_panel_field.html"
 
     def render_as_field(self):
-        context = {
+        return mark_safe(render_to_string(self.field_template, {
             'field': self.bound_field,
             'field_type': self.field_type(),
-        }
-        return mark_safe(render_to_string(self.field_template, context))
+        }))
 
-    @classmethod
-    def required_fields(cls):
-        return [cls.field_name]
+    def required_fields(self):
+        return [self.field_name]
 
-    @classmethod
-    def get_comparison_class(cls):
+    def get_comparison_class(self):
         # Hide fields with hidden widget
-        widget_override = cls.widget_overrides().get(cls.field_name, None)
+        widget_override = self.widget_overrides().get(self.field_name, None)
         if widget_override and widget_override.is_hidden:
             return
 
         try:
-            field = cls.model._meta.get_field(cls.field_name)
+            field = self.db_field
 
             if field.choices:
                 return compare.ChoiceFieldComparison
@@ -491,54 +468,37 @@ class BaseFieldPanel(EditHandler):
 
         return compare.FieldComparison
 
-    @classmethod
-    def get_comparison(cls):
-        comparator_class = cls.get_comparison_class()
+    def get_comparison(self):
+        comparator_class = self.get_comparison_class()
 
         if comparator_class:
-            field = cls.model._meta.get_field(cls.field_name)
-            return [curry(comparator_class, field)]
-        else:
-            return []
-
-
-class FieldPanel:
-    def __init__(self, field_name, classname="", widget=None):
-        self.field_name = field_name
-        self.classname = classname
-        self.widget = widget
+            return [curry(comparator_class, self.db_field)]
+        return []
 
-    def bind_to_model(self, model):
-        base = {
-            'model': model,
-            'field_name': self.field_name,
-            'classname': self.classname,
-        }
+    def on_model_bound(self):
+        self.db_field = self.model._meta.get_field(self.field_name)
 
-        if self.widget:
-            base['widget'] = self.widget
+    def on_instance_bound(self):
+        self.bound_field = self.form[self.field_name]
+        self.heading = self.bound_field.label
+        self.help_text = self.bound_field.help_text
 
-        return type(str('_FieldPanel'), (BaseFieldPanel,), base)
+    def __repr__(self):
+        class_name = self.__class__.__name__
+        try:
+            bound_to = force_text(getattr(self, 'instance',
+                                          getattr(self, 'model')))
+        except AttributeError:
+            return "<%s '%s'>" % (class_name, self.field_name)
+        return "<%s '%s' bound to %s>" % (class_name, self.field_name, bound_to)
 
 
-class BaseRichTextFieldPanel(BaseFieldPanel):
-    @classmethod
-    def get_comparison_class(cls):
+class RichTextFieldPanel(FieldPanel):
+    def get_comparison_class(self):
         return compare.RichTextFieldComparison
 
 
-class RichTextFieldPanel:
-    def __init__(self, field_name):
-        self.field_name = field_name
-
-    def bind_to_model(self, model):
-        return type(str('_RichTextFieldPanel'), (BaseRichTextFieldPanel,), {
-            'model': model,
-            'field_name': self.field_name,
-        })
-
-
-class BaseChooserPanel(BaseFieldPanel):
+class BaseChooserPanel(FieldPanel):
     """
     Abstract superclass for panels that provide a modal interface for choosing (or creating)
     a database object such as an image, resulting in an ID that is used to populate
@@ -559,7 +519,7 @@ class BaseChooserPanel(BaseFieldPanel):
             # if the ForeignKey is null=False, Django decides to raise
             # a DoesNotExist exception here, rather than returning None
             # like every other unpopulated field type. Yay consistency!
-            return None
+            return
 
     def render_as_field(self):
         instance_obj = self.get_chosen_item()
@@ -571,124 +531,131 @@ class BaseChooserPanel(BaseFieldPanel):
         return mark_safe(render_to_string(self.field_template, context))
 
 
-class BasePageChooserPanel(BaseChooserPanel):
+class PageChooserPanel(BaseChooserPanel):
     object_type_name = "page"
 
-    @classmethod
-    def widget_overrides(cls):
-        return {cls.field_name: widgets.AdminPageChooser(
-            target_models=cls.target_models(),
-            can_choose_root=cls.can_choose_root)}
+    def __init__(self, field_name, page_type=None, can_choose_root=False):
+        super().__init__(field_name=field_name)
+
+        if page_type:
+            # Convert single string/model into list
+            if not isinstance(page_type, (list, tuple)):
+                page_type = [page_type]
+        else:
+            page_type = []
 
-    @cached_classmethod
-    def target_models(cls):
-        if cls.page_type:
+        self.page_type = page_type
+        self.can_choose_root = can_choose_root
+
+    def clone(self):
+        return self.__class__(
+            field_name=self.field_name,
+            page_type=self.page_type,
+            can_choose_root=self.can_choose_root,
+        )
+
+    def widget_overrides(self):
+        return {self.field_name: widgets.AdminPageChooser(
+            target_models=self.target_models(),
+            can_choose_root=self.can_choose_root)}
+
+    def target_models(self):
+        if self.page_type:
             target_models = []
 
-            for page_type in cls.page_type:
+            for page_type in self.page_type:
                 try:
                     target_models.append(resolve_model_string(page_type))
                 except LookupError:
                     raise ImproperlyConfigured(
                         "{0}.page_type must be of the form 'app_label.model_name', given {1!r}".format(
-                            cls.__name__, page_type
+                            self.__class__.__name__, page_type
                         )
                     )
                 except ValueError:
                     raise ImproperlyConfigured(
                         "{0}.page_type refers to model {1!r} that has not been installed".format(
-                            cls.__name__, page_type
+                            self.__class__.__name__, page_type
                         )
                     )
 
             return target_models
-        else:
-            return [cls.model._meta.get_field(cls.field_name).remote_field.model]
+        return [self.db_field.remote_field.model]
 
 
-class PageChooserPanel:
-    def __init__(self, field_name, page_type=None, can_choose_root=False):
-        self.field_name = field_name
-
-        if page_type:
-            # Convert single string/model into list
-            if not isinstance(page_type, (list, tuple)):
-                page_type = [page_type]
-        else:
-            page_type = []
-
-        self.page_type = page_type
-        self.can_choose_root = can_choose_root
-
-    def bind_to_model(self, model):
-        return type(str('_PageChooserPanel'), (BasePageChooserPanel,), {
-            'model': model,
-            'field_name': self.field_name,
-            'page_type': self.page_type,
-            'can_choose_root': self.can_choose_root,
-        })
-
+class InlinePanel(EditHandler):
+    def __init__(self, relation_name, panels=None, heading='', label='',
+                 min_num=None, max_num=None, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.relation_name = relation_name
+        self.panels = panels
+        self.heading = heading or label
+        self.label = label
+        self.min_num = min_num
+        self.max_num = max_num
 
-class BaseInlinePanel(EditHandler):
-    @classmethod
-    def get_panel_definitions(cls):
+    def clone(self):
+        return self.__class__(
+            relation_name=self.relation_name,
+            panels=self.panels,
+            heading=self.heading,
+            label=self.label,
+            help_text=self.help_text,
+            min_num=self.min_num,
+            max_num=self.max_num,
+            classname=self.classname,
+        )
+
+    def get_panel_definitions(self):
         # Look for a panels definition in the InlinePanel declaration
-        if cls.panels is not None:
-            return cls.panels
+        if self.panels is not None:
+            return self.panels
         # Failing that, get it from the model
-        else:
-            return extract_panel_definitions_from_model_class(
-                cls.related.related_model,
-                exclude=[cls.related.field.name]
-            )
-
-    _child_edit_handler_class = None
-
-    @classmethod
-    def get_child_edit_handler_class(cls):
-        if cls._child_edit_handler_class is None:
-            panels = cls.get_panel_definitions()
-            cls._child_edit_handler_class = MultiFieldPanel(
-                panels,
-                heading=cls.heading
-            ).bind_to_model(cls.related.related_model)
-
-        return cls._child_edit_handler_class
-
-    @classmethod
-    def required_formsets(cls):
-        child_edit_handler_class = cls.get_child_edit_handler_class()
+        return extract_panel_definitions_from_model_class(
+            self.related.related_model,
+            exclude=[self.related.field.name]
+        )
+
+    def get_child_edit_handler(self):
+        panels = self.get_panel_definitions()
+        child_edit_handler = MultiFieldPanel(panels, heading=self.heading)
+        return child_edit_handler.bind_to_model(self.related.related_model)
+
+    def required_formsets(self):
+        child_edit_handler = self.get_child_edit_handler()
         return {
-            cls.relation_name: {
-                'fields': child_edit_handler_class.required_fields(),
-                'widgets': child_edit_handler_class.widget_overrides(),
-                'min_num': cls.min_num,
-                'validate_min': cls.min_num is not None,
-                'max_num': cls.max_num,
-                'validate_max': cls.max_num is not None
+            self.relation_name: {
+                'fields': child_edit_handler.required_fields(),
+                'widgets': child_edit_handler.widget_overrides(),
+                'min_num': self.min_num,
+                'validate_min': self.min_num is not None,
+                'max_num': self.max_num,
+                'validate_max': self.max_num is not None
             }
         }
 
-    @classmethod
-    def html_declarations(cls):
-        return cls.get_child_edit_handler_class().html_declarations()
+    def html_declarations(self):
+        return self.get_child_edit_handler().html_declarations()
 
-    @classmethod
-    def get_comparison(cls):
-        field = cls.model._meta.get_field(cls.relation_name)
+    def get_comparison(self):
         field_comparisons = []
 
-        for panel in cls.get_panel_definitions():
-            field_comparisons.extend(panel.bind_to_model(cls.related.related_model).get_comparison())
+        for panel in self.get_panel_definitions():
+            field_comparisons.extend(
+                panel.bind_to_model(self.related.related_model)
+                .get_comparison())
 
-        return [curry(compare.ChildRelationComparison, field, field_comparisons)]
+        return [curry(compare.ChildRelationComparison, self.db_field,
+                      field_comparisons)]
 
-    def __init__(self, instance=None, form=None):
-        super().__init__(instance=instance, form=form)
+    def on_model_bound(self):
+        self.db_field = self.model._meta.get_field(self.relation_name)
+        manager = getattr(self.model, self.relation_name)
+        self.related = manager.rel
 
-        self.formset = form.formsets[self.__class__.relation_name]
+    def on_instance_bound(self):
+        self.formset = self.form.formsets[self.relation_name]
 
-        child_edit_handler_class = self.__class__.get_child_edit_handler_class()
         self.children = []
         for subform in self.formset.forms:
             # override the DELETE field to have a hidden input
@@ -698,9 +665,10 @@ class BaseInlinePanel(EditHandler):
             if self.formset.can_order:
                 subform.fields['ORDER'].widget = forms.HiddenInput()
 
+            child_edit_handler = self.get_child_edit_handler()
             self.children.append(
-                child_edit_handler_class(instance=subform.instance, form=subform)
-            )
+                child_edit_handler.bind_to_instance(instance=subform.instance,
+                                                    form=subform))
 
         # if this formset is valid, it may have been re-ordered; respect that
         # in case the parent form errored and we need to re-render
@@ -712,7 +680,9 @@ class BaseInlinePanel(EditHandler):
         if self.formset.can_order:
             empty_form.fields['ORDER'].widget = forms.HiddenInput()
 
-        self.empty_child = child_edit_handler_class(instance=empty_form.instance, form=empty_form)
+        self.empty_child = self.get_child_edit_handler()
+        self.empty_child = self.empty_child.bind_to_instance(
+            instance=empty_form.instance, form=empty_form)
 
     template = "wagtailadmin/edit_handlers/inline_panel.html"
 
@@ -733,46 +703,23 @@ class BaseInlinePanel(EditHandler):
         }))
 
 
-class InlinePanel:
-    def __init__(self, relation_name, panels=None, classname='', heading='', label='', help_text='', min_num=None, max_num=None):
-        self.relation_name = relation_name
-        self.panels = panels
-        self.heading = heading or label
-        self.label = label
-        self.help_text = help_text
-        self.min_num = min_num
-        self.max_num = max_num
-        self.classname = classname
-
-    def bind_to_model(self, model):
-        related = getattr(model, self.relation_name).rel
-
-        return type(str('_InlinePanel'), (BaseInlinePanel,), {
-            'model': model,
-            'relation_name': self.relation_name,
-            'related': related,
-            'panels': self.panels,
-            'heading': self.heading,
-            'label': self.label,
-            'help_text': self.help_text,
-            # TODO: can we pick this out of the foreign key definition as an alternative?
-            # (with a bit of help from the inlineformset object, as we do for label/heading)
-            'min_num': self.min_num,
-            'max_num': self.max_num,
-            'classname': self.classname,
-        })
-
-
 # This allows users to include the publishing panel in their own per-model override
 # without having to write these fields out by hand, potentially losing 'classname'
 # and therefore the associated styling of the publishing panel
-def PublishingPanel():
-    return MultiFieldPanel([
-        FieldRowPanel([
-            FieldPanel('go_live_at'),
-            FieldPanel('expire_at'),
-        ], classname="label-above"),
-    ], ugettext_lazy('Scheduled publishing'), classname="publishing")
+class PublishingPanel(MultiFieldPanel):
+    def __init__(self, **kwargs):
+        updated_kwargs = {
+            'children': [
+                FieldRowPanel([
+                    FieldPanel('go_live_at'),
+                    FieldPanel('expire_at'),
+                ], classname="label-above"),
+            ],
+            'heading': ugettext_lazy('Scheduled publishing'),
+            'classname': 'publishing',
+        }
+        updated_kwargs.update(kwargs)
+        super().__init__(**updated_kwargs)
 
 
 # Now that we've defined EditHandlers, we can set up wagtailcore.Page to have some.
@@ -815,14 +762,14 @@ def get_edit_handler(cls):
     if cls.settings_panels:
         tabs.append(ObjectList(cls.settings_panels, heading=ugettext_lazy('Settings'), classname="settings"))
 
-    EditHandler = TabbedInterface(tabs, base_form_class=cls.base_form_class)
-    return EditHandler.bind_to_model(cls)
+    edit_handler = TabbedInterface(tabs, base_form_class=cls.base_form_class)
+    return edit_handler.bind_to_model(cls)
 
 
 Page.get_edit_handler = get_edit_handler
 
 
-class BaseStreamFieldPanel(BaseFieldPanel):
+class StreamFieldPanel(FieldPanel):
     def classes(self):
         classes = super().classes()
         classes.append("stream-field")
@@ -834,12 +781,10 @@ class BaseStreamFieldPanel(BaseFieldPanel):
 
         return classes
 
-    @classmethod
-    def html_declarations(cls):
-        return cls.block_def.all_html_declarations()
+    def html_declarations(self):
+        return self.block_def.all_html_declarations()
 
-    @classmethod
-    def get_comparison_class(cls):
+    def get_comparison_class(self):
         return compare.StreamFieldComparison
 
     def id_for_label(self):
@@ -847,16 +792,6 @@ class BaseStreamFieldPanel(BaseFieldPanel):
         # attach the label to any specific one
         return ""
 
-
-class StreamFieldPanel:
-    def __init__(self, field_name, classname=''):
-        self.field_name = field_name
-        self.classname = classname
-
-    def bind_to_model(self, model):
-        return type(str('_StreamFieldPanel'), (BaseStreamFieldPanel,), {
-            'model': model,
-            'field_name': self.field_name,
-            'block_def': model._meta.get_field(self.field_name).stream_block,
-            'classname': self.classname,
-        })
+    def on_model_bound(self):
+        super().on_model_bound()
+        self.block_def = self.db_field.stream_block

+ 120 - 64
wagtail/admin/tests/test_edit_handlers.py

@@ -3,7 +3,7 @@ from datetime import date
 import mock
 from django import forms
 from django.core import checks
-from django.core.exceptions import ImproperlyConfigured
+from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
 from django.test import TestCase, override_settings
 
 from wagtail.tests.testapp.forms import ValidatedPageForm
@@ -21,6 +21,14 @@ from wagtail.images.edit_handlers import ImageChooserPanel
 
 
 class TestGetFormForModel(TestCase):
+    def test_get_form_without_model(self):
+        edit_handler = ObjectList()
+        with self.assertRaisesMessage(
+                AttributeError,
+                'ObjectList is not bound to a model yet. '
+                'Use `.bind_to_model(model)` before using this method.'):
+            edit_handler.get_form_class()
+
     def test_get_form_for_model(self):
         EventPageForm = get_form_for_model(EventPage, form_class=WagtailAdminPageForm)
         form = EventPageForm()
@@ -128,8 +136,8 @@ class TestPageEditHandlers(TestCase):
         """
         Forms for pages should have a base class of WagtailAdminPageForm.
         """
-        EditHandler = EventPage.get_edit_handler()
-        EventPageForm = EditHandler.get_form_class(EventPage)
+        edit_handler = EventPage.get_edit_handler()
+        EventPageForm = edit_handler.get_form_class()
 
         # The generated form should inherit from WagtailAdminPageForm
         self.assertTrue(issubclass(EventPageForm, WagtailAdminPageForm))
@@ -140,8 +148,8 @@ class TestPageEditHandlers(TestCase):
         ValidatedPage sets a custom base_form_class. This should be used as the
         base class when constructing a form for ValidatedPages
         """
-        EditHandler = ValidatedPage.get_edit_handler()
-        GeneratedValidatedPageForm = EditHandler.get_form_class(ValidatedPage)
+        edit_handler = ValidatedPage.get_edit_handler()
+        GeneratedValidatedPageForm = edit_handler.get_form_class()
 
         # The generated form should inherit from ValidatedPageForm, because
         # ValidatedPage.base_form_class == ValidatedPageForm
@@ -159,7 +167,7 @@ class TestPageEditHandlers(TestCase):
             id='wagtailadmin.E001')
 
         invalid_edit_handler = checks.Error(
-            "ValidatedPage.get_edit_handler().get_form_class(ValidatedPage) does not extend WagtailAdminPageForm",
+            "ValidatedPage.get_edit_handler().get_form_class() does not extend WagtailAdminPageForm",
             hint="Ensure that the EditHandler for ValidatedPage creates a subclass of WagtailAdminPageForm",
             obj=ValidatedPage,
             id='wagtailadmin.E002')
@@ -180,9 +188,9 @@ class TestPageEditHandlers(TestCase):
         ValidatedPage.base_form_class, or provide a custom form class for the
         edit handler. Check the generated form class is of the correct type.
         """
-        ValidatedPage.edit_handler = TabbedInterface([])
-        with mock.patch.object(ValidatedPage, 'edit_handler', new=TabbedInterface([]), create=True):
-            form_class = ValidatedPage.get_edit_handler().get_form_class(ValidatedPage)
+        ValidatedPage.edit_handler = TabbedInterface()
+        with mock.patch.object(ValidatedPage, 'edit_handler', new=TabbedInterface(), create=True):
+            form_class = ValidatedPage.get_edit_handler().get_form_class()
             self.assertTrue(issubclass(form_class, WagtailAdminPageForm))
             errors = ValidatedPage.check()
             self.assertEqual(errors, [])
@@ -219,7 +227,7 @@ class TestExtractPanelDefinitionsFromModelClass(TestCase):
 class TestTabbedInterface(TestCase):
     def setUp(self):
         # a custom tabbed interface for EventPage
-        self.EventPageTabbedInterface = TabbedInterface([
+        self.event_page_tabbed_interface = TabbedInterface([
             ObjectList([
                 FieldPanel('title', widget=forms.Textarea),
                 FieldPanel('date_from'),
@@ -231,7 +239,7 @@ class TestTabbedInterface(TestCase):
         ]).bind_to_model(EventPage)
 
     def test_get_form_class(self):
-        EventPageForm = self.EventPageTabbedInterface.get_form_class(EventPage)
+        EventPageForm = self.event_page_tabbed_interface.get_form_class()
         form = EventPageForm()
 
         # form must include the 'speakers' formset required by the speakers InlinePanel
@@ -241,11 +249,11 @@ class TestTabbedInterface(TestCase):
         self.assertEqual(type(form.fields['title'].widget), forms.Textarea)
 
     def test_render(self):
-        EventPageForm = self.EventPageTabbedInterface.get_form_class(EventPage)
+        EventPageForm = self.event_page_tabbed_interface.get_form_class()
         event = EventPage(title='Abergavenny sheepdog trials')
         form = EventPageForm(instance=event)
 
-        tabbed_interface = self.EventPageTabbedInterface(
+        tabbed_interface = self.event_page_tabbed_interface.bind_to_instance(
             instance=event,
             form=form
         )
@@ -269,15 +277,15 @@ class TestTabbedInterface(TestCase):
 
     def test_required_fields(self):
         # required_fields should report the set of form fields to be rendered recursively by children of TabbedInterface
-        result = set(self.EventPageTabbedInterface.required_fields())
+        result = set(self.event_page_tabbed_interface.required_fields())
         self.assertEqual(result, set(['title', 'date_from', 'date_to']))
 
     def test_render_form_content(self):
-        EventPageForm = self.EventPageTabbedInterface.get_form_class(EventPage)
+        EventPageForm = self.event_page_tabbed_interface.get_form_class()
         event = EventPage(title='Abergavenny sheepdog trials')
         form = EventPageForm(instance=event)
 
-        tabbed_interface = self.EventPageTabbedInterface(
+        tabbed_interface = self.event_page_tabbed_interface.bind_to_instance(
             instance=event,
             form=form
         )
@@ -293,7 +301,7 @@ class TestTabbedInterface(TestCase):
 class TestObjectList(TestCase):
     def setUp(self):
         # a custom ObjectList for EventPage
-        self.EventPageObjectList = ObjectList([
+        self.event_page_object_list = ObjectList([
             FieldPanel('title', widget=forms.Textarea),
             FieldPanel('date_from'),
             FieldPanel('date_to'),
@@ -301,7 +309,7 @@ class TestObjectList(TestCase):
         ], heading='Event details', classname="shiny").bind_to_model(EventPage)
 
     def test_get_form_class(self):
-        EventPageForm = self.EventPageObjectList.get_form_class(EventPage)
+        EventPageForm = self.event_page_object_list.get_form_class()
         form = EventPageForm()
 
         # form must include the 'speakers' formset required by the speakers InlinePanel
@@ -311,11 +319,11 @@ class TestObjectList(TestCase):
         self.assertEqual(type(form.fields['title'].widget), forms.Textarea)
 
     def test_render(self):
-        EventPageForm = self.EventPageObjectList.get_form_class(EventPage)
+        EventPageForm = self.event_page_object_list.get_form_class()
         event = EventPage(title='Abergavenny sheepdog trials')
         form = EventPageForm(instance=event)
 
-        object_list = self.EventPageObjectList(
+        object_list = self.event_page_object_list.bind_to_instance(
             instance=event,
             form=form
         )
@@ -345,7 +353,12 @@ class TestFieldPanel(TestCase):
         self.event = EventPage(title='Abergavenny sheepdog trials',
                                date_from=date(2014, 7, 20), date_to=date(2014, 7, 21))
 
-        self.EndDatePanel = FieldPanel('date_to', classname='full-width').bind_to_model(EventPage)
+        self.end_date_panel = (FieldPanel('date_to', classname='full-width')
+                               .bind_to_model(EventPage))
+
+    def test_invalid_field(self):
+        with self.assertRaises(FieldDoesNotExist):
+            FieldPanel('barbecue').bind_to_model(Page)
 
     def test_render_as_object(self):
         form = self.EventPageForm(
@@ -354,7 +367,7 @@ class TestFieldPanel(TestCase):
 
         form.is_valid()
 
-        field_panel = self.EndDatePanel(
+        field_panel = self.end_date_panel.bind_to_instance(
             instance=self.event,
             form=form
         )
@@ -381,7 +394,7 @@ class TestFieldPanel(TestCase):
 
         form.is_valid()
 
-        field_panel = self.EndDatePanel(
+        field_panel = self.end_date_panel.bind_to_instance(
             instance=self.event,
             form=form
         )
@@ -401,7 +414,7 @@ class TestFieldPanel(TestCase):
         self.assertNotIn('<p class="error-message">', result)
 
     def test_required_fields(self):
-        result = self.EndDatePanel.required_fields()
+        result = self.end_date_panel.required_fields()
         self.assertEqual(result, ['date_to'])
 
     def test_error_message_is_rendered(self):
@@ -411,7 +424,7 @@ class TestFieldPanel(TestCase):
 
         form.is_valid()
 
-        field_panel = self.EndDatePanel(
+        field_panel = self.end_date_panel.bind_to_instance(
             instance=self.event,
             form=form
         )
@@ -428,7 +441,7 @@ class TestFieldRowPanel(TestCase):
         self.event = EventPage(title='Abergavenny sheepdog trials',
                                date_from=date(2014, 7, 20), date_to=date(2014, 7, 21))
 
-        self.DatesPanel = FieldRowPanel([
+        self.dates_panel = FieldRowPanel([
             FieldPanel('date_from', classname='col4'),
             FieldPanel('date_to', classname='coltwo'),
         ]).bind_to_model(EventPage)
@@ -440,7 +453,7 @@ class TestFieldRowPanel(TestCase):
 
         form.is_valid()
 
-        field_panel = self.DatesPanel(
+        field_panel = self.dates_panel.bind_to_instance(
             instance=self.event,
             form=form
         )
@@ -459,7 +472,7 @@ class TestFieldRowPanel(TestCase):
 
         form.is_valid()
 
-        field_panel = self.DatesPanel(
+        field_panel = self.dates_panel.bind_to_instance(
             instance=self.event,
             form=form
         )
@@ -485,7 +498,7 @@ class TestFieldRowPanel(TestCase):
 
         form.is_valid()
 
-        field_panel = self.DatesPanel(
+        field_panel = self.dates_panel.bind_to_instance(
             instance=self.event,
             form=form
         )
@@ -501,7 +514,7 @@ class TestFieldRowPanel(TestCase):
 
         form.is_valid()
 
-        field_panel = self.DatesPanel(
+        field_panel = self.dates_panel.bind_to_instance(
             instance=self.event,
             form=form
         )
@@ -517,7 +530,7 @@ class TestFieldRowPanel(TestCase):
 
         form.is_valid()
 
-        field_panel = self.DatesPanel(
+        field_panel = self.dates_panel.bind_to_instance(
             instance=self.event,
             form=form
         )
@@ -527,6 +540,38 @@ class TestFieldRowPanel(TestCase):
         self.assertIn('<li class="field-col col4', result)
 
 
+class TestFieldRowPanelWithChooser(TestCase):
+    def setUp(self):
+        self.EventPageForm = get_form_for_model(
+            EventPage, form_class=WagtailAdminPageForm, formsets=[])
+        self.event = EventPage(title='Abergavenny sheepdog trials',
+                               date_from=date(2014, 7, 19), date_to=date(2014, 7, 21))
+
+        self.dates_panel = FieldRowPanel([
+            FieldPanel('date_from'),
+            ImageChooserPanel('feed_image'),
+        ]).bind_to_model(EventPage)
+
+    def test_render_as_object(self):
+        form = self.EventPageForm(
+            {'title': 'Pontypridd sheepdog trials', 'date_from': '2014-07-20', 'date_to': '2014-07-22'},
+            instance=self.event)
+
+        form.is_valid()
+
+        field_panel = self.dates_panel.bind_to_instance(
+            instance=self.event,
+            form=form
+        )
+        result = field_panel.render_as_object()
+
+        # check that the populated form field is included
+        self.assertIn('value="2014-07-20"', result)
+
+        # there should be no errors on this field
+        self.assertNotIn('<p class="error-message">', result)
+
+
 class TestPageChooserPanel(TestCase):
     fixtures = ['test.json']
 
@@ -534,11 +579,12 @@ 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.EditHandler = ObjectList([PageChooserPanel('page')]).bind_to_model(PageChooserModel)
-        self.MyPageChooserPanel = self.EditHandler.children[0]
+        self.edit_handler = (ObjectList([PageChooserPanel('page')])
+                             .bind_to_model(PageChooserModel))
+        self.my_page_chooser_panel = self.edit_handler.children[0]
 
         # build a form class containing the fields that MyPageChooserPanel wants
-        self.PageChooserForm = self.EditHandler.get_form_class(PageChooserModel)
+        self.PageChooserForm = self.edit_handler.get_form_class()
 
         # a test instance of PageChooserModel, pointing to the 'christmas' page
         self.christmas_page = Page.objects.get(slug='christmas')
@@ -546,7 +592,8 @@ class TestPageChooserPanel(TestCase):
         self.test_instance = model.objects.create(page=self.christmas_page)
 
         self.form = self.PageChooserForm(instance=self.test_instance)
-        self.page_chooser_panel = self.MyPageChooserPanel(instance=self.test_instance, form=self.form)
+        self.page_chooser_panel = self.my_page_chooser_panel.bind_to_instance(
+            instance=self.test_instance, form=self.form)
 
     def test_page_chooser_uses_correct_widget(self):
         self.assertEqual(type(self.form.fields['page'].widget), AdminPageChooser)
@@ -561,14 +608,15 @@ class TestPageChooserPanel(TestCase):
     def test_render_js_init_with_can_choose_root_true(self):
         # construct an alternative page chooser panel object, with can_choose_root=True
 
-        MyPageObjectList = ObjectList([
+        my_page_object_list = ObjectList([
             PageChooserPanel('page', can_choose_root=True)
         ]).bind_to_model(PageChooserModel)
-        MyPageChooserPanel = MyPageObjectList.children[0]
-        PageChooserForm = MyPageObjectList.get_form_class(EventPageChooserModel)
+        my_page_chooser_panel = my_page_object_list.children[0]
+        PageChooserForm = my_page_object_list.get_form_class()
 
         form = PageChooserForm(instance=self.test_instance)
-        page_chooser_panel = MyPageChooserPanel(instance=self.test_instance, form=form)
+        page_chooser_panel = my_page_chooser_panel.bind_to_instance(
+            instance=self.test_instance, form=form)
         result = page_chooser_panel.render_as_field()
 
         # the canChooseRoot flag on createPageChooser should now be true
@@ -592,7 +640,8 @@ class TestPageChooserPanel(TestCase):
     def test_render_as_empty_field(self):
         test_instance = PageChooserModel()
         form = self.PageChooserForm(instance=test_instance)
-        page_chooser_panel = self.MyPageChooserPanel(instance=test_instance, form=form)
+        page_chooser_panel = self.my_page_chooser_panel.bind_to_instance(
+            instance=test_instance, form=form)
         result = page_chooser_panel.render_as_field()
 
         self.assertIn('<p class="help">help text</p>', result)
@@ -603,19 +652,21 @@ class TestPageChooserPanel(TestCase):
         form = self.PageChooserForm({'page': ''}, instance=self.test_instance)
         self.assertFalse(form.is_valid())
 
-        page_chooser_panel = self.MyPageChooserPanel(instance=self.test_instance, form=form)
+        page_chooser_panel = self.my_page_chooser_panel.bind_to_instance(
+            instance=self.test_instance, form=form)
         self.assertIn('<span>This field is required.</span>', page_chooser_panel.render_as_field())
 
     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
-        MyPageObjectList = ObjectList([
+        my_page_object_list = ObjectList([
             PageChooserPanel('page', 'tests.EventPage')
         ]).bind_to_model(EventPageChooserModel)
-        MyPageChooserPanel = MyPageObjectList.children[0]
-        PageChooserForm = MyPageObjectList.get_form_class(EventPageChooserModel)
+        my_page_chooser_panel = my_page_object_list.children[0]
+        PageChooserForm = my_page_object_list.get_form_class()
         form = PageChooserForm(instance=self.test_instance)
-        page_chooser_panel = MyPageChooserPanel(instance=self.test_instance, form=form)
+        page_chooser_panel = my_page_chooser_panel.bind_to_instance(
+            instance=self.test_instance, form=form)
 
         result = page_chooser_panel.render_as_field()
         expected_js = 'createPageChooser("{id}", ["{model}"], {parent}, false, null);'.format(
@@ -626,11 +677,13 @@ 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
-        MyPageObjectList = ObjectList([PageChooserPanel('page')]).bind_to_model(EventPageChooserModel)
-        MyPageChooserPanel = MyPageObjectList.children[0]
-        PageChooserForm = MyPageObjectList.get_form_class(EventPageChooserModel)
+        my_page_object_list = (ObjectList([PageChooserPanel('page')])
+                               .bind_to_model(EventPageChooserModel))
+        my_page_chooser_panel = my_page_object_list.children[0]
+        PageChooserForm = my_page_object_list.get_form_class()
         form = PageChooserForm(instance=self.test_instance)
-        page_chooser_panel = MyPageChooserPanel(instance=self.test_instance, form=form)
+        page_chooser_panel = my_page_chooser_panel.bind_to_instance(
+            instance=self.test_instance, form=form)
 
         result = page_chooser_panel.render_as_field()
         expected_js = 'createPageChooser("{id}", ["{model}"], {parent}, false, null);'.format(
@@ -640,14 +693,14 @@ class TestPageChooserPanel(TestCase):
 
     def test_target_models(self):
         result = PageChooserPanel(
-            'barbecue',
+            'page',
             'wagtailcore.site'
         ).bind_to_model(PageChooserModel).target_models()
         self.assertEqual(result, [Site])
 
     def test_target_models_malformed_type(self):
         result = PageChooserPanel(
-            'barbecue',
+            'page',
             'snowman'
         ).bind_to_model(PageChooserModel)
         self.assertRaises(ImproperlyConfigured,
@@ -655,7 +708,7 @@ class TestPageChooserPanel(TestCase):
 
     def test_target_models_nonexistent_type(self):
         result = PageChooserPanel(
-            'barbecue',
+            'page',
             'snowman.lorry'
         ).bind_to_model(PageChooserModel)
         self.assertRaises(ImproperlyConfigured,
@@ -670,10 +723,10 @@ 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
         """
-        SpeakerObjectList = ObjectList([
+        speaker_object_list = ObjectList([
             InlinePanel('speakers', label="Speakers", classname="classname-for-speakers")
         ]).bind_to_model(EventPage)
-        EventPageForm = SpeakerObjectList.get_form_class(EventPage)
+        EventPageForm = speaker_object_list.get_form_class()
 
         # SpeakerInlinePanel should instruct the form class to include a 'speakers' formset
         self.assertEqual(['speakers'], list(EventPageForm.formsets.keys()))
@@ -681,7 +734,8 @@ class TestInlinePanel(TestCase, WagtailTestUtils):
         event_page = EventPage.objects.get(slug='christmas')
 
         form = EventPageForm(instance=event_page)
-        panel = SpeakerObjectList(instance=event_page, form=form)
+        panel = speaker_object_list.bind_to_instance(instance=event_page,
+                                                     form=form)
 
         result = panel.render_as_field()
 
@@ -720,22 +774,23 @@ class TestInlinePanel(TestCase, WagtailTestUtils):
         Check that inline panel renders the panels listed in the InlinePanel definition
         where one is specified
         """
-        SpeakerObjectList = ObjectList([
+        speaker_object_list = ObjectList([
             InlinePanel('speakers', label="Speakers", panels=[
                 FieldPanel('first_name', widget=forms.Textarea),
                 ImageChooserPanel('image'),
             ]),
         ]).bind_to_model(EventPage)
-        SpeakerInlinePanel = SpeakerObjectList.children[0]
-        EventPageForm = SpeakerObjectList.get_form_class(EventPage)
+        speaker_inline_panel = speaker_object_list.children[0]
+        EventPageForm = speaker_object_list.get_form_class()
 
-        # SpeakerInlinePanel should instruct the form class to include a 'speakers' formset
+        # speaker_inline_panel should instruct the form class to include a 'speakers' formset
         self.assertEqual(['speakers'], list(EventPageForm.formsets.keys()))
 
         event_page = EventPage.objects.get(slug='christmas')
 
         form = EventPageForm(instance=event_page)
-        panel = SpeakerInlinePanel(instance=event_page, form=form)
+        panel = speaker_inline_panel.bind_to_instance(
+            instance=event_page, form=form)
 
         result = panel.render_as_field()
 
@@ -781,17 +836,18 @@ class TestInlinePanel(TestCase, WagtailTestUtils):
         https://github.com/wagtail/wagtail/pull/2699
         https://github.com/wagtail/wagtail/issues/3227
         """
-        SpeakerObjectList = ObjectList([
+        speaker_object_list = ObjectList([
             InlinePanel('speakers', label="Speakers", panels=[
                 FieldPanel('first_name', widget=forms.Textarea),
                 ImageChooserPanel('image'),
             ]),
         ]).bind_to_model(EventPage)
-        SpeakerInlinePanel = SpeakerObjectList.children[0]
-        EventPageForm = SpeakerObjectList.get_form_class(EventPage)
+        speaker_inline_panel = speaker_object_list.children[0]
+        EventPageForm = speaker_object_list.get_form_class()
         event_page = EventPage.objects.get(slug='christmas')
         form = EventPageForm(instance=event_page)
-        panel = SpeakerInlinePanel(instance=event_page, form=form)
+        panel = speaker_inline_panel.bind_to_instance(
+            instance=event_page, form=form)
 
         self.assertIn('maxForms: 1000', panel.render_js_init())
 

+ 5 - 5
wagtail/admin/tests/test_rich_text.py

@@ -23,11 +23,11 @@ class BaseRichTextEditHandlerTestCase(TestCase):
         """
         from wagtail.tests.testapp.models import DefaultRichBlockFieldPage
 
-        block_page_edit_handler = DefaultRichBlockFieldPage.get_edit_handler()
-        if block_page_edit_handler._form_class:
-            rich_text_block = block_page_edit_handler._form_class.base_fields['body'].block.child_blocks['rich_text']
-            if hasattr(rich_text_block, 'field'):
-                del rich_text_block.field
+        rich_text_block = (DefaultRichBlockFieldPage.get_edit_handler()
+                           .get_form_class().base_fields['body'].block
+                           .child_blocks['rich_text'])
+        if hasattr(rich_text_block, 'field'):
+            del rich_text_block.field
 
         for page_class in get_page_models():
             page_class.get_edit_handler.cache_clear()

+ 15 - 12
wagtail/admin/views/pages.py

@@ -183,8 +183,8 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
             return result
 
     page = page_class(owner=request.user)
-    edit_handler_class = page_class.get_edit_handler()
-    form_class = edit_handler_class.get_form_class(page_class)
+    edit_handler = page_class.get_edit_handler()
+    form_class = edit_handler.get_form_class()
 
     next_url = get_valid_next_url_from_request(request)
 
@@ -270,12 +270,13 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
             messages.validation_error(
                 request, _("The page could not be created due to validation errors"), form
             )
-            edit_handler = edit_handler_class(instance=page, form=form)
+            edit_handler = edit_handler.bind_to_instance(instance=page,
+                                                         form=form)
             has_unsaved_changes = True
     else:
         signals.init_new_page.send(sender=create, page=page, parent=parent_page)
         form = form_class(instance=page, parent_page=parent_page)
-        edit_handler = edit_handler_class(instance=page, form=form)
+        edit_handler = edit_handler.bind_to_instance(instance=page, form=form)
         has_unsaved_changes = False
 
     return render(request, 'wagtailadmin/pages/create.html', {
@@ -307,8 +308,8 @@ def edit(request, page_id):
         if hasattr(result, 'status_code'):
             return result
 
-    edit_handler_class = page_class.get_edit_handler()
-    form_class = edit_handler_class.get_form_class(page_class)
+    edit_handler = page_class.get_edit_handler()
+    form_class = edit_handler.get_form_class()
 
     next_url = get_valid_next_url_from_request(request)
 
@@ -460,7 +461,8 @@ def edit(request, page_id):
                     request, _("The page could not be saved due to validation errors"), form
                 )
 
-            edit_handler = edit_handler_class(instance=page, form=form)
+            edit_handler = edit_handler.bind_to_instance(instance=page,
+                                                         form=form)
             errors_debug = (
                 repr(edit_handler.form.errors) +
                 repr([
@@ -472,7 +474,7 @@ def edit(request, page_id):
             has_unsaved_changes = True
     else:
         form = form_class(instance=page, parent_page=parent)
-        edit_handler = edit_handler_class(instance=page, form=form)
+        edit_handler = edit_handler.bind_to_instance(instance=page, form=form)
         has_unsaved_changes = False
 
     # Check for revisions still undergoing moderation and warn
@@ -564,7 +566,7 @@ class PreviewOnEdit(View):
                                  id=self.args[0]).get_latest_revision_as_page()
 
     def get_form(self, page, query_dict):
-        form_class = page.get_edit_handler().get_form_class(page._meta.model)
+        form_class = page.get_edit_handler().get_form_class()
         parent_page = page.get_parent().specific
 
         if self.session_key not in self.request.session:
@@ -1022,11 +1024,12 @@ def revisions_revert(request, page_id, revision_id):
     content_type = ContentType.objects.get_for_model(page)
     page_class = content_type.model_class()
 
-    edit_handler_class = page_class.get_edit_handler()
-    form_class = edit_handler_class.get_form_class(page_class)
+    edit_handler = page_class.get_edit_handler()
+    form_class = edit_handler.get_form_class()
 
     form = form_class(instance=revision_page)
-    edit_handler = edit_handler_class(instance=revision_page, form=form)
+    edit_handler = edit_handler.bind_to_instance(instance=revision_page,
+                                                 form=form)
 
     user_avatar = render_to_string('wagtailadmin/shared/user_avatar.html', {'user': revision.user})
 

+ 4 - 12
wagtail/contrib/forms/edit_handlers.py

@@ -5,7 +5,7 @@ from django.utils.translation import ugettext as _
 from wagtail.admin.edit_handlers import EditHandler
 
 
-class BaseFormSubmissionsPanel(EditHandler):
+class FormSubmissionsPanel(EditHandler):
     template = "wagtailforms/edit_handlers/form_responses_panel.html"
 
     def render(self):
@@ -23,14 +23,6 @@ class BaseFormSubmissionsPanel(EditHandler):
             'last_submit_time': submissions.order_by('submit_time').last().submit_time,
         }))
 
-
-class FormSubmissionsPanel:
-    def __init__(self, heading=None):
-        self.heading = heading
-
-    def bind_to_model(self, model):
-        heading = _('{} submissions').format(model.get_verbose_name())
-        return type(str('_FormResponsesPanel'), (BaseFormSubmissionsPanel,), {
-            'model': model,
-            'heading': self.heading or heading,
-        })
+    def on_model_bound(self):
+        if not self.heading:
+            self.heading = _('%s submissions') % self.model.get_verbose_name()

+ 4 - 2
wagtail/contrib/forms/tests/test_views.py

@@ -29,7 +29,8 @@ class TestFormResponsesPanel(TestCase):
 
         submissions_panel = FormSubmissionsPanel().bind_to_model(FormPage)
 
-        self.panel = submissions_panel(self.form_page, self.FormPageForm())
+        self.panel = submissions_panel.bind_to_instance(
+            instance=self.form_page, form=self.FormPageForm())
 
     def test_render_with_submissions(self):
         """Show the panel with the count of submission and a link to the list_submissions view."""
@@ -67,7 +68,8 @@ class TestFormResponsesPanelWithCustomSubmissionClass(TestCase):
 
         submissions_panel = FormSubmissionsPanel().bind_to_model(FormPageWithCustomSubmission)
 
-        self.panel = submissions_panel(self.form_page, self.FormPageForm())
+        self.panel = submissions_panel.bind_to_instance(self.form_page,
+                                                        self.FormPageForm())
 
     def test_render_with_submissions(self):
         """Show the panel with the count of submission and a link to the list_submissions view."""

+ 6 - 4
wagtail/contrib/modeladmin/views.py

@@ -104,7 +104,7 @@ class WMABaseView(TemplateView):
 
 class ModelFormView(WMABaseView, FormView):
 
-    def get_edit_handler_class(self):
+    def get_edit_handler(self):
         if hasattr(self.model, 'edit_handler'):
             edit_handler = self.model.edit_handler
         else:
@@ -114,7 +114,7 @@ class ModelFormView(WMABaseView, FormView):
         return edit_handler.bind_to_model(self.model)
 
     def get_form_class(self):
-        return self.get_edit_handler_class().get_form_class(self.model)
+        return self.get_edit_handler().get_form_class()
 
     def get_success_url(self):
         return self.index_url
@@ -136,11 +136,13 @@ class ModelFormView(WMABaseView, FormView):
 
     def get_context_data(self, **kwargs):
         instance = self.get_instance()
-        edit_handler_class = self.get_edit_handler_class()
+        edit_handler = self.get_edit_handler()
         form = self.get_form()
+        edit_handler = edit_handler.bind_to_instance(
+            instance=instance, form=form)
         context = {
             'is_multipart': form.is_multipart(),
-            'edit_handler': edit_handler_class(instance=instance, form=form),
+            'edit_handler': edit_handler,
             'form': form,
         }
         context.update(kwargs)

+ 7 - 6
wagtail/contrib/settings/tests/test_admin.py

@@ -9,6 +9,7 @@ from wagtail.contrib.settings.views import get_setting_edit_handler
 from wagtail.tests.testapp.models import (
     FileUploadSetting, IconSetting, PanelSettings, TabbedSettings, TestSetting)
 from wagtail.tests.utils import WagtailTestUtils
+from wagtail.admin.edit_handlers import FieldPanel, ObjectList, TabbedInterface
 from wagtail.core import hooks
 from wagtail.core.models import Page, Site
 
@@ -225,24 +226,24 @@ class TestEditHandlers(TestCase):
 
     def test_default_model_introspection(self):
         handler = get_setting_edit_handler(TestSetting)
-        self.assertEqual(handler.__name__, '_ObjectList')
+        self.assertIsInstance(handler, ObjectList)
         self.assertEqual(len(handler.children), 2)
         first = handler.children[0]
-        self.assertEqual(first.__name__, '_FieldPanel')
+        self.assertIsInstance(first, FieldPanel)
         self.assertEqual(first.field_name, 'title')
         second = handler.children[1]
-        self.assertEqual(second.__name__, '_FieldPanel')
+        self.assertIsInstance(second, FieldPanel)
         self.assertEqual(second.field_name, 'email')
 
     def test_with_custom_panels(self):
         handler = get_setting_edit_handler(PanelSettings)
-        self.assertEqual(handler.__name__, '_ObjectList')
+        self.assertIsInstance(handler, ObjectList)
         self.assertEqual(len(handler.children), 1)
         first = handler.children[0]
-        self.assertEqual(first.__name__, '_FieldPanel')
+        self.assertIsInstance(first, FieldPanel)
         self.assertEqual(first.field_name, 'title')
 
     def test_with_custom_edit_handler(self):
         handler = get_setting_edit_handler(TabbedSettings)
-        self.assertEqual(handler.__name__, '_TabbedInterface')
+        self.assertIsInstance(handler, TabbedInterface)
         self.assertEqual(len(handler.children), 2)

+ 8 - 6
wagtail/contrib/settings/views.py

@@ -8,7 +8,7 @@ from django.utils.translation import ugettext as _
 
 from wagtail.admin import messages
 from wagtail.admin.edit_handlers import (
-    ObjectList, extract_panel_definitions_from_model_class)
+    ObjectList, TabbedInterface, extract_panel_definitions_from_model_class)
 from wagtail.core.models import Site
 
 from .forms import SiteSwitchForm
@@ -51,8 +51,8 @@ def edit(request, app_name, model_name, site_pk):
     setting_type_name = model._meta.verbose_name
 
     instance = model.for_site(site)
-    edit_handler_class = get_setting_edit_handler(model)
-    form_class = edit_handler_class.get_form_class(model)
+    edit_handler = get_setting_edit_handler(model)
+    form_class = edit_handler.get_form_class()
 
     if request.method == 'POST':
         form = form_class(request.POST, request.FILES, instance=instance)
@@ -70,10 +70,12 @@ def edit(request, app_name, model_name, site_pk):
             return redirect('wagtailsettings:edit', app_name, model_name, site.pk)
         else:
             messages.error(request, _("The setting could not be saved due to errors."))
-            edit_handler = edit_handler_class(instance=instance, form=form)
+            edit_handler = edit_handler.bind_to_instance(
+                instance=instance, form=form)
     else:
         form = form_class(instance=instance)
-        edit_handler = edit_handler_class(instance=instance, form=form)
+        edit_handler = edit_handler.bind_to_instance(
+            instance=instance, form=form)
 
     # Show a site switcher form if there are multiple sites
     site_switcher = None
@@ -88,5 +90,5 @@ def edit(request, app_name, model_name, site_pk):
         'form': form,
         'site': site,
         'site_switcher': site_switcher,
-        'tabbed': edit_handler_class.__name__ == '_TabbedInterface',
+        'tabbed': isinstance(edit_handler, TabbedInterface),
     })

+ 3 - 15
wagtail/documents/edit_handlers.py

@@ -3,20 +3,8 @@ from wagtail.admin.edit_handlers import BaseChooserPanel
 from .widgets import AdminDocumentChooser
 
 
-class BaseDocumentChooserPanel(BaseChooserPanel):
+class DocumentChooserPanel(BaseChooserPanel):
     object_type_name = "document"
 
-    @classmethod
-    def widget_overrides(cls):
-        return {cls.field_name: AdminDocumentChooser}
-
-
-class DocumentChooserPanel:
-    def __init__(self, field_name):
-        self.field_name = field_name
-
-    def bind_to_model(self, model):
-        return type(str('_DocumentChooserPanel'), (BaseDocumentChooserPanel,), {
-            'model': model,
-            'field_name': self.field_name,
-        })
+    def widget_overrides(self):
+        return {self.field_name: AdminDocumentChooser}

+ 4 - 17
wagtail/images/edit_handlers.py

@@ -6,29 +6,16 @@ from wagtail.admin.edit_handlers import BaseChooserPanel
 from .widgets import AdminImageChooser
 
 
-class BaseImageChooserPanel(BaseChooserPanel):
+class ImageChooserPanel(BaseChooserPanel):
     object_type_name = "image"
 
-    @classmethod
-    def widget_overrides(cls):
-        return {cls.field_name: AdminImageChooser}
+    def widget_overrides(self):
+        return {self.field_name: AdminImageChooser}
 
-    @classmethod
-    def get_comparison_class(cls):
+    def get_comparison_class(self):
         return ImageFieldComparison
 
 
-class ImageChooserPanel:
-    def __init__(self, field_name):
-        self.field_name = field_name
-
-    def bind_to_model(self, model):
-        return type(str('_ImageChooserPanel'), (BaseImageChooserPanel,), {
-            'model': model,
-            'field_name': self.field_name,
-        })
-
-
 class ImageFieldComparison(ForeignObjectComparison):
     def htmldiff(self):
         image_a, image_b = self.get_objects()

+ 6 - 23
wagtail/snippets/edit_handlers.py

@@ -6,21 +6,11 @@ from wagtail.admin.edit_handlers import BaseChooserPanel
 from .widgets import AdminSnippetChooser
 
 
-class BaseSnippetChooserPanel(BaseChooserPanel):
+class SnippetChooserPanel(BaseChooserPanel):
     object_type_name = 'item'
 
-    _target_model = None
-
-    @classmethod
-    def widget_overrides(cls):
-        return {cls.field_name: AdminSnippetChooser(model=cls.target_model())}
-
-    @classmethod
-    def target_model(cls):
-        if cls._target_model is None:
-            cls._target_model = cls.model._meta.get_field(cls.field_name).remote_field.model
-
-        return cls._target_model
+    def widget_overrides(self):
+        return {self.field_name: AdminSnippetChooser(model=self.target_model)}
 
     def render_as_field(self):
         instance_obj = self.get_chosen_item()
@@ -29,13 +19,6 @@ class BaseSnippetChooserPanel(BaseChooserPanel):
             self.object_type_name: instance_obj,
         }))
 
-
-class SnippetChooserPanel:
-    def __init__(self, field_name):
-        self.field_name = field_name
-
-    def bind_to_model(self, model):
-        return type(str('_SnippetChooserPanel'), (BaseSnippetChooserPanel,), {
-            'model': model,
-            'field_name': self.field_name,
-        })
+    def on_model_bound(self):
+        super().on_model_bound()
+        self.target_model = self.db_field.remote_field.model

+ 18 - 21
wagtail/snippets/tests.py

@@ -370,18 +370,18 @@ class TestSnippetChooserPanel(TestCase, WagtailTestUtils):
         test_snippet = model.objects.create(
             advert=Advert.objects.create(text=self.advert_text))
 
-        self.edit_handler_class = get_snippet_edit_handler(model)
-        self.form_class = self.edit_handler_class.get_form_class(model)
+        self.edit_handler = get_snippet_edit_handler(model)
+        self.form_class = self.edit_handler.get_form_class()
         form = self.form_class(instance=test_snippet)
-        edit_handler = self.edit_handler_class(instance=test_snippet, form=form)
+        edit_handler = self.edit_handler.bind_to_instance(instance=test_snippet,
+                                                          form=form)
 
         self.snippet_chooser_panel = [
             panel for panel in edit_handler.children
             if getattr(panel, 'field_name', None) == 'advert'][0]
 
     def test_create_snippet_chooser_panel_class(self):
-        self.assertEqual(type(self.snippet_chooser_panel).__name__,
-                         '_SnippetChooserPanel')
+        self.assertIsInstance(self.snippet_chooser_panel, SnippetChooserPanel)
 
     def test_render_as_field(self):
         field_html = self.snippet_chooser_panel.render_as_field()
@@ -392,7 +392,8 @@ class TestSnippetChooserPanel(TestCase, WagtailTestUtils):
     def test_render_as_empty_field(self):
         test_snippet = SnippetChooserModel()
         form = self.form_class(instance=test_snippet)
-        edit_handler = self.edit_handler_class(instance=test_snippet, form=form)
+        edit_handler = self.edit_handler.bind_to_instance(instance=test_snippet,
+                                                          form=form)
 
         snippet_chooser_panel = [
             panel for panel in edit_handler.children
@@ -410,7 +411,7 @@ class TestSnippetChooserPanel(TestCase, WagtailTestUtils):
     def test_target_model_autodetected(self):
         result = SnippetChooserPanel(
             'advert'
-        ).bind_to_model(SnippetChooserModel).target_model()
+        ).bind_to_model(SnippetChooserModel).target_model
         self.assertEqual(result, Advert)
 
 
@@ -694,14 +695,14 @@ class TestDeleteOnlyPermissions(TestCase, WagtailTestUtils):
 
 class TestSnippetEditHandlers(TestCase, WagtailTestUtils):
     def test_standard_edit_handler(self):
-        edit_handler_class = get_snippet_edit_handler(StandardSnippet)
-        form_class = edit_handler_class.get_form_class(StandardSnippet)
+        edit_handler = get_snippet_edit_handler(StandardSnippet)
+        form_class = edit_handler.get_form_class()
         self.assertTrue(issubclass(form_class, WagtailAdminModelForm))
         self.assertFalse(issubclass(form_class, FancySnippetForm))
 
     def test_fancy_edit_handler(self):
-        edit_handler_class = get_snippet_edit_handler(FancySnippet)
-        form_class = edit_handler_class.get_form_class(FancySnippet)
+        edit_handler = get_snippet_edit_handler(FancySnippet)
+        form_class = edit_handler.get_form_class()
         self.assertTrue(issubclass(form_class, WagtailAdminModelForm))
         self.assertTrue(issubclass(form_class, FancySnippetForm))
 
@@ -941,18 +942,17 @@ class TestSnippetChooserPanelWithCustomPrimaryKey(TestCase, WagtailTestUtils):
             )
         )
 
-        self.edit_handler_class = get_snippet_edit_handler(model)
-        self.form_class = self.edit_handler_class.get_form_class(model)
+        self.edit_handler = get_snippet_edit_handler(model)
+        self.form_class = self.edit_handler.get_form_class()
         form = self.form_class(instance=test_snippet)
-        edit_handler = self.edit_handler_class(instance=test_snippet, form=form)
+        edit_handler = self.edit_handler.bind_to_instance(instance=test_snippet, form=form)
 
         self.snippet_chooser_panel = [
             panel for panel in edit_handler.children
             if getattr(panel, 'field_name', None) == 'advertwithcustomprimarykey'][0]
 
     def test_create_snippet_chooser_panel_class(self):
-        self.assertEqual(type(self.snippet_chooser_panel).__name__,
-                         '_SnippetChooserPanel')
+        self.assertIsInstance(self.snippet_chooser_panel, SnippetChooserPanel)
 
     def test_render_as_field(self):
         field_html = self.snippet_chooser_panel.render_as_field()
@@ -963,7 +963,7 @@ class TestSnippetChooserPanelWithCustomPrimaryKey(TestCase, WagtailTestUtils):
     def test_render_as_empty_field(self):
         test_snippet = SnippetChooserModelWithCustomPrimaryKey()
         form = self.form_class(instance=test_snippet)
-        edit_handler = self.edit_handler_class(instance=test_snippet, form=form)
+        edit_handler = self.edit_handler.bind_to_instance(instance=test_snippet, form=form)
 
         snippet_chooser_panel = [
             panel for panel in edit_handler.children
@@ -981,12 +981,10 @@ class TestSnippetChooserPanelWithCustomPrimaryKey(TestCase, WagtailTestUtils):
     def test_target_model_autodetected(self):
         result = SnippetChooserPanel(
             'advertwithcustomprimarykey'
-        ).bind_to_model(SnippetChooserModelWithCustomPrimaryKey).target_model()
+        ).bind_to_model(SnippetChooserModelWithCustomPrimaryKey).target_model
         self.assertEqual(result, AdvertWithCustomPrimaryKey)
 
 
-
-
 class TestSnippetChooseWithCustomPrimaryKey(TestCase, WagtailTestUtils):
     fixtures = ['test.json']
 
@@ -1014,7 +1012,6 @@ class TestSnippetChooseWithCustomPrimaryKey(TestCase, WagtailTestUtils):
         self.assertEqual(response.context['items'][0].text, "advert 1")
 
 
-
 class TestSnippetChosenWithCustomPrimaryKey(TestCase, WagtailTestUtils):
     fixtures = ['test.json']
 

+ 12 - 8
wagtail/snippets/views/snippets.py

@@ -129,8 +129,8 @@ def create(request, app_label, model_name):
         return permission_denied(request)
 
     instance = model()
-    edit_handler_class = get_snippet_edit_handler(model)
-    form_class = edit_handler_class.get_form_class(model)
+    edit_handler = get_snippet_edit_handler(model)
+    form_class = edit_handler.get_form_class()
 
     if request.method == 'POST':
         form = form_class(request.POST, request.FILES, instance=instance)
@@ -153,10 +153,12 @@ def create(request, app_label, model_name):
             return redirect('wagtailsnippets:list', app_label, model_name)
         else:
             messages.error(request, _("The snippet could not be created due to errors."))
-            edit_handler = edit_handler_class(instance=instance, form=form)
+            edit_handler = edit_handler.bind_to_instance(instance=instance,
+                                                         form=form)
     else:
         form = form_class(instance=instance)
-        edit_handler = edit_handler_class(instance=instance, form=form)
+        edit_handler = edit_handler.bind_to_instance(instance=instance,
+                                                     form=form)
 
     return render(request, 'wagtailsnippets/snippets/create.html', {
         'model_opts': model._meta,
@@ -173,8 +175,8 @@ def edit(request, app_label, model_name, pk):
         return permission_denied(request)
 
     instance = get_object_or_404(model, pk=unquote(pk))
-    edit_handler_class = get_snippet_edit_handler(model)
-    form_class = edit_handler_class.get_form_class(model)
+    edit_handler = get_snippet_edit_handler(model)
+    form_class = edit_handler.get_form_class()
 
     if request.method == 'POST':
         form = form_class(request.POST, request.FILES, instance=instance)
@@ -197,10 +199,12 @@ def edit(request, app_label, model_name, pk):
             return redirect('wagtailsnippets:list', app_label, model_name)
         else:
             messages.error(request, _("The snippet could not be saved due to errors."))
-            edit_handler = edit_handler_class(instance=instance, form=form)
+            edit_handler = edit_handler.bind_to_instance(instance=instance,
+                                                         form=form)
     else:
         form = form_class(instance=instance)
-        edit_handler = edit_handler_class(instance=instance, form=form)
+        edit_handler = edit_handler.bind_to_instance(instance=instance,
+                                                     form=form)
 
     return render(request, 'wagtailsnippets/snippets/edit.html', {
         'model_opts': model._meta,