Repository: django-cms/django-cms

Issue:
import of cms plugin model causes IndexError
Created cms based on instructions from the tutorial with a custom plugin.

When the model upon which the custom plugin is based is imported (ie "from cnt.models import Hello") the plugin model class gets imported a second time. During this second import, the **new** function tries to set the db_table name again, even though it's already been updated. This results in an IndexError.

~/dev/cmstest/../cmstest/urls.py in <module>
from cnt.models import Hello ...
▶ Local vars
~/dev/cmstest/../cmstest/cnt/models.py in <module>
class Hello(CMSPlugin): ...
▶ Local vars
~/dev/django-cms/cms/models/pluginmodel.py in **new**
            table_name = 'cmsplugin_%s' % splitted[1] ...
▶ Local vars



Discussion:
I have encountered this problem as well while testing out a Django-CMS 2.1 plugin in 2.2rc2.

Could you please provide a complete traceback (switch to the copy-past textarea, if using the Django debug traceback view), and pertinent settings, installed apps etc?

Were either of you following steps in the documentation that may've resulted in it, or is it a case of "should've worked, didn't"?

So, I was trying to reproduce this with a simpler version of the code that I am using on my site, but for whatever reason it worked fine in that test.  I can give you the traceback along with a few more details as to what is going on (I did a bit of quick and simple debugging) and a stupid fix I did to get my plugin working...

The traceback for me occurred when trying to do pretty much any manage.py command.  For example, in this traceback I was running a shell:

```
Traceback (most recent call last):
  File "manage.py", line 34, in <module>
    execute_manager(settings)
  File "/home/thalin/carnagetest/lib/python2.6/site-packages/django/core/management/__init__.py", line 438, in execute_manager
    utility.execute()
  File "/home/thalin/carnagetest/lib/python2.6/site-packages/django/core/management/__init__.py", line 379, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/thalin/carnagetest/lib/python2.6/site-packages/django/core/management/base.py", line 191, in run_from_argv
    self.execute(*args, **options.__dict__)
  File "/home/thalin/carnagetest/lib/python2.6/site-packages/django/core/management/base.py", line 220, in execute
    output = self.handle(*args, **options)
  File "/home/thalin/carnagetest/lib/python2.6/site-packages/django/core/management/base.py", line 351, in handle
    return self.handle_noargs(**options)
  File "/home/thalin/carnagetest/lib/python2.6/site-packages/django/core/management/commands/shell.py", line 47, in handle_noargs
    loaded_models = get_models()
  File "/home/thalin/carnagetest/lib/python2.6/site-packages/django/db/models/loading.py", line 167, in get_models
    self._populate()
  File "/home/thalin/carnagetest/lib/python2.6/site-packages/django/db/models/loading.py", line 61, in _populate
    self.load_app(app_name, True)
  File "/home/thalin/carnagetest/lib/python2.6/site-packages/django/db/models/loading.py", line 78, in load_app
    models = import_module('.models', app_name)
  File "/home/thalin/carnagetest/lib/python2.6/site-packages/django/utils/importlib.py", line 35, in import_module
   __import__(name)
  File "/home/thalin/carnagetest/carnage/riftguild/models.py", line 233, in <module>
    class NewsPlugin(CMSPlugin):
  File "/home/thalin/carnagetest/lib/python2.6/site-packages/cms/models/pluginmodel.py", line 58, in __new__
    table_name = 'cmsplugin_%s' % splitted[1]
IndexError: list index out of range
```

The problem here is that this is getting the plugin twice - once after already having changed db_table to be cmsplugin_newsplugin; this is where the problem is since the splitter is splitting on "riftguild_" and that string does not occur in the db_table string.

My naive fix was to change line 53 in `cms/models/pluginmodel.py` from

```
if [base for base in bases if isinstance(base, PluginModelBase)]:
```

to

```
if [base for base in bases if isinstance(base, PluginModelBase)] and \
    not new_class._meta.db_table.startswith('cmsplugin'):
```

That made everything happy.

For what it's worth, the failing case has the following INSTALLED_APPS:

`````` python
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.redirects',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.sitemaps',
    'django.contrib.staticfiles',
    'django.contrib.admindocs',
    'django.contrib.markup',
    'riftguild',
    'registration',
    'django_authopenid',
    'djangobb_forum',
    'haystack',
    'messages',
    'django_extensions',
    'debug_toolbar',
    'south',
    'cms',
    'menus',
    'mptt',
    'sekizai',
    'cms.plugins.text',
    'cms.plugins.picture',
    'cms.plugins.link',
    'cms.plugins.file',
    'cms.plugins.snippet',
    'cms.plugins.googlemap',
    'djangobb_cms',
    'django_tables2',
)```
``````

Can't reproduce, please provide a (unit-)test for this.

I am experiencing this too, and while I don't have a test case (no git clone to hand), I've stepped through, and I think I can see hints of the problem.

I believe it stems from the lines

```
splitter = '%s_' % new_class._meta.app_label
splitted = new_class._meta.db_table.split(splitter, 1)
table_name = 'cmsplugin_%s' % splitted[1]
```

expecting the app label, and the db_table, to relate to each other. 
By way of example:
- I have a news application, whose app label is appropriately enough, `news`
- this causes `splitter` to be **news_**
- I define the `db_table` to be `cmsplugin_recentarticlelist`, because I'm contrary.
- `splitted` then attempts to explode the db_table based on `news_`, which isn't in the string, thus the resultant list has a length of 1
- It then tries to force `table_name` to use the second index in the list, which doesn't exist.

Thinking on it, if that's something others can confirm/replicate, the fix might be as simple as

```
if splitter in new_class._meta.db_table:
    splitted = new_class._meta.db_table.split(splitter, 1)
    table_name = 'cmsplugin_%s' % splitted[1]
else:
    table_name = new_class._meta.db_table
```

to ensure the string is in there, but I'm just speculating on the possible variants, here.

I tried working on a test case, but the problem actually arises before the test runner gets to call the TestCase, and I'm not sure of the best answer to that. I can continue to work on the test case if I can solve it.

[Using this gist](https://gist.github.com/1225233) as an additional app in the test project's INSTALLED_APPS yields an IndexError on **NewPluginTestCustomisedDBTable** when the test runner is loading everything.

Today I installed version 2.2. and I got an error that was not in version 2.1

<pre>
Environment:

Django Version: 1.3
Python Version: 2.6.4
Installed Applications:
['django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.admin',
 'django.contrib.sites',
 'cms',
 'menus',
 'mptt',
 'south',
 'appmedia',
 'sekizai',
 'cms.plugins.text',
 'cms.plugins.picture',
 'cms.plugins.file',
 'cms.plugins.flash',
 'cms.plugins.link',
 'cms.plugins.snippet',
 'cms.plugins.googlemap',
 'cms.plugins.teaser',
 'cms.plugins.video',
 'cms.plugins.twitter',
 'cms.plugins.inherit',
 'roadcoating.plugins.filepath',
 'roadcoating.plugins.plaintext']
Installed Middleware:
('django.middleware.common.CommonMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'cms.middleware.multilingual.MultilingualURLMiddleware',
 'cms.middleware.page.CurrentPageMiddleware',
 'cms.middleware.user.CurrentUserMiddleware',
 'cms.middleware.toolbar.ToolbarMiddleware')


Traceback:
File "/usr/lib64/python2.6/site-packages/django/core/handlers/base.py" in get_response
  89.                     response = middleware_method(request)
File "/usr/lib64/python2.6/site-packages/cms/middleware/toolbar.py" in process_request
  33.         request.toolbar = CMSToolbar(request)
File "/usr/lib64/python2.6/site-packages/cms/cms_toolbar.py" in __init__
  61.         self.init()
File "/usr/lib64/python2.6/site-packages/cms/cms_toolbar.py" in init
  65.         self.can_change = (self.request.current_page and
File "/usr/lib64/python2.6/site-packages/cms/middleware/page.py" in __get__
  8.             request._current_page_cache = get_page_from_request(request)
File "/usr/lib64/python2.6/site-packages/cms/utils/page_resolver.py" in get_page_from_request
  91.     pages_root = urllib.unquote(reverse("pages-root"))
File "/usr/lib64/python2.6/site-packages/cms/models/__init__.py" in new_reverse
  51.             url = django.core.urlresolvers.old_reverse(viewname, urlconf=urlconf, args=args, kwargs=kwargs, prefix=prefix, current_app=current_app)
File "/usr/lib64/python2.6/site-packages/django/core/urlresolvers.py" in reverse
  391.             *args, **kwargs)))
File "/usr/lib64/python2.6/site-packages/django/core/urlresolvers.py" in reverse
  312.         possibilities = self.reverse_dict.getlist(lookup_view)
File "/usr/lib64/python2.6/site-packages/django/core/urlresolvers.py" in _get_reverse_dict
  229.             self._populate()
File "/usr/lib64/python2.6/site-packages/django/core/urlresolvers.py" in _populate
  197.         for pattern in reversed(self.url_patterns):
File "/usr/lib64/python2.6/site-packages/django/core/urlresolvers.py" in _get_url_patterns
  279.         patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
File "/usr/lib64/python2.6/site-packages/django/core/urlresolvers.py" in _get_urlconf_module
  274.             self._urlconf_module = import_module(self.urlconf_name)
File "/usr/lib64/python2.6/site-packages/django/utils/importlib.py" in import_module
  35.     __import__(name)
File "/srv/www/ypt/roadcoating/www/roadcoating/urls.py" in <module>
  6. admin.autodiscover()
File "/usr/lib64/python2.6/site-packages/django/contrib/admin/__init__.py" in autodiscover
  26.             import_module('%s.admin' % app)
File "/usr/lib64/python2.6/site-packages/django/utils/importlib.py" in import_module
  35.     __import__(name)
File "/usr/lib64/python2.6/site-packages/cms/admin/__init__.py" in <module>
  9. plugin_pool.plugin_pool.discover_plugins()
File "/usr/lib64/python2.6/site-packages/cms/plugin_pool.py" in discover_plugins
  20.         load('cms_plugins')
File "/usr/lib64/python2.6/site-packages/cms/utils/django_load.py" in load
  42.         get_module(app, modname, verbose, failfast)
File "/usr/lib64/python2.6/site-packages/cms/utils/django_load.py" in get_module
  21.         module = import_module(module_name)
File "/usr/lib64/python2.6/site-packages/django/utils/importlib.py" in import_module
  35.     __import__(name)
File "/srv/www/ypt/roadcoating/www/roadcoating/plugins/filepath/cms_plugins.py" in <module>
  3. from plugins.filepath.models import FilePathPlugin as FilePathPluginModel
File "/srv/www/ypt/roadcoating/www/roadcoating/plugins/filepath/models.py" in <module>
  27. class FilePathPlugin(CMSPlugin):
File "/usr/lib64/python2.6/site-packages/cms/models/pluginmodel.py" in __new__
  56.             table_name = 'cmsplugin_%s' % splitted[1]

Exception Type: IndexError at /
Exception Value: list index out of range
</pre>

@kezabelle thanks for the work, see comment on gist


Resolving patch:
diff --git a/cms/models/pluginmodel.py b/cms/models/pluginmodel.py
--- a/cms/models/pluginmodel.py
+++ b/cms/models/pluginmodel.py
@@ -52,8 +52,12 @@ def __new__(cls, name, bases, attrs):
         # 'myapp_' bit from the db_table name.
         if [base for base in bases if isinstance(base, PluginModelBase)]:
             splitter = '%s_' % new_class._meta.app_label
-            splitted = new_class._meta.db_table.split(splitter, 1)
-            table_name = 'cmsplugin_%s' % splitted[1]
+            
+            if splitter in new_class._meta.db_table:
+                splitted = new_class._meta.db_table.split(splitter, 1)
+                table_name = 'cmsplugin_%s' % splitted[1]
+            else:
+                table_name = new_class._meta.db_table
             new_class._meta.db_table = table_name
         
         return new_class


Test patch:
diff --git a/cms/tests/plugins.py b/cms/tests/plugins.py
--- a/cms/tests/plugins.py
+++ b/cms/tests/plugins.py
@@ -607,6 +607,70 @@ def test_copy_plugin_with_m2m(self):
         expected = [self.section_count for i in range(len(db_counts))]
         self.assertEqual(expected, db_counts)
         
+class PluginsMetaOptionsTests(TestCase):
+    ''' TestCase set for ensuring that bugs like #992 are caught '''
+
+    # these plugins are inlined because, due to the nature of the #992
+    # ticket, we cannot actually import a single file with all the
+    # plugin variants in, because that calls __new__, at which point the
+    # error with splitted occurs.
+
+    def test_meta_options_as_defaults(self):
+        ''' handling when a CMSPlugin meta options are computed defaults '''
+        # this plugin relies on the base CMSPlugin and Model classes to
+        # decide what the app_label and db_table should be
+        class TestPlugin(CMSPlugin):
+            pass
+
+        plugin = TestPlugin()
+        self.assertEqual(plugin._meta.db_table, 'cmsplugin_testplugin')
+        self.assertEqual(plugin._meta.app_label, 'tests') # because it's inlined
+
+    def test_meta_options_as_declared_defaults(self):
+        ''' handling when a CMSPlugin meta options are declared as per defaults '''
+        # here, we declare the db_table and app_label explicitly, but to the same
+        # values as would be computed, thus making sure it's not a problem to
+        # supply options.
+        class TestPlugin2(CMSPlugin):
+            class Meta:
+                db_table = 'cmsplugin_testplugin2'
+                app_label = 'tests'
+
+        plugin = TestPlugin2()
+        self.assertEqual(plugin._meta.db_table, 'cmsplugin_testplugin2')
+        self.assertEqual(plugin._meta.app_label, 'tests') # because it's inlined
+
+    def test_meta_options_custom_app_label(self):
+        ''' make sure customised meta options on CMSPlugins don't break things '''
+
+        class TestPlugin3(CMSPlugin):
+            class Meta:
+                app_label = 'one_thing'
+
+        plugin = TestPlugin3()
+        self.assertEqual(plugin._meta.db_table, 'cmsplugin_testplugin3') # because it's inlined
+        self.assertEqual(plugin._meta.app_label, 'one_thing')
+
+    def test_meta_options_custom_db_table(self):
+        ''' make sure custom database table names are OK. '''
+        class TestPlugin4(CMSPlugin):
+            class Meta:
+                db_table = 'or_another'
+
+        plugin = TestPlugin4()
+        self.assertEqual(plugin._meta.db_table, 'or_another')
+        self.assertEqual(plugin._meta.app_label, 'tests') # because it's inlined
+
+    def test_meta_options_custom_both(self):
+        ''' We should be able to customise app_label and db_table together '''
+        class TestPlugin5(CMSPlugin):
+            class Meta:
+                app_label = 'one_thing'
+                db_table = 'or_another'
+
+        plugin = TestPlugin5()
+        self.assertEqual(plugin._meta.db_table, 'or_another')
+        self.assertEqual(plugin._meta.app_label, 'one_thing')
 
 class SekizaiTests(TestCase):
     def test_post_patch_check(self):