Repository: django-cms/django-cms

Issue:
`manage.py cms check` shouldn't report warning for relationship to a plugin's own "base class"
I don't know if the use of an abstract class is related here, but in this project, I define an (abstract) base plugin model class, which is then extended with a concrete class, which is itself extended for another concrete plugin model.

So, we ultimately have these models defined in modules X and Y:
- X.A (CMSPlugin), abstract
  - X.B (A), concrete
    - Y.C (B), concrete

`python manage.py cms check` reports:

> project.apps.moduleX.models.B has a foreign key from project.apps.moduleY.models.C, but no "copy_relations" method defined. [WARNING]

Also a bit strange is that this warning is emitted 3X.

I think I was the last person to touch this bit of code, and will probably fix it at some point, but I didn't want to lose sight of this issue.



Discussion:
Just to be clear, the condition it is reporting is true, its just that this is not the sort of condition that the developer needs warnings about.


Resolving patch:
diff --git a/cms/utils/check.py b/cms/utils/check.py
--- a/cms/utils/check.py
+++ b/cms/utils/check.py
@@ -272,7 +272,7 @@ def get_class(method_name, model):
                     c_to_s(rel.model),
                 ))
             for rel in plugin_class._meta.get_all_related_objects():
-                if rel.model != CMSPlugin and rel.model != AliasPluginModel:
+                if rel.model != CMSPlugin and not issubclass(rel.model, plugin.model) and rel.model != AliasPluginModel:
                     section.warn('%s has a foreign key from %s,\n    but no "copy_relations" method defined.' % (
                         c_to_s(plugin_class),
                         c_to_s(rel.model),
diff --git a/develop.py b/develop.py
--- a/develop.py
+++ b/develop.py
@@ -10,7 +10,7 @@
 import sys
 import warnings
 
-from django.utils.six.moves import StringIO
+from StringIO import StringIO
 
 from django import VERSION
 from django.core.exceptions import DjangoRuntimeWarning
@@ -28,17 +28,20 @@
 from cms.utils.compat import DJANGO_1_6
 import menus
 
-__doc__ = '''django CMS development helper script. 
+__doc__ = '''django CMS development helper script.
 
 To use a different database, set the DATABASE_URL environment variable to a
-dj-database-url compatible value.  The AUTH_USER_MODEL environment variable can be
-used to change the user model in the same manner as the --user option.
+dj-database-url compatible value.  The AUTH_USER_MODEL environment variable can
+be used to change the user model in the same manner as the --user option.
 
 Usage:
-    develop.py test [--parallel | --failfast] [--migrate] [--user=<user>] [<test-label>...] [--xvfb]
+    develop.py test [--parallel | --failfast] [--migrate] [--user=<user>]
+                    [<test-label>...] [--xvfb]
     develop.py timed test [test-label...] [--xvfb]
-    develop.py isolated test [<test-label>...] [--parallel] [--migrate] [--xvfb]
-    develop.py server [--port=<port>] [--bind=<bind>] [--migrate] [--user=<user>]
+    develop.py isolated test [<test-label>...] [--parallel] [--migrate]
+                             [--xvfb]
+    develop.py server [--port=<port>] [--bind=<bind>] [--migrate]
+                      [--user=<user>]
     develop.py shell
     develop.py compilemessages
     develop.py makemessages
@@ -48,16 +51,18 @@
     develop.py authors
 
 Options:
-    -h --help                   Show this screen.
-    --version                   Show version.
-    --parallel                  Run tests in parallel.
-    --migrate                   Use south migrations in test or server command.
-    --merge                     Merge migrations
-    --failfast                  Stop tests on first failure (only if not --parallel).
-    --port=<port>               Port to listen on [default: 8000].
-    --bind=<bind>               Interface to bind to [default: 127.0.0.1].
-    --user=<user>               Specify which user model to run tests with (if other than auth.User).
-    --xvfb                      Use a virtual X framebuffer for frontend testing, requires xvfbwrapper to be installed.
+    -h --help             Show this screen.
+    --version             Show version.
+    --parallel            Run tests in parallel.
+    --migrate             Use south migrations in test or server command.
+    --merge               Merge migrations
+    --failfast            Stop tests on first failure (only if not --parallel).
+    --port=<port>         Port to listen on [default: 8000].
+    --bind=<bind>         Interface to bind to [default: 127.0.0.1].
+    --user=<user>         Specify which user model to run tests with (if other
+                          than auth.User).
+    --xvfb                Use a virtual X framebuffer for frontend testing,
+                          requires xvfbwrapper to be installed.
 '''
 
 
@@ -67,11 +72,15 @@ def server(bind='127.0.0.1', port=8000, migrate_cmd=False):
         if DJANGO_1_6:
             from south.management.commands import syncdb, migrate
             if migrate_cmd:
-                syncdb.Command().handle_noargs(interactive=False, verbosity=1, database='default')
+                syncdb.Command().handle_noargs(interactive=False, verbosity=1,
+                                               database='default')
                 migrate.Command().handle(interactive=False, verbosity=1)
             else:
-                syncdb.Command().handle_noargs(interactive=False, verbosity=1, database='default', migrate=False, migrate_all=True)
-                migrate.Command().handle(interactive=False, verbosity=1, fake=True)
+                syncdb.Command().handle_noargs(interactive=False, verbosity=1,
+                                               database='default',
+                                               migrate=False, migrate_all=True)
+                migrate.Command().handle(interactive=False, verbosity=1,
+                                         fake=True)
         else:
             call_command("migrate", database='default')
         User = get_user_model()
@@ -88,7 +97,8 @@ def server(bind='127.0.0.1', port=8000, migrate_cmd=False):
             usr.is_active = True
             usr.save()
             print('')
-            print("A admin user (username: admin, password: admin) has been created.")
+            print("A admin user (username: admin, password: admin) "
+                  "has been created.")
             print('')
     from django.contrib.staticfiles.management.commands import runserver
     rs = runserver.Command()
@@ -115,7 +125,8 @@ def _split(itr, num):
 
 def _get_test_labels():
     test_labels = []
-    for module in [name for _, name, _ in pkgutil.iter_modules([os.path.join("cms","tests")])]:
+    for module in [name for _, name, _ in pkgutil.iter_modules(
+            [os.path.join("cms", "tests")])]:
         clsmembers = pyclbr.readmodule("cms.tests.%s" % module)
         for clsname, cls in clsmembers.items():
             for method, _ in cls.methods.items():
@@ -125,7 +136,8 @@ def _get_test_labels():
     return test_labels
 
 
-def _test_run_worker(test_labels, failfast=False, test_runner='django.test.simple.DjangoTestSuiteRunner'):
+def _test_run_worker(test_labels, failfast=False,
+                     test_runner='django.test.simple.DjangoTestSuiteRunner'):
     warnings.filterwarnings(
         'error', r"DateTimeField received a naive datetime",
         RuntimeWarning, r'django\.db\.models\.fields')
@@ -150,13 +162,20 @@ def isolated(test_labels, parallel=False):
         mapper = pool.map
     else:
         mapper = map
-    results = mapper(_test_in_subprocess, ([test_label] for test_label in test_labels))
-    failures = [test_label for test_label, return_code in zip(test_labels, results) if return_code != 0]
+    results = mapper(
+        _test_in_subprocess,
+        ([test_label] for test_label in test_labels)
+    )
+    failures = [test_label for test_label, return_code in zip(
+        test_labels, results) if return_code != 0]
     return failures
 
 
 def timed(test_labels):
-    return _test_run_worker(test_labels, test_runner='cms.test_utils.runners.TimedTestRunner')
+    return _test_run_worker(
+        test_labels,
+        test_runner='cms.test_utils.runners.TimedTestRunner'
+    )
 
 
 def test(test_labels, parallel=False, failfast=False):
@@ -194,35 +213,41 @@ def makemigrations(migrate_plugins=True, merge=False, squash=False):
         # core applications
         'cms', 'menus',
         # testing applications
-        'meta', 'manytomany_rel', 'fileapp', 'placeholderapp', 'sampleapp', 'fakemlng', 'one_thing', 'extensionapp',
-        'objectpermissionsapp', 'bunch_of_plugins',
+        'meta', 'manytomany_rel', 'fileapp', 'placeholderapp', 'sampleapp',
+        'fakemlng', 'one_thing', 'extensionapp', 'objectpermissionsapp',
+        'bunch_of_plugins', 'mti_pluginapp',
     ]
     if os.environ.get("AUTH_USER_MODEL") == "emailuserapp.EmailUser":
         applications.append('emailuserapp')
     if migrate_plugins:
         applications.extend([
             # official plugins
-            'djangocms_inherit', 'djangocms_googlemap', 'djangocms_column', 'djangocms_style', 'djangocms_link',
-            'djangocms_file', 'djangocms_text_ckeditor', 'djangocms_picture', 'djangocms_teaser', 'djangocms_file',
-            'djangocms_flash', 'djangocms_video',
+            'djangocms_inherit', 'djangocms_googlemap', 'djangocms_column',
+            'djangocms_style', 'djangocms_link', 'djangocms_file',
+            'djangocms_text_ckeditor', 'djangocms_picture', 'djangocms_teaser',
+            'djangocms_file', 'djangocms_flash', 'djangocms_video',
         ])
     if DJANGO_1_6:
         from south.exceptions import NoMigrations
         from south.migration import Migrations
 
         if merge:
-            raise DjangoRuntimeWarning(u'Option not implemented for Django 1.6')
+            raise DjangoRuntimeWarning(
+                u'Option not implemented for Django 1.6')
         for application in applications:
             try:
                 Migrations(application)
             except NoMigrations:
-                print('ATTENTION: No migrations found for {0}, creating initial migrations.'.format(application))
+                print('ATTENTION: No migrations found for {0}, creating '
+                      'initial migrations.'.format(application))
                 try:
                     call_command('schemamigration', application, initial=True)
                 except SystemExit:
                     pass
             except ImproperlyConfigured:
-                print('WARNING: The app: {0} could not be found.'.format(application))
+                print('WARNING: The app: {0} could not be found.'.format(
+                    application
+                ))
             else:
                 try:
                     call_command('schemamigration', application, auto=True)
@@ -244,7 +269,10 @@ def generate_authors():
 
     # Get our list of authors
     print("Collecting author names")
-    r = subprocess.Popen(["git", "log", "--use-mailmap", "--format=%aN"], stdout=subprocess.PIPE)
+    r = subprocess.Popen(
+        ["git", "log", "--use-mailmap", "--format=%aN"],
+        stdout=subprocess.PIPE
+    )
     seen_authors = []
     authors = []
     with open('AUTHORS', 'r') as f:
@@ -283,7 +311,10 @@ def main():
 
     default_name = ':memory:' if args['test'] else 'local.sqlite'
 
-    db_url = os.environ.get("DATABASE_URL", "sqlite://localhost/%s" % default_name)
+    db_url = os.environ.get(
+        "DATABASE_URL",
+        "sqlite://localhost/%s" % default_name
+    )
     migrate = args.get('--migrate', False)
 
     with temp_dir() as STATIC_ROOT:
@@ -300,7 +331,8 @@ def main():
             }
 
             if args['test']:
-                configs['SESSION_ENGINE'] = "django.contrib.sessions.backends.cache"
+                configs['SESSION_ENGINE'] = ""
+                "django.contrib.sessions.backends.cache"
 
             # Command line option takes precedent over environment variable
             auth_user_model = args['--user']
@@ -311,7 +343,8 @@ def main():
             if auth_user_model:
                 if VERSION[:2] < (1, 5):
                     print()
-                    print("Custom user models are not supported before Django 1.5")
+                    print("Custom user models are not supported "
+                          "before Django 1.5")
                     print()
                 else:
                     configs['AUTH_USER_MODEL'] = auth_user_model
@@ -337,7 +370,9 @@ def null_context():
 
                 with context:
                     if args['isolated']:
-                        failures = isolated(args['<test-label>'], args['--parallel'])
+                        failures = isolated(
+                            args['<test-label>'], args['--parallel']
+                        )
                         print()
                         print("Failed tests")
                         print("============")
@@ -350,10 +385,18 @@ def null_context():
                     elif args['timed']:
                         num_failures = timed(args['<test-label>'])
                     else:
-                        num_failures = test(args['<test-label>'], args['--parallel'], args['--failfast'])
+                        num_failures = test(
+                            args['<test-label>'],
+                            args['--parallel'],
+                            args['--failfast']
+                        )
                     sys.exit(num_failures)
             elif args['server']:
-                server(args['--bind'], args['--port'], args.get('--migrate', True))
+                server(
+                    args['--bind'],
+                    args['--port'],
+                    args.get('--migrate', True)
+                )
             elif args['shell']:
                 shell()
             elif args['compilemessages']:
@@ -363,7 +406,10 @@ def null_context():
             elif args['makemigrations']:
                 makemigrations(merge=args['--merge'])
             elif args['squashmigrations']:
-                squashmigrations(args['<applications-name>'], args['<migration-name>'])
+                squashmigrations(
+                    args['<applications-name>'],
+                    args['<migration-name>']
+                )
 
 
 if __name__ == '__main__':


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
@@ -124,6 +124,7 @@ def configure(db_url, **extra):
             'cms.test_utils.project.objectpermissionsapp',
             'cms.test_utils.project.bunch_of_plugins',
             'cms.test_utils.project.extensionapp',
+            'cms.test_utils.project.mti_pluginapp',
             'reversion',
             'sekizai',
             'hvad',
@@ -295,6 +296,7 @@ def configure(db_url, **extra):
             'bunch_of_plugins': 'cms.test_utils.project.bunch_of_plugins.migrations_django',
             'extensionapp': 'cms.test_utils.project.extensionapp.migrations_django',
             'objectpermissionsapp': 'cms.test_utils.project.objectpermissionsapp.migrations_django',
+            'mti_pluginapp': 'cms.test_utils.project.mti_pluginapp.migrations_django',
         }
     if DJANGO_1_5:
         defaults['MIDDLEWARE_CLASSES'].append('django.middleware.transaction.TransactionMiddleware')
diff --git a/cms/test_utils/project/mti_pluginapp/__init__.py b/cms/test_utils/project/mti_pluginapp/__init__.py
new file mode 100644
diff --git a/cms/test_utils/project/mti_pluginapp/cms_plugins.py b/cms/test_utils/project/mti_pluginapp/cms_plugins.py
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/mti_pluginapp/cms_plugins.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+from cms.plugin_base import CMSPluginBase
+from cms.plugin_pool import plugin_pool
+
+from .models import TestPluginAlphaModel, TestPluginBetaModel
+
+
+class TestPluginAlpha(CMSPluginBase):
+    model = TestPluginAlphaModel
+    render_template = 'mti_pluginapp/alpha.html'
+    name = 'test mti plugin alpha'
+
+    def render(self, context, instance, placeholder):
+        context['alpha'] = instance.alpha
+        return context
+
+plugin_pool.register_plugin(TestPluginAlpha)
+
+
+class TestPluginBeta(CMSPluginBase):
+    model = TestPluginBetaModel
+    render_template = 'mti_pluginapp/beta.html'
+    name = 'test mti plugin beta'
+
+    def render(self, context, instance, placeholder):
+        context['alpha'] = instance.alpha
+        context['beta'] = instance.beta
+        return context
+
+plugin_pool.register_plugin(TestPluginBeta)
diff --git a/cms/test_utils/project/mti_pluginapp/migrations/0001_initial.py b/cms/test_utils/project/mti_pluginapp/migrations/0001_initial.py
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/mti_pluginapp/migrations/0001_initial.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'TestPluginAlphaModel'
+        db.create_table(u'mti_pluginapp_testpluginalphamodel', (
+            (u'cmsplugin_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['cms.CMSPlugin'], unique=True, primary_key=True)),
+            ('alpha', self.gf('django.db.models.fields.CharField')(default='test plugin alpha', max_length=32)),
+        ))
+        db.send_create_signal(u'mti_pluginapp', ['TestPluginAlphaModel'])
+
+        # Adding model 'TestPluginBetaModel'
+        db.create_table(u'mti_pluginapp_testpluginbetamodel', (
+            (u'testpluginalphamodel_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['mti_pluginapp.TestPluginAlphaModel'], unique=True, primary_key=True)),
+            ('beta', self.gf('django.db.models.fields.CharField')(default='test plugin beta', max_length=32)),
+        ))
+        db.send_create_signal(u'mti_pluginapp', ['TestPluginBetaModel'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'TestPluginAlphaModel'
+        db.delete_table(u'mti_pluginapp_testpluginalphamodel')
+
+        # Deleting model 'TestPluginBetaModel'
+        db.delete_table(u'mti_pluginapp_testpluginbetamodel')
+
+
+    models = {
+        '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': '255', 'db_index': 'True'})
+        },
+        u'mti_pluginapp.testpluginalphamodel': {
+            'Meta': {'object_name': 'TestPluginAlphaModel', '_ormbases': ['cms.CMSPlugin']},
+            'alpha': ('django.db.models.fields.CharField', [], {'default': "'test plugin alpha'", 'max_length': '32'}),
+            u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        u'mti_pluginapp.testpluginbetamodel': {
+            'Meta': {'object_name': 'TestPluginBetaModel', '_ormbases': [u'mti_pluginapp.TestPluginAlphaModel']},
+            'beta': ('django.db.models.fields.CharField', [], {'default': "'test plugin beta'", 'max_length': '32'}),
+            u'testpluginalphamodel_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['mti_pluginapp.TestPluginAlphaModel']", 'unique': 'True', 'primary_key': 'True'})
+        }
+    }
+
+    complete_apps = ['mti_pluginapp']
\ No newline at end of file
diff --git a/cms/test_utils/project/mti_pluginapp/migrations/__init__.py b/cms/test_utils/project/mti_pluginapp/migrations/__init__.py
new file mode 100644
diff --git a/cms/test_utils/project/mti_pluginapp/migrations_django/0001_initial.py b/cms/test_utils/project/mti_pluginapp/migrations_django/0001_initial.py
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/mti_pluginapp/migrations_django/0001_initial.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('cms', '0003_auto_20140926_2347'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='TestPluginAlphaModel',
+            fields=[
+                ('cmsplugin_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='cms.CMSPlugin')),
+                ('alpha', models.CharField(default=b'test plugin alpha', max_length=32, verbose_name=b'name')),
+            ],
+            options={
+                'abstract': False,
+            },
+            bases=('cms.cmsplugin',),
+        ),
+        migrations.CreateModel(
+            name='TestPluginBetaModel',
+            fields=[
+                ('testpluginalphamodel_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='mti_pluginapp.TestPluginAlphaModel')),
+                ('beta', models.CharField(default=b'test plugin beta', max_length=32, verbose_name=b'name')),
+            ],
+            options={
+                'abstract': False,
+            },
+            bases=('mti_pluginapp.testpluginalphamodel',),
+        ),
+    ]
diff --git a/cms/test_utils/project/mti_pluginapp/migrations_django/__init__.py b/cms/test_utils/project/mti_pluginapp/migrations_django/__init__.py
new file mode 100644
diff --git a/cms/test_utils/project/mti_pluginapp/models.py b/cms/test_utils/project/mti_pluginapp/models.py
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/mti_pluginapp/models.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+
+from django.db import models
+
+from cms.models import CMSPlugin
+
+
+class TestPluginAlphaModel(CMSPlugin):
+    """
+    Nothing interesting here, move along.
+    """
+    alpha = models.CharField('name', blank=False, default='test plugin alpha', max_length=32)
+
+
+class TestPluginBetaModel(TestPluginAlphaModel):
+    """
+    NOTE: This is the subject of our test. A plugin which inherits from
+    another concrete plugin via MTI or Multi-Table Inheritence.
+    """
+    beta = models.CharField('name', blank=False, default='test plugin beta', max_length=32)
+
diff --git a/cms/test_utils/project/mti_pluginapp/templates/mti_pluginapp/alpha.html b/cms/test_utils/project/mti_pluginapp/templates/mti_pluginapp/alpha.html
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/mti_pluginapp/templates/mti_pluginapp/alpha.html
@@ -0,0 +1 @@
+{{ alpha }}
\ No newline at end of file
diff --git a/cms/test_utils/project/mti_pluginapp/templates/mti_pluginapp/beta.html b/cms/test_utils/project/mti_pluginapp/templates/mti_pluginapp/beta.html
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/mti_pluginapp/templates/mti_pluginapp/beta.html
@@ -0,0 +1 @@
+{{ alpha }}|{{ beta }}
\ No newline at end of file
diff --git a/cms/tests/admin.py b/cms/tests/admin.py
--- a/cms/tests/admin.py
+++ b/cms/tests/admin.py
@@ -318,7 +318,7 @@ def test_delete(self):
         page.publish('en')
         with self.login_user_context(admin_user):
             data = {'post': 'yes'}
-            with self.assertNumQueries(FuzzyInt(300, 405)):
+            with self.assertNumQueries(FuzzyInt(300, 407)):
                 response = self.client.post(URL_CMS_PAGE_DELETE % page.pk, data)
             self.assertRedirects(response, URL_CMS_PAGE)
 
@@ -335,7 +335,7 @@ def test_delete_diff_language(self):
         page.publish('en')
         with self.login_user_context(admin_user):
             data = {'post': 'yes'}
-            with self.assertNumQueries(FuzzyInt(300, 392)):
+            with self.assertNumQueries(FuzzyInt(300, 394)):
                 response = self.client.post(URL_CMS_PAGE_DELETE % page.pk, data)
             self.assertRedirects(response, URL_CMS_PAGE)
 
diff --git a/cms/tests/plugins.py b/cms/tests/plugins.py
--- a/cms/tests/plugins.py
+++ b/cms/tests/plugins.py
@@ -1580,3 +1580,45 @@ def test_import_broken_plugin(self):
                 plugin_pool.discovered = False
                 self.assertRaises(ImportError, plugin_pool.discover_plugins)
 
+class MTIPluginsTestCase(PluginsTestBaseCase):
+    def test_add_edit_plugin(self):
+        from cms.test_utils.project.mti_pluginapp.models import TestPluginBetaModel
+
+        """
+        Test that we can instantiate and use a MTI plugin
+        """
+
+        # Create a page
+        page_data = self.get_new_page_data()
+        self.client.post(URL_CMS_PAGE_ADD, page_data)
+        page = Page.objects.all()[0]
+
+        # Add the MTI plugin
+        plugin_data = {
+            'plugin_type': "TestPluginBeta",
+            'plugin_language': settings.LANGUAGES[0][0],
+            'placeholder_id': page.placeholders.get(slot="body").pk,
+            'plugin_parent': '',
+        }
+        response = self.client.post(URL_CMS_PLUGIN_ADD, plugin_data)
+        self.assertEqual(response.status_code, 200)
+        plugin_id = self.get_response_pk(response)
+        self.assertEqual(plugin_id, CMSPlugin.objects.all()[0].pk)
+
+        # Test we can open the change form for the MTI plugin
+        edit_url = "%s%s/" % (URL_CMS_PLUGIN_EDIT, plugin_id)
+        response = self.client.get(edit_url)
+        self.assertEqual(response.status_code, 200)
+
+        # Edit the MTI plugin
+        data = {
+            "alpha": "ALPHA",
+            "beta": "BETA"
+        }
+        response = self.client.post(edit_url, data)
+        self.assertEqual(response.status_code, 200)
+
+        # Test that the change was properly stored in the DB
+        plugin_model = TestPluginBetaModel.objects.all()[0]
+        self.assertEqual("ALPHA", plugin_model.alpha)
+        self.assertEqual("BETA", plugin_model.beta)