Repository: django-cms/django-cms

Issue:
Same instance of _render_meta for all plugins instances
I have discovered that that in trunk _render_meta is same instance for all plugins - id(instance._render_meta) - gives same address for any plugin instnce.

This make text_enabled =True for all plugins after several requests.



Discussion:

Resolving patch:
diff --git a/cms/models/pluginmodel.py b/cms/models/pluginmodel.py
--- a/cms/models/pluginmodel.py
+++ b/cms/models/pluginmodel.py
@@ -19,29 +19,43 @@
 from mptt.models import MPTTModel, MPTTModelBase
 
 
+class BoundRenderMeta(object):
+    def __init__(self, meta):
+        self.index = 0
+        self.total = 1
+        self.text_enabled = getattr(meta, 'text_enabled', False)
+
+
 class PluginModelBase(MPTTModelBase):
     """
-    Metaclass for all plugins.
+    Metaclass for all CMSPlugin subclasses. This class should not be used for
+    any other type of models.
     """
     def __new__(cls, name, bases, attrs):
-        render_meta = attrs.pop('RenderMeta', None)
-        if render_meta is not None:
-            attrs['_render_meta'] = render_meta()
+        # remove RenderMeta from the plugin class
+        attr_meta = attrs.pop('RenderMeta', None)
+
+        # create a new class (using the super-metaclass)
         new_class = super(PluginModelBase, cls).__new__(cls, name, bases, attrs)
-        found = False
-        bbases = bases
-        while bbases:
-            bcls = bbases[0]
-            if bcls.__name__ == "CMSPlugin":
-                found = True
-                bbases = False
-            else:
-                bbases = bcls.__bases__  
-        if found:
-            if new_class._meta.db_table.startswith("%s_" % new_class._meta.app_label):
-                table = "cmsplugin_" + new_class._meta.db_table.split("%s_" % new_class._meta.app_label, 1)[1]
-                new_class._meta.db_table = table
-        return new_class 
+        
+        # if there is a RenderMeta in attrs, use this one
+        if attr_meta:
+            meta = attr_meta
+        else:
+            # else try to use the one from the superclass (if present)
+            meta = getattr(new_class, '_render_meta', None)
+        
+        # set a new BoundRenderMeta to prevent leaking of state
+        new_class._render_meta = BoundRenderMeta(meta)
+
+        # turn 'myapp_mymodel' into 'cmsplugin_mymodel' by removing the
+        # 'myapp_' bit from the db_table name.
+        splitter = '%s_' % new_class._meta.app_label
+        splitted = new_class._meta.db_table.split(splitter, 1)
+        table_name = 'cmsplugin_%s' % splitted[1]
+        new_class._meta.db_table = table_name
+        
+        return new_class
          
     
 class CMSPlugin(MPTTModel):


Test patch:
diff --git a/cms/tests/plugins.py b/cms/tests/plugins.py
--- a/cms/tests/plugins.py
+++ b/cms/tests/plugins.py
@@ -4,18 +4,19 @@
 from cms.conf.patch import post_patch_check
 from cms.exceptions import PluginAlreadyRegistered, PluginNotRegistered
 from cms.models import Page, Placeholder
-from cms.models.pluginmodel import CMSPlugin
+from cms.models.pluginmodel import CMSPlugin, PluginModelBase
 from cms.plugin_base import CMSPluginBase
 from cms.plugin_pool import plugin_pool
 from cms.plugins.file.models import File
 from cms.plugins.inherit.models import InheritPagePlaceholder
 from cms.plugins.link.forms import LinkForm
+from cms.plugins.link.models import Link
 from cms.plugins.text.models import Text
 from cms.plugins.text.utils import (plugin_tags_to_id_list, 
     plugin_tags_to_admin_html)
 from cms.plugins.twitter.models import TwitterRecentEntries
-from cms.test_utils.testcases import (CMSTestCase, URL_CMS_PAGE, URL_CMS_PAGE_ADD, 
-    URL_CMS_PLUGIN_ADD, URL_CMS_PLUGIN_EDIT, URL_CMS_PAGE_CHANGE, 
+from cms.test_utils.testcases import (CMSTestCase, URL_CMS_PAGE, 
+    URL_CMS_PAGE_ADD, URL_CMS_PLUGIN_ADD, URL_CMS_PLUGIN_EDIT, URL_CMS_PAGE_CHANGE, 
     URL_CMS_PLUGIN_REMOVE)
 from cms.test_utils.util.context_managers import SettingsOverride
 from cms.utils.copy_plugins import copy_plugins_to
@@ -73,8 +74,6 @@ def get_request(self, *args, **kwargs):
 
 
 class PluginsTestCase(PluginsTestBaseCase):
-
-
     def test_add_edit_plugin(self):
         """
         Test that you can add a text plugin
@@ -620,3 +619,37 @@ def test_does_not_verify_existance_of_url(self):
         form = LinkForm(
             {'name': 'Linkname', 'url': 'http://www.nonexistant.test'})
         self.assertEquals(form.is_valid(), True)
+
+
+class NoDatabasePluginTests(TestCase):
+    def test_render_meta_is_unique(self):
+        text = Text()
+        link = Link()
+        self.assertNotEqual(id(text._render_meta), id(link._render_meta))
+    
+    def test_render_meta_does_not_leak(self):
+        text = Text()
+        link = Link()
+        
+        text._render_meta.text_enabled = False
+        link._render_meta.text_enabled = False
+        
+        self.assertFalse(text._render_meta.text_enabled)
+        self.assertFalse(link._render_meta.text_enabled)
+        
+        link._render_meta.text_enabled = True
+
+        self.assertFalse(text._render_meta.text_enabled)
+        self.assertTrue(link._render_meta.text_enabled)
+    
+    def test_db_table_hack(self):
+        # TODO: Django tests seem to leak models from test methods, somehow
+        # we should clear django.db.models.loading.app_cache in tearDown.
+        plugin_class = PluginModelBase('TestPlugin', (CMSPlugin,), {'__module__': 'cms.tests.plugins'})
+        self.assertEqual(plugin_class._meta.db_table, 'cmsplugin_testplugin')
+    
+    def test_db_table_hack_with_mixin(self):
+        class LeftMixin: pass
+        class RightMixin: pass
+        plugin_class = PluginModelBase('TestPlugin2', (LeftMixin, CMSPlugin, RightMixin), {'__module__': 'cms.tests.plugins'})
+        self.assertEqual(plugin_class._meta.db_table, 'cmsplugin_testplugin2')