Repository: getnikola/nikola

Issue:
Plugins should be able to add templates to the theme
These templates should be lower priority than the themes, so themes can actually override them.

My use case is the series plugin: I need a series.tmpl that creates the page for each series, and a series_index.tmpl that has the list of all series.

So, the plugin would provide fallback templates in a `templates` folder, for Mako and Jinja2, and then the user and the theme maker can override them.

I'll try to make it very painless by just using whatever is in that folder without configuration or extra code in the plugin.



Discussion:

Resolving patch:
diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py
--- a/nikola/plugin_categories.py
+++ b/nikola/plugin_categories.py
@@ -25,6 +25,8 @@
 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 from __future__ import absolute_import
+import sys
+import os
 
 __all__ = [
     'Command',
@@ -55,6 +57,24 @@ class BasePlugin(IPlugin):
     def set_site(self, site):
         """Sets site, which is a Nikola instance."""
         self.site = site
+        self.inject_templates()
+
+    def inject_templates(self):
+        """If this plugin contains a 'templates' folder,
+        then templates/mako or templates/jinja will be inserted very early in
+        the theme chain."""
+
+        # Sorry, found no other way to get this
+        mod_path = sys.modules[self.__class__.__module__].__file__
+        mod_dir = os.path.dirname(mod_path)
+        tmpl_dir = os.path.join(
+            mod_dir,
+            'templates',
+            self.site.template_system.name
+        )
+        if os.path.isdir(tmpl_dir):
+            # Inject tmpl_dir low in the theme chain
+            self.site.template_system.inject_directory(tmpl_dir)
 
 
 class Command(BasePlugin, DoitCommand):
@@ -135,6 +155,8 @@ def group_task(self):
 class Task(BaseTask):
     """Plugins of this type are task generators."""
 
+    name = "dummy_task"
+
 
 class LateTask(BaseTask):
     """Plugins of this type are executed after all plugins of type Task."""
@@ -145,16 +167,12 @@ class LateTask(BaseTask):
 class TemplateSystem(BasePlugin):
     """Plugins of this type wrap templating systems."""
 
-    name = "dummy templates"
+    name = "dummy_templates"
 
     def set_directories(self, directories, cache_folder):
         """Sets the list of folders where templates are located and cache."""
         raise NotImplementedError()
 
-    def set_site(self, site):
-        """Sets the site."""
-        self.site = site
-
     def template_deps(self, template_name):
         """Returns filenames which are dependencies for a template."""
         raise NotImplementedError()
@@ -168,8 +186,12 @@ def render_template(self, template_name, output_name, context):
         raise NotImplementedError()
 
     def render_template_to_string(self, template, context):
-        """ Renders template to a string using context. """
+        """Renders template to a string using context. """
+        raise NotImplementedError()
 
+    def inject_directory(self, directory):
+        """Injects the directory with the lowest priority in the
+        template search mechanism."""
         raise NotImplementedError()
 
 
diff --git a/nikola/plugins/template/jinja.py b/nikola/plugins/template/jinja.py
--- a/nikola/plugins/template/jinja.py
+++ b/nikola/plugins/template/jinja.py
@@ -58,7 +58,19 @@ def set_directories(self, directories, cache_folder):
         """Create a template lookup."""
         if jinja2 is None:
             req_missing(['jinja2'], 'use this theme')
-        self.lookup.loader = jinja2.FileSystemLoader(directories,
+        self.directories = directories
+        self.create_lookup()
+
+    def inject_directory(self, directory):
+        """if it's not there, add the directory to the lookup with lowest priority, and
+        recreate the lookup."""
+        if directory not in self.directories:
+            self.directories.append(directory)
+            self.create_lookup()
+
+    def create_lookup(self):
+        """Create a template lookup object."""
+        self.lookup.loader = jinja2.FileSystemLoader(self.directories,
                                                      encoding='utf-8')
 
     def set_site(self, site):
diff --git a/nikola/plugins/template/mako.py b/nikola/plugins/template/mako.py
--- a/nikola/plugins/template/mako.py
+++ b/nikola/plugins/template/mako.py
@@ -50,6 +50,8 @@ class MakoTemplates(TemplateSystem):
     lookup = None
     cache = {}
     filters = {}
+    directories = []
+    cache_dir = None
 
     def get_deps(self, filename):
         text = util.read_file(filename)
@@ -65,7 +67,7 @@ def get_deps(self, filename):
         return deps
 
     def set_directories(self, directories, cache_folder):
-        """Create a template lookup."""
+        """Set directories and create a template lookup."""
         cache_dir = os.path.join(cache_folder, '.mako.tmp')
         # Workaround for a Mako bug, Issue #825
         if sys.version_info[0] == 2:
@@ -74,12 +76,24 @@ def set_directories(self, directories, cache_folder):
             except UnicodeEncodeError:
                 cache_dir = tempfile.mkdtemp()
                 LOGGER.warning('Because of a Mako bug, setting cache_dir to {0}'.format(cache_dir))
-
         if os.path.exists(cache_dir):
             shutil.rmtree(cache_dir)
+        self.directories = directories
+        self.cache_dir = cache_dir
+        self.create_lookup()
+
+    def inject_directory(self, directory):
+        """if it's not there, add the directory to the lookup with lowest priority, and
+        recreate the lookup."""
+        if directory not in self.directories:
+            self.directories.append(directory)
+            self.create_lookup()
+
+    def create_lookup(self):
+        """Create a template lookup object."""
         self.lookup = TemplateLookup(
-            directories=directories,
-            module_directory=cache_dir,
+            directories=self.directories,
+            module_directory=self.cache_dir,
             output_encoding='utf-8')
 
     def set_site(self, site):


Test patch:
diff --git a/tests/test_compile_markdown.py b/tests/test_compile_markdown.py
--- a/tests/test_compile_markdown.py
+++ b/tests/test_compile_markdown.py
@@ -18,10 +18,14 @@
 
 
 class FakeSite(object):
-    config = {
-        "MARKDOWN_EXTENSIONS": ['fenced_code', 'codehilite'],
-        "LOGGING_HANDLERS": {'stderr': {'loglevel': 'WARNING', 'bubble': True}}
-    }
+    def __init__(self):
+        self.config = {
+            "MARKDOWN_EXTENSIONS": ['fenced_code', 'codehilite'],
+            "LOGGING_HANDLERS": {'stderr': {'loglevel': 'WARNING', 'bubble': True}}
+        }
+        # This is to make plugin initialization happy
+        self.template_system = self
+        self.name = 'mako'
 
 
 class CompileMarkdownTests(unittest.TestCase):
diff --git a/tests/test_rst_compiler.py b/tests/test_rst_compiler.py
--- a/tests/test_rst_compiler.py
+++ b/tests/test_rst_compiler.py
@@ -119,6 +119,9 @@ def __init__(self):
                      slug='fake-post')
         ]
         self.debug = True
+        # This is to make plugin initialization happy
+        self.template_system = self
+        self.name = 'mako'
 
     def render_template(self, name, _, context):
         return('<img src="IMG.jpg">')