Repository: ckan/ckan

Issue:
ckan_extends cache depends on template paths
### CKAN Version if known (or site URL)
all versions

### Please describe the expected behaviour
`{% ckan_extends %}` should extend templates from the next path in the template paths list

### Please describe the actual behaviour
`{% ckan_extends %}` emits a node with a numeric index into the template path list, which is cached with the rest of the compiled template. If the plugins enabled or template paths change after caching the template `{% ckan_extends %}` will extend from the wrong position in the template paths list. This behavior could lead to an infinite loop, a skipped intermediate template or a `TemplateNotFound` error (even for templates present in ckan itself)

### What steps can be taken to reproduce the issue? 
- create a template using `{% ckan_extends %}`, enable the `jinja2_cache_dir` setting
- visit a page that causes the template to be cached
- modify the plugins list to add/remove plugins with template directories or add/remove modify the `template_extra_paths` setting
- visit the page again for unexpected behavior

This is where the numeric index is added to the extends node, which is cached:
https://github.com/ckan/ckan/blob/621b008c9b2e7e07aef7410f85a61fec94154df0/ckan/lib/jinja_extensions.py#L132

This is where the cached numeric index is used on the current template path list: https://github.com/ckan/ckan/blob/621b008c9b2e7e07aef7410f85a61fec94154df0/ckan/lib/jinja_extensions.py#L189-L192

I suggest we switch this to emit the current template directory instead of a number so that we won't get this unexpected behavior.


Discussion:

Resolving patch:
diff --git a/ckan/lib/jinja_extensions.py b/ckan/lib/jinja_extensions.py
--- a/ckan/lib/jinja_extensions.py
+++ b/ckan/lib/jinja_extensions.py
@@ -106,13 +106,13 @@ def parse(self, parser):
         node = nodes.Extends(lineno)
         template_path = parser.filename
         # find where in the search path this template is from
-        index = 0
+        current_path = None
         if not hasattr(self, 'searchpath'):
             return node
         for searchpath in self.searchpath:
             if template_path.startswith(searchpath):
+                current_path = searchpath
                 break
-            index += 1
 
         # get filename from full path
         filename = template_path[len(searchpath) + 1:]
@@ -128,8 +128,8 @@ def parse(self, parser):
                              % template_path)
 
         # provide our magic format
-        # format is *<search path parent index>*<template name>
-        magic_filename = '*' + str(index) + '*' + filename
+        # format is *<search path parent directory>*<template name>
+        magic_filename = '*' + current_path + '*' + filename
         # set template
         node.template = nodes.Const(magic_filename)
         return node
@@ -183,13 +183,14 @@ class CkanFileSystemLoader(loaders.FileSystemLoader):
     def get_source(self, environment, template):
         # if the template name starts with * then this should be
         # treated specially.
-        # format is *<search path parent index>*<template name>
+        # format is *<search path parent directory>*<template name>
         # so we only search from then downwards.  This allows recursive
         # ckan_extends tags
         if template.startswith('*'):
             parts = template.split('*')
             template = parts[2]
-            searchpaths = self.searchpath[int(parts[1]) + 1:]
+            index = self.searchpath.index(parts[1])
+            searchpaths = self.searchpath[index + 1:]
         else:
             searchpaths = self.searchpath
         # end of ckan changes


Test patch: