Repository: django-cms/django-cms

Issue:
Publish page does not preserve plugin order
Add three link plugins to a placeholder in the following order and with the following names:
- Link 1
- Link 2
- Link 3

Now publish your changes to create the 'live' version. Now go back to draft mode and drag Link 3 so it comes after Link 2. Both structure and content mode show the plugins now in the correct 1,2,3 order. Click publish and note that the live version still has the plugins in the original order 1,3,2.



Discussion:
Paging @digi604 since he was the last to touch the [following line of code](https://github.com/divio/django-cms/blame/develop/cms/models/pluginmodel.py#L280):

```
new_plugin.position = CMSPlugin.objects.filter(parent=parent, language=target_language, placeholder=target_placeholder).count()
```

Why would a `new_plugin`'s position be set to a count() of items rather than the actual position specified on the 'old' plugin it is being copied from? This completely breaks plugin ordering on the published version of the page since the specified order in draft mode is completely ignored. It looks like `new_plugin.position` should never be set so that `plugin_instance.position` is just preserved as-is.

`position` attribute management looks a bit broken.
As discussed on IRC probably moving the position changing code in a single place should make easier to test it


Resolving patch:
diff --git a/cms/models/pluginmodel.py b/cms/models/pluginmodel.py
--- a/cms/models/pluginmodel.py
+++ b/cms/models/pluginmodel.py
@@ -252,7 +252,7 @@ def copy_plugin(self, target_placeholder, target_language, parent_cache, no_sign
 
          # get a new generic plugin instance
          # assign the position in the plugin tree
-         # save it to let mptt calculate the tree attributes
+         # save it to let mptt/treebeard calculate the tree attributes
          # then get a copy of the current plugin instance
          # assign to it the id of the generic plugin instance above;
            this will effectively change the generic plugin created above
@@ -278,7 +278,6 @@ def copy_plugin(self, target_placeholder, target_language, parent_cache, no_sign
         new_plugin.placeholder = target_placeholder
         # we assign a parent to our new plugin
         parent_cache[self.pk] = new_plugin
-        parent = None
         if self.parent:
             parent = parent_cache[self.parent_id]
             parent = CMSPlugin.objects.get(pk=parent.pk)
@@ -286,7 +285,6 @@ def copy_plugin(self, target_placeholder, target_language, parent_cache, no_sign
             new_plugin.parent = parent
         new_plugin.language = target_language
         new_plugin.plugin_type = self.plugin_type
-        new_plugin.position = CMSPlugin.objects.filter(parent=parent, language=target_language, placeholder=target_placeholder).count()
         if no_signals:
             from cms.signals import pre_save_plugins
 
@@ -306,11 +304,8 @@ def copy_plugin(self, target_placeholder, target_language, parent_cache, no_sign
             plugin_instance.depth = new_plugin.depth
             plugin_instance.path = new_plugin.path
             plugin_instance.numchild = new_plugin.numchild
-            # added to retain the position when creating a public copy of a plugin
-            plugin_instance.position = new_plugin.position
             plugin_instance._no_reorder = True
             plugin_instance.save()
-            #new_plugin._inst = plugin_instance
             old_instance = plugin_instance.__class__.objects.get(pk=self.pk)
             plugin_instance.copy_relations(old_instance)
         if no_signals:


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
@@ -214,12 +214,13 @@ def configure(db_url, **extra):
         CMS_PLACEHOLDER_CONF={
             'col_sidebar': {
                 'plugins': ('FilePlugin', 'FlashPlugin', 'LinkPlugin', 'PicturePlugin',
-                'TextPlugin', 'SnippetPlugin'),
+                            'TextPlugin', 'MultiColumnPlugin', 'SnippetPlugin'),
                 'name': gettext("sidebar column")
             },
             'col_left': {
                 'plugins': ('FilePlugin', 'FlashPlugin', 'LinkPlugin', 'PicturePlugin',
-                'TextPlugin', 'SnippetPlugin', 'GoogleMapPlugin', 'MultiColumnPlugin', 'StylePlugin'),
+                            'TextPlugin', 'SnippetPlugin', 'GoogleMapPlugin',
+                            'MultiColumnPlugin', 'StylePlugin'),
                 'name': gettext("left column"),
                 'plugin_modules': {
                     'LinkPlugin': 'Different Grouper'
@@ -230,7 +231,8 @@ def configure(db_url, **extra):
             },
             'col_right': {
                 'plugins': ('FilePlugin', 'FlashPlugin', 'LinkPlugin', 'PicturePlugin',
-                'TextPlugin', 'SnippetPlugin', 'GoogleMapPlugin', 'MultiColumnPlugin', 'StylePlugin'),
+                            'TextPlugin', 'SnippetPlugin', 'GoogleMapPlugin', 'MultiColumnPlugin',
+                            'StylePlugin'),
                 'name': gettext("right column")
             },
             'extra_context': {
diff --git a/cms/tests/plugins.py b/cms/tests/plugins.py
--- a/cms/tests/plugins.py
+++ b/cms/tests/plugins.py
@@ -181,7 +181,7 @@ def test_plugin_order(self):
         Test that plugin position is saved after creation
         """
         page_en = api.create_page("PluginOrderPage", "col_two.html", "en",
-                              slug="page1", published=True, in_navigation=True)
+                                  slug="page1", published=True, in_navigation=True)
         ph_en = page_en.placeholders.get(slot="col_left")
 
         # We check created objects and objects from the DB to be sure the position value
@@ -200,6 +200,51 @@ def test_plugin_order(self):
             rendered_placeholder = ph_en.render(self.get_context(page_en.get_absolute_url(), page=page_en), None)
             self.assertEqual(rendered_placeholder, "I'm the firstI'm the second")
 
+    def test_plugin_order_alt(self):
+        """
+        Test that plugin position is saved after creation
+        """
+        draft_page = api.create_page("PluginOrderPage", "col_two.html", "en",
+                                     slug="page1", published=False, in_navigation=True)
+        placeholder = draft_page.placeholders.get(slot="col_left")
+
+        # We check created objects and objects from the DB to be sure the position value
+        # has been saved correctly
+        text_plugin_2 = api.add_plugin(placeholder, "TextPlugin", "en", body="I'm the second")
+        text_plugin_3 = api.add_plugin(placeholder, "TextPlugin", "en", body="I'm the third")
+        # Publish to create a 'live' version
+        draft_page.publish('en')
+        draft_page = draft_page.reload()
+        placeholder = draft_page.placeholders.get(slot="col_left")
+
+        # Add a plugin and move it to the first position
+        text_plugin_1 = api.add_plugin(placeholder, "TextPlugin", "en", body="I'm the first")
+        data = {
+            'placeholder_id': placeholder.id,
+            'plugin_id': text_plugin_1.id,
+            'plugin_parent': '',
+            'plugin_language': 'en',
+            'plugin_order[]': [text_plugin_1.id, text_plugin_2.id, text_plugin_3.id],
+        }
+        self.client.post(URL_CMS_PLUGIN_MOVE, data)
+
+        draft_page.publish('en')
+        draft_page = draft_page.reload()
+        live_page = draft_page.get_public_object()
+        placeholder = draft_page.placeholders.get(slot="col_left")
+        live_placeholder = live_page.placeholders.get(slot="col_left")
+
+        with SettingsOverride(CMS_PERMISSION=False):
+            self.assertEqual(CMSPlugin.objects.get(pk=text_plugin_1.pk).position, 0)
+            self.assertEqual(CMSPlugin.objects.get(pk=text_plugin_2.pk).position, 1)
+            self.assertEqual(CMSPlugin.objects.get(pk=text_plugin_3.pk).position, 2)
+
+            ## Finally we render the placeholder to test the actual content
+            rendered_placeholder = placeholder.render(self.get_context(draft_page.get_absolute_url(), page=draft_page), None)
+            self.assertEqual(rendered_placeholder, "I'm the firstI'm the secondI'm the third")
+            rendered_live_placeholder = live_placeholder.render(self.get_context(live_page.get_absolute_url(), page=live_page), None)
+            self.assertEqual(rendered_live_placeholder, "I'm the firstI'm the secondI'm the third")
+
     def test_add_cancel_plugin(self):
         """
         Test that you can cancel a new plugin before editing and
diff --git a/cms/tests/static_placeholder.py b/cms/tests/static_placeholder.py
--- a/cms/tests/static_placeholder.py
+++ b/cms/tests/static_placeholder.py
@@ -157,7 +157,7 @@ def test_copy_plugin(self):
                 'source_plugin_id': sourceplugin.pk,
                 'target_language': 'en',
                 'target_placeholder_id': static_placeholder_target.draft.pk,
-                'targetplugin_id': targetplugin.pk,
+                'target_plugin_id': targetplugin.pk,
             })
             response = self.admin_class.copy_plugins(request)