from djangocms_text.models import Text from cms.api import add_plugin, create_page from cms.models import Page from cms.models.placeholdermodel import Placeholder from cms.models.pluginmodel import CMSPlugin from cms.tests.test_plugins import PluginsTestBaseCase from cms.utils.plugins import copy_plugins_to_placeholder class NestedPluginsTestCase(PluginsTestBaseCase): def compare_plugin_tree(self, tree, placeholder): counter = 1 plugins = placeholder.get_plugins() for plugin, data in zip(plugins, tree, strict=False): msg = 'Expected %s %s. Got %s instead.' self.assertEqual(plugin.pk, data[0], msg % ('id', data[0], plugin.pk)) self.assertEqual(plugin.position, counter, msg % ('position', counter, plugin.position)) self.assertEqual(plugin.parent_id, data[1], msg % ('parent', data[1], plugin.parent_id)) counter += 1 def copy_placeholders_and_check_results(self, placeholders): """ This function is not itself a test; rather, it can be used by any test that has created placeholders. It will check that whatever the plugin structure in the placeholder, it will be copied accurately when they are copied. placeholders is a list of placeholders """ for original_placeholder in placeholders: # get the plugins original_plugins = original_placeholder.get_plugins() # copy them to a new placeholder copied_placeholder = Placeholder.objects.create(slot=original_placeholder.slot) copy_plugins_to_placeholder( original_placeholder.get_plugins(), placeholder=copied_placeholder, ) copied_plugins = copied_placeholder.get_plugins() # we should find the same number of plugins in both placeholders self.assertEqual( original_plugins.count(), copied_plugins.count() ) # quick check: make sure the two querysets match: for original, copy in zip(original_plugins, copied_plugins, strict=False): self.assertEqual( Text.objects.get(id=original.id).body, Text.objects.get(id=copy.id).body ) self.assertEqual(original.position, copy.position) self.assertEqual( original._get_descendants_count(), copy._get_descendants_count() ) # just in case the test method that called us wants it: return copied_placeholder def test_plugin_deep_nesting_and_copying(self): """ Create a deeply-nested plugin structure, tests its properties, and tests that it is copied accurately when the placeholder containing them is copied. The structure below isn't arbitrary, but has been designed to test various conditions, including: * nodes four levels deep * multiple successive level increases * multiple successive level decreases * successive nodes on the same level followed by level changes * multiple level decreases between successive nodes * siblings with and without children * nodes and branches added to the tree out of sequence First we create the structure: 11 1 2 12 4 10 8 3 9 5 6 7 13 14 and then we move it all around. """ placeholder = Placeholder(slot="some_slot") placeholder.save() # a good idea, if not strictly necessary # plugin in placeholder plugin_1 = add_plugin( placeholder, "TextPlugin", "en", body="01" ) # child of plugin_1 plugin_2 = add_plugin( placeholder, "TextPlugin", "en", body="02", target=plugin_1, ) # plugin_2 should be plugin_1's only child # for a single item we use assertSequenceEqual self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_1.pk).get_children(), [CMSPlugin.objects.get(id=plugin_2.pk)] ) # create a second child of plugin_1 plugin_3 = add_plugin( placeholder, "TextPlugin", "en", body="03", target=plugin_1 ) # plugin_2 & plugin_3 should be plugin_1's children # for multiple items we use assertSequenceEqual, because # assertSequenceEqual may re-order the list without warning self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_1.pk).get_children(), [ CMSPlugin.objects.get(id=plugin_2.pk), CMSPlugin.objects.get(id=plugin_3.pk), ]) # child of plugin_2 plugin_4 = add_plugin( placeholder, "TextPlugin", "en", body="04", target=plugin_2 ) # plugin_4 should be plugin_2's child self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_2.pk).get_children(), [CMSPlugin.objects.get(id=plugin_4.pk)]) # 2,3 & 4 should be descendants of 1 self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_1.pk).get_descendants(), [ # note path ordering of MP reflected here: CMSPlugin.objects.get(id=plugin_2.pk), CMSPlugin.objects.get(id=plugin_4.pk), CMSPlugin.objects.get(id=plugin_3.pk), ], ) # create a second root plugin plugin_5 = add_plugin(placeholder, "TextPlugin", "en", body="05") # child of plugin_5 plugin_6 = add_plugin( placeholder, "TextPlugin", "en", body="06", target=plugin_5 ) # plugin_6 should be plugin_5's child self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_5.pk).get_children(), [CMSPlugin.objects.get(id=plugin_6.pk)]) # child of plugin_5 plugin_7 = add_plugin( placeholder, "TextPlugin", "en", body="07", target=plugin_5 ) # plugin_7 should be plugin_5's child self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_5.pk).get_children(), [ CMSPlugin.objects.get(id=plugin_6.pk), CMSPlugin.objects.get(id=plugin_7.pk) ]) # 6 & 7 should be descendants of 5 self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_5.pk).get_descendants(), [ CMSPlugin.objects.get(id=plugin_6.pk), CMSPlugin.objects.get(id=plugin_7.pk), ]) # another child of plugin_2 plugin_2 = self.reload(plugin_2) plugin_8 = add_plugin( placeholder, "TextPlugin", "en", body="08", target=plugin_2 ) # plugin_4 should be plugin_2's child self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_2.pk).get_children(), [ CMSPlugin.objects.get(id=plugin_4.pk), CMSPlugin.objects.get(id=plugin_8.pk), ]) # child of plugin_3 plugin_9 = add_plugin( placeholder, "TextPlugin", "en", body="09", target=self.reload(plugin_3), ) # plugin_9 should be plugin_3's child self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_3.pk).get_children(), [CMSPlugin.objects.get(id=plugin_9.pk)]) # child of plugin_4 plugin_10 = add_plugin( placeholder, "TextPlugin", "en", body="10", target=self.reload(plugin_4) ) # plugin_10 should be plugin_4's child self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_4.pk).get_children(), [CMSPlugin.objects.get(id=plugin_10.pk)]) original_plugins = placeholder.get_plugins() self.assertEqual(original_plugins.count(), 10) # elder sibling of plugin_1 plugin_11 = add_plugin( placeholder, "TextPlugin", "en", body="11", target=plugin_1, position="left" ) self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_1.pk).get_children(), [ CMSPlugin.objects.get(id=plugin_2.pk), CMSPlugin.objects.get(id=plugin_3.pk) ]) # elder sibling of plugin_4 plugin_12 = add_plugin( placeholder, "TextPlugin", "en", body="12", target=self.reload(plugin_4), position="left" ) self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_2.pk).get_children(), [ CMSPlugin.objects.get(id=plugin_12.pk), CMSPlugin.objects.get(id=plugin_4.pk), CMSPlugin.objects.get(id=plugin_8.pk) ]) # younger sibling of plugin_7 plugin_13 = add_plugin( placeholder, "TextPlugin", "en", body="13", target=self.reload(plugin_7), position="right" ) self.assertSequenceEqual( CMSPlugin.objects.get(id=plugin_5.pk).get_children(), [ CMSPlugin.objects.get(id=plugin_6.pk), CMSPlugin.objects.get(id=plugin_7.pk), CMSPlugin.objects.get(id=plugin_13.pk) ]) # new sibling of plugin_5 plugin_14 = add_plugin( placeholder, "TextPlugin", "en", body="14" ) self.assertSequenceEqual( CMSPlugin.objects.filter(parent__isnull=True), [ CMSPlugin.objects.get(id=plugin_11.pk), CMSPlugin.objects.get(id=plugin_1.pk), CMSPlugin.objects.get(id=plugin_5.pk), CMSPlugin.objects.get(id=plugin_14.pk) ]) tree = [ (plugin_11.pk, None), (plugin_1.pk, None), (plugin_2.pk, plugin_1.pk), (plugin_12.pk, plugin_2.pk), (plugin_4.pk, plugin_2.pk), (plugin_10.pk, plugin_4.pk), (plugin_8.pk, plugin_2.pk), (plugin_3.pk, plugin_1.pk), (plugin_9.pk, plugin_3.pk), (plugin_5.pk, None), (plugin_6.pk, plugin_5.pk), (plugin_7.pk, plugin_5.pk), (plugin_13.pk, plugin_5.pk), (plugin_14.pk, None), ] self.copy_placeholders_and_check_results([placeholder]) self.compare_plugin_tree(tree, placeholder) # now let's move plugins around in the tree # move plugin_2 before plugin_11 tree = [ (plugin_2.pk, None), (plugin_12.pk, plugin_2.pk), (plugin_4.pk, plugin_2.pk), (plugin_10.pk, plugin_4.pk), (plugin_8.pk, plugin_2.pk), (plugin_11.pk, None), (plugin_1.pk, None), (plugin_3.pk, plugin_1.pk), (plugin_9.pk, plugin_3.pk), (plugin_5.pk, None), (plugin_6.pk, plugin_5.pk), (plugin_7.pk, plugin_5.pk), (plugin_13.pk, plugin_5.pk), (plugin_14.pk, None), ] placeholder.move_plugin(plugin=self.reload(plugin_2), target_position=1) self.compare_plugin_tree(tree, placeholder) self.copy_placeholders_and_check_results([placeholder]) # move plugin_6 after plugin_7 tree = [ (plugin_2.pk, None), (plugin_12.pk, plugin_2.pk), (plugin_4.pk, plugin_2.pk), (plugin_10.pk, plugin_4.pk), (plugin_8.pk, plugin_2.pk), (plugin_11.pk, None), (plugin_1.pk, None), (plugin_3.pk, plugin_1.pk), (plugin_9.pk, plugin_3.pk), (plugin_5.pk, None), (plugin_7.pk, plugin_5.pk), (plugin_6.pk, plugin_5.pk), (plugin_13.pk, plugin_5.pk), (plugin_14.pk, None), ] plugin_7 = self.reload(plugin_7) placeholder.move_plugin( plugin=self.reload(plugin_6), target_position=plugin_7.position, target_plugin=plugin_7.parent, ) self.compare_plugin_tree(tree, placeholder) self.copy_placeholders_and_check_results([placeholder]) # move plugin_3 before plugin_2 tree = [ (plugin_3.pk, None), (plugin_9.pk, plugin_3.pk), (plugin_2.pk, None), (plugin_12.pk, plugin_2.pk), (plugin_4.pk, plugin_2.pk), (plugin_10.pk, plugin_4.pk), (plugin_8.pk, plugin_2.pk), (plugin_11.pk, None), (plugin_1.pk, None), (plugin_5.pk, None), (plugin_7.pk, plugin_5.pk), (plugin_6.pk, plugin_5.pk), (plugin_13.pk, plugin_5.pk), (plugin_14.pk, None), ] placeholder.move_plugin( plugin=self.reload(plugin_3), target_position=1, target_plugin=None, ) self.compare_plugin_tree(tree, placeholder) self.copy_placeholders_and_check_results([placeholder]) # make plugin_3 plugin_2's first-child tree = [ (plugin_2.pk, None), (plugin_3.pk, plugin_2.pk), (plugin_9.pk, plugin_3.pk), (plugin_12.pk, plugin_2.pk), (plugin_4.pk, plugin_2.pk), (plugin_10.pk, plugin_4.pk), (plugin_8.pk, plugin_2.pk), (plugin_11.pk, None), (plugin_1.pk, None), (plugin_5.pk, None), (plugin_7.pk, plugin_5.pk), (plugin_6.pk, plugin_5.pk), (plugin_13.pk, plugin_5.pk), (plugin_14.pk, None), ] placeholder.move_plugin( plugin=self.reload(plugin_3), target_position=2, target_plugin=self.reload(plugin_2), ) self.compare_plugin_tree(tree, placeholder) self.copy_placeholders_and_check_results([placeholder]) # make plugin_7 plugin_2's first-child tree = [ (plugin_2.pk, None), (plugin_7.pk, plugin_2.pk), (plugin_3.pk, plugin_2.pk), (plugin_9.pk, plugin_3.pk), (plugin_12.pk, plugin_2.pk), (plugin_4.pk, plugin_2.pk), (plugin_10.pk, plugin_4.pk), (plugin_8.pk, plugin_2.pk), (plugin_11.pk, None), (plugin_1.pk, None), (plugin_5.pk, None), (plugin_6.pk, plugin_5.pk), (plugin_13.pk, plugin_5.pk), (plugin_14.pk, None), ] plugin_2 = self.reload(plugin_2) placeholder.move_plugin( plugin=self.reload(plugin_7), target_position=plugin_2.position + 1, target_plugin=plugin_2, ) self.compare_plugin_tree(tree, placeholder) self.copy_placeholders_and_check_results([placeholder]) def test_nested_plugin_on_page(self): """ Validate a textplugin with a nested link plugin mptt values are correctly showing a parent child relationship of a nested plugin """ with self.settings(CMS_PERMISSION=False): # setup page 1 page_one = create_page("Three Placeholder", "col_three.html", "en", position="last-child", in_navigation=True) page_one_ph_two = page_one.get_placeholders("en").get(slot="col_left") # add a plugin pre_nesting_body = "

the nested text plugin with a link inside

" text_plugin = add_plugin(page_one_ph_two, "TextPlugin", "en", body=pre_nesting_body) # prepare nesting plugin page_one_ph_two = self.reload(page_one_ph_two) text_plugin = self.reload(text_plugin) link_plugin = add_plugin(page_one_ph_two, "LinkPlugin", "en", target=text_plugin) link_plugin.name = "django-cms Link" link_plugin.external_link = "https://www.django-cms.org" # reloading needs to be done after every save link_plugin = self.reload(link_plugin) text_plugin = self.reload(text_plugin) # mptt related insertion correct? msg = "parent plugin right is not updated, child not inserted correctly" self.assertTrue(link_plugin.position == text_plugin.position + 1, msg=msg) msg = "link has no parent" self.assertFalse(link_plugin.parent is None, msg=msg) # add the link plugin to the body # emulate the editor in admin that adds some txt for the nested plugin in_txt = 'Link' nesting_body = f"{text_plugin.body}

{in_txt % (link_plugin.id)}

" text_plugin.body = nesting_body text_plugin.save() text_plugin = self.reload(text_plugin) # none of the descendants should have a placeholder other then my own one self.assertEqual(text_plugin.get_descendants().exclude(placeholder=text_plugin.placeholder).count(), 0) post_add_plugin_count = CMSPlugin.objects.count() self.assertEqual(post_add_plugin_count, 2) def test_copy_page_nested_plugin(self): """ Test to verify that page copy with a nested plugin works page one - 3 placeholder col_sidebar: 1 text plugin col_left: 1 text plugin with nested link plugin col_right: no plugin page two (copy target) Verify copied page, placeholders, plugins and body text """ with self.settings(CMS_PERMISSION=False): # setup page 1 page_one = create_page("Three Placeholder", "col_three.html", "en", position="last-child", in_navigation=True) page_one_ph_one = page_one.get_placeholders("en").get(slot="col_sidebar") page_one_ph_two = page_one.get_placeholders("en").get(slot="col_left") page_one.get_placeholders("en").get(slot="col_right") # add the text plugin to placeholder one text_plugin_en = add_plugin(page_one_ph_one, "TextPlugin", "en", body="Hello World") self.assertEqual(text_plugin_en.id, CMSPlugin.objects.all()[0].id) self.assertEqual(text_plugin_en.get_children().count(), 0) pre_add_plugin_count = CMSPlugin.objects.count() self.assertEqual(pre_add_plugin_count, 1) ### # add a plugin to placeholder two ### pre_nesting_body = "

the nested text plugin with a link inside

" text_plugin_two = add_plugin(page_one_ph_two, "TextPlugin", "en", body=pre_nesting_body) text_plugin_two = self.reload(text_plugin_two) # prepare nesting plugin page_one_ph_two = self.reload(page_one_ph_two) text_plugin_two = self.reload(text_plugin_two) link_plugin = add_plugin(page_one_ph_two, "LinkPlugin", "en", target=text_plugin_two) link_plugin.name = "django-cms Link" link_plugin.external_link = "https://www.django-cms.org" link_plugin.parent = text_plugin_two link_plugin.save() link_plugin = self.reload(link_plugin) text_plugin_two = self.reload(text_plugin_two) in_txt = """""" nesting_body = f"{text_plugin_two.body}

{in_txt % (link_plugin.id)}

" # emulate the editor in admin that adds some txt for the nested plugin text_plugin_two.body = nesting_body text_plugin_two.save() text_plugin_two = self.reload(text_plugin_two) # the link is attached as a child? self.assertEqual(text_plugin_two.get_children().count(), 1) post_add_plugin_count = CMSPlugin.objects.count() self.assertEqual(post_add_plugin_count, 3) page_one.save() # get the plugins from the original page page_one = self.reload(page_one) page_one_ph_one = page_one.get_placeholders("en").get(slot="col_sidebar") page_one_ph_two = page_one.get_placeholders("en").get(slot="col_left") page_one_ph_three = page_one.get_placeholders("en").get(slot="col_right") # verify that the plugins got created org_placeholder_one_plugins = page_one_ph_one.get_plugins() self.assertEqual(len(org_placeholder_one_plugins), 1) org_placeholder_two_plugins = page_one_ph_two.get_plugins() self.assertEqual(len(org_placeholder_two_plugins), 2) org_placeholder_three_plugins = page_one_ph_three.get_plugins() self.assertEqual(len(org_placeholder_three_plugins), 0) self.assertEqual(page_one.get_placeholders("en").count(), 3) placeholder_count = Placeholder.objects.count() self.assertEqual(placeholder_count, 3) self.assertEqual(CMSPlugin.objects.count(), 3) ## # setup page_copy_target page ## page_copy_target = create_page("Three Placeholder - page copy target", "col_three.html", "en", position="last-child", in_navigation=True) all_page_count = Page.objects.count() pre_copy_placeholder_count = Placeholder.objects.count() self.assertEqual(pre_copy_placeholder_count, 6) # copy the page superuser = self.get_superuser() with self.login_user_context(superuser): page_two = self.copy_page(page_one, page_copy_target) # validate the expected pages,placeholders,plugins,pluginbodies after_copy_page_plugin_count = CMSPlugin.objects.count() self.assertEqual(after_copy_page_plugin_count, 6) # check the amount of copied stuff after_copy_page_count = Page.objects.count() after_copy_placeholder_count = Placeholder.objects.count() self.assertGreater(after_copy_page_count, all_page_count, "no new page after copy") self.assertGreater(after_copy_page_plugin_count, post_add_plugin_count, "plugin count is not grown") self.assertGreater(after_copy_placeholder_count, pre_copy_placeholder_count, "placeholder count is not grown") self.assertEqual(after_copy_page_count, 3, "no new page after copy") # original placeholder page_one = self.reload(page_one) page_one_ph_one = page_one.get_placeholders("en").get(slot="col_sidebar") page_one_ph_two = page_one.get_placeholders("en").get(slot="col_left") page_one_ph_three = page_one.get_placeholders("en").get(slot="col_right") # check if there are multiple pages assigned to this placeholders found_page = page_one_ph_one.page if page_one_ph_one else None self.assertEqual(found_page, page_one) found_page = page_one_ph_two.page if page_one_ph_two else None self.assertEqual(found_page, page_one) found_page = page_one_ph_three.page if page_one_ph_three else None self.assertEqual(found_page, page_one) page_two = self.reload(page_two) page_two_ph_one = page_two.get_placeholders("en").get(slot="col_sidebar") page_two_ph_two = page_two.get_placeholders("en").get(slot="col_left") page_two_ph_three = page_two.get_placeholders("en").get(slot="col_right") # check if there are multiple pages assigned to this placeholders found_page = page_two_ph_one.page if page_two_ph_one else None self.assertEqual(found_page, page_two) found_page = page_two_ph_two.page if page_two_ph_two else None self.assertEqual(found_page, page_two) found_page = page_two_ph_three.page if page_two_ph_three else None self.assertEqual(found_page, page_two) # check the stored placeholders org vs copy msg = f'placehoder ids copy:{page_two_ph_one.pk} org:{page_one_ph_one.pk} copied page {page_two.pk} are identical - tree broken' self.assertNotEqual(page_two_ph_one.pk, page_one_ph_one.pk, msg) msg = f'placehoder ids copy:{page_two_ph_two.pk} org:{page_one_ph_two.pk} copied page {page_two.pk} are identical - tree broken' self.assertNotEqual(page_two_ph_two.pk, page_one_ph_two.pk, msg) msg = f'placehoder ids copy:{page_two_ph_three.pk} org:{page_one_ph_three.pk} copied page {page_two.pk} are identical - tree broken' self.assertNotEqual(page_two_ph_three.pk, page_one_ph_three.pk, msg) # get the plugins from the original page org_placeholder_one_plugins = page_one_ph_one.get_plugins() self.assertEqual(len(org_placeholder_one_plugins), 1) org_placeholder_two_plugins = page_one_ph_two.get_plugins() self.assertEqual(len(org_placeholder_two_plugins), 2) org_placeholder_three_plugins = page_one_ph_three.get_plugins() self.assertEqual(len(org_placeholder_three_plugins), 0) # get the plugins from the copied page copied_placeholder_one_plugins = page_two_ph_one.get_plugins() self.assertEqual(len(copied_placeholder_one_plugins), 1) copied_placeholder_two_plugins = page_two_ph_two.get_plugins() self.assertEqual(len(copied_placeholder_two_plugins), 2) copied_placeholder_three_plugins = page_two_ph_three.get_plugins() self.assertEqual(len(copied_placeholder_three_plugins), 0) # verify the plugins got copied # placeholder 1 count_plugins_copied = len(copied_placeholder_one_plugins) count_plugins_org = len(org_placeholder_one_plugins) msg = f"plugin count {count_plugins_copied} {count_plugins_org} for placeholder one not equal" self.assertEqual(count_plugins_copied, count_plugins_org, msg) # placeholder 2 count_plugins_copied = len(copied_placeholder_two_plugins) count_plugins_org = len(org_placeholder_two_plugins) msg = f"plugin count {count_plugins_copied} {count_plugins_org} for placeholder two not equal" self.assertEqual(count_plugins_copied, count_plugins_org, msg) # placeholder 3 count_plugins_copied = len(copied_placeholder_three_plugins) count_plugins_org = len(org_placeholder_three_plugins) msg = f"plugin count {count_plugins_copied} {count_plugins_org} for placeholder three not equal" self.assertEqual(count_plugins_copied, count_plugins_org, msg) # verify the body of text plugin with nested link plugin # org to copied org_nested_text_plugin = None # do this iteration to find the real text plugin with the attached link # the inheritance mechanism for the cmsplugins works through # (tuple)get_plugin_instance() for x in org_placeholder_two_plugins: if x.plugin_type == "TextPlugin": instance = x.get_plugin_instance()[0] if instance.body.startswith(pre_nesting_body): org_nested_text_plugin = instance break copied_nested_text_plugin = None for x in copied_placeholder_two_plugins: if x.plugin_type == "TextPlugin": instance = x.get_plugin_instance()[0] if instance.body.startswith(pre_nesting_body): copied_nested_text_plugin = instance break msg = "original nested text plugin not found" self.assertNotEqual(org_nested_text_plugin, None, msg=msg) msg = "copied nested text plugin not found" self.assertNotEqual(copied_nested_text_plugin, None, msg=msg) # get the children ids of the texplugin with a nested link # to check if the body of the text is generated correctly org_link_child_plugin = org_nested_text_plugin.get_children()[0] copied_link_child_plugin = copied_nested_text_plugin.get_children()[0] # validate the textplugin body texts msg = "org plugin and copied plugin are the same" self.assertTrue(org_link_child_plugin.id != copied_link_child_plugin.id, msg) needle = "%s" msg = "child plugin id differs to parent in body" # linked child is in body self.assertTrue(org_nested_text_plugin.body.find(needle % (org_link_child_plugin.id)) != -1, msg) msg = "copy: child plugin id differs to parent in body" self.assertTrue(copied_nested_text_plugin.body.find(needle % (copied_link_child_plugin.id)) != -1, msg) # really nothing else msg = "child link plugin id differs to parent body" self.assertTrue(org_nested_text_plugin.body.find(needle % (copied_link_child_plugin.id)) == -1, msg) msg = "copy: child link plugin id differs to parent body" self.assertTrue(copied_nested_text_plugin.body.find(needle % (org_link_child_plugin.id)) == -1, msg) # now reverse lookup the placeholders from the plugins org_placeholder = org_link_child_plugin.placeholder copied_placeholder = copied_link_child_plugin.placeholder msg = "placeholder of the original plugin and copied plugin are the same" ok = (org_placeholder.id != copied_placeholder.id) self.assertTrue(ok, msg) def test_copy_page_nested_plugin_moved_parent_plugin(self): """ Test to verify that page copy with a nested plugin works when a plugin with child got moved to another placeholder page one - 3 placeholder col_sidebar: 1 text plugin col_left: 1 text plugin with nested link plugin col_right: no plugin page two (copy target) step2: move the col_left text plugin to col_right col_sidebar: 1 text plugin col_left: no plugin col_right: 1 text plugin with nested link plugin verify the copied page structure """ with self.settings(CMS_PERMISSION=False): # setup page 1 page_one = create_page("Three Placeholder", "col_three.html", "en", position="last-child", in_navigation=True) page_one_ph_one = page_one.get_placeholders("en").get(slot="col_sidebar") page_one_ph_two = page_one.get_placeholders("en").get(slot="col_left") page_one.get_placeholders("en").get(slot="col_right") # add the text plugin to placeholder one text_plugin_en = add_plugin(page_one_ph_one, "TextPlugin", "en", body="Hello World") self.assertEqual(text_plugin_en.id, CMSPlugin.objects.all()[0].id) self.assertEqual(text_plugin_en.get_children().count(), 0) pre_add_plugin_count = CMSPlugin.objects.count() self.assertEqual(pre_add_plugin_count, 1) # add a plugin to placeholder twho pre_nesting_body = "

the nested text plugin with a link inside

" text_plugin_two = add_plugin(page_one_ph_two, "TextPlugin", "en", body=pre_nesting_body) text_plugin_two = self.reload(text_plugin_two) # prepare nesting plugin page_one_ph_two = self.reload(page_one_ph_two) text_plugin_two = self.reload(text_plugin_two) link_plugin = add_plugin(page_one_ph_two, "LinkPlugin", "en", target=text_plugin_two) link_plugin.name = "django-cms Link" link_plugin.external_link = "https://www.django-cms.org" link_plugin.parent = text_plugin_two link_plugin.save() # reload after every save link_plugin = self.reload(link_plugin) text_plugin_two = self.reload(text_plugin_two) in_txt = """""" nesting_body = f"{text_plugin_two.body}

{in_txt % (link_plugin.id)}

" # emulate the editor in admin that adds some txt for the nested plugin text_plugin_two.body = nesting_body text_plugin_two.save() text_plugin_two = self.reload(text_plugin_two) # the link is attached as a child? self.assertEqual(text_plugin_two.get_children().count(), 1) post_add_plugin_count = CMSPlugin.objects.count() self.assertEqual(post_add_plugin_count, 3) page_one.save() # get the plugins from the original page page_one = self.reload(page_one) page_one_ph_one = page_one.get_placeholders("en").get(slot="col_sidebar") page_one_ph_two = page_one.get_placeholders("en").get(slot="col_left") page_one_ph_three = page_one.get_placeholders("en").get(slot="col_right") # verify the plugins got created org_placeholder_one_plugins = page_one_ph_one.get_plugins() self.assertEqual(len(org_placeholder_one_plugins), 1) org_placeholder_two_plugins = page_one_ph_two.get_plugins() self.assertEqual(len(org_placeholder_two_plugins), 2) org_placeholder_three_plugins = page_one_ph_three.get_plugins() self.assertEqual(len(org_placeholder_three_plugins), 0) self.assertEqual(page_one.get_placeholders("en").count(), 3) placeholder_count = Placeholder.objects.count() self.assertEqual(placeholder_count, 3) self.assertEqual(CMSPlugin.objects.count(), 3) # setup page_copy_target page_copy_target = create_page("Three Placeholder - page copy target", "col_three.html", "en", position="last-child", in_navigation=True) all_page_count = Page.objects.count() pre_copy_placeholder_count = Placeholder.objects.count() self.assertEqual(pre_copy_placeholder_count, 6) superuser = self.get_superuser() with self.login_user_context(superuser): # now move the parent text plugin to another placeholder post_data = { 'placeholder_id': page_one_ph_three.id, 'plugin_id': text_plugin_two.id, 'target_language': 'en', 'target_position': page_one_ph_three.get_next_plugin_position('en', insert_order='last'), 'plugin_parent': '', } edit_url = self.get_move_plugin_uri(text_plugin_two) response = self.client.post(edit_url, post_data) self.assertEqual(response.status_code, 200) # check if the plugin got moved page_one = self.reload(page_one) self.reload(text_plugin_two) page_one_ph_one = page_one.get_placeholders("en").get(slot="col_sidebar") page_one_ph_two = page_one.get_placeholders("en").get(slot="col_left") page_one_ph_three = page_one.get_placeholders("en").get(slot="col_right") org_placeholder_one_plugins = page_one_ph_one.get_plugins() self.assertEqual(len(org_placeholder_one_plugins), 1) org_placeholder_two_plugins = page_one_ph_two.get_plugins() # the plugin got moved and child got moved self.assertEqual(len(org_placeholder_two_plugins), 0) org_placeholder_three_plugins = page_one_ph_three.get_plugins() self.assertEqual(len(org_placeholder_three_plugins), 2) # copy the page page_two = self.copy_page(page_one, page_copy_target) # validate the expected pages,placeholders,plugins,pluginbodies after_copy_page_plugin_count = CMSPlugin.objects.count() self.assertEqual(after_copy_page_plugin_count, 6) after_copy_page_count = Page.objects.count() after_copy_placeholder_count = Placeholder.objects.count() self.assertGreater(after_copy_page_count, all_page_count, "no new page after copy") self.assertGreater(after_copy_page_plugin_count, post_add_plugin_count, "plugin count is not grown") self.assertGreater(after_copy_placeholder_count, pre_copy_placeholder_count, "placeholder count is not grown") self.assertEqual(after_copy_page_count, 3, "no new page after copy") # validate the structure # original placeholder page_one = self.reload(page_one) page_one_ph_one = page_one.get_placeholders("en").get(slot="col_sidebar") page_one_ph_two = page_one.get_placeholders("en").get(slot="col_left") page_one_ph_three = page_one.get_placeholders("en").get(slot="col_right") # check if there are multiple pages assigned to this placeholders found_page = page_one_ph_one.page if page_one_ph_one else None self.assertEqual(found_page, page_one) found_page = page_one_ph_two.page if page_one_ph_two else None self.assertEqual(found_page, page_one) found_page = page_one_ph_three.page if page_one_ph_three else None self.assertEqual(found_page, page_one) page_two = self.reload(page_two) page_two_ph_one = page_two.get_placeholders("en").get(slot="col_sidebar") page_two_ph_two = page_two.get_placeholders("en").get(slot="col_left") page_two_ph_three = page_two.get_placeholders("en").get(slot="col_right") # check if there are multiple pages assigned to this placeholders found_page = page_two_ph_one.page if page_two_ph_one else None self.assertEqual(found_page, page_two) found_page = page_two_ph_two.page if page_two_ph_two else None self.assertEqual(found_page, page_two) found_page = page_two_ph_three.page if page_two_ph_three else None self.assertEqual(found_page, page_two) # check the stored placeholders org vs copy msg = f'placehoder ids copy:{page_two_ph_one.pk} org:{page_one_ph_one.pk} copied page {page_two.pk} are identical - tree broken' self.assertNotEqual(page_two_ph_one.pk, page_one_ph_one.pk, msg) msg = f'placehoder ids copy:{page_two_ph_two.pk} org:{page_one_ph_two.pk} copied page {page_two.pk} are identical - tree broken' self.assertNotEqual(page_two_ph_two.pk, page_one_ph_two.pk, msg) msg = f'placehoder ids copy:{page_two_ph_three.pk} org:{page_one_ph_three.pk} copied page {page_two.pk} are identical - tree broken' self.assertNotEqual(page_two_ph_three.pk, page_one_ph_three.pk, msg) # get the plugins from the original page org_placeholder_one_plugins = page_one_ph_one.get_plugins() self.assertEqual(len(org_placeholder_one_plugins), 1) org_placeholder_two_plugins = page_one_ph_two.get_plugins() self.assertEqual(len(org_placeholder_two_plugins), 0) org_placeholder_three_plugins = page_one_ph_three.get_plugins() self.assertEqual(len(org_placeholder_three_plugins), 2) # get the plugins from the copied page copied_placeholder_one_plugins = page_two_ph_one.get_plugins() self.assertEqual(len(copied_placeholder_one_plugins), 1) copied_placeholder_two_plugins = page_two_ph_two.get_plugins() self.assertEqual(len(copied_placeholder_two_plugins), 0) copied_placeholder_three_plugins = page_two_ph_three.get_plugins() self.assertEqual(len(copied_placeholder_three_plugins), 2) # verify the plugins got copied # placeholder 1 count_plugins_copied = len(copied_placeholder_one_plugins) count_plugins_org = len(org_placeholder_one_plugins) msg = f"plugin count {count_plugins_copied} {count_plugins_org} for placeholder one not equal" self.assertEqual(count_plugins_copied, count_plugins_org, msg) # placeholder 2 count_plugins_copied = len(copied_placeholder_two_plugins) count_plugins_org = len(org_placeholder_two_plugins) msg = f"plugin count {count_plugins_copied} {count_plugins_org} for placeholder two not equal" self.assertEqual(count_plugins_copied, count_plugins_org, msg) # placeholder 3 count_plugins_copied = len(copied_placeholder_three_plugins) count_plugins_org = len(org_placeholder_three_plugins) msg = f"plugin count {count_plugins_copied} {count_plugins_org} for placeholder three not equal" self.assertEqual(count_plugins_copied, count_plugins_org, msg) # verify the body of text plugin with nested link plugin # org to copied org_nested_text_plugin = None # do this iteration to find the real text plugin with the attached link # the inheritance mechanism for the cmsplugins works through # (tuple)get_plugin_instance() for x in org_placeholder_three_plugins: if x.plugin_type == "TextPlugin": instance = x.get_plugin_instance()[0] if instance.body.startswith(pre_nesting_body): org_nested_text_plugin = instance break copied_nested_text_plugin = None for x in copied_placeholder_three_plugins: if x.plugin_type == "TextPlugin": instance = x.get_plugin_instance()[0] if instance.body.startswith(pre_nesting_body): copied_nested_text_plugin = instance break msg = "original nested text plugin not found" self.assertNotEqual(org_nested_text_plugin, None, msg=msg) msg = "copied nested text plugin not found" self.assertNotEqual(copied_nested_text_plugin, None, msg=msg) # get the children ids of the texplugin with a nested link # to check if the body of the text is generated correctly org_link_child_plugin = org_nested_text_plugin.get_children()[0] copied_link_child_plugin = copied_nested_text_plugin.get_children()[0] # validate the textplugin body texts msg = "org plugin and copied plugin are the same" self.assertNotEqual(org_link_child_plugin.id, copied_link_child_plugin.id, msg) needle = "%s" msg = "child plugin id differs to parent in body" # linked child is in body self.assertTrue(org_nested_text_plugin.body.find(needle % (org_link_child_plugin.id)) != -1, msg) msg = "copy: child plugin id differs to parent in body plugin_obj_id" self.assertTrue(copied_nested_text_plugin.body.find(needle % (copied_link_child_plugin.id)) != -1, msg) # really nothing else msg = "child link plugin id differs to parent body" self.assertTrue(org_nested_text_plugin.body.find(needle % (copied_link_child_plugin.id)) == -1, msg) msg = "copy: child link plugin id differs to parent body" self.assertTrue(copied_nested_text_plugin.body.find(needle % (org_link_child_plugin.id)) == -1, msg) # now reverse lookup the placeholders from the plugins org_placeholder = org_link_child_plugin.placeholder copied_placeholder = copied_link_child_plugin.placeholder msg = "placeholder of the original plugin and copied plugin are the same" self.assertNotEqual(org_placeholder.id, copied_placeholder.id, msg) def test_add_child_plugin(self): page_one = create_page("Three Placeholder", "col_three.html", "en", position="last-child", in_navigation=True) page_one_ph_one = page_one.get_placeholders("en").get(slot="col_sidebar") # add the text plugin to placeholder one text_plugin_en = add_plugin(page_one_ph_one, "TextPlugin", "en", body="Hello World") superuser = self.get_superuser() with self.login_user_context(superuser): post_data = { 'name': 'test', 'external_link': 'http://www.example.org/' } add_url = self.get_add_plugin_uri(page_one_ph_one, 'LinkPlugin', parent=text_plugin_en) response = self.client.post(add_url, post_data) self.assertEqual(response.status_code, 200) self.assertTemplateUsed( response, 'admin/cms/page/plugin/confirm_form.html' ) link_plugin = CMSPlugin.objects.get(parent_id=text_plugin_en.pk) self.assertEqual(link_plugin.parent_id, text_plugin_en.pk) self.assertEqual(link_plugin.position, text_plugin_en.position + 1) def test_plugin_deep_nesting_and_copying_issue_position_parent_child_discrepency(self): """ Captures an edge case issue where plugins have been seen to have a higher position than their parent. When the placeholder is copied the parent defaults to None because the plugin is not yet created / remapped. Plugins first created in this order: Plugin 1 (pk1, position 1) Plugin 2 (pk2, position 2) Plugin 3 (pk3, position 3) Then a top level plugin is made a child of another. The result is a child with a lower id and higher position that it's parent. Plugin 1 (pk1, position 1) Plugin 3 has children (pk3, position 3) Plugin 2 (pk2, position 2) """ placeholder = Placeholder(slot="some_slot") placeholder.save() # plugins in placeholder plugin_1 = add_plugin(placeholder, "TextPlugin", "en", body="01") plugin_2 = add_plugin(placeholder, "TextPlugin", "en", body="02") plugin_3 = add_plugin(placeholder, "TextPlugin", "en", body="03") expected_tree = [ (plugin_1.pk, None), (plugin_2.pk, None), (plugin_3.pk, None), ] self.copy_placeholders_and_check_results([placeholder]) self.compare_plugin_tree(expected_tree, placeholder) plugin_2.parent = plugin_3 plugin_2.save() self.reload(plugin_2) self.reload(plugin_3) expected_tree = [ (plugin_1.pk, None), (plugin_2.pk, plugin_3.pk), (plugin_3.pk, None), ] placeholder._recalculate_plugin_positions("en") self.copy_placeholders_and_check_results([placeholder]) self.compare_plugin_tree(expected_tree, placeholder) def test_recalculate_plugin_positions(self): """ Test to verify that the plugin positions are recalculated correctly when a plugin is moved to another placeholder. """ import random placeholder = Placeholder(slot="some_slot") placeholder.save() # plugins in placeholder n0 = 10 # Number of plugins in the first positions n = 50 # Number of plugins after the gap (of at least n) order = list(random.sample(range(1, n0 + 1), n0)) order += list(random.sample(range(n0 + n + 1, n0 + 3 * n + 2), n)) for lang in ("en", "it"): for i, pos in enumerate(order): CMSPlugin.objects.create( plugin_type=str(i), placeholder=placeholder, language=lang, position=pos, parent=None, ) placeholder._recalculate_plugin_positions("en") plugins = placeholder.get_plugins("en").order_by("position") for i, plugin in enumerate(plugins, start=1): self.assertEqual(plugin.position, i) def test_recalculate_plugin_positions_with_gap_and_reversed_order(self): """Regression test for GitHub issue #8665. When the position sequence contains a gap, the old single-statement compaction could assign a position that another, not-yet-processed row still held, raising an ``IntegrityError`` (duplicate position) mid-statement. The order in which rows are processed — and therefore whether it failed — depended on the query plan. This builds the deterministic worst case: a gap at position 1 with positions that strictly *decrease* as the id increases. A naive row-by-row compaction in id order then tries to write a position that a higher-id row still occupies (e.g. the first row, at position n+1, is compacted to n, which the second row still holds). The two-phase park-then-unpark recalculation must complete without error and produce a dense 1..n. """ placeholder = Placeholder(slot="some_slot") placeholder.save() n = 6 # Positions n+1, n, ..., 2 assigned in ascending-id order: a gap at position 1 and a # descending position/id correlation. Keep a second language with its own gap to make # sure the recalculation is scoped to the requested language only. created = [] for i in range(n): created.append( CMSPlugin.objects.create( plugin_type=str(i), placeholder=placeholder, language="en", position=n + 1 - i, # n+1, n, ..., 2 parent=None, ) ) it_plugin = CMSPlugin.objects.create( plugin_type="it", placeholder=placeholder, language="it", position=5, parent=None ) # Must not raise IntegrityError, regardless of backend / row-processing order. placeholder._recalculate_plugin_positions("en") positions = list( placeholder.get_plugins("en").order_by("position").values_list("position", flat=True) ) # Dense 1..n, no negative leftovers from the parking phase, no duplicates. self.assertEqual(positions, list(range(1, n + 1))) # Order is preserved: the row originally at the smallest position ends up first. ordered_ids = list( placeholder.get_plugins("en").order_by("position").values_list("pk", flat=True) ) self.assertEqual(ordered_ids, [p.pk for p in reversed(created)]) # The other language was left untouched. it_plugin.refresh_from_db() self.assertEqual(it_plugin.position, 5) def test_move_plugin_heals_preexisting_gap(self): """Regression test for GitHub issue #8665 via the move path. ``move_plugin`` shifts plugins (temporarily into negative positions) and then calls ``_recalculate_plugin_positions``. If the placeholder already contains a gap — e.g. because third-party code deleted a plugin with a plain ``plugin.delete()`` instead of ``placeholder.delete_plugin()`` — the recalculation must still compact to a dense ``1..n`` rather than raising an ``IntegrityError``. """ placeholder = Placeholder(slot="some_slot") placeholder.save() plugins = [add_plugin(placeholder, "TextPlugin", "en", body=f"plugin {i}") for i in range(5)] # Simulate a third-party plain delete that leaves a gap (no position recalculation). CMSPlugin.objects.filter(pk=plugins[1].pk).delete() gappy = list( placeholder.get_plugins("en").order_by("position").values_list("position", flat=True) ) self.assertEqual(gappy, [1, 3, 4, 5]) # gap at position 2 # Move the last remaining plugin to the top. Must not raise and must compact the gap away. placeholder.move_plugin(plugins[4].cmsplugin_ptr, target_position=1) positions = list( placeholder.get_plugins("en").order_by("position").values_list("position", flat=True) ) self.assertEqual(positions, [1, 2, 3, 4]) def test_move_to_placeholder_heals_preexisting_gap(self): """Regression test for GitHub issue #8665 via the cross-placeholder move path. Moving a plugin to another placeholder reassigns it and recalculates positions in *both* placeholders. If either one already contains a gap (e.g. from a third-party plain ``plugin.delete()``), the recalculation must still compact both to a dense ``1..n`` instead of raising an ``IntegrityError``. """ source = Placeholder(slot="source") source.save() target = Placeholder(slot="target") target.save() source_plugins = [add_plugin(source, "TextPlugin", "en", body=f"s{i}") for i in range(5)] target_plugins = [add_plugin(target, "TextPlugin", "en", body=f"t{i}") for i in range(5)] # Simulate third-party plain deletes that leave gaps in both placeholders. CMSPlugin.objects.filter(pk=source_plugins[1].pk).delete() CMSPlugin.objects.filter(pk=target_plugins[3].pk).delete() moved = source_plugins[4].cmsplugin_ptr moved.refresh_from_db() # Must not raise and must leave both placeholders dense. source.move_plugin(moved, target_position=2, target_placeholder=target) source_positions = list( source.get_plugins("en").order_by("position").values_list("position", flat=True) ) target_positions = list( target.get_plugins("en").order_by("position").values_list("position", flat=True) ) self.assertEqual(source_positions, [1, 2, 3]) # 5 created, 1 deleted, 1 moved out self.assertEqual(target_positions, [1, 2, 3, 4, 5]) # 5 created, 1 deleted, 1 moved in, healed # The moved plugin landed at the requested target position. moved.refresh_from_db() self.assertEqual(moved.placeholder_id, target.pk) self.assertEqual(moved.position, 2) def test_delete_plugins_multiple_blocks(self): """``delete_plugins`` removes several non-adjacent plugins and recompacts the placeholder.""" placeholder = Placeholder(slot="slot") placeholder.save() plugins = [add_plugin(placeholder, "TextPlugin", "en", body=str(i)) for i in range(6)] # Delete the 2nd and 4th plugins (positions 2 and 4). placeholder.delete_plugins([plugins[1].cmsplugin_ptr, plugins[3].cmsplugin_ptr]) remaining = list(placeholder.get_plugins("en").order_by("position").values_list("pk", "position")) self.assertEqual( remaining, [(plugins[0].pk, 1), (plugins[2].pk, 2), (plugins[4].pk, 3), (plugins[5].pk, 4)], ) def test_delete_plugins_with_descendants_and_overlap(self): """Passing a parent and one of its descendants deletes the whole subtree once, no error.""" placeholder = Placeholder(slot="slot") placeholder.save() before = add_plugin(placeholder, "TextPlugin", "en", body="before") parent = add_plugin(placeholder, "TextPlugin", "en", body="parent") child1 = add_plugin(placeholder, "TextPlugin", "en", target=parent, body="c1") add_plugin(placeholder, "TextPlugin", "en", target=parent, body="c2") after = add_plugin(placeholder, "TextPlugin", "en", body="after") # Overlapping selection: the parent and one of its children. The whole subtree is removed # exactly once and the remaining plugins are recompacted. placeholder.delete_plugins([parent.cmsplugin_ptr, child1.cmsplugin_ptr]) remaining = list(placeholder.get_plugins("en").order_by("position").values_list("pk", "position")) self.assertEqual(remaining, [(before.pk, 1), (after.pk, 2)]) def test_delete_plugins_cross_language(self): """``delete_plugins`` recompacts each affected language and leaves untouched ones alone.""" placeholder = Placeholder(slot="slot") placeholder.save() en = [add_plugin(placeholder, "TextPlugin", "en", body=f"en{i}") for i in range(4)] de = [add_plugin(placeholder, "TextPlugin", "de", body=f"de{i}") for i in range(4)] for _i in range(3): add_plugin(placeholder, "TextPlugin", "it", body="it") # One plugin from each of two languages, in a single call. placeholder.delete_plugins([en[1].cmsplugin_ptr, de[0].cmsplugin_ptr]) for language in ("en", "de"): self.assertEqual( list(placeholder.get_plugins(language).order_by("position").values_list("position", flat=True)), [1, 2, 3], ) # The untouched language is intact. self.assertEqual( list(placeholder.get_plugins("it").order_by("position").values_list("position", flat=True)), [1, 2, 3], ) def test_delete_plugins_trailing_and_empty(self): """Deleting the trailing plugins leaves a dense placeholder; empty input is a no-op.""" placeholder = Placeholder(slot="slot") placeholder.save() plugins = [add_plugin(placeholder, "TextPlugin", "en", body=str(i)) for i in range(4)] placeholder.delete_plugins([]) # no-op self.assertEqual(placeholder.get_plugins("en").count(), 4) # Delete the last two (trailing): no gap is opened, the rest stays dense. placeholder.delete_plugins([plugins[2].cmsplugin_ptr, plugins[3].cmsplugin_ptr]) self.assertEqual( list(placeholder.get_plugins("en").order_by("position").values_list("position", flat=True)), [1, 2], ) def test_recalculate_plugin_positions_empty_placeholder(self): """ Test to verify that recalculating plugin positions on an empty placeholder doesn't error and leaves the plugin list empty for a given language. """ # Create an empty placeholder for language "en" placeholder = Placeholder(slot="empty_slot") placeholder.save() # Invoke the recalculation function for the language "en". # Adjust the call to _recalculate_plugin_positions as needed. placeholder._recalculate_plugin_positions("en") # Assert that the plugin list for the placeholder in language "en" remains empty. plugins = list(placeholder.get_plugins(language="en")) self.assertEqual(plugins, [])