Repository: django-cms/django-cms

Issue:
Copying page does not copy it's extensions
When copying a page, it's page and title extensions do not get copied over. Not sure if this was on purpose though..



Discussion:
Sounds like page/title extensions need something like http://docs.django-cms.org/en/latest/extending_cms/custom_plugins.html#copy-relations (a hook to copy themselves over to the new page/title).

They have `copy_relation` http://django-cms.readthedocs.org/en/latest/extending_cms/extending_page_title.html#handling-relations
But as of now it's called only when publishing / unpublishing the page


Resolving patch:
diff --git a/cms/extensions/extension_pool.py b/cms/extensions/extension_pool.py
--- a/cms/extensions/extension_pool.py
+++ b/cms/extensions/extension_pool.py
@@ -63,20 +63,34 @@ def _receiver(self, sender, **kwargs):
             self._copy_page_extensions(draft_page, public_page, language)
             self._remove_orphaned_page_extensions()
         if self.title_extensions:
-            self._copy_title_extensions(draft_page, public_page, language)
+            self._copy_title_extensions(draft_page, None, language)
             self._remove_orphaned_title_extensions()
 
-    def _copy_page_extensions(self, draft_page, public_page, language):
+    def _copy_page_extensions(self, source_page, target_page, language):
         for extension in self.page_extensions:
-            for instance in extension.objects.filter(extended_object=draft_page):
-                instance.copy_to_public(public_page, language)
+            for instance in extension.objects.filter(extended_object=source_page):
+                instance.copy_to_public(target_page, language)
 
-    def _copy_title_extensions(self, draft_page, public_page, language):
-        draft_title = draft_page.title_set.get(language=language)
-        public_title = draft_title.publisher_public
+    def _copy_title_extensions(self, source_page, target_page, language):
+        source_title = source_page.title_set.get(language=language)
+        if target_page:
+            target_title = target_page.title_set.get(language=language)
+        else:
+            target_title = source_title.publisher_public
         for extension in self.title_extensions:
-            for instance in extension.objects.filter(extended_object=draft_title):
-                instance.copy_to_public(public_title, language)
+            for instance in extension.objects.filter(extended_object=source_title):
+                instance.copy_to_public(target_title, language)
+
+    def copy_extensions(self, source_page, target_page, languages=None):
+        if not languages:
+            languages = source_page.get_languages()
+        if self.page_extensions:
+            self._copy_page_extensions(source_page, target_page, None)
+            self._remove_orphaned_page_extensions()
+        for language in languages:
+            if self.title_extensions:
+                self._copy_title_extensions(source_page, target_page, language)
+                self._remove_orphaned_title_extensions()
 
     def _remove_orphaned_page_extensions(self):
         for extension in self.page_extensions:
@@ -92,6 +106,23 @@ def _remove_orphaned_title_extensions(self):
                 draft_extension=None
             ).delete()
 
+    def get_page_extensions(self, page=None):
+        extensions = []
+        for extension in self.page_extensions:
+            if page:
+                extensions.extend(list(extension.objects.filter(extended_object=page)))
+            else:
+                extensions.extend(list(extension.objects.all()))
+        return extensions
+
+    def get_title_extensions(self, title=None):
+        extensions = []
+        for extension in self.title_extensions:
+            if title:
+                extensions.extend(list(extension.objects.filter(extended_object=title)))
+            else:
+                extensions.extend(list(extension.objects.all()))
+        return extensions
 
 extension_pool = ExtensionPool()
 
diff --git a/cms/models/pagemodel.py b/cms/models/pagemodel.py
--- a/cms/models/pagemodel.py
+++ b/cms/models/pagemodel.py
@@ -309,6 +309,7 @@ def copy_page(self, target, site, position='first-child',
         Note for issue #1166: when copying pages there is no need to check for
         conflicting URLs as pages are copied unpublished.
         """
+        from cms.extensions import extension_pool
         pages = [self] + list(self.get_descendants().order_by('-rght'))
 
         site_reverse_ids = Page.objects.filter(site=site, reverse_id__isnull=False).values_list('reverse_id', flat=True)
@@ -327,6 +328,7 @@ def copy_page(self, target, site, position='first-child',
             tree[0].old_pk = tree[0].pk
 
         first = True
+        first_page = None
         # loop over all affected pages (self is included in descendants)
         for page in pages:
             titles = list(page.title_set.all())
@@ -352,6 +354,7 @@ def copy_page(self, target, site, position='first-child',
                 else:
                     page.parent = None
                 page.insert_at(target, position)
+                first_page = page
             else:
                 count = 1
                 found = False
@@ -415,8 +418,10 @@ def copy_page(self, target, site, position='first-child',
                     page.placeholders.add(ph)
                 if plugins:
                     copy_plugins_to(plugins, ph)
+            extension_pool.copy_extensions(Page.objects.get(pk=origin_id), page)
         # invalidate the menu for this site
         menu_pool.clear(site_id=site.pk)
+        return first_page
 
     def save(self, no_signals=False, commit=True, **kwargs):
         """


Test patch:
diff --git a/cms/tests/extensions.py b/cms/tests/extensions.py
--- a/cms/tests/extensions.py
+++ b/cms/tests/extensions.py
@@ -91,6 +91,53 @@ class TestNoneExtension(object):
 
         return TestNoneExtension
 
+    def test_copy_extensions(self):
+        root = create_page('Root', "nav_playground.html", "en", published=True)
+        page = create_page('Test Page Extension', "nav_playground.html", "en",
+                           parent=root.get_draft_object())
+        subpage = create_page('Test subpage Extension', "nav_playground.html", "en",
+                              parent=page)
+        page = Page.objects.get(pk=page.pk)
+        page_extension = MyPageExtension(extended_object=page, extra='page extension 1')
+        page_extension.save()
+        page.mypageextension = page_extension
+        title = page.get_title_obj()
+        title_extension = MyTitleExtension(extended_object=title, extra_title='title extension 1')
+        title_extension.save()
+        page.mytitleextension = title_extension
+
+        subpage_extension = MyPageExtension(extended_object=subpage, extra='page extension 2')
+        subpage_extension.save()
+        subpage.mypageextension = subpage_extension
+        subtitle = subpage.get_title_obj()
+        subtitle_extension = MyTitleExtension(extended_object=subtitle, extra_title='title extension 2')
+        subtitle_extension.save()
+        subpage.mytitleextension = subtitle_extension
+
+        # asserting original extensions
+        self.assertEqual(len(extension_pool.get_page_extensions()), 2)
+        self.assertEqual(len(extension_pool.get_title_extensions()), 2)
+
+        copied_page = page.copy_page(root.get_draft_object(), page.site, position='last-child')
+
+        # asserting original + copied extensions
+        self.assertEqual(len(extension_pool.get_page_extensions()), 4)
+        self.assertEqual(len(extension_pool.get_title_extensions()), 4)
+
+        # testing extension content
+        old_page_extensions = [page_extension, subpage_extension]
+        old_title_extension = [title_extension, subtitle_extension]
+        for index, new_page in enumerate([copied_page] + list(copied_page.get_descendants())):
+            self.assertEqual(extension_pool.get_page_extensions(new_page)[0].extra,
+                             old_page_extensions[index].extra)
+            self.assertEqual(extension_pool.get_title_extensions(new_page.title_set.get(language='en'))[0].extra_title,
+                             old_title_extension[index].extra_title)
+            # check that objects are actually different
+            self.assertNotEqual(extension_pool.get_page_extensions(new_page)[0].pk,
+                                old_page_extensions[index].pk)
+            self.assertNotEqual(extension_pool.get_title_extensions(new_page.title_set.get(language='en'))[0].pk,
+                                old_title_extension[index].pk)
+
     def test_publish_page_extension(self):
         page = create_page('Test Page Extension', "nav_playground.html", "en")
         page_extension = MyPageExtension(extended_object=page, extra='page extension 1')