Skip to content

Instantly share code, notes, and snippets.

@stephenmcd
Created May 8, 2012 12:00
Show Gist options
  • Save stephenmcd/2634461 to your computer and use it in GitHub Desktop.
Save stephenmcd/2634461 to your computer and use it in GitHub Desktop.
Allow circular Django template inheritance, so projects can both override and extend a reusable app's templates.
from django.template.loader_tags import ExtendsNode
def get_extends_parent(self, context):
"""
Patched onto Django's ``ExtendsNode.get_parent`` which is
responsible for loading the template to extend from with
the ``extends`` template tag.
This patch allows the template foo/bar.html to extend
foo/bar.html, given that there is another version of it that
can be loaded. This allows templates to be created in a project
that extend their app template counterparts.
We first load the template we're extending, which in the case
of a circular extend, will be the same template the tag is
being called in. We can then look at the first node of the
loaded template, and compare its arg to the arg given within
the call to this instance of extend. If they match, we then
have a circular extend.
Once a circular extend is detected, we then take advantage of
the fact that each of the template loaders takes an optional
list of directories to use. We then build a *full* list of
template directories, given the known values the loaders use:
app template directories, and the ``TEMPLATE_DIRS`` setting.
With this full list, we can remove the directory that the
loaded parent template used (accessible via its ``origin``),
and pass the list of all template directories (minus one)
to the template loaders. Since all directories are given,
the first template loader should be able to find the
intended parent, and use it.
As a final step, we actually store the filtered directory list
in a context dictionary for each template name this occurs
for, and on subsequent runs for the same template name in
a single page, we reuse this directory list, removing each
absolute directory that gets used. This allows for
inheritence to reach beyond a single step, so a project
template can extend itself from a third-party app version,
which extends itself from our own app's version.
Works at least against app_directories, filesystem and cached
loaders.
"""
from django.conf import settings
from django.template.loader import template_source_loaders
from django.template.loaders.app_directories import app_template_dirs
from django.template.loader import get_template, find_template
parent = self.parent_name.resolve(context)
if hasattr(parent, "render"):
# {% extends %} arg is a template object, just return it.
return parent
# {% extends %} arg is a template name, load it.
t = get_template(parent)
if t.nodelist and isinstance(t.nodelist[0], ExtendsNode):
first_node_template = t.nodelist[0].parent_name.resolve(context)
if first_node_template == parent:
# First node in the parent is an extends tag, with the same
# arg as this extends instance, so a circular extend has
# been detected. Create a full list of template directories,
# remove the loaded parent's directory from it, and find
# the same template name within the filtered list of template
# directories. We also store the list of directories to reuse
# in the template context, and remove each directory we use
# each time a circular extend occurs. This way circular
# inheritence can each beyond a single level.
template_dirname = lambda t: t.origin.name[:-len(parent) - 1]
context_name = "circular_extends_available_dirs"
if context_name not in context:
context[context_name] = {}
if parent not in context[context_name]:
context[context_name][parent] = list(app_template_dirs +
settings.TEMPLATE_DIRS)
# This is the first circular extend for this template name.
# Remove the initial directory for the first inheritence
# step.
context[context_name][parent].remove(template_dirname(t))
next = find_template(parent, context[context_name][parent])[0]
# Remove the origin for the next parent (the one we'll be using)
# from the cached list of available template directories, so
# that it won't be used on subsequent circular extends for this
# template name.
context[context_name][parent].remove(template_dirname(next))
return next
return get_template(parent)
ExtendsNode.get_parent = get_extends_parent
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment