Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify template inheritance #132

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions cruds_adminlte/template_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""The Template Loader.

Using the standard django template inheritance and extension mechanism,
it would happen that you would end up with lots of template overrides.

That is, you cannot, by default, override a block defined in an included
template (like the sidebar, or the headbar) thus, in order to define the
sidebar you would need to override that template. And if you want different
sidebars for different things... you get the point.

The Template Loader those a simple trick: it "compiles" a template by embedding
into its contents the contents of the included templates. So that at the eyes
of Django Template System when you extend from such compiled template, all is one
single template the block of which can be extended.

Note that we can only handle embedding of calls to include which do not have a 'with'.
Also, we do not currently support embedding a template that uses 'extends' itself.

THIS IS EXPERIMENTAL FEATURE.
Our solution is heavily based in the code by github's @uw-it-aca/django-template-preprocess
which is in turn based of django-compressor.
Also borrowing some code of django's builtin cached template loader.
"""
from importlib import import_module
from django.template import engines
from django.template import loader
from django.utils.encoding import smart_text
import hashlib

from django.template import TemplateDoesNotExist
from django.template.backends.django import copy_exception

from django.template.loaders.base import Loader as BaseLoader
from fnmatch import fnmatch
import codecs
import re
import os


class Loader(BaseLoader):
# List of templates on which rendering we intervene.
templates = ['adminlte/base.html',
r'^adminlte/lib/.*',
r'^cruds/.*',
]

def __init__(self, engine, templates=None):
if templates:
self.templates = templates
self.engine = engine
#self.loaders = engine.get_template_loaders(loaders)
super().__init__(engine)

def check_intervene(self, template_name):
for m in self.templates:
if m == template_name or re.match(m, template_name):
return True
return False

def get_template_sources(self, template_name):
if self.check_intervene(template_name):
for loader in self.loaders:
if isinstance(loader, self.__class__):
continue
for t in loader.get_template_sources(template_name):
t.wrapped_loader = t.loader
t.loader = self
yield t

def get_contents(self, origin):
loader = origin.wrapped_loader
content = loader.get_contents(origin)
content = process_template_content(content)
return content

_loaders = None

@property
def loaders(self):
if self._loaders:
return self._loaders
template_source_loaders = self.engine.loaders[:]
template_source_loaders = self.engine.get_template_loaders(template_source_loaders)
loaders = []
# Unwrap the loaders inside the CachedTemplateLoader if applicable.
for loader in template_source_loaders:
if isinstance(loader, self.__class__):
continue
if hasattr(loader, 'loaders'):
loaders.extend(loader.loaders)
else:
loaders.append(loader)
self._loaders = loaders
return loaders

def get_template(self, template_name, skip=None):
template = super().get_template(template_name, skip)
return template


def handle_includes(content, seen_templates, template_processor):
def insert_template(match):
name = match.group(1)

if name in seen_templates:
raise Exception("Recursive template includes")

seen_templates[name] = True

content = template_processor(name, seen_templates)
return content

# FIXME: Make sure we do not process an include with 'with'
content = re.sub(r"""{%\s*include\s*['"]([^"']+?)["']\s*%}""",
insert_template,
content, flags=re.UNICODE)

return content


def process_sub_template(name, seen_templates):
content = loader.get_template(name).template.source
return process_template_content(content,
seen_templates,
subcall=True)


def process_template_content(content,
seen_templates=None,
subcall=False):
if seen_templates is None:
seen_templates = {}
content = handle_includes(content,
seen_templates=seen_templates,
template_processor=process_sub_template,
)
return content

9 changes: 7 additions & 2 deletions tests/demo_project/demo/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,21 @@
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
str(DEMO_ROOT / 'demo' / 'templates'),
str(DEMO_ROOT / 'project_templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
'loaders': [
'cruds_adminlte.template_loader.Loader',
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
]
}
},
]

Expand Down
41 changes: 41 additions & 0 deletions tests/demo_project/demo/templates/adminlte/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{% extends "adminlte/base.html" %}
{% load i18n %}

{% block nav_links %}
<li>
<a href="{% url 'testapp_author_list' %}">
<i class="fa fa-pencil"></i> <span>{% trans "Authors" %}</span>
</a>
</li>
<li>
<a href="{% url 'testapp_addresses_list' %}">
<i class="fa fa-address-book-o"></i> <span>{% trans "Addresses" %}</span>
</a>
</li>
<li>
<a href="{% url 'testapp_customer_list' %}">
<i class="fa fa-users"></i> <span>{% trans "Customers" %}</span>
</a>
</li>
<li>
<a href="{% url 'testapp_invoice_list' %}">
<i class="fa fa-file-pdf-o"></i> <span>{% trans "Invoices" %}</span>
</a>
</li>
<li class="header">AUTH</li>
<li>
<a href="{% url 'auth_user_list' %}">
<i class="fa fa-user"></i> <span>{% trans "Users" %}</span>
</a>
</li>
<li>
<a href="{% url 'auth_group_list' %}">
<i class="fa fa-users"></i> <span>{% trans "Groups" %}</span>
</a>
</li>
<li>
<a href="{% url 'auth_permission_list' %}">
<i class="fa fa-id-card-o"></i> <span>{% trans "Permissions" %}</span>
</a>
</li>
{% endblock nav_links %}
53 changes: 0 additions & 53 deletions tests/demo_project/demo/templates/adminlte/lib/_main_sidebar.html

This file was deleted.

44 changes: 44 additions & 0 deletions tests/demo_project/demo/templates/cruds/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{% extends "cruds/base.html" %}

{% block title %}CRUDS Demo{% endblock %}

{% load i18n %}

{% block nav_links %}
<li>
<a href="{% url 'testapp_author_list' %}">
<i class="fa fa-pencil"></i> <span>{% trans "Authors" %}</span>
</a>
</li>
<li>
<a href="{% url 'testapp_addresses_list' %}">
<i class="fa fa-address-book-o"></i> <span>{% trans "Addresses" %}</span>
</a>
</li>
<li>
<a href="{% url 'testapp_customer_list' %}">
<i class="fa fa-users"></i> <span>{% trans "Customers" %}</span>
</a>
</li>
<li>
<a href="{% url 'testapp_invoice_list' %}">
<i class="fa fa-file-pdf-o"></i> <span>{% trans "Invoices" %}</span>
</a>
</li>
<li class="header">AUTH</li>
<li>
<a href="{% url 'auth_user_list' %}">
<i class="fa fa-user"></i> <span>{% trans "Users" %}</span>
</a>
</li>
<li>
<a href="{% url 'auth_group_list' %}">
<i class="fa fa-users"></i> <span>{% trans "Groups" %}</span>
</a>
</li>
<li>
<a href="{% url 'auth_permission_list' %}">
<i class="fa fa-id-card-o"></i> <span>{% trans "Permissions" %}</span>
</a>
</li>
{% endblock nav_links %}
12 changes: 12 additions & 0 deletions tests/demo_project/project_templates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Thought the right place to put the project templates would be
the "project app" (that is, the app that holds the settings file)

It is not uncommon for a non-app template directory to exist.
Such directory gets picked up the filesystem.Loader instead of
the app_directories.Loader.

In order to properly test template loading / rendering features,
we need to supply our test project with such a directory.

That is, this directory exists in order to be able to test against
the filesystem.Loader
5 changes: 5 additions & 0 deletions tests/demo_project/project_templates/project_homepage.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "adminlte/base.html" %}

{% block body %}
Hi there, I am the project_homepage.html
{% endblock %}
46 changes: 46 additions & 0 deletions tests/test_template_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
from cruds_adminlte.template_loader import Loader
from django.template import loader
from django.template import engines


def test_template_loading():
"""Make sure that both app_directories and filesystem Loaders work. """
tpl = loader.get_template('homepage.html')
assert "tests/demo_project/demo/templates/homepage.html" in tpl.origin.name
tpl_content = tpl.render()
assert "Hello." in tpl_content

tpl = loader.get_template('project_homepage.html')
assert "tests/demo_project/project_templates/project_homepage.html" in tpl.origin.name
tpl_content = tpl.render()
assert "Hi there, I am the project_homepage.html" in tpl_content

tpl = loader.get_template('adminlte/base.html')
assert "tests/demo_project/demo/templates/adminlte/base.html" in tpl.origin.name

tpl = loader.get_template('cruds/base.html')
assert "tests/demo_project/demo/templates/cruds/base.html" in tpl.origin.name


def test_get_loaders():
tpl_index = loader.get_template('adminlte/index.html')
assert "block nav_links_ul" not in tpl_index.template.source

tpl_base = loader.get_template('adminlte/base.html')
assert "tests/demo_project/demo/templates/adminlte/base.html" in tpl_base.origin.name
base_content = tpl_base.render()
assert "fa-address-book-o" in base_content

tpl_base2 = loader.get_template('cruds/base.html')
assert "templates/cruds/base.html" in tpl_base2.origin.name
base_content = tpl_base2.render()
assert "fa-address-book-o" in base_content

# Now we ask for a template that is outside of the library,
# but extends the library
tpl = loader.get_template('homepage.html')
assert "templates/homepage.html" in tpl.origin.name
tpl_content = tpl.render()
assert "Hello." in tpl_content
assert "fa-address-book-o" in tpl_content