Skip to content

Commit

Permalink
Merge branch 'release/0.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
Fantomas42 committed Aug 18, 2014
2 parents 96537f2 + af09234 commit 0c0aac0
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 60 deletions.
1 change: 1 addition & 0 deletions .coveragerc
@@ -1,6 +1,7 @@
[run]
source = app_namespace
omit = app_namespace/tests/*
app_namespace/demo/*

[report]
exclude_lines =
Expand Down
22 changes: 16 additions & 6 deletions .travis.yml
@@ -1,16 +1,26 @@
language: python
python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
- 2.6
- 2.7
- 3.2
- 3.3
env:
- DJANGO=1.4
- DJANGO=1.5
- DJANGO=1.6
matrix:
exclude:
- python: 3.2
env: DJANGO=1.4
- python: 3.3
env: DJANGO=1.4
install:
- pip install -U setuptools
- python bootstrap.py
- ./bin/buildout
- ./bin/buildout versions:django=$DJANGO
before_script:
- ./bin/flake8 app_namespace
script:
- ./bin/cover
- ./bin/test-and-cover
after_success:
- ./bin/coveralls
93 changes: 87 additions & 6 deletions README.rst
Expand Up @@ -11,23 +11,104 @@ template at the same time.
The default Django loaders require you to copy the entire template you want
to override, even if you only want to override one small block.

Template usage example (extend and override the title block of Django admin
base template): ::
This is the issue that this package tries to resolve.

Examples:
---------

You want to change the titles of the admin site, you would originally
created this template: ::

$ cat my-project/templates/admin/base_site.html
{% extends "admin/base.html" %}
{% load i18n %}

{% block title %}{{ title }} | My Project{% endblock %}

{% block branding %}
<h1 id="site-name">My Project</h1>
{% endblock %}

{% block nav-global %}{% endblock %}

Extend and override version with a namespace: ::

$ cat my-project/templates/admin/base_site.html
{% extends "admin:admin/base_site.html" %}

{% block title %}{{ title }} - My Project{% endblock %}

Simply add this line into the ``TEMPLATE_LOADERS`` setting of your project to
benefit this feature once the module installed. ::
{% block branding %}
<h1 id="site-name">My Project</h1>
{% endblock %}

Note that in this version the block ``nav-global`` does not have to be
present because of the inheritance.

Shorter version without namespace: ::

$ cat my-project/templates/admin/base_site.html
{% extends ":admin/base_site.html" %}

{% block title %}{{ title }} - My Project{% endblock %}

{% block branding %}
<h1 id="site-name">My Project</h1>
{% endblock %}

If we do not specify the application namespace, the first matching template
will be used. This is useful when several applications provide the same
templates but with different features.

Example of multiple empty namespaces: ::

$ cat my-project/application/templates/application/template.html
{% block content%}
<p>Application</p>
{% endblock content%}

$ cat my-project/application_extension/templates/application/template.html
{% extends ":application/template.html" %}
{% block content%}
{{ block.super }}
<p>Application extension</p>
{% endblock content%}

$ cat my-project/templates/application/template.html
{% extends ":application/template.html" %}
{% block content%}
{{ block.super }}
<p>Application project</p>
{% endblock content%}

Will render: ::

<p>Application</p>
<p>Application extension</p>
<p>Application project</p>

Installation
------------

Add ``app_namespace.Loader`` to the ``TEMPLATE_LOADERS`` setting of your
project. ::

TEMPLATE_LOADERS = [
'app_namespace.Loader',
... # Others template loader
... # Other template loaders
]

Based on: http://djangosnippets.org/snippets/1376/
Known limitations
=================

``app_namespace.Loader`` can not work properly if you use it in conjunction
with ``django.template.loaders.cached.Loader`` and inheritance based on
empty namespaces.

Notes
-----

Based originally on: http://djangosnippets.org/snippets/1376/

Requires: Django >= 1.4

Expand Down
1 change: 1 addition & 0 deletions app_namespace/demo/__init__.py
@@ -0,0 +1 @@
"""Demo of the app_namespace app"""
3 changes: 3 additions & 0 deletions app_namespace/demo/application/__init__.py
@@ -0,0 +1,3 @@
"""
demo.application
"""
33 changes: 33 additions & 0 deletions app_namespace/demo/application/templates/application/template.html
@@ -0,0 +1,33 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="author" content="Fantomas42">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Django-app-namespace-template-loader{% endblock title %}</title>
{% block style %}
{% endblock style %}
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12 col-md-offset-0">
<h1>{% block header %}Header{% endblock header %}</h1>
<p>{% block content %}Content{% endblock content %}</p>
<ul>
{% block list %}
<li>
<code>application:application/template.html</code>
</li>
{% endblock%}
</ul>
<footer>
<p class="text-right">
{% block footer %}Footer{% endblock footer %}
</p>
</footer>
</div>
</div>
</div>
</body>
</html>
3 changes: 3 additions & 0 deletions app_namespace/demo/application_extension/__init__.py
@@ -0,0 +1,3 @@
"""
demo.application_extension
"""
@@ -0,0 +1,14 @@
{% extends ":application/template.html" %}

{% block title %}Django-app-namespace-template-loader{% endblock title %}

{% block header %}Django-app-namespace-loader{% endblock header %}

{% block content %}This page has been generated by a combination of extend and override of these templates:{% endblock content %}

{% block list %}
<li>
<code>application_extension:application/template.html</code>
</li>
{{ block.super }}
{% endblock%}
28 changes: 28 additions & 0 deletions app_namespace/demo/settings.py
@@ -0,0 +1,28 @@
"""Settings for the app_namespace demo"""
import os

PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))

DEBUG = True
TEMPLATE_DEBUG = DEBUG

STATIC_URL = '/static/'

SECRET_KEY = 'secret-key'

ROOT_URLCONF = 'app_namespace.demo.urls'

TEMPLATE_LOADERS = (
'app_namespace.Loader',
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)

TEMPLATE_DIRS = (
os.path.join(PROJECT_ROOT, 'templates')
)

INSTALLED_APPS = (
'app_namespace.demo.application_extension',
'app_namespace.demo.application'
)
22 changes: 22 additions & 0 deletions app_namespace/demo/templates/application/template.html
@@ -0,0 +1,22 @@
{% extends ":application/template.html" %}

{% block title %}Demo - {{ block.super }}{% endblock title %}

{% block style %}
<link href="//netdna.bootstrapcdn.com/bootstrap/3.0.1/css/bootstrap.min.css" rel="stylesheet">
<style type="text/css">
body { background-color: #eee; }
.container .row div { background-color: white; }
</style>
{% endblock style %}

{% block list %}
<li>
<code>application/template.html</code>
</li>
{{ block.super }}
{% endblock list %}

{% block footer %}
Demo made by <a href="https://github.com/Fantomas42">Fantomas42</a>.
{% endblock footer %}
11 changes: 11 additions & 0 deletions app_namespace/demo/urls.py
@@ -0,0 +1,11 @@
"""Urls for the app_namespace demo"""
from django.conf.urls import url
from django.conf.urls import patterns
from django.views.generic import TemplateView


urlpatterns = patterns(
'',
url(r'^$', TemplateView.as_view(
template_name='application/template.html')),
)
67 changes: 52 additions & 15 deletions app_namespace/loader.py
Expand Up @@ -2,14 +2,18 @@
import os
import sys

from django.utils import six
import six

from django.conf import settings
from django.utils._os import safe_join
from django.template.loader import BaseLoader
from django.utils.importlib import import_module
from django.utils.functional import cached_property
from django.template.base import TemplateDoesNotExist
from django.core.exceptions import ImproperlyConfigured
from django.utils.datastructures import SortedDict # Deprecated in Django 1.9

FS_ENCODING = sys.getfilesystemencoding() or sys.getdefaultencoding()


class Loader(BaseLoader):
Expand All @@ -19,17 +23,30 @@ class Loader(BaseLoader):
"""
is_usable = True

def __init__(self, *args, **kwargs):
super(Loader, self).__init__(self, *args, **kwargs)
self._already_used = []

def reset(self):
"""
Empty the cache of paths already used.
"""
self._already_used = []

def get_app_template_path(self, app, template_path):
"""
Return the full path of a template located in an app.
"""
return safe_join(self.app_templates_dirs[app], template_path)

@cached_property
def app_templates_dirs(self):
"""
Build a cached dict with settings.INSTALLED_APPS as keys
and the 'templates' directory of each application as values.
"""
app_templates_dirs = {}
app_templates_dirs = SortedDict()
for app in settings.INSTALLED_APPS:
if not six.PY3:
fs_encoding = (sys.getfilesystemencoding() or
sys.getdefaultencoding())
try:
mod = import_module(app)
except ImportError as e: # pragma: no cover
Expand All @@ -39,8 +56,8 @@ def app_templates_dirs(self):
templates_dir = os.path.join(os.path.dirname(mod.__file__),
'templates')
if os.path.isdir(templates_dir):
if not six.PY3:
templates_dir = templates_dir.decode(fs_encoding)
if six.PY2:
templates_dir = templates_dir.decode(FS_ENCODING)
app_templates_dirs[app] = templates_dir
if '.' in app:
app_templates_dirs[app.split('.')[-1]] = templates_dir
Expand All @@ -53,17 +70,37 @@ def load_template_source(self, template_name, template_dirs=None):
is the name of the application and the last item is the true
value of 'template_name' provided by the specified application.
"""
if not ':' in template_name:
if ':' not in template_name:
self.reset()
raise TemplateDoesNotExist(template_name)

try:
app, template_path = template_name.split(':')
app, template_path = template_name.split(':')

file_path = safe_join(self.app_templates_dirs[app],
template_path)
with open(file_path, 'rb') as fp:
return (fp.read().decode(settings.FILE_CHARSET),
'app_namespace:%s:%s' % (app, file_path))
if app:
return self.load_template_source_inner(
template_name, app, template_path)

for app in self.app_templates_dirs:
file_path = self.get_app_template_path(app, template_path)
if file_path in self._already_used:
continue
try:
template = self.load_template_source_inner(
template_name, app, template_path)
self._already_used.append(file_path)
return template
except TemplateDoesNotExist:
pass
raise TemplateDoesNotExist(template_name)

def load_template_source_inner(self, template_name, app, template_path):
"""
Try to load 'template_path' in the templates directory of 'app'.
"""
try:
file_path = self.get_app_template_path(app, template_path)
with open(file_path, 'rb') as fp:
template = fp.read().decode(settings.FILE_CHARSET)
return (template, 'app_namespace:%s:%s' % (app, file_path))
except (IOError, KeyError, ValueError):
raise TemplateDoesNotExist(template_name)

0 comments on commit 0c0aac0

Please sign in to comment.