Repository: django-cms/django-cms

Issue:
management command lang_copy
added the management command lang_copy to copy the whole content from one lang to another



Discussion:

Resolving patch:
diff --git a/cms/api.py b/cms/api.py
--- a/cms/api.py
+++ b/cms/api.py
@@ -7,6 +7,7 @@
 calling these methods!
 """
 import datetime
+from cms.utils import copy_plugins
 from cms.utils.compat.type_checks import string_types
 from cms.utils.conf import get_cms_setting
 from django.core.exceptions import PermissionDenied, ValidationError
@@ -385,6 +386,11 @@ def get_page_draft(page):
     """
     Returns the draft version of a page, regardless if the passed in
     page is a published version or a draft version.
+
+    :param page: The page to get the draft version
+    :type page: :class:`cms.models.pagemodel.Page` instance
+    :return page: draft version of the page
+    :type page: :class:`cms.models.pagemodel.Page` instance
     """
     if page:
         if page.publisher_is_draft:
@@ -393,3 +399,37 @@ def get_page_draft(page):
             return page.publisher_draft
     else:
         return None
+
+
+def copy_plugins_to_language(page, source_language, target_language,
+                             only_empty=True):
+    """
+    Copy the plugins to another language in the same page for all the page
+    placeholders.
+
+    By default plugins are copied only if placeholder has no plugin for the
+    target language; use ``only_empty=False`` to change this.
+
+    .. warning: This function skips permissions checks
+
+    :param page: the page to copy
+    :type page: :class:`cms.models.pagemodel.Page` instance
+    :param string source_language: The source language code,
+     must be in :setting:`django:LANGUAGES`
+    :param string target_language: The source language code,
+     must be in :setting:`django:LANGUAGES`
+    :param bool only_empty: if False, plugin are copied even if
+     plugins exists in the target language (on a placeholder basis).
+    :return int: number of copied plugins
+    """
+    copied = 0
+    placeholders = page.placeholders.all()
+    for placeholder in placeholders:
+        # only_empty is True we check if the placeholder already has plugins and
+        # we skip it if has some
+        if not only_empty or not placeholder.cmsplugin_set.filter(language=target_language).exists():
+            plugins = list(
+                placeholder.cmsplugin_set.filter(language=source_language).order_by('tree_id', 'level', 'position'))
+            copied_plugins = copy_plugins.copy_plugins_to(plugins, placeholder, target_language)
+            copied += len(copied_plugins)
+    return copied
\ No newline at end of file
diff --git a/cms/management/commands/cms.py b/cms/management/commands/cms.py
--- a/cms/management/commands/cms.py
+++ b/cms/management/commands/cms.py
@@ -6,6 +6,7 @@
 from cms.management.commands.subcommands.moderator import ModeratorCommand
 from cms.management.commands.subcommands.uninstall import UninstallCommand
 from cms.management.commands.subcommands.mptt import FixMPTTCommand
+from cms.management.commands.subcommands.copy_lang import CopyLangCommand
 from cms.management.commands.subcommands.delete_orphaned_plugins import DeleteOrphanedPluginsCommand
 from django.core.management.base import BaseCommand
 from optparse import make_option
@@ -27,6 +28,7 @@ class Command(SubcommandsCommand):
         'list': ListCommand,
         'moderator': ModeratorCommand,
         'fix-mptt': FixMPTTCommand,
+        'copy-lang': CopyLangCommand,
         'delete_orphaned_plugins': DeleteOrphanedPluginsCommand,
         'check': CheckInstallation,
     }
diff --git a/cms/management/commands/subcommands/copy_lang.py b/cms/management/commands/subcommands/copy_lang.py
new file mode 100644
--- /dev/null
+++ b/cms/management/commands/subcommands/copy_lang.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+from optparse import make_option
+
+from django.conf import settings
+
+from django.core.management.base import BaseCommand, CommandError
+
+from cms.api import copy_plugins_to_language
+from cms.models import Title, Page
+from cms.utils.i18n import get_language_list
+
+
+class CopyLangCommand(BaseCommand):
+    args = '<language_from language_to>'
+    help = u'duplicate the cms content from one lang to another (to boot a new lang) using draft pages'
+
+    def handle(self, *args, **kwargs):
+        verbose = 'verbose' in args
+        only_empty = 'force-copy' not in args
+        site = [arg.split("=")[1] for arg in args if arg.startswith("site")]
+        if site:
+            site = site.pop()
+        else:
+            site = settings.SITE_ID
+
+        #test both langs
+        try:
+            assert len(args) >= 2
+
+            from_lang = args[0]
+            to_lang = args[1]
+            
+            assert from_lang != to_lang
+        except AssertionError:
+            raise CommandError("Error: bad arguments -- Usage: manage.py cms copy-lang <lang_from> <lang_to>")
+
+        try:
+            assert from_lang in get_language_list(site)
+            assert to_lang in get_language_list(site)
+        except AssertionError:
+            raise CommandError("Both languages have to be present in settings.LANGUAGES and settings.CMS_LANGUAGES")
+
+        for page in Page.objects.on_site(site).drafts():
+            # copy title
+            if from_lang in page.get_languages():
+                try:
+                    title = page.get_title_obj(to_lang, fallback=False)
+                except Title.DoesNotExist:
+                    title = page.get_title_obj(from_lang)
+                    if verbose:
+                        self.stdout.write('copying title %s from language %s\n' % (title.title, from_lang))
+                    title.id = None
+                    title.language = to_lang
+                    title.save()
+                # copy plugins using API
+                if verbose:
+                    self.stdout.write('copying plugins for %s from %s\n' % (page.get_page_title(from_lang), from_lang))
+                copy_plugins_to_language(page, from_lang, to_lang, only_empty)
+            else:
+                if verbose:
+                    self.stdout.write('Skipping page %s, language %s not defined\n' % (page, from_lang))
+
+        self.stdout.write(u"all done")


Test patch:
diff --git a/cms/tests/management.py b/cms/tests/management.py
--- a/cms/tests/management.py
+++ b/cms/tests/management.py
@@ -1,11 +1,14 @@
 # -*- coding: utf-8 -*-
 from __future__ import with_statement
+from django.contrib.sites.models import Site
+from django.core.management import CommandError
 from cms.models import Page
 from django.core import management
+from cms.test_utils.fixtures.navextenders import NavextendersFixture
 
 from cms.test_utils.testcases import CMSTestCase
 from cms.test_utils.util.context_managers import SettingsOverride
-from cms.api import create_page, add_plugin
+from cms.api import create_page, add_plugin, create_title
 from cms.management.commands import cms
 from cms.management.commands.subcommands.list import plugin_report
 from cms.models.pluginmodel import CMSPlugin
@@ -223,3 +226,210 @@ def test_uninstall_plugins_with_plugin(self):
             command.handle("uninstall", "plugins", PLUGIN, interactive=False)
             self.assertEqual(out.getvalue(), "1 'TextPlugin' plugins uninstalled\n")
             self.assertEqual(CMSPlugin.objects.filter(plugin_type=PLUGIN).count(), 0)
+
+
+class PageFixtureManagementTestCase(NavextendersFixture, CMSTestCase):
+
+    def _fill_page_body(self, page, lang):
+        ph_en = page.placeholders.get(slot="body")
+        # add misc plugins
+        mcol1 = add_plugin(ph_en, "MultiColumnPlugin", lang, position="first-child")
+        col1 = add_plugin(ph_en, "ColumnPlugin", lang, position="first-child", target=mcol1)
+        col2 = add_plugin(ph_en, "ColumnPlugin", lang, position="first-child", target=mcol1)
+        mcol2 = add_plugin(ph_en, "MultiColumnPlugin", lang, position="first-child", target=col2)
+        col3 = add_plugin(ph_en, "ColumnPlugin", lang, position="first-child", target=mcol2)
+        col4 = add_plugin(ph_en, "ColumnPlugin", lang, position="first-child", target=mcol2)
+        # add a *nested* link plugin
+        link_plugin_en = add_plugin(ph_en, "LinkPlugin", lang, target=col4,
+                                    name="A Link", url="https://www.django-cms.org")
+
+    def setUp(self):
+        pages = Page.objects.drafts()
+        for page in pages:
+            self._fill_page_body(page, "en")
+
+    def test_copy_langs(self):
+        """
+        Variuos check here:
+
+         * plugins are exactly doubled, half per language with no orphaned plugin
+         * the bottom-most plugins in the nesting chain mantains the same position and the same content
+         * the top-most plugin are of the same type
+        """
+        site = 1
+        number_start_plugins = CMSPlugin.objects.all().count()
+
+        out = StringIO()
+        command = cms.Command()
+        command.stdout = out
+        command.handle("copy-lang", "en", "de")
+        pages = Page.objects.on_site(site).drafts()
+        for page in pages:
+            self.assertEqual(set((u'en', u'de')), set(page.get_languages()))
+        # These asserts that no orphaned plugin exists
+        self.assertEqual(CMSPlugin.objects.all().count(), number_start_plugins*2)
+        self.assertEqual(CMSPlugin.objects.filter(language='en').count(), number_start_plugins)
+        self.assertEqual(CMSPlugin.objects.filter(language='de').count(), number_start_plugins)
+
+        root_page = Page.objects.on_site(site).get_home()
+        root_plugins = CMSPlugin.objects.filter(placeholder=root_page.placeholders.get(slot="body"))
+
+        first_plugin_en, _ = root_plugins.get(language='en', parent=None).get_plugin_instance()
+        first_plugin_de, _ = root_plugins.get(language='de', parent=None).get_plugin_instance()
+
+        self.assertEqual(first_plugin_en.plugin_type, first_plugin_de.plugin_type)
+
+        link_en, _ = root_plugins.get(language='en', plugin_type='LinkPlugin').get_plugin_instance()
+        link_de, _ = root_plugins.get(language='de', plugin_type='LinkPlugin').get_plugin_instance()
+
+        self.assertEqual(link_en.url, link_de.url)
+        self.assertEqual(link_en.get_position_in_placeholder(), link_de.get_position_in_placeholder())
+
+    def test_copy_existing_title(self):
+        """
+        Even if a title already exists the copy is successfull, the original
+        title remains untouched
+        """
+        site = 1
+        number_start_plugins = CMSPlugin.objects.all().count()
+
+        # create an empty title language
+        root_page = Page.objects.on_site(site).get_home()
+        create_title("de", "root page de", root_page)
+
+        out = StringIO()
+        command = cms.Command()
+        command.stdout = out
+        command.handle("copy-lang", "en", "de")
+        pages = Page.objects.on_site(site).drafts()
+        for page in pages:
+            self.assertEqual(set((u'en', u'de')), set(page.get_languages()))
+
+        # Original Title untouched
+        self.assertEqual("root page de", Page.objects.on_site(site).get_home().get_title("de"))
+
+        # Plugins still copied
+        self.assertEqual(CMSPlugin.objects.all().count(), number_start_plugins*2)
+        self.assertEqual(CMSPlugin.objects.filter(language='en').count(), number_start_plugins)
+        self.assertEqual(CMSPlugin.objects.filter(language='de').count(), number_start_plugins)
+
+    def test_copy_filled_placeholder(self):
+        """
+        If an existing title in the target language has plugins in a placeholder
+        that placeholder is skipped
+        """
+        site = 1
+        number_start_plugins = CMSPlugin.objects.all().count()
+
+        # create an empty title language
+        root_page = Page.objects.on_site(site).get_home()
+        create_title("de", "root page de", root_page)
+        ph = root_page.placeholders.get(slot="body")
+        text_plugin_de = add_plugin(ph, "TextPlugin", "de", body="Hello World")
+
+        out = StringIO()
+        command = cms.Command()
+        command.stdout = out
+        command.handle("copy-lang", "en", "de")
+
+        self.assertEqual(CMSPlugin.objects.filter(language='en').count(), number_start_plugins)
+        # one placeholder (with 7 plugins) is skipped, so the difference must be 6
+        self.assertEqual(CMSPlugin.objects.filter(language='de').count(), number_start_plugins-6)
+
+    def test_copy_filled_placeholder_force_copy(self):
+        """
+        If an existing title in the target language has plugins in a placeholder
+        and the command is called with *force-copy*, the plugins are copied on
+        top of the existing one
+        """
+        site = 1
+        number_start_plugins = CMSPlugin.objects.all().count()
+
+        # create an empty title language
+        root_page = Page.objects.on_site(site).get_home()
+        create_title("de", "root page de", root_page)
+        ph = root_page.placeholders.get(slot="body")
+        text_plugin_de = add_plugin(ph, "TextPlugin", "de", body="Hello World")
+
+        root_plugins = CMSPlugin.objects.filter(placeholder=ph)
+        text_de_orig, _ = root_plugins.get(language='de', plugin_type='TextPlugin').get_plugin_instance()
+
+        out = StringIO()
+        command = cms.Command()
+        command.stdout = out
+        command.handle("copy-lang", "en", "de", "force-copy")
+
+        root_plugins = CMSPlugin.objects.filter(placeholder=root_page.placeholders.get(slot="body"))
+
+        self.assertEqual(CMSPlugin.objects.filter(language='en').count(), number_start_plugins)
+        # we have an existing plugin in one placeholder, so we have one more
+        self.assertEqual(CMSPlugin.objects.filter(language='de').count(), number_start_plugins+1)
+
+    def test_copy_from_non_existing_lang(self):
+        """
+        If an existing title in the target language has plugins in a placeholder
+        and the command is called with *force-copy*, the plugins are copied on
+        top of the existing one
+        """
+        site = 1
+        out = StringIO()
+        command = cms.Command()
+        command.stdout = out
+        command.handle("copy-lang", "de", "fr", "verbose")
+        text = out.getvalue()
+        page_count = Page.objects.on_site(site).drafts().count() + 1
+        for idx in range(1, page_count):
+            self.assertTrue(text.find("Skipping page page%d, language de not defined" % idx) > -1)
+
+    def test_copy_site_safe(self):
+        """
+        Check that copy of languages on one site does not interfere with other
+        sites
+        """
+        site_other = 1
+        site_active = 2
+        origina_site1_langs = {}
+
+        number_start_plugins = CMSPlugin.objects.all().count()
+        site_obj = Site.objects.create(domain="sample2.com", name="sample2.com", pk=site_active)
+
+        for page in Page.objects.on_site(1).drafts():
+            origina_site1_langs[page.pk] = set(page.get_languages())
+
+        p1 = create_page('page1', published=True, in_navigation=True, language='de', template='nav_playground.html', site=site_obj)
+        p4 = create_page('page4', published=True, in_navigation=True, language='de', template='nav_playground.html', site=site_obj)
+        p2 = create_page('page2', published=True, in_navigation=True, parent=p1, language='de', template='nav_playground.html', site=site_obj)
+
+        for page in Page.objects.on_site(site_active).drafts():
+            self._fill_page_body(page, 'de')
+
+        number_site2_plugins = CMSPlugin.objects.all().count() - number_start_plugins
+
+        out = StringIO()
+        command = cms.Command()
+        command.stdout = out
+        command.handle("copy-lang", "de", "fr", "site=%s" % site_active)
+
+        for page in Page.objects.on_site(site_other).drafts():
+            self.assertEqual(origina_site1_langs[page.pk], set(page.get_languages()))
+
+        for page in Page.objects.on_site(site_active).drafts():
+            self.assertEqual(set(('de', 'fr')), set(page.get_languages()))
+
+        # plugins for site 1
+        self.assertEqual(CMSPlugin.objects.filter(language='en').count(), number_start_plugins)
+        # plugins for site 2 de
+        self.assertEqual(CMSPlugin.objects.filter(language='de').count(), number_site2_plugins)
+        # plugins for site 2 fr
+        self.assertEqual(CMSPlugin.objects.filter(language='fr').count(), number_site2_plugins)
+        # global number of plugins
+        self.assertEqual(CMSPlugin.objects.all().count(), number_start_plugins + number_site2_plugins*2)
+
+    def test_copy_bad_languages(self):
+        out = StringIO()
+        command = cms.Command()
+        command.stdout = out
+        with self.assertRaises(CommandError) as command_error:
+            command.handle("copy-lang", "it", "fr")
+
+        self.assertEqual(str(command_error.exception), 'Both languages have to be present in settings.LANGUAGES and settings.CMS_LANGUAGES')
\ No newline at end of file