Repository: django-cms/django-cms

Issue:
show_uncached_placeholder uses caching with CMS_PLACEHOLDER_CACHE=True
Using `show_uncached_placholder` with `CMS_PLACEHOLDER_CACHE` set to `True` (which is the default!) causes the plugin rendering system to cache the placeholder nevertheless, which can trigger issues in `restore_sekizai_context`.

Specifically what happens is the template tag hits:
- Attempts to render the placeholder at https://github.com/divio/django-cms/blob/3.0.9/cms/templatetags/cms_tags.py#L558
- Correctly bypasses caching in that function at https://github.com/divio/django-cms/blob/3.0.9/cms/templatetags/cms_tags.py#L580
- Enters actual rendering via `render_placeholder` at https://github.com/divio/django-cms/blob/3.0.9/cms/templatetags/cms_tags.py#L597
- The step above does not pass any information with regards to caching to `render_placeholder`
- `render_placeholder` checks for `CMS_PLACEHOLDER_CACHE` at https://github.com/divio/django-cms/blob/3.0.9/cms/templatetags/cms_tags.py#L597
- Triggers `restore_sekizai_context` at https://github.com/divio/django-cms/blob/3.0.9/cms/plugin_rendering.py#L135
- Because only half of the pipeline is in "caching mode", `restore_sekizai_context` may trigger a `TypeError: 'NoneType' object is unsubscriptable` at https://github.com/divio/django-cms/blob/3.0.9/cms/utils/placeholder.py#L148

Example traceback:

```
Traceback (most recent call last):
  File "/path/to/env/lib/python2.6/site-packages/django/core/handlers/base.py", line 137, in get_response
    response = response.render()
  File "/path/to/env/lib/python2.6/site-packages/django/template/response.py", line 105, in render
    self.content = self.rendered_content
  File "/path/to/env/lib/python2.6/site-packages/django/template/response.py", line 82, in rendered_content
    content = template.render(context)
  File "/path/to/env/lib/python2.6/site-packages/django/template/base.py", line 140, in render
    return self._render(context)
  File "/path/to/env/lib/python2.6/site-packages/django/template/base.py", line 134, in _render


  ... Repeated calls to render/_render removed

    return self.render_tag(context, **kwargs)
  File "/path/to/env/lib/python2.6/site-packages/classytags/helpers.py", line 78, in render_tag
    data = self.get_context(context, **kwargs)
  File "/path/to/env/lib/python2.6/site-packages/cms/templatetags/cms_tags.py", line 619, in get_context
    return _show_placeholder_for_page(**self.get_kwargs(*args, **kwargs))
  File "/path/to/env/lib/python2.6/site-packages/cms/templatetags/cms_tags.py", line 597, in _show_placeholder_for_page
    content = render_placeholder(placeholder, context, placeholder_name)
  File "/path/to/env/lib/python2.6/site-packages/cms/plugin_rendering.py", line 135, in render_placeholder
    restore_sekizai_context(context, cached_value['sekizai'])
  File "/path/to/env/lib/python2.6/site-packages/cms/utils/placeholder.py", line 148, in restore_sekizai_context
    sekizai_namespace = sekizai_container[key]
TypeError: 'NoneType' object is unsubscriptable
```



Discussion:
While I only had a cursory look into this issue, it feels like passing a `no_cache` parameter to `render_placeholder` could solve this.

@ojii The revenge of the micro-features: it's solved in #3768!
In the above PR adds a new templatetag in the process (actually that was the scope of the PR)
That should be trivial to add tests for this case also

Great job sleuthing, @ojii. Thanks for the patch, @yakky. It looks like your patch is based against 3.1.x. Does this need to be rebased to 3.0.x?

Technically speaking #3768 is a new feature, so I'd need to rework it as a patch (which means keeping 99% of the code and just removing the templatetag declaration)
I'd vote to merge it as is (but it requires docs, that I'll write shortly) given the negligible impact of the "feature" part, but I'm open to both solutions

@ojii, do you have an opinion on @yakky's comment above?

@yakky @ojii: What's the way forward on this guys?

To me we can just merge #3768 

If I read #3768 correctly, it doesn't actually fix this issue. It adds the ability to fix this issue. So after 3768 this still needs to be tested and fixed

Sorry, writing on the go and cannot clarify myself: I meant "to me we can merge #3768 as is and then write the fix on top of it instead of extracting the code needed to fix this and not merge #3768"
Anyway, I have the fix for this on top of #3768 ready

Okay that sounds like a reasonable way to do it.

If we don't want to **add** things in 3.0.10 we can leave the new templatetag undocumented and document it in 3.1

can we really not have a clean patch for 3.0.10?

Yeah, sure.
We can just leave the templatetag out

I'd really appreciate that. Sorry for always being the annoying guy being pedantic about these things but I do believe they're important.

@ojii I tend to agree with you, but in this case I consider the new templatetag a bug fix, as the current API is missing a piece.
But I won't loose my sleep without the templatetag in 3.0.x :)


Resolving patch:
diff --git a/cms/models/placeholdermodel.py b/cms/models/placeholdermodel.py
--- a/cms/models/placeholdermodel.py
+++ b/cms/models/placeholdermodel.py
@@ -128,7 +128,7 @@ def has_add_permission(self, request):
     def has_delete_permission(self, request):
         return self._get_permission(request, 'delete')
 
-    def render(self, context, width, lang=None, editable=True):
+    def render(self, context, width, lang=None, editable=True, use_cache=True):
         '''
         Set editable = False to disable front-end rendering for this render.
         '''
@@ -138,7 +138,8 @@ def render(self, context, width, lang=None, editable=True):
         width = width or self.default_width
         if width:
             context.update({'width': width})
-        return render_placeholder(self, context, lang=lang, editable=editable)
+        return render_placeholder(self, context, lang=lang, editable=editable,
+                                  use_cache=use_cache)
 
     def _get_attached_fields(self):
         """
diff --git a/cms/plugin_rendering.py b/cms/plugin_rendering.py
--- a/cms/plugin_rendering.py
+++ b/cms/plugin_rendering.py
@@ -88,7 +88,8 @@ def render_plugins(plugins, context, placeholder, processors=None):
 
 
 def render_placeholder(placeholder, context_to_copy,
-        name_fallback="Placeholder", lang=None, default=None, editable=True):
+        name_fallback="Placeholder", lang=None, default=None, editable=True,
+        use_cache=True):
     """
     Renders plugins for a placeholder on the given page using shallow copies of the
     given context, and returns a string containing the rendered output.
@@ -128,7 +129,7 @@ def render_placeholder(placeholder, context_to_copy,
         processors = None
         edit = False
     from django.core.cache import cache
-    if get_cms_setting('PLACEHOLDER_CACHE'):
+    if get_cms_setting('PLACEHOLDER_CACHE') and use_cache:
         cache_key = placeholder.get_cache_key(lang)
         if not edit and placeholder and not hasattr(placeholder, 'cache_checked'):
             cached_value = cache.get(cache_key)
@@ -177,7 +178,7 @@ def render_placeholder(placeholder, context_to_copy,
     context['edit'] = edit
     result = render_to_string("cms/toolbar/content.html", context)
     changes = watcher.get_changes()
-    if placeholder and not edit and placeholder.cache_placeholder and get_cms_setting('PLACEHOLDER_CACHE'):
+    if placeholder and not edit and placeholder.cache_placeholder and get_cms_setting('PLACEHOLDER_CACHE') and use_cache:
         cache.set(cache_key, {'content': result, 'sekizai': changes}, get_cms_setting('CACHE_DURATIONS')['content'])
     context.pop()
     return result
diff --git a/cms/templatetags/cms_tags.py b/cms/templatetags/cms_tags.py
--- a/cms/templatetags/cms_tags.py
+++ b/cms/templatetags/cms_tags.py
@@ -1116,6 +1116,7 @@ def _get_value(self, context, editable=True, **kwargs):
         request = context.get('request', None)
         placeholder = kwargs.get('placeholder')
         width = kwargs.get('width')
+        nocache = kwargs.get('nocache', False)
         language = kwargs.get('language')
 
         if not request:
@@ -1126,7 +1127,8 @@ def _get_value(self, context, editable=True, **kwargs):
             request.placeholders = []
         if placeholder.has_change_permission(request):
             request.placeholders.append(placeholder)
-        return safe(placeholder.render(context, width, lang=language, editable=editable))
+        return safe(placeholder.render(context, width, lang=language,
+                                       editable=editable, use_cache=not nocache))
 
     def get_value_for_context(self, context, **kwargs):
         return self._get_value(context, editable=False, **kwargs)


Test patch: