Repository: wagtail/wagtail

Issue:
Fields added to the model but not allocated to a panel appear outside the tab interface
When fields are added to the model but not allocated to a panel (e.g not added to `BlogPage.content_panels`), they're displayed without label or js enhancement at the bottom of the page outside the tabbed interface. It's an ugliness I'd like to be tackled if possible. 

Perhaps the new field(s) should appear in the first tab, "Content", or they shouldn't appear at all and perhaps display a warning.



Discussion:
If this functionality is removed, a hidden panel should be added.

Right now, the only way I've been able to hide the slug from the user (for a page that autogenerates the slug from another model) is by monkey patching the WagtailAdminModelForm like so:

```
#
#    HACK -- MONKEY PATCH TO HIDE THE HIDDEN FIELDS THAT CANNOT BE EXCLUDED
#
from wagtail.wagtailadmin.edit_handlers import WagtailAdminModelForm

WagtailAdminModelFormInit = WagtailAdminModelForm.__init__

def PatchedWagtailAdminModelFormInit(self, *args, **kwargs):
    WagtailAdminModelFormInit(self, *args, **kwargs)

    if isinstance(self.instance, FooPage):
        for field_name in HIDDEN_FIELDS:
            self.fields[field_name].required = False
            self.fields[field_name].widget = forms.HiddenInput()

WagtailAdminModelForm.__init__ = PatchedWagtailAdminModelFormInit
#
#    END HACK
#
```

This works since the panel outputs these hidden fields.  Granted it is awful...


Resolving patch:
diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py
--- a/wagtail/wagtailadmin/edit_handlers.py
+++ b/wagtail/wagtailadmin/edit_handlers.py
@@ -149,12 +149,18 @@ class EditHandler(object):
     def widget_overrides(cls):
         return {}
 
-    # return list of formset names that this EditHandler requires to be present
-    # as children of the ClusterForm
+    # return list of fields that this EditHandler expects to find on the form
     @classmethod
-    def required_formsets(cls):
+    def required_fields(cls):
         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):
+        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
@@ -164,6 +170,7 @@ 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
 
@@ -217,19 +224,16 @@ def render_as_field(self):
         # by default, assume that the subclass provides a catch-all render() method
         return self.render()
 
-    def rendered_fields(self):
-        """
-        return a list of the fields of the passed form which are rendered by this
-        EditHandler.
-        """
-        return []
-
     def render_missing_fields(self):
         """
-        Helper function: render all of the fields of the form that are not accounted for
-        in rendered_fields
+        Helper function: render all of the fields that are defined on the form but not "claimed" by
+        any panels via required_fields. These fields are most likely to be hidden fields introduced
+        by the forms framework itself, such as ORDER / DELETE fields on formset members.
+
+        (If they aren't actually hidden fields, then they will appear as ugly unstyled / label-less fields
+        outside of the panel furniture. But there's not much we can do about that.)
         """
-        rendered_fields = self.rendered_fields()
+        rendered_fields = self.required_fields()
         missing_fields_html = [
             text_type(self.form[field_name])
             for field_name in self.form.fields
@@ -240,8 +244,8 @@ def render_missing_fields(self):
 
     def render_form_content(self):
         """
-        Render this as an 'object', along with any unaccounted-for fields to make this
-        a valid submittable form
+        Render this as an 'object', ensuring that all fields necessary for a valid form
+        submission are included
         """
         return mark_safe(self.render_as_object() + self.render_missing_fields())
 
@@ -264,14 +268,26 @@ def widget_overrides(cls):
 
         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 = []
+            formsets = {}
             for handler_class in cls.children:
-                formsets.extend(handler_class.required_formsets())
+                formsets.update(handler_class.required_formsets())
             cls._required_formsets = formsets
 
         return cls._required_formsets
@@ -289,13 +305,6 @@ def render(self):
             'self': self
         }))
 
-    def rendered_fields(self):
-        result = []
-        for handler in self.children:
-            result += handler.rendered_fields()
-
-        return result
-
 
 class BaseTabbedInterface(BaseCompositeEditHandler):
     template = "wagtailadmin/edit_handlers/tabbed_interface.html"
@@ -398,7 +407,8 @@ def render_as_field(self, show_help_text=True, extra_context={}):
         context.update(extra_context)
         return mark_safe(render_to_string(self.field_template, context))
 
-    def rendered_fields(self):
+    @classmethod
+    def required_fields(self):
         return [self.field_name]
 
 
@@ -526,15 +536,13 @@ def get_child_edit_handler_class(cls):
 
     @classmethod
     def required_formsets(cls):
-        return [cls.relation_name]
-
-    @classmethod
-    def widget_overrides(cls):
-        overrides = cls.get_child_edit_handler_class().widget_overrides()
-        if overrides:
-            return {cls.relation_name: overrides}
-        else:
-            return {}
+        child_edit_handler_class = cls.get_child_edit_handler_class()
+        return {
+            cls.relation_name: {
+                'fields': child_edit_handler_class.required_fields(),
+                'widgets': child_edit_handler_class.widget_overrides(),
+            }
+        }
 
     def __init__(self, instance=None, form=None):
         super(BaseInlinePanel, self).__init__(instance=instance, form=form)


Test patch:
diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py
--- a/wagtail/tests/models.py
+++ b/wagtail/tests/models.py
@@ -495,6 +495,11 @@ class TaggedPageTag(TaggedItemBase):
 class TaggedPage(Page):
     tags = ClusterTaggableManager(through=TaggedPageTag, blank=True)
 
+TaggedPage.content_panels = [
+    FieldPanel('title', classname="full title"),
+    FieldPanel('tags'),
+]
+
 
 class PageChooserModel(models.Model):
     page = models.ForeignKey('wagtailcore.Page', help_text='help text')
diff --git a/wagtail/wagtailadmin/tests/test_edit_handlers.py b/wagtail/wagtailadmin/tests/test_edit_handlers.py
--- a/wagtail/wagtailadmin/tests/test_edit_handlers.py
+++ b/wagtail/wagtailadmin/tests/test_edit_handlers.py
@@ -171,18 +171,9 @@ def test_render(self):
         # this result should not include fields that are not covered by the panel definition
         self.assertNotIn('signup_link', result)
 
-    def test_rendered_fields(self):
-        EventPageForm = self.EventPageTabbedInterface.get_form_class(EventPage)
-        event = EventPage(title='Abergavenny sheepdog trials')
-        form = EventPageForm(instance=event)
-
-        tabbed_interface = self.EventPageTabbedInterface(
-            instance=event,
-            form=form
-        )
-
-        # rendered_fields should report the set of form fields rendered recursively as part of TabbedInterface
-        result = set(tabbed_interface.rendered_fields())
+    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())
         self.assertEqual(result, set(['title', 'date_from', 'date_to']))
 
     def test_render_form_content(self):
@@ -198,9 +189,9 @@ def test_render_form_content(self):
         result = tabbed_interface.render_form_content()
         # rendered output should contain field content as above
         self.assertIn('Abergavenny sheepdog trials</textarea>', result)
-        # rendered output should also contain all other fields that are in the form but not represented
+        # rendered output should NOT include fields that are in the model but not represented
         # in the panel definition
-        self.assertIn('signup_link', result)
+        self.assertNotIn('signup_link', result)
 
 
 class TestObjectList(TestCase):
@@ -308,15 +299,8 @@ def test_render_as_field(self):
         # there should be no errors on this field
         self.assertNotIn('<p class="error-message">', result)
 
-    def test_rendered_fields(self):
-        form = self.EventPageForm(
-            {'title': 'Pontypridd sheepdog trials', 'date_from': '2014-07-20', 'date_to': '2014-07-22'},
-            instance=self.event)
-        field_panel = self.EndDatePanel(
-            instance=self.event,
-            form=form
-        )
-        result = field_panel.rendered_fields()
+    def test_required_fields(self):
+        result = self.EndDatePanel.required_fields()
         self.assertEqual(result, ['date_to'])
 
     def test_error_message_is_rendered(self):
@@ -430,6 +414,7 @@ def test_render(self):
         result = panel.render_as_field()
 
         self.assertIn('<label for="id_speakers-0-first_name">Name:</label>', result)
+        self.assertIn('value="Father"', result)
         self.assertIn('<label for="id_speakers-0-last_name">Surname:</label>', result)
         self.assertIn('<label for="id_speakers-0-image">Image:</label>', result)
         self.assertIn('value="Choose an image"', result)
@@ -451,7 +436,7 @@ def test_render_with_panel_overrides(self):
         where one is specified
         """
         SpeakerInlinePanel = InlinePanel(EventPage, 'speakers', label="Speakers", panels=[
-            FieldPanel('first_name'),
+            FieldPanel('first_name', widget=forms.Textarea),
             ImageChooserPanel('image'),
         ])
         EventPageForm = SpeakerInlinePanel.get_form_class(EventPage)
@@ -466,12 +451,13 @@ def test_render_with_panel_overrides(self):
 
         result = panel.render_as_field()
 
+        # rendered panel should contain first_name rendered as a text area, but no last_name field
         self.assertIn('<label for="id_speakers-0-first_name">Name:</label>', result)
+        self.assertIn('Father</textarea>', result)
         self.assertNotIn('<label for="id_speakers-0-last_name">Surname:</label>', result)
 
-        # surname field is still rendered as a 'stray' label-less field: see #338.
-        # (Temporarily adding a test for this, so that we can verify that it fails when #338 is fixed...)
-        self.assertIn('<input id="id_speakers-0-last_name"', result)
+        # test for #338: surname field should not be rendered as a 'stray' label-less field
+        self.assertNotIn('<input id="id_speakers-0-last_name"', result)
 
         self.assertIn('<label for="id_speakers-0-image">Image:</label>', result)
         self.assertIn('value="Choose an image"', result)