Repository: django-cms/django-cms

Issue:
Problem with nested plugins in version 3.0.0beta3
I am trying create nested plugins hierarchy. I created two simple plugins:

``` python
class TestPlugin(CMSPluginBase):

    name = _('Test')
    module = _('Test')
    render_template = 'some_existing_template.html'
    allow_children = True


class SubTestPlugin(CMSPluginBase):

    name = _('SubTest')
    module = _('Test')
    render_template = 'some_existing_template.html'
    allow_children = True
```

When I chose the `TestPlugin` and into it I add the `SubTestPlugin` I can't add another one in the `SubTestPlugin`. Menu icon in the `SubTestPlugin` is not visable.

![cms](https://f.cloud.github.com/assets/1870738/1929037/54a7dbe8-7e90-11e3-89b6-3ed4997c03ba.png)

When I try drag the `SupTestPlugin` UI broke up and JS error occurs: **Uncaught TypeError: Cannot read property 'plugin_type' of null**:

![drag](https://f.cloud.github.com/assets/1870738/1929055/b9087e3a-7e90-11e3-9f0d-96463d7cf41a.png)

Another strange thing occurs when I double click on the `SubTestPlugin`, after that action popup with `TestPlugin` settings appears. 



Discussion:
The drag issue is documented in #2410 

what is in your render_template?


Resolving patch:
diff --git a/cms/plugin_pool.py b/cms/plugin_pool.py
--- a/cms/plugin_pool.py
+++ b/cms/plugin_pool.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
-
 from django.db.models import signals
+from django.template import TemplateDoesNotExist
 import warnings
 from cms.exceptions import PluginAlreadyRegistered, PluginNotRegistered
 from cms.plugin_base import CMSPluginBase
@@ -42,6 +42,41 @@ def register_plugin(self, plugin):
                 "CMS Plugins must be subclasses of CMSPluginBase, %r is not."
                 % plugin
             )
+        if plugin.render_plugin:
+            if not plugin.render_template and not hasattr(plugin.model, 'render_template'):
+                raise ImproperlyConfigured(
+                    "CMS Plugins must define a render template or set render_plugin=False: %s"
+                    % plugin
+                )
+            else:
+                from django.template import loader
+
+                template = hasattr(plugin.model,
+                                   'render_template') and plugin.model.render_template or plugin.render_template
+                try:
+                    t = loader.get_template(plugin.render_template)
+                except TemplateDoesNotExist:
+                    raise ImproperlyConfigured(
+                        "CMS Plugins must define a render template (%s) that exist: %s"
+                        % (plugin, template)
+                    )
+                if plugin.allow_children:
+                    found = False
+                    from cms.templatetags.cms_tags import RenderPlugin
+                    for node in t:
+                        if isinstance(node, RenderPlugin):
+                            found = True
+                    if not found:
+                        raise ImproperlyConfigured(
+                            "CMS Plugins that define allow_children=True must render the children correctly with {%% render_plugin %%}: %s"
+                            % (plugin)
+                        )
+        else:
+            if plugin.allow_children:
+                raise ImproperlyConfigured(
+                    "CMS Plugins can not define render_plugin=False and allow_children=True: %s"
+                    % plugin
+                )
         plugin_name = plugin.__name__
         if plugin_name in self.plugins:
             raise PluginAlreadyRegistered(


Test patch:
diff --git a/cms/test_utils/project/placeholderapp/cms_plugins.py b/cms/test_utils/project/placeholderapp/cms_plugins.py
--- a/cms/test_utils/project/placeholderapp/cms_plugins.py
+++ b/cms/test_utils/project/placeholderapp/cms_plugins.py
@@ -6,6 +6,7 @@
 class EmptyPlugin(CMSPluginBase):
     name = _("Test Plugin")
     text_enabled = True
+    render_template = 'cms/content.html'
 
     def render(self, context, instance, placeholder):
         return context
diff --git a/cms/test_utils/project/pluginapp/plugins/meta/cms_plugins.py b/cms/test_utils/project/pluginapp/plugins/meta/cms_plugins.py
--- a/cms/test_utils/project/pluginapp/plugins/meta/cms_plugins.py
+++ b/cms/test_utils/project/pluginapp/plugins/meta/cms_plugins.py
@@ -6,24 +6,29 @@
 
 class TestPlugin(CMSPluginBase):
     model = TestPluginModel
+    render_template = 'cms/content.html'
 plugin_pool.register_plugin(TestPlugin)
 
 
 class TestPlugin2(CMSPluginBase):
     model = TestPluginModel2
+    render_template = 'cms/content.html'
 plugin_pool.register_plugin(TestPlugin2)
 
 
 class TestPlugin3(CMSPluginBase):
     model = TestPluginModel3
+    render_template = 'cms/content.html'
 plugin_pool.register_plugin(TestPlugin3)
 
 
 class TestPlugin4(CMSPluginBase):
     model = TestPluginModel4
+    render_template = 'cms/content.html'
 plugin_pool.register_plugin(TestPlugin4)
 
 
 class TestPlugin5(CMSPluginBase):
     model = TestPluginModel5
+    render_template = 'cms/content.html'
 plugin_pool.register_plugin(TestPlugin5)
diff --git a/cms/test_utils/project/pluginapp/plugins/validation/__init__.py b/cms/test_utils/project/pluginapp/plugins/validation/__init__.py
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/pluginapp/plugins/validation/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/cms/test_utils/project/pluginapp/plugins/validation/cms_plugins.py b/cms/test_utils/project/pluginapp/plugins/validation/cms_plugins.py
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/pluginapp/plugins/validation/cms_plugins.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+from cms.plugin_base import CMSPluginBase
+
+
+class NonExisitngRenderTemplate(CMSPluginBase):
+    name = 'SubTest'
+    module = 'Test'
+    render_template = 'i_do_not_exist.html'
+    allow_children = True
+
+
+class NoSubPluginRender(CMSPluginBase):
+    name = 'SubSubTest'
+    module = 'Test'
+    render_template = 'plugins/sub_empty_plugin.html'
+    allow_children = True
+
+
+class NoRender(CMSPluginBase):
+    name = 'norender'
+
+
+class NoRenderButChildren(CMSPluginBase):
+    name = 'norender2'
+    render_plugin = False
+    allow_children = True
\ No newline at end of file
diff --git a/cms/test_utils/project/pluginapp/plugins/validation/models.py b/cms/test_utils/project/pluginapp/plugins/validation/models.py
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/pluginapp/plugins/validation/models.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/cms/test_utils/project/templates/allow_children_plugin.html b/cms/test_utils/project/templates/allow_children_plugin.html
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/templates/allow_children_plugin.html
@@ -0,0 +1,6 @@
+{% load cms_tags %}
+<div class="column" style="width: {{ instance.width }}; float: left;">
+{% for plugin in instance.child_plugin_instances %}
+    	{% render_plugin plugin %}
+{% endfor %}
+</div>
\ No newline at end of file
diff --git a/cms/test_utils/project/templates/plugins/sub_empty_plugin.html b/cms/test_utils/project/templates/plugins/sub_empty_plugin.html
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/templates/plugins/sub_empty_plugin.html
@@ -0,0 +1,3 @@
+{% load cms_tags %}
+{{ instance }}
+
diff --git a/cms/test_utils/project/templates/plugins/sub_plugin.html b/cms/test_utils/project/templates/plugins/sub_plugin.html
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/templates/plugins/sub_plugin.html
@@ -0,0 +1,5 @@
+{% load cms_tags %}
+{{ instance }}
+{% for plugin in instance.child_plugin_instances %}
+    	{% render_plugin plugin %}
+{% endfor %}
diff --git a/cms/tests/plugins.py b/cms/tests/plugins.py
--- a/cms/tests/plugins.py
+++ b/cms/tests/plugins.py
@@ -11,6 +11,7 @@
 from cms.models.pluginmodel import CMSPlugin, PluginModelBase
 from cms.plugin_base import CMSPluginBase
 from cms.plugin_pool import plugin_pool
+from cms.test_utils.project.pluginapp.plugins.validation.cms_plugins import NonExisitngRenderTemplate, NoSubPluginRender, NoRender, NoRenderButChildren
 from djangocms_googlemap.models import GoogleMap
 from djangocms_inherit.cms_plugins import InheritPagePlaceholderPlugin
 from cms.utils.plugins import get_plugins_for_page
@@ -35,7 +36,7 @@
 from django.contrib import admin
 from django.contrib.auth.models import User
 from django.core import urlresolvers
-from django.core.exceptions import ValidationError
+from django.core.exceptions import ValidationError, ImproperlyConfigured
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.core.management import call_command
 from django.forms.widgets import Media
@@ -48,7 +49,7 @@ class DumbFixturePlugin(CMSPluginBase):
     name = "Dumb Test Plugin. It does nothing."
     render_template = ""
     admin_preview = False
-    allow_children = True
+    render_plugin = False
 
     def render(self, context, instance, placeholder):
         return context
@@ -56,6 +57,7 @@ def render(self, context, instance, placeholder):
 
 class DumbFixturePluginWithUrls(DumbFixturePlugin):
     name = DumbFixturePlugin.name + " With custom URLs."
+    render_plugin = False
 
     def _test_view(self, request):
         return http.HttpResponse("It works")
@@ -420,6 +422,12 @@ def test_deep_copy_plugins(self):
         mcol1 = self.reload(mcol1)
         self.assertEquals(mcol1.get_descendants().count(), 2)
 
+    def test_plugin_validation(self):
+        self.assertRaises(ImproperlyConfigured, plugin_pool.register_plugin, NoSubPluginRender)
+        self.assertRaises(ImproperlyConfigured, plugin_pool.register_plugin, NonExisitngRenderTemplate)
+        self.assertRaises(ImproperlyConfigured, plugin_pool.register_plugin, NoRender)
+        self.assertRaises(ImproperlyConfigured, plugin_pool.register_plugin, NoRenderButChildren)
+
 
 
     def test_remove_plugin_before_published(self):
@@ -858,8 +866,8 @@ def test_plugin_move_with_reload(self):
                 'requires_reload': False
             },
         }
-        ReloadDrivenPlugin = type('ReloadDrivenPlugin', (CMSPluginBase,), dict(action_options=action_options))
-        NonReloadDrivenPlugin = type('NonReloadDrivenPlugin', (CMSPluginBase,), dict(action_options=non_reload_action_options))
+        ReloadDrivenPlugin = type('ReloadDrivenPlugin', (CMSPluginBase,), dict(action_options=action_options, render_plugin=False))
+        NonReloadDrivenPlugin = type('NonReloadDrivenPlugin', (CMSPluginBase,), dict(action_options=non_reload_action_options, render_plugin=False))
         plugin_pool.register_plugin(ReloadDrivenPlugin)
         plugin_pool.register_plugin(NonReloadDrivenPlugin)
         page = create_page("page", "nav_playground.html", "en", published=True)
@@ -910,8 +918,8 @@ def test_plugin_copy_with_reload(self):
                 'requires_reload': False
             },
         }
-        ReloadDrivenPlugin = type('ReloadDrivenPlugin', (CMSPluginBase,), dict(action_options=action_options))
-        NonReloadDrivenPlugin = type('NonReloadDrivenPlugin', (CMSPluginBase,), dict(action_options=non_reload_action_options))
+        ReloadDrivenPlugin = type('ReloadDrivenPlugin', (CMSPluginBase,), dict(action_options=action_options, render_plugin=False))
+        NonReloadDrivenPlugin = type('NonReloadDrivenPlugin', (CMSPluginBase,), dict(action_options=non_reload_action_options, render_plugin=False))
         plugin_pool.register_plugin(ReloadDrivenPlugin)
         plugin_pool.register_plugin(NonReloadDrivenPlugin)
         page = create_page("page", "nav_playground.html", "en", published=True)
@@ -961,7 +969,7 @@ def test_plugin_require_parent(self):
         in the plugin pool when a placeholder is specified
         """
         ParentRequiredPlugin = type('ParentRequiredPlugin', (CMSPluginBase,),
-                                    dict(require_parent=True))
+                                    dict(require_parent=True, render_plugin=False))
         plugin_pool.register_plugin(ParentRequiredPlugin)
         page = create_page("page", "nav_playground.html", "en", published=True)
         placeholder = page.placeholders.get(slot='body')
@@ -976,9 +984,9 @@ def test_plugin_parent_classes(self):
         toolbar plugin struct for those given parent Plugins
         """
         ParentClassesPlugin = type('ParentClassesPlugin', (CMSPluginBase,),
-                                    dict(parent_classes=['GenericParentPlugin']))
-        GenericParentPlugin = type('GenericParentPlugin', (CMSPluginBase,), {})
-        KidnapperPlugin = type('KidnapperPlugin', (CMSPluginBase,), {})
+                                    dict(parent_classes=['GenericParentPlugin'], render_plugin=False))
+        GenericParentPlugin = type('GenericParentPlugin', (CMSPluginBase,), {'render_plugin':False})
+        KidnapperPlugin = type('KidnapperPlugin', (CMSPluginBase,), {'render_plugin':False})
 
         expected_struct = {'module': u'Generic',
                             'name': u'Parent Classes Plugin',
@@ -1010,7 +1018,7 @@ def test_plugin_child_classes_from_settings(self):
         page = create_page("page", "nav_playground.html", "en", published=True)
         placeholder = page.placeholders.get(slot='body')
         ChildClassesPlugin = type('ChildClassesPlugin', (CMSPluginBase,),
-                                    dict(child_classes=['TextPlugin']))
+                                    dict(child_classes=['TextPlugin'], render_template='allow_children_plugin.html'))
         plugin_pool.register_plugin(ChildClassesPlugin)
         plugin = add_plugin(placeholder, ChildClassesPlugin, settings.LANGUAGES[0][0])
         plugin = plugin.get_plugin_class_instance()
@@ -1033,7 +1041,7 @@ def test_plugin_parent_classes_from_settings(self):
         page = create_page("page", "nav_playground.html", "en", published=True)
         placeholder = page.placeholders.get(slot='body')
         ParentClassesPlugin = type('ParentClassesPlugin', (CMSPluginBase,),
-                                    dict(parent_classes=['TextPlugin']))
+                                    dict(parent_classes=['TextPlugin'], render_plugin=False))
         plugin_pool.register_plugin(ParentClassesPlugin)
         plugin = add_plugin(placeholder, ParentClassesPlugin, settings.LANGUAGES[0][0])
         plugin = plugin.get_plugin_class_instance()