Repository: django-cms/django-cms

Issue:
fix-tree command does not set position
For some as yet unknown reason, I've found plugins that have their position set to None (will open a ticket once I trace that bug down). Calling the fix-tree command does nothing to repair this since for CMSPlugin's, it just defers to django-treebeard's `fix_tree()` method, but of course django-treebeard knows nothing about django-cms's custom 'position' field.

This breaks the page since `cms.utils.plugins.build_plugin_tree()` attempts to [sort plugins](https://github.com/divio/django-cms/blob/develop/cms/utils/plugins.py#L283-L299) by ('parent_id', 'position'), resulting in a `TypeError: unorderable types: int() < NoneType()`



Discussion:
All plugins with position == None were orphaned plugins.

@jsma does this happens with #3661?
Orphaned plugins will go away in 3.1

Actually yes, the site is running on a branch with that PR already merged. I'm still not clear on how position got set to None, haven't had a chance to find the cause. Setting that aside, do you think there is value in making sure `position` matches what treebeard has to say about `path` in the fix-tree command?

@jsma definitely yes


Resolving patch:
diff --git a/cms/models/pluginmodel.py b/cms/models/pluginmodel.py
--- a/cms/models/pluginmodel.py
+++ b/cms/models/pluginmodel.py
@@ -5,6 +5,7 @@
 import os
 import warnings
 
+from django.conf import settings
 from django.core.urlresolvers import NoReverseMatch
 from django.core.exceptions import ValidationError, ObjectDoesNotExist
 from django.db import models
@@ -82,7 +83,6 @@ class CMSPlugin(six.with_metaclass(PluginModelBase, MP_Node)):
     child_plugin_instances = None
     translatable_content_excluded_fields = []
 
-
     class Meta:
         app_label = 'cms'
 
@@ -319,6 +319,26 @@ def copy_plugin(self, target_placeholder, target_language, parent_cache, no_sign
 
         return new_plugin
 
+    @classmethod
+    def fix_tree(cls, destructive=False):
+        """
+        Fixes the plugin tree by first calling treebeard fix_tree and the recalculating
+        the correct position property for each plugin.
+        """
+        from cms.utils.plugins import reorder_plugins
+
+        super(CMSPlugin, cls).fix_tree(destructive)
+        for placeholder in Placeholder.objects.all():
+            for language, __ in settings.LANGUAGES:
+                order = CMSPlugin.objects.filter(placeholder_id=placeholder.pk, language=language,
+                                                 parent_id__isnull=True
+                                                 ).values_list('pk', flat=True)
+                reorder_plugins(placeholder, None, language, order)
+                for plugin in CMSPlugin.objects.filter(placeholder_id=placeholder.pk,
+                                                       language=language).order_by('depth', 'path'):
+                    order = CMSPlugin.objects.filter(parent_id=plugin.pk).values_list('pk', flat=True)
+                    reorder_plugins(placeholder, plugin.pk, language, order)
+
     def post_copy(self, old_instance, new_old_ziplist):
         """
         Handle more advanced cases (eg Text Plugins) after the original is


Test patch:
diff --git a/cms/tests/nested_plugins.py b/cms/tests/nested_plugins.py
--- a/cms/tests/nested_plugins.py
+++ b/cms/tests/nested_plugins.py
@@ -131,6 +131,105 @@ def plugin_list_from_tree(roots, plugin_list):
         # just in case the test method that called us wants it:
         return copied_placeholder
 
+    def test_plugin_fix_tree(self):
+        """
+        Tests CMSPlugin.fix_tree by creating a plugin structure, setting the position
+        value to Null for all the plugins and then rebuild the tree.
+
+        The structure below isn't arbitrary, but has been designed to test
+        various conditions, including:
+
+        * nodes four levels deep
+        * siblings with and without children
+
+             1
+                 2
+                     4
+                          10
+                     8
+                 3
+                     9
+             5
+                 6
+                 7
+        """
+
+        placeholder = Placeholder(slot=u"some_slot")
+        placeholder.save()  # a good idea, if not strictly necessary
+
+        # plugin in placeholder
+        plugin_1 = add_plugin(placeholder, u"TextPlugin", u"en", body=u"01")
+
+        # IMPORTANT: plugins must be reloaded, before they can be assigned
+        # as a parent. Otherwise, the Tree structure doesn't seem to rebuild
+        # properly.
+
+        # child of plugin_1
+        plugin_1 = self.reload(plugin_1)
+        plugin_2 = add_plugin(placeholder, u"TextPlugin", u"en",  # nopyflakes
+                              body=u"02", target=plugin_1,
+        )
+
+        # create a second child of plugin_1
+        plugin_1 = self.reload(plugin_1)
+        plugin_3 = add_plugin(placeholder, u"TextPlugin", u"en",  # nopyflakes
+                              body=u"03", target=plugin_1
+        )
+
+        # child of plugin_2
+        plugin_2 = self.reload(plugin_2)
+        plugin_4 = add_plugin(placeholder, u"TextPlugin", u"en",  # nopyflakes
+                              body=u"04", target=plugin_2
+        )
+
+
+        plugin_1 = self.reload(plugin_1)  # nopyflakes
+        # create a second root plugin
+        plugin_5 = add_plugin(placeholder, u"TextPlugin", u"en", body=u"05")
+        left = CMSPlugin.objects.filter(parent__isnull=True).order_by('path')[0]
+        plugin_5 = self.reload(plugin_5)
+        plugin_5 = plugin_5.move(left, pos='right')
+        self.reorder_positions(plugin_5)
+        self.reorder_positions(plugin_2)
+
+        # child of plugin_5
+        plugin_5 = self.reload(plugin_5)
+        plugin_6 = add_plugin(placeholder, u"TextPlugin", u"en",  # nopyflakes
+                              body=u"06", target=plugin_5
+        )
+
+        # child of plugin_6
+        plugin_5 = self.reload(plugin_5)
+        plugin_7 = add_plugin(placeholder, u"TextPlugin", u"en",  # nopyflakes
+                              body=u"07", target=plugin_5
+        )
+
+        # another child of plugin_2
+        plugin_2 = self.reload(plugin_2)
+        plugin_8 = add_plugin(placeholder, u"TextPlugin", u"en",  # nopyflakes
+                              body=u"08", target=plugin_2
+        )
+
+        # child of plugin_3
+        plugin_3 = self.reload(plugin_3)
+        plugin_9 = add_plugin(placeholder, u"TextPlugin", u"en",  # nopyflakes
+                              body=u"09", target=plugin_3
+        )
+
+        # child of plugin_4
+        plugin_4 = self.reload(plugin_4)
+        plugin_10 = add_plugin(placeholder, u"TextPlugin", u"en",  # nopyflakes
+                               body=u"10", target=plugin_4
+        )
+
+        original_plugins = (placeholder.get_plugins().order_by('position', 'path'))
+
+        CMSPlugin.objects.update(position=None)
+        CMSPlugin.fix_tree()
+
+        new_plugins = list(placeholder.get_plugins().order_by('position', 'path'))
+        self.assertSequenceEqual(original_plugins, new_plugins)
+
     def test_plugin_deep_nesting_and_copying(self):
         """
         Create a deeply-nested plugin structure, tests its properties, and tests
@@ -159,11 +258,11 @@ def test_plugin_deep_nesting_and_copying(self):
                      8
                  3
                      9
-              5
+             5
                  6
                  7
                  13
-              14
+             14
 
         and then we move it all around.
         """