Repository: django-cms/django-cms

Issue:
Language fallback for placeholders
Currently I'm using this approach. I think this would improve django-cms language fallbacks.

<pre>diff --git a/cms/plugin_rendering.py b/cms/plugin_rendering.py
index 221273a..26b12f2 100644
--- a/cms/plugin_rendering.py
+++ b/cms/plugin_rendering.py
@@ -1,6 +1,7 @@
 from cms.utils import get_language_from_request
 from cms import settings
 from cms.utils.placeholder import get_page_from_placeholder_if_exists
+from cms.utils.i18n import get_fallback_languages, get_default_language
 from django.conf import settings as django_settings
 from django.utils.importlib import import_module
 from django.core.exceptions import ImproperlyConfigured
@@ -125,7 +126,15 @@ def render_placeholder(placeholder, context_to_copy):
     from cms.plugins.utils import get_plugins
     context = copy.copy(context_to_copy) 
     request = context['request']
-    plugins = [plugin for plugin in get_plugins(request, placeholder)]
+    lang = get_language_from_request(request)
+    if settings.CMS_LANGUAGE_FALLBACK and lang != get_default_language():
+        fallbacks = get_fallback_languages(lang)
+        for l in fallbacks:
+            plugins = [plugin for plugin in get_plugins(request, placeholder, l)]
+            if plugins:
+                break
+    else:
+        plugins = [plugin for plugin in get_plugins(request, placeholder)]
     page = get_page_from_placeholder_if_exists(placeholder)
     if page:
         template = page.template</pre>



Discussion:
I do not think that this should be the default behavior. There should be a way to explicitly enable this for each placeholder.

You're right. Add a check here using settings to enable it is easy.

We already have way too many settings, so 'just add another one' will not work. Give me a very good reason to add a setting and prove that it can't be done differently more elegant.

could be done in CMS_PLACEHOLDER_CONF, for each placeholder seperatly:

``` python
CMS_PLACEHOLDER_CONF = {
    'header_image':{
        'plugins': ('ImagePlugin'),
        'name': ugettext("Header"),
        'language_fallback': True
}
```

Once 2.3 feature discussion starts, feel free to bring that up again. For now, all i care about is finally getting 2.2 out


Resolving patch:
diff --git a/cms/plugin_rendering.py b/cms/plugin_rendering.py
--- a/cms/plugin_rendering.py
+++ b/cms/plugin_rendering.py
@@ -4,7 +4,8 @@
 from cms.utils import get_language_from_request
 from cms.utils.conf import get_cms_setting
 from cms.utils.django_load import iterload_objects
-from cms.utils.placeholder import get_placeholder_conf
+from cms.utils.placeholder import get_placeholder_conf, get_page_from_placeholder_if_exists
+from cms.utils.i18n import get_fallback_languages, get_default_language
 from django.template import Template, Context
 from django.template.defaultfilters import title
 from django.template.loader import render_to_string
@@ -95,12 +96,25 @@ def render_placeholder(placeholder, context_to_copy, name_fallback="Placeholder"
     context = context_to_copy
     context.push()
     request = context['request']
-    plugins = [plugin for plugin in get_plugins(request, placeholder, lang=lang)]
     page = placeholder.page if placeholder else None
     if page:
         template = page.template
     else:
         template = None
+    # It's kind of duplicate of the similar call in `get_plugins`, but it's required
+    # to have a valid language in this function for `get_fallback_languages` to work
+    if not lang:
+        lang = get_language_from_request(request)
+    plugins = [plugin for plugin in get_plugins(request, placeholder, lang=lang)]
+    # If no plugin is present in the current placeholder we loop in the fallback languages
+    # and get the first available set of plugins
+    if (len(plugins)==0 and placeholder and lang != get_default_language() and
+        get_placeholder_conf("language_fallback", placeholder.slot, template, False)):
+        fallbacks = get_fallback_languages(lang)
+        for fallback_language in fallbacks:
+            plugins = [plugin for plugin in get_plugins(request, placeholder, fallback_language)]
+            if plugins:
+                break
     # Add extra context as defined in settings, but do not overwrite existing context variables,
     # since settings are general and database/template are specific
     # TODO this should actually happen as a plugin context processor, but these currently overwrite


Test patch:
diff --git a/cms/tests/placeholder.py b/cms/tests/placeholder.py
--- a/cms/tests/placeholder.py
+++ b/cms/tests/placeholder.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import with_statement
 from cms import constants
-from cms.api import add_plugin, create_page
+from cms.api import add_plugin, create_page, create_title
 from cms.exceptions import DuplicatePlaceholderWarning
 from cms.models.fields import PlaceholderField
 from cms.models.placeholdermodel import Placeholder
@@ -244,6 +244,47 @@ def test_placeholder_scanning_nested_super(self):
     def test_placeholder_field_no_related_name(self):
         self.assertRaises(ValueError, PlaceholderField, 'placeholder', related_name='+')
 
+    def test_plugins_language_fallback(self):
+        """ Tests language_fallback placeholder configuration """
+        page_en = create_page('page_en', 'col_two.html', 'en')
+        title_de = create_title("de", "page_de", page_en)
+        placeholder_en = page_en.placeholders.get(slot='col_left')
+        placeholder_de = title_de.page.placeholders.get(slot='col_left')
+        add_plugin(placeholder_en, TextPlugin, 'en', body='en body')
+
+        class NoPushPopContext(Context):
+            def push(self):
+                pass
+            pop = push
+        context_en = NoPushPopContext()
+        context_en['request'] = self.get_request(language="en")
+        context_de = NoPushPopContext()
+        context_de['request'] = self.get_request(language="de")
+
+        # First test the default (non-fallback) behavior)
+        ## English page should have the text plugin
+        content_en = render_placeholder(placeholder_en, context_en)
+        self.assertRegexpMatches(content_en,"^en body$")
+
+        ## Deutsch page should have no text
+        content_de = render_placeholder(placeholder_en, context_de)
+        self.assertNotRegexpMatches(content_de,"^en body$")
+
+        conf = {
+            'col_left': {
+                'language_fallback': True,
+            },
+        }
+        with SettingsOverride(CMS_PLACEHOLDER_CONF=conf):
+            ## Deutsch page should have no text
+            content_de = render_placeholder(placeholder_en, context_de)
+            self.assertRegexpMatches(content_de,"^en body$")
+
+            # Then we add a plugin to check for proper rendering
+            add_plugin(placeholder_de, TextPlugin, 'de', body='de body')
+            content_de = render_placeholder(placeholder_de, context_de)
+            self.assertRegexpMatches(content_de,"^de body$")
+
 
 class PlaceholderActionTests(FakemlngFixtures, CMSTestCase):
     def test_placeholder_no_action(self):
diff --git a/cms/tests/templatetags.py b/cms/tests/templatetags.py
--- a/cms/tests/templatetags.py
+++ b/cms/tests/templatetags.py
@@ -251,7 +251,7 @@ def test_cached_show_placeholder_sekizai(self):
             self.assertIn('JAVASCRIPT', output)
 
     def test_show_placeholder_for_page_marks_output_safe(self):
-        from django.core.cache import cache;
+        from django.core.cache import cache
 
         cache.clear()
         page = create_page('Test', 'col_two.html', 'en')