Repository: django-cms/django-cms

Issue:
wrong table name when migrating a custom plugin
Hi,

When trying to do a migration for these new models.

``` python
class SlideModel(models.Model):
    slide_name = models.CharField(max_length=30)
    slide_title = models.CharField(max_length=30, blank=True)
    slide_image = ThumbnailerImageField(upload_to='slider')
    slide_image_alt_text = models.CharField(max_length=30, blank=True)
    slide_content = models.TextField(blank=True)
    slide_link = models.URLField(blank=True)

    class Meta:
        verbose_name = 'slide'
        verbose_name_plural = 'slides'

    def __unicode__(self):
        return self.slide_name + " - " + self.slide_title


class AdSliderModel(CMSPlugin):
    slides = models.ManyToManyField('SlideModel', blank=True)

    def copy_relations(self, old_instance):
        self.slides = old_instance.slides.all()

```

using 

`./manage schemamigration atario_ad_slider --auto`

generates the following migration

``` python
class Migration(SchemaMigration):

    def forwards(self, orm):
        # Adding model 'SlideModel'
        db.create_table(u'atario_ad_slider_slidemodel', (
            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
            ('slide_name', self.gf('django.db.models.fields.CharField')(max_length=30)),
            ('slide_title', self.gf('django.db.models.fields.CharField')(max_length=30, blank=True)),
            ('slide_image', self.gf('django.db.models.fields.files.ImageField')(max_length=100)),
            ('slide_image_alt_text', self.gf('django.db.models.fields.CharField')(max_length=30, blank=True)),
            ('slide_content', self.gf('django.db.models.fields.TextField')(blank=True)),
            ('slide_link', self.gf('django.db.models.fields.URLField')(max_length=200, blank=True)),
        ))
        db.send_create_signal(u'atario_ad_slider', ['SlideModel'])

        # Adding model 'AdSliderModel'
        db.create_table(u'cmsplugin_adslidermodel', (
            (u'cmsplugin_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['cms.CMSPlugin'], unique=True, primary_key=True)),
        ))
        db.send_create_signal(u'atario_ad_slider', ['AdSliderModel'])

        # Adding M2M table for field slides on 'AdSliderModel'
        m2m_table_name = db.shorten_name(u'cmsplugin_adslidermodel_slides')
        db.create_table(m2m_table_name, (
            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
            ('adslidermodel', models.ForeignKey(orm[u'atario_ad_slider.adslidermodel'], null=False)),
            ('slidemodel', models.ForeignKey(orm[u'atario_ad_slider.slidemodel'], null=False))
        ))
        db.create_unique(m2m_table_name, ['adslidermodel_id', 'slidemodel_id'])


    def backwards(self, orm):
        # Deleting model 'SlideModel'
        db.delete_table(u'atario_ad_slider_slidemodel')

        # Deleting model 'AdSliderModel'
        db.delete_table(u'cmsplugin_adslidermodel')

        # Removing M2M table for field slides on 'AdSliderModel'
        db.delete_table(db.shorten_name(u'cmsplugin_adslidermodel_slides'))


    models = {
        u'atario_ad_slider.adslidermodel': {
            'Meta': {'object_name': 'AdSliderModel', 'db_table': "u'cmsplugin_adslidermodel'", '_ormbases': ['cms.CMSPlugin']},
            u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'}),
            'slides': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['atario_ad_slider.SlideModel']", 'symmetrical': 'False', 'blank': 'True'})
        },
        u'atario_ad_slider.slidemodel': {
            'Meta': {'object_name': 'SlideModel'},
            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
            'slide_content': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
            'slide_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
            'slide_image_alt_text': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
            'slide_link': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
            'slide_name': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
            'slide_title': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'})
        },
        'cms.cmsplugin': {
            'Meta': {'object_name': 'CMSPlugin'},
            'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
            'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
            'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
            'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}),
            'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}),
            'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
            'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
        },
        'cms.placeholder': {
            'Meta': {'object_name': 'Placeholder'},
            'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}),
            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
            'slot': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'})
        }
    }

    complete_apps = ['atario_ad_slider']
```

The main issue here is that django's orm is looking for a table named 'atario_ad_slider_adslidermodel_slides' while the migration creates this table with the name 'cmsplugin_adslidermodel_slides'

everything works fine when I 'migrate' the database with 

`./manage syncdb --all`

thanks for your help



Discussion:
I have the same problem. The migration creates this:

``` python
m2m_table_name = db.shorten_name(u'cmsplugin_documentfeatured_documents')
```

but the ORM is looking for 'documents_documentfeatured_documents'

As a workaround, I edit the migration file before running it.

Just for reference, looks like the OP cross-posted this as a South ticket too: http://south.aeracode.org/ticket/1314

Looks like its their error, and they're saying "won't fix" =(

Also happens in the freeze of the model schema. Be aware when doing a data migration.


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,5 +1,6 @@
 # -*- coding: utf-8 -*-
 from datetime import date
+from django.db.models import ManyToManyField
 import os
 import warnings
 import json
@@ -55,16 +56,20 @@ def __new__(cls, name, bases, attrs):
 
         # turn 'myapp_mymodel' into 'cmsplugin_mymodel' by removing the
         # 'myapp_' bit from the db_table name.
+        splitter = '%s_' % new_class._meta.app_label
         if [base for base in bases if isinstance(base, PluginModelBase)]:
-            splitter = '%s_' % new_class._meta.app_label
-
             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
-
+        for att in attrs.values():
+            if isinstance(att, ManyToManyField):
+                if splitter in att.rel.through._meta.db_table:
+                    splitted = att.rel.through._meta.db_table.split(splitter, 1)
+                    table_name = 'cmsplugin_%s' % splitted[1]
+                    att.rel.through._meta.db_table = table_name
         return new_class
 
 


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
@@ -97,7 +97,6 @@ def configure(db_url, **extra):
             'cms.plugins.inherit',
             'cms.test_utils.project.sampleapp',
             'cms.test_utils.project.placeholderapp',
-            'cms.test_utils.project.pluginapp',
             'cms.test_utils.project.pluginapp.plugins.manytomany_rel',
             'cms.test_utils.project.pluginapp.plugins.extra_context',
             'cms.test_utils.project.fakemlng',
diff --git a/cms/test_utils/project/pluginapp/models.py b/cms/test_utils/project/pluginapp/models.py
deleted file mode 100644
--- a/cms/test_utils/project/pluginapp/models.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from cms.utils.compat.dj import python_2_unicode_compatible
-from django.db import models
-
-
-@python_2_unicode_compatible
-class Section(models.Model):
-    name = models.CharField(max_length=50)
-    
-    def __str__(self):
-        return self.name
-
-
-@python_2_unicode_compatible
-class Article(models.Model):
-    title = models.CharField(max_length=50)
-    section = models.ForeignKey(Section)
-    
-    def __str__(self):
-        return u"%s -- %s" % (self.title, self.section) 
diff --git a/cms/test_utils/project/pluginapp/plugins/manytomany_rel/admin.py b/cms/test_utils/project/pluginapp/plugins/manytomany_rel/admin.py
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/pluginapp/plugins/manytomany_rel/admin.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+from cms.test_utils.project.pluginapp.plugins.manytomany_rel.models import Article, Section
+from django.contrib import admin
+
+from django.contrib.admin import ModelAdmin
+
+admin.site.register(Section, ModelAdmin)
+admin.site.register(Article, ModelAdmin)
diff --git a/cms/test_utils/project/pluginapp/plugins/manytomany_rel/cms_plugins.py b/cms/test_utils/project/pluginapp/plugins/manytomany_rel/cms_plugins.py
--- a/cms/test_utils/project/pluginapp/plugins/manytomany_rel/cms_plugins.py
+++ b/cms/test_utils/project/pluginapp/plugins/manytomany_rel/cms_plugins.py
@@ -3,19 +3,17 @@
 from cms.plugin_base import CMSPluginBase
 from cms.plugin_pool import plugin_pool
 
-from cms.test_utils.project.pluginapp.plugins.manytomany_rel.models import ArticlePluginModel
-from cms.test_utils.project.pluginapp.models import Article
-
+from cms.test_utils.project.pluginapp.plugins.manytomany_rel.models import ArticlePluginModel, Article
 
 
 class ArticlePlugin(CMSPluginBase):
     model = ArticlePluginModel
     name = _("Articles")
-    render_template = "newsroom/plugins/articles.html"
+    render_template = "articles.html"
     admin_preview = False
 
     def render(self, context, instance, placeholder):
-        article_qs = Article.published_objects.all(section__name__in=instance.sections)        
+        article_qs = Article.objects.filter(section__in=instance.sections.all())
         context.update({'instance':instance,
                         'article_qs':article_qs,
                         'placeholder':placeholder})
diff --git a/cms/test_utils/project/pluginapp/plugins/manytomany_rel/models.py b/cms/test_utils/project/pluginapp/plugins/manytomany_rel/models.py
--- a/cms/test_utils/project/pluginapp/plugins/manytomany_rel/models.py
+++ b/cms/test_utils/project/pluginapp/plugins/manytomany_rel/models.py
@@ -3,13 +3,29 @@
 
 from cms.models import CMSPlugin
 
-from cms.test_utils.project.pluginapp.models import Section
+@python_2_unicode_compatible
+class Article(models.Model):
+    title = models.CharField(max_length=50)
+    section = models.ForeignKey('Section')
+
+    def __str__(self):
+        return u"%s -- %s" % (self.title, self.section)
+
+@python_2_unicode_compatible
+class Section(models.Model):
+    name = models.CharField(max_length=50)
+
+    def __str__(self):
+        return self.name
+
+
+
 
 
 @python_2_unicode_compatible
 class ArticlePluginModel(CMSPlugin):
     title = models.CharField(max_length=50)
-    sections =  models.ManyToManyField(Section)
+    sections = models.ManyToManyField('Section')
     
     def __str__(self):
         return self.title
diff --git a/cms/test_utils/project/templates/articles.html b/cms/test_utils/project/templates/articles.html
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/templates/articles.html
@@ -0,0 +1,6 @@
+<h2>{{ instance.title }}</h2>
+<ul>
+    {% for article in article_qs %}
+    <li>{{ article.title }}</li>
+    {% endfor %}
+</ul>
\ No newline at end of file
diff --git a/cms/tests/plugins.py b/cms/tests/plugins.py
--- a/cms/tests/plugins.py
+++ b/cms/tests/plugins.py
@@ -22,9 +22,7 @@
 from cms.toolbar.toolbar import CMSToolbar
 from djangocms_text_ckeditor.models import Text
 from djangocms_text_ckeditor.utils import plugin_tags_to_id_list
-from cms.test_utils.project.pluginapp.models import Article, Section
-from cms.test_utils.project.pluginapp.plugins.manytomany_rel.models import (
-    ArticlePluginModel)
+from cms.test_utils.project.pluginapp.plugins.manytomany_rel.models import Article, Section, ArticlePluginModel
 from cms.test_utils.testcases import CMSTestCase, URL_CMS_PAGE, URL_CMS_PLUGIN_MOVE, URL_CMS_PAGE_ADD, \
     URL_CMS_PLUGIN_ADD, URL_CMS_PLUGIN_EDIT, URL_CMS_PAGE_CHANGE, URL_CMS_PLUGIN_REMOVE
 from cms.sitemaps.cms_sitemap import CMSSitemap
@@ -1112,6 +1110,7 @@ def test_add_plugin_with_m2m(self):
         page_data = self.get_new_page_data()
         self.client.post(URL_CMS_PAGE_ADD, page_data)
         page = Page.objects.all()[0]
+        page.publish('en')
         placeholder = page.placeholders.get(slot="body")
         plugin_data = {
             'plugin_type': "ArticlePlugin",
@@ -1146,6 +1145,10 @@ def test_add_plugin_with_m2m(self):
         self.assertEqual(ArticlePluginModel.objects.count(), 1)
         plugin = ArticlePluginModel.objects.all()[0]
         self.assertEquals(self.section_count, plugin.sections.count())
+        response = self.client.get('/en/?edit')
+        self.assertEquals(response.status_code, 200)
+        self.assertEquals(plugin.sections.through._meta.db_table, 'cmsplugin_articlepluginmodel_sections')
+
 
     def test_add_plugin_with_m2m_and_publisher(self):
         self.assertEqual(ArticlePluginModel.objects.count(), 0)