Repository: django-cms/django-cms

Issue:
nested plugin without configuration lost on publish
I'm using cms 8ee0e4af50, djangocms-text-ckeditor 2.1, Django 1.6.1, postgres.

I have a ckeditor text plugin with a custom nested plugin that doesn't have configuration (doesn't have a model, uses CMSPlugin as model). On publish that plugin vanishes.

Edit: note that nested plugins with a model defined, such as Link, do get published



Discussion:
Any chance you can post the plugin in question here?

@ojii I'm running that build here with (Django 1.6.1 and Python 2.7.x) and I have multiple instances of configurationless plugins (no custom model) which work just fine.

Just a thought, can you double-check that your configurationless plugin's context contains 'placeholder'?  That's a requirement, I believe for text-enabled plugins.

The plugins image tag gets removed from the text plugin body on publish. so this is probably a djangocms-text-ckeditor issue more so than CMS.  The plugin renders fine in draft mode, but on hitting publish it vanishes.

Plugin code:

``` python

class NextMoviePlugin(CMSPluginBase):
    name = 'Next Movie Plugin'
    render_template = 'etoile/next_movie.html'
    text_enabled = True

    def render(self, context, instance, placeholder):
        try:
            context['movie'] = Movie.objects.filter(show_date__gt=datetime.now()).order_by('show_date')[0]
        except IndexError:
            context['movie'] = None
        return context

plugin_pool.register_plugin(NextMoviePlugin)
```

The DB query does return an object in the live database.

Just a thought, Ojii, do you have any Plugin Processors defined for `settings.CMS_PLUGIN_PROCESSORS`?

Nope.

Can you really not reproduce this in text-ckeditor? Seems odd that this would only happen to this plugin as the plugin does remarkably little. Again I yearn for a test suite for text-ckeditor where I could prove this bug...

Actually, I **can** reproduce this. Exactly as you say. If the plugin is used _inside_ another plugin **and** it doesn't have its own model, then it renders in "content" mode, but not visible when you publish. The solution appears to be to create a model for it (I made one containing a single field), then it works as expected.

I've also tried the plugin as a plugin-in-a-plugin using django-grid, and it renders fine, so, its doesn't appear to be related to being insider another plugin. I don't know of another plugin that would qualify as the _host_ for a text_enabled plugin, so I couldn't run a test to definitely say the issue is on the side of ckeditor or, just django-cms’ implementation of text-enabled.

So, to be clear, this is a real, reproducible issue.


Resolving patch:
diff --git a/cms/models/pluginmodel.py b/cms/models/pluginmodel.py
--- a/cms/models/pluginmodel.py
+++ b/cms/models/pluginmodel.py
@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
 from datetime import date
-import os
-import warnings
 import json
 
+import os
+import warnings
 from cms.exceptions import DontUsePageAttributeWarning
 from cms.models.placeholdermodel import Placeholder
 from cms.plugin_rendering import PluginContext, render_plugin
@@ -186,6 +186,7 @@ def render_plugin(self, context=None, placeholder=None, admin=False, processors=
             return render_plugin(context, instance, placeholder, template, processors, context.current_app)
         else:
             from cms.middleware.toolbar import toolbar_plugin_processor
+
             if processors and toolbar_plugin_processor in processors:
                 if not placeholder:
                     placeholder = self.placeholder
@@ -274,6 +275,9 @@ def copy_plugin(self, target_placeholder, target_language, parent_cache):
         new_plugin.position = self.position
         new_plugin.save()
         if plugin_instance:
+            if plugin_instance.__class__ == CMSPlugin:
+                #get a new instance so references do not get mixed up
+                plugin_instance = CMSPlugin.objects.get(pk=plugin_instance.pk)
             plugin_instance.pk = new_plugin.pk
             plugin_instance.id = new_plugin.pk
             plugin_instance.placeholder = target_placeholder
@@ -284,7 +288,8 @@ def copy_plugin(self, target_placeholder, target_language, parent_cache):
             plugin_instance.cmsplugin_ptr = new_plugin
             plugin_instance.language = target_language
             plugin_instance.parent = new_plugin.parent
-            plugin_instance.position = new_plugin.position  # added to retain the position when creating a public copy of a plugin
+            # added to retain the position when creating a public copy of a plugin
+            plugin_instance.position = new_plugin.position
             plugin_instance.save()
             old_instance = plugin_instance.__class__.objects.get(pk=self.pk)
             plugin_instance.copy_relations(old_instance)
@@ -392,7 +397,7 @@ def get_translatable_content(self):
         fields = []
         for field in self._meta.fields:
             if ((isinstance(field, models.CharField) or isinstance(field, models.TextField)) and not field.choices and
-                    field.editable and field.name not in self.translatable_content_excluded_fields and field):
+                field.editable and field.name not in self.translatable_content_excluded_fields and field):
                 fields.append(field)
 
         translatable_fields = {}
@@ -416,6 +421,7 @@ def set_translatable_content(self, fields):
 
         return True
 
+
 reversion_register(CMSPlugin)
 
 
diff --git a/cms/utils/copy_plugins.py b/cms/utils/copy_plugins.py
--- a/cms/utils/copy_plugins.py
+++ b/cms/utils/copy_plugins.py
@@ -18,6 +18,7 @@ def copy_plugins_to(plugin_list, to_placeholder, to_language=None, parent_plugin
         else:
             plugin_language = old_plugin.language
             # do the simple copying
+
         new_plugin = old_plugin.copy_plugin(to_placeholder, plugin_language, old_parent_cache)
         if first:
             first = False


Test patch:
diff --git a/cms/test_utils/cli.py b/cms/test_utils/cli.py
--- a/cms/test_utils/cli.py
+++ b/cms/test_utils/cli.py
@@ -100,6 +100,7 @@ def configure(db_url, **extra):
             'cms.test_utils.project.pluginapp.plugins.extra_context',
             'cms.test_utils.project.pluginapp.plugins.meta',
             'cms.test_utils.project.pluginapp.plugins.one_thing',
+
             'cms.test_utils.project.fakemlng',
             'cms.test_utils.project.fileapp',
             'cms.test_utils.project.objectpermissionsapp',
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
@@ -1,12 +1,12 @@
 from cms.plugin_pool import plugin_pool
 from cms.plugin_base import CMSPluginBase
-from django.utils.translation import ugettext_lazy as _
 from django.conf import settings
 
+
 class EmptyPlugin(CMSPluginBase):
-    name = _("Test Plugin")
+    name = "Empty Plugin"
     text_enabled = True
-    render_template = 'cms/content.html'
+    render_plugin = False
 
     def render(self, context, instance, placeholder):
         return context
@@ -14,4 +14,5 @@ def render(self, context, instance, placeholder):
     def icon_src(self, instance):
         return settings.STATIC_URL + u"cms/img/icons/plugins/image.png"
 
+
 plugin_pool.register_plugin(EmptyPlugin)
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.placeholderapp.cms_plugins import EmptyPlugin
 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
@@ -423,7 +424,6 @@ def test_deep_copy_plugins(self):
         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)
@@ -689,15 +689,12 @@ def test_copy_textplugin(self):
         plugin.save()
 
         plugin_ref_1_base = CMSPlugin(
-            plugin_type='TextPlugin',
+            plugin_type='EmptyPlugin',
             placeholder=placeholder,
             position=0,
             language=self.FIRST_LANG)
         plugin_ref_1_base.insert_at(plugin_base, position='last-child', save=False)
-
-        plugin_ref_1 = Text(body='')
-        plugin_ref_1_base.set_base_attr(plugin_ref_1)
-        plugin_ref_1.save()
+        plugin_ref_1_base.save()
 
         plugin_ref_2_base = CMSPlugin(
             plugin_type='TextPlugin',
@@ -712,7 +709,7 @@ def test_copy_textplugin(self):
         plugin_ref_2.save()
 
         plugin.body = ' <img id="plugin_obj_%s" src=""/><img id="plugin_obj_%s" src=""/>' % (
-            str(plugin_ref_1.pk), str(plugin_ref_2.pk))
+            str(plugin_ref_1_base.pk), str(plugin_ref_2.pk))
         plugin.save()
 
         page_data = self.get_new_page_data()
@@ -743,8 +740,8 @@ def test_copy_textplugin(self):
         self.assertEquals(CMSPlugin.objects.filter(language=self.FIRST_LANG).count(), 3)
         self.assertEquals(CMSPlugin.objects.filter(language=self.SECOND_LANG).count(), 3)
         self.assertEquals(CMSPlugin.objects.count(), 6)
-        plugins = list(Text.objects.all())
-        new_plugin = plugins[3]
+        plugins = list(CMSPlugin.objects.all())
+        new_plugin = plugins[3].get_plugin_instance()[0]
         idlist = sorted(plugin_tags_to_id_list(new_plugin.body))
         expected = sorted([plugins[4].pk, plugins[5].pk])
         self.assertEquals(idlist, expected)