├── .coveragerc
├── .travis.yml
├── LICENSE
├── MANIFEST.in
├── README.rst
├── app_namespace
├── __init__.py
├── demo
│ ├── __init__.py
│ ├── application
│ │ ├── __init__.py
│ │ └── templates
│ │ │ └── application
│ │ │ └── template.html
│ ├── application_appconfig
│ │ ├── __init__.py
│ │ ├── apps.py
│ │ └── templates
│ │ │ └── application
│ │ │ └── template.html
│ ├── application_extension
│ │ ├── __init__.py
│ │ └── templates
│ │ │ └── application
│ │ │ └── template.html
│ ├── settings.py
│ ├── templates
│ │ └── application
│ │ │ └── template.html
│ └── urls.py
├── loader.py
└── tests
│ ├── __init__.py
│ ├── settings.py
│ ├── template.html
│ ├── tests.py
│ └── urls.py
├── bootstrap.py
├── buildout.cfg
├── setup.cfg
├── setup.py
└── versions.cfg
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | source = app_namespace
3 | omit = app_namespace/tests/*
4 | app_namespace/demo/*
5 |
6 | [report]
7 | exclude_lines =
8 | pragma: no cover
9 | raise
10 | show_missing = True
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: python
3 | python:
4 | - 2.7
5 | - 3.4
6 | - 3.5
7 | env:
8 | - DJANGO=1.8
9 | - DJANGO=1.9
10 | - DJANGO=1.10
11 | install:
12 | - pip install -U setuptools
13 | - python bootstrap.py
14 | - ./bin/buildout versions:django=$DJANGO
15 | before_script:
16 | - ./bin/flake8 app_namespace
17 | script:
18 | - ./bin/test-and-cover
19 | after_success:
20 | - ./bin/coveralls
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009-2014, Julien Fache
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are
6 | met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above
11 | copyright notice, this list of conditions and the following
12 | disclaimer in the documentation and/or other materials provided
13 | with the distribution.
14 | * Neither the name of the author nor the names of other
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include README.rst
3 | include versions.cfg
4 | include buildout.cfg
5 | include bootstrap.py
6 | recursive-include app_namespace/demo *
7 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ====================================
2 | Django App Namespace Template Loader
3 | ====================================
4 |
5 | |travis-develop| |coverage-develop|
6 |
7 | Provides a template loader that allows you to load a template from a
8 | specific application. This allows you to both **extend** and **override** a
9 | template at the same time.
10 |
11 | The default Django loaders require you to copy the entire template you want
12 | to override, even if you only want to override one small block.
13 |
14 | This is the issue that this package tries to resolve.
15 |
16 | Examples:
17 | ---------
18 |
19 | You want to change the titles of the admin site, you would originally
20 | created this template: ::
21 |
22 | $ cat my-project/templates/admin/base_site.html
23 | {% extends "admin/base.html" %}
24 | {% load i18n %}
25 |
26 | {% block title %}{{ title }} | My Project{% endblock %}
27 |
28 | {% block branding %}
29 |
My Project
30 | {% endblock %}
31 |
32 | {% block nav-global %}{% endblock %}
33 |
34 | Extend and override version with a namespace: ::
35 |
36 | $ cat my-project/templates/admin/base_site.html
37 | {% extends "admin:admin/base_site.html" %}
38 |
39 | {% block title %}{{ title }} - My Project{% endblock %}
40 |
41 | {% block branding %}
42 | My Project
43 | {% endblock %}
44 |
45 | Note that in this version the block ``nav-global`` does not have to be
46 | present because of the inheritance.
47 |
48 | Shorter version without namespace: ::
49 |
50 | $ cat my-project/templates/admin/base_site.html
51 | {% extends ":admin/base_site.html" %}
52 |
53 | {% block title %}{{ title }} - My Project{% endblock %}
54 |
55 | {% block branding %}
56 | My Project
57 | {% endblock %}
58 |
59 | If we do not specify the application namespace, the first matching template
60 | will be used. This is useful when several applications provide the same
61 | templates but with different features.
62 |
63 | Example of multiple empty namespaces: ::
64 |
65 | $ cat my-project/application/templates/application/template.html
66 | {% block content %}
67 | Application
68 | {% endblock content %}
69 |
70 | $ cat my-project/application_extension/templates/application/template.html
71 | {% extends ":application/template.html" %}
72 | {% block content %}
73 | {{ block.super }}
74 | Application extension
75 | {% endblock content %}
76 |
77 | $ cat my-project/templates/application/template.html
78 | {% extends ":application/template.html" %}
79 | {% block content %}
80 | {{ block.super }}
81 | Application project
82 | {% endblock content %}
83 |
84 | Will render: ::
85 |
86 | Application
87 | Application extension
88 | Application project
89 |
90 | Installation
91 | ------------
92 |
93 | First of all install ``django-app-namespace-template-loader`` with your
94 | favorite package manager. Example : ::
95 |
96 | $ pip install django-app-namespace-template-loader
97 |
98 | Once installed, add ``app_namespace.Loader`` to the ``TEMPLATE_LOADERS``
99 | setting of your project. ::
100 |
101 | TEMPLATE_LOADERS = [
102 | 'app_namespace.Loader',
103 | ... # Other template loaders
104 | ]
105 |
106 | With Django >= 1.8 ``app_namespace.Loader`` should be added to the
107 | ``'loaders'`` section in the OPTIONS dict of the ``DjangoTemplates`` backend
108 | instead. ::
109 |
110 | TEMPLATES = [
111 | {
112 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
113 | 'OPTIONS': {
114 | 'loaders': [
115 | 'app_namespace.Loader',
116 | 'django.template.loaders.filesystem.Loader',
117 | 'django.template.loaders.app_directories.Loader',
118 | ],
119 | },
120 | },
121 | ]
122 |
123 | Note: With Django 1.8, ``app_namespace.Loader`` should be first in the list
124 | of loaders.
125 |
126 | Known limitations
127 | =================
128 |
129 | ``app_namespace.Loader`` can not work properly if you use it in conjunction
130 | with ``django.template.loaders.cached.Loader`` and inheritance based on
131 | empty namespaces.
132 |
133 | Notes
134 | -----
135 |
136 | Based originally on: http://djangosnippets.org/snippets/1376/
137 |
138 | Requires: Django >= 1.8
139 |
140 | Tested with Python 2.7, 3.3, 3.4.
141 |
142 | If you want to use this application for previous versions of Django, use the
143 | version 0.3.1 of the package.
144 |
145 | If you want to use this application with Python 2.6, use the version 0.2 of
146 | the package.
147 |
148 | .. |travis-develop| image:: https://travis-ci.org/Fantomas42/django-app-namespace-template-loader.png?branch=develop
149 | :alt: Build Status - develop branch
150 | :target: http://travis-ci.org/Fantomas42/django-app-namespace-template-loader
151 | .. |coverage-develop| image:: https://coveralls.io/repos/Fantomas42/django-app-namespace-template-loader/badge.png?branch=develop
152 | :alt: Coverage of the code
153 | :target: https://coveralls.io/r/Fantomas42/django-app-namespace-template-loader
154 |
--------------------------------------------------------------------------------
/app_namespace/__init__.py:
--------------------------------------------------------------------------------
1 | """App namespace template loader"""
2 | from app_namespace.loader import Loader
3 |
4 | __all__ = [Loader.__name__]
5 |
--------------------------------------------------------------------------------
/app_namespace/demo/__init__.py:
--------------------------------------------------------------------------------
1 | """Demo of the app_namespace app"""
2 |
--------------------------------------------------------------------------------
/app_namespace/demo/application/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | demo.application
3 | """
4 |
--------------------------------------------------------------------------------
/app_namespace/demo/application/templates/application/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% block title %}Django-app-namespace-template-loader{% endblock title %}
8 | {% block style %}
9 | {% endblock style %}
10 |
11 |
12 |
13 |
14 |
15 |
{% block header %}Header{% endblock header %}
16 |
{% block content %}Content{% endblock content %}
17 |
18 | {% block list %}
19 |
20 | application:application/template.html
21 |
22 | {% endblock%}
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app_namespace/demo/application_appconfig/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | demo.application_extension
3 | """
4 |
--------------------------------------------------------------------------------
/app_namespace/demo/application_appconfig/apps.py:
--------------------------------------------------------------------------------
1 | """Apps for application_appconfig"""
2 | from django.apps import AppConfig
3 |
4 |
5 | class ApplicationConfig(AppConfig):
6 | name = __name__
7 | label = 'appconfig'
8 |
--------------------------------------------------------------------------------
/app_namespace/demo/application_appconfig/templates/application/template.html:
--------------------------------------------------------------------------------
1 | {% extends ":application/template.html" %}
2 |
3 | {% block list %}
4 |
5 | application_appconfig:application/template.html
6 |
7 | {{ block.super }}
8 | {% endblock%}
9 |
--------------------------------------------------------------------------------
/app_namespace/demo/application_extension/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | demo.application_extension
3 | """
4 |
--------------------------------------------------------------------------------
/app_namespace/demo/application_extension/templates/application/template.html:
--------------------------------------------------------------------------------
1 | {% extends ":application/template.html" %}
2 |
3 | {% block title %}Django-app-namespace-template-loader{% endblock title %}
4 |
5 | {% block header %}Django-app-namespace-loader{% endblock header %}
6 |
7 | {% block content %}This page has been generated by a combination of extend and override of these templates:{% endblock content %}
8 |
9 | {% block list %}
10 |
11 | application_extension:application/template.html
12 |
13 | {{ block.super }}
14 | {% endblock%}
15 |
--------------------------------------------------------------------------------
/app_namespace/demo/settings.py:
--------------------------------------------------------------------------------
1 | """Settings for the app_namespace demo"""
2 | import os
3 |
4 | PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
5 |
6 | DEBUG = True
7 |
8 | STATIC_URL = '/static/'
9 |
10 | SECRET_KEY = 'secret-key'
11 |
12 | ROOT_URLCONF = 'app_namespace.demo.urls'
13 |
14 | TEMPLATES = [
15 | {
16 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
17 | 'DIRS': [
18 | os.path.join(PROJECT_ROOT, 'templates'),
19 | ],
20 | 'OPTIONS': {
21 | 'debug': DEBUG,
22 | 'loaders': ('app_namespace.Loader',
23 | 'django.template.loaders.filesystem.Loader',
24 | 'django.template.loaders.app_directories.Loader')
25 | }
26 | }
27 | ]
28 |
29 | INSTALLED_APPS = (
30 | 'app_namespace.demo.application_extension',
31 | 'app_namespace.demo.application_appconfig.apps.ApplicationConfig',
32 | 'app_namespace.demo.application',
33 | )
34 |
35 | SILENCED_SYSTEM_CHECKS = ['1_7.W001', '1_8.W001']
36 |
--------------------------------------------------------------------------------
/app_namespace/demo/templates/application/template.html:
--------------------------------------------------------------------------------
1 | {% extends ":application/template.html" %}
2 |
3 | {% block title %}Demo - {{ block.super }}{% endblock title %}
4 |
5 | {% block style %}
6 |
7 |
11 | {% endblock style %}
12 |
13 | {% block list %}
14 |
15 | application/template.html
16 |
17 | {{ block.super }}
18 | {% endblock list %}
19 |
20 | {% block footer %}
21 | Demo made by Fantomas42 .
22 | {% endblock footer %}
23 |
--------------------------------------------------------------------------------
/app_namespace/demo/urls.py:
--------------------------------------------------------------------------------
1 | """Urls for the app_namespace demo"""
2 | from django.conf.urls import url
3 | from django.views.generic import TemplateView
4 |
5 |
6 | urlpatterns = [
7 | url(r'^$', TemplateView.as_view(
8 | template_name='application/template.html')),
9 | ]
10 |
--------------------------------------------------------------------------------
/app_namespace/loader.py:
--------------------------------------------------------------------------------
1 | """Template loader for app-namespace"""
2 | import errno
3 | import io
4 | import os
5 | from collections import OrderedDict
6 |
7 | import django
8 | from django.apps import apps
9 | try:
10 | from django.template import Origin
11 | except ImportError: # pragma: no cover
12 | class Origin(object):
13 | def __init__(self, **kwargs):
14 | for k, v in kwargs.items():
15 | setattr(self, k, v)
16 | from django.template import TemplateDoesNotExist
17 | from django.template.loaders.base import Loader as BaseLoader
18 | from django.utils._os import safe_join
19 | from django.utils._os import upath
20 | from django.utils.functional import cached_property
21 |
22 |
23 | class NamespaceOrigin(Origin):
24 |
25 | def __init__(self, app_name, *args, **kwargs):
26 | self.app_name = app_name
27 | super(NamespaceOrigin, self).__init__(*args, **kwargs)
28 |
29 |
30 | class Loader(BaseLoader):
31 | """
32 | App namespace loader for allowing you to both extend and override
33 | a template provided by an app at the same time.
34 | """
35 | is_usable = True
36 |
37 | def __init__(self, *args, **kwargs):
38 | super(Loader, self).__init__(*args, **kwargs)
39 | self._already_used = []
40 |
41 | def reset(self, mandatory_on_django_18):
42 | """
43 | Empty the cache of paths already used.
44 | """
45 | if django.VERSION[1] == 8:
46 | if not mandatory_on_django_18:
47 | return
48 | self._already_used = []
49 |
50 | def get_app_template_path(self, app, template_name):
51 | """
52 | Return the full path of a template name located in an app.
53 | """
54 | return safe_join(self.app_templates_dirs[app], template_name)
55 |
56 | @cached_property
57 | def app_templates_dirs(self):
58 | """
59 | Build a cached dict with settings.INSTALLED_APPS as keys
60 | and the 'templates' directory of each application as values.
61 | """
62 | app_templates_dirs = OrderedDict()
63 | for app_config in apps.get_app_configs():
64 | templates_dir = os.path.join(
65 | getattr(app_config, 'path', '/'), 'templates')
66 | if os.path.isdir(templates_dir):
67 | templates_dir = upath(templates_dir)
68 | app_templates_dirs[app_config.name] = templates_dir
69 | app_templates_dirs[app_config.label] = templates_dir
70 | return app_templates_dirs
71 |
72 | def get_contents(self, origin):
73 | """
74 | Try to load the origin.
75 | """
76 | try:
77 | path = self.get_app_template_path(
78 | origin.app_name, origin.template_name)
79 | with io.open(path, encoding=self.engine.file_charset) as fp:
80 | return fp.read()
81 | except KeyError:
82 | raise TemplateDoesNotExist(origin)
83 | except IOError as error:
84 | if error.errno == errno.ENOENT:
85 | raise TemplateDoesNotExist(origin)
86 | raise
87 |
88 | def get_template_sources(self, template_name):
89 | """
90 | Build a list of Origin to load 'template_name' splitted with ':'.
91 | The first item is the name of the application and the last item
92 | is the true value of 'template_name' provided by the specified
93 | application.
94 | """
95 | if ':' not in template_name:
96 | self.reset(True)
97 | return
98 |
99 | app, template_path = template_name.split(':')
100 | if app:
101 | yield NamespaceOrigin(
102 | app_name=app,
103 | name='app_namespace:%s:%s' % (app, template_name),
104 | template_name=template_path,
105 | loader=self)
106 | return
107 |
108 | self.reset(False)
109 | for app in self.app_templates_dirs:
110 | file_path = self.get_app_template_path(app, template_path)
111 | if file_path in self._already_used:
112 | continue
113 | self._already_used.append(file_path)
114 | yield NamespaceOrigin(
115 | app_name=app,
116 | name='app_namespace:%s:%s' % (app, template_name),
117 | template_name=template_path,
118 | loader=self)
119 |
120 | def load_template_source(self, *ka):
121 | """
122 | Backward compatible method for Django < 2.0.
123 | """
124 | template_name = ka[0]
125 | for origin in self.get_template_sources(template_name):
126 | try:
127 | return self.get_contents(origin), origin.name
128 | except TemplateDoesNotExist:
129 | pass
130 | raise TemplateDoesNotExist(template_name)
131 |
--------------------------------------------------------------------------------
/app_namespace/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """Tests for app_namespace"""
2 |
--------------------------------------------------------------------------------
/app_namespace/tests/settings.py:
--------------------------------------------------------------------------------
1 | """Settings for testing app_namespace"""
2 | DATABASES = {'default': {'NAME': 'app_namespace.db',
3 | 'ENGINE': 'django.db.backends.sqlite3'}}
4 |
5 | SECRET_KEY = 'secret-key'
6 |
7 | TEMPLATES = [
8 | {
9 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
10 | 'OPTIONS': {
11 | 'loaders': ('app_namespace.Loader',
12 | 'django.template.loaders.app_directories.Loader')
13 | }
14 | }
15 | ]
16 |
17 | ROOT_URLCONF = 'app_namespace.tests.urls'
18 |
19 | INSTALLED_APPS = ('django.contrib.auth',
20 | 'django.contrib.admin',
21 | 'django.contrib.contenttypes')
22 |
23 | SILENCED_SYSTEM_CHECKS = ['1_7.W001']
24 |
--------------------------------------------------------------------------------
/app_namespace/tests/template.html:
--------------------------------------------------------------------------------
1 | {% extends ":admin/base.html" %}
2 |
--------------------------------------------------------------------------------
/app_namespace/tests/tests.py:
--------------------------------------------------------------------------------
1 | """Tests for app_namespace"""
2 | import os
3 | import shutil
4 | import sys
5 | import tempfile
6 |
7 | from app_namespace import Loader
8 |
9 | import django
10 | from django.core.urlresolvers import reverse
11 | from django.template import TemplateDoesNotExist
12 | from django.template.base import Context
13 | from django.template.base import Template
14 | from django.template.engine import Engine
15 | from django.template.loaders import app_directories
16 | from django.test import TestCase
17 | from django.test.utils import override_settings
18 |
19 |
20 | class LoaderTestCase(TestCase):
21 |
22 | def test_load_template(self):
23 | libraries = {
24 | 'i18n': 'django.templatetags.i18n',
25 | 'static': 'django.templatetags.static',
26 | 'admin_static': 'django.contrib.admin.templatetags.admin_static'}
27 |
28 | def build_engine():
29 | try:
30 | return Engine(libraries=libraries)
31 | except TypeError:
32 | return Engine()
33 |
34 | app_namespace_loader = Loader(build_engine())
35 | app_directory_loader = app_directories.Loader(build_engine())
36 |
37 | template_directory = app_directory_loader.load_template(
38 | 'admin/base.html')[0]
39 | template_namespace = app_namespace_loader.load_template(
40 | 'admin:admin/base.html')[0]
41 | context = Context({})
42 | self.assertEquals(template_directory.render(context),
43 | template_namespace.render(context))
44 |
45 | def test_load_template_source(self):
46 | app_namespace_loader = Loader(Engine())
47 | app_directory_loader = app_directories.Loader(Engine())
48 |
49 | template_directory = app_directory_loader.load_template_source(
50 | 'admin/base.html')
51 | template_namespace = app_namespace_loader.load_template_source(
52 | 'admin:admin/base.html')
53 | self.assertEquals(template_directory[0], template_namespace[0])
54 | self.assertTrue('app_namespace:admin:' in template_namespace[1])
55 | self.assertTrue('admin/base.html' in template_namespace[1])
56 |
57 | self.assertRaises(TemplateDoesNotExist,
58 | app_namespace_loader.load_template_source,
59 | 'no-namespace-template')
60 | self.assertRaises(TemplateDoesNotExist,
61 | app_namespace_loader.load_template_source,
62 | 'no.app.namespace:template')
63 |
64 | def test_load_template_source_empty_namespace(self):
65 | app_namespace_loader = Loader(Engine())
66 | app_directory_loader = app_directories.Loader(Engine())
67 |
68 | template_directory = app_directory_loader.load_template_source(
69 | 'admin/base.html')
70 | template_namespace = app_namespace_loader.load_template_source(
71 | ':admin/base.html')
72 |
73 | self.assertEquals(template_directory[0], template_namespace[0])
74 | self.assertTrue('app_namespace:django.contrib.admin:' in
75 | template_namespace[1])
76 | self.assertTrue('admin/base.html' in template_namespace[1])
77 |
78 | self.assertRaises(TemplateDoesNotExist,
79 | app_namespace_loader.load_template_source,
80 | ':template')
81 |
82 | def test_load_template_source_dotted_namespace(self):
83 | app_namespace_loader = Loader(Engine())
84 |
85 | template_short = app_namespace_loader.load_template_source(
86 | 'admin:admin/base.html')
87 | template_dotted = app_namespace_loader.load_template_source(
88 | 'django.contrib.admin:admin/base.html')
89 |
90 | self.assertEquals(template_short[0], template_dotted[0])
91 |
92 | def test_load_template_invalid_namespace_valid_template(self):
93 | app_namespace_loader = Loader(Engine())
94 | with self.assertRaises(TemplateDoesNotExist):
95 | app_namespace_loader.load_template_source(
96 | 'invalid:admin/base.html')
97 |
98 | def test_load_template_valid_namespace_invalid_template(self):
99 | app_namespace_loader = Loader(Engine())
100 | with self.assertRaises(TemplateDoesNotExist):
101 | app_namespace_loader.load_template_source(
102 | 'admin:admin/base_invalid.html')
103 |
104 |
105 | @override_settings(
106 | TEMPLATES=[
107 | {
108 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
109 | 'OPTIONS': {
110 | 'loaders': ('app_namespace.Loader',
111 | 'django.template.loaders.app_directories.Loader')
112 | }
113 | }
114 | ]
115 | )
116 | class TemplateTestCase(TestCase):
117 | maxDiff = None
118 |
119 | def test_extend_and_override(self):
120 | """
121 | Here we simulate the existence of a template
122 | named admin/base_site.html on the filesystem
123 | overriding the title markup of the template.
124 | In this test we can view the advantage of using
125 | the app_namespace template loader.
126 | """
127 | context = Context({})
128 | mark = 'Django administration'
129 | mark_title = 'APP NAMESPACE '
130 |
131 | template_directory = Template(
132 | '{% extends "admin/base.html" %}'
133 | '{% block title %}APP NAMESPACE{% endblock %}'
134 | ).render(context)
135 |
136 | template_namespace = Template(
137 | '{% extends "admin:admin/base_site.html" %}'
138 | '{% block title %}APP NAMESPACE{% endblock %}'
139 | ).render(context)
140 |
141 | self.assertTrue(mark in template_namespace)
142 | self.assertTrue(mark_title in template_namespace)
143 | self.assertTrue(mark not in template_directory)
144 | self.assertTrue(mark_title in template_directory)
145 |
146 | template_directory = Template(
147 | '{% extends "admin/base.html" %}'
148 | '{% load i18n %}'
149 | '{% block title %}APP NAMESPACE{% endblock %}'
150 | '{% block branding %}'
151 | '{% endblock %}'
154 | '{% block nav-global %}{% endblock %}'
155 | ).render(context)
156 |
157 | self.assertHTMLEqual(template_directory, template_namespace)
158 | self.assertTrue(mark in template_directory)
159 | self.assertTrue(mark_title in template_directory)
160 |
161 | def test_extend_empty_namespace(self):
162 | """
163 | Test that a ":" prefix (empty namespace) gets handled.
164 | """
165 | context = Context({})
166 | mark = 'Django administration'
167 | mark_title = 'APP NAMESPACE '
168 |
169 | template_namespace = Template(
170 | '{% extends ":admin/base_site.html" %}'
171 | '{% block title %}APP NAMESPACE{% endblock %}'
172 | ).render(context)
173 |
174 | self.assertTrue(mark in template_namespace)
175 | self.assertTrue(mark_title in template_namespace)
176 |
177 | def test_extend_with_super(self):
178 | """
179 | Here we simulate the existence of a template
180 | named admin/base_site.html on the filesystem
181 | overriding the title markup of the template
182 | with a {{ super }}.
183 | """
184 | context = Context({})
185 | mark_ok = ' | Django site admin - APP NAMESPACE '
186 | mark_ko = ' - APP NAMESPACE '
187 |
188 | template_directory = Template(
189 | '{% extends "admin/base.html" %}'
190 | '{% block title %}{{ block.super }} - APP NAMESPACE{% endblock %}'
191 | ).render(context)
192 |
193 | template_namespace = Template(
194 | '{% extends "admin:admin/base_site.html" %}'
195 | '{% block title %}{{ block.super }} - APP NAMESPACE{% endblock %}'
196 | ).render(context)
197 |
198 | self.assertTrue(mark_ok in template_namespace)
199 | self.assertTrue(mark_ko in template_directory)
200 |
201 |
202 | @override_settings(
203 | TEMPLATES=[
204 | {
205 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
206 | 'OPTIONS': {
207 | 'loaders': (
208 | 'app_namespace.Loader',
209 | 'django.template.loaders.app_directories.Loader')
210 | }
211 | }
212 | ]
213 | )
214 | class MultiAppTestCase(TestCase):
215 | """
216 | Test case creating multiples apps containing templates
217 | with the same path which extends with an empty namespace.
218 |
219 | Each template will use a {{ block.super }} with an unique
220 | identifier to test the multiple cumulations in the final
221 | rendering.
222 | """
223 | maxDiff = None
224 | template_initial = """
225 | {%% block content %%}
226 | %(app)s
227 | {%% endblock content %%}
228 | """
229 | template_extend = """
230 | {%% extends ":template.html" %%}
231 | {%% block content %%}
232 | %(app)s
233 | {{ block.super }}
234 | {%% endblock content %%}
235 | """
236 | template_app = """
237 | from django.apps import AppConfig
238 | class ApplicationConfig(AppConfig):
239 | name = __name__
240 | label = '%(app)s'
241 | """
242 |
243 | def setUp(self):
244 | super(MultiAppTestCase, self).setUp()
245 | # Create a temp directory containing apps
246 | # accessible on the PYTHONPATH.
247 | self.app_directory = tempfile.mkdtemp()
248 | sys.path.append(self.app_directory)
249 |
250 | # Create the apps with the overrided template
251 | self.apps = ['test-template-app-%s' % i for i in range(5)]
252 | for app in self.apps:
253 | app_path = os.path.join(self.app_directory, app)
254 | app_template_path = os.path.join(app_path, 'templates')
255 | os.makedirs(app_template_path)
256 | with open(os.path.join(app_path, '__init__.py'), 'w') as f:
257 | f.write('')
258 | with open(os.path.join(app_path, 'apps.py'), 'w') as f:
259 | f.write(self.template_app % {'app': app})
260 | with open(os.path.join(app_template_path,
261 | 'template.html'), 'w') as f:
262 | f.write((app != self.apps[-1] and
263 | self.template_extend or self.template_initial) %
264 | {'app': app})
265 |
266 | def tearDown(self):
267 | super(MultiAppTestCase, self).tearDown()
268 | sys.path.remove(self.app_directory)
269 | for app in self.apps:
270 | del sys.modules[app]
271 | shutil.rmtree(self.app_directory)
272 |
273 | def multiple_extend_empty_namespace(self, apps=None):
274 | if apps is None:
275 | apps = self.apps
276 | with self.settings(INSTALLED_APPS=apps):
277 | context = Context({})
278 | template = Template(
279 | self.template_extend % {'app': 'top-level'}
280 | ).render(context)
281 | previous_app = ''
282 | for test_app in ['top-level'] + self.apps:
283 | self.assertTrue(test_app in template)
284 | if previous_app:
285 | self.assertTrue(template.index(test_app) >
286 | template.index(previous_app))
287 | previous_app = test_app
288 |
289 | def test_multiple_extend_empty_namespace(self):
290 | self.multiple_extend_empty_namespace()
291 |
292 | def test_app_config_multiple_extend_empty_namespace(self):
293 | apps_config = ['%s.apps.ApplicationConfig' % app
294 | for app in self.apps]
295 | self.multiple_extend_empty_namespace(apps_config)
296 |
297 | @override_settings(
298 | TEMPLATES=[
299 | {
300 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
301 | 'OPTIONS': {
302 | 'loaders': [
303 | ('django.template.loaders.cached.Loader', [
304 | 'app_namespace.Loader',
305 | 'django.template.loaders.app_directories.Loader']),
306 | ]
307 | }
308 | }
309 | ]
310 | )
311 | def test_cached_multiple_extend_empty_namespace(self):
312 | with self.assertRaises(RuntimeError):
313 | self.multiple_extend_empty_namespace()
314 |
315 |
316 | class ViewTestCase(TestCase):
317 |
318 | def load_view_twice(self):
319 | url = reverse('template-view')
320 | r1 = self.client.get(url).content
321 | r2 = self.client.get(url).content
322 | self.assertEquals(r1, r2)
323 |
324 | @override_settings(
325 | TEMPLATES=[
326 | {
327 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
328 | 'DIRS': [
329 | os.path.abspath(os.path.dirname(__file__)),
330 | ],
331 | 'OPTIONS': {
332 | 'loaders': (
333 | 'app_namespace.Loader',
334 | 'django.template.loaders.filesystem.Loader',
335 | 'django.template.loaders.app_directories.Loader',
336 | )
337 | }
338 | }
339 | ]
340 | )
341 | def test_load_view_twice_app_namespace_first(self):
342 | self.load_view_twice()
343 |
344 | @override_settings(
345 | TEMPLATES=[
346 | {
347 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
348 | 'DIRS': [
349 | os.path.abspath(os.path.dirname(__file__)),
350 | ],
351 | 'OPTIONS': {
352 | 'loaders': (
353 | 'django.template.loaders.filesystem.Loader',
354 | 'django.template.loaders.app_directories.Loader',
355 | 'app_namespace.Loader',
356 | )
357 | }
358 | }
359 | ]
360 | )
361 | def test_load_view_twice_app_namespace_last(self):
362 | if django.VERSION[1] == 8:
363 | with self.assertRaises(TemplateDoesNotExist):
364 | self.load_view_twice()
365 | else:
366 | self.load_view_twice()
367 |
--------------------------------------------------------------------------------
/app_namespace/tests/urls.py:
--------------------------------------------------------------------------------
1 | """Urls for testing app_namespace"""
2 | from django.conf.urls import include
3 | from django.conf.urls import url
4 | from django.contrib import admin
5 | from django.views.generic import TemplateView
6 |
7 | admin.autodiscover()
8 |
9 | urlpatterns = [
10 | url(r'^$', TemplateView.as_view(
11 | template_name='template.html'),
12 | name='template-view'),
13 | url(r'^admin/', include(admin.site.urls)),
14 | ]
15 |
--------------------------------------------------------------------------------
/bootstrap.py:
--------------------------------------------------------------------------------
1 | ##############################################################################
2 | #
3 | # Copyright (c) 2006 Zope Foundation and Contributors.
4 | # All Rights Reserved.
5 | #
6 | # This software is subject to the provisions of the Zope Public License,
7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11 | # FOR A PARTICULAR PURPOSE.
12 | #
13 | ##############################################################################
14 | """Bootstrap a buildout-based project
15 |
16 | Simply run this script in a directory containing a buildout.cfg.
17 | The script accepts buildout command-line options, so you can
18 | use the -c option to specify an alternate configuration file.
19 | """
20 |
21 | import os
22 | import shutil
23 | import sys
24 | import tempfile
25 |
26 | from optparse import OptionParser
27 |
28 | tmpeggs = tempfile.mkdtemp()
29 |
30 | usage = '''\
31 | [DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
32 |
33 | Bootstraps a buildout-based project.
34 |
35 | Simply run this script in a directory containing a buildout.cfg, using the
36 | Python that you want bin/buildout to use.
37 |
38 | Note that by using --find-links to point to local resources, you can keep
39 | this script from going over the network.
40 | '''
41 |
42 | parser = OptionParser(usage=usage)
43 | parser.add_option("-v", "--version", help="use a specific zc.buildout version")
44 |
45 | parser.add_option("-t", "--accept-buildout-test-releases",
46 | dest='accept_buildout_test_releases',
47 | action="store_true", default=False,
48 | help=("Normally, if you do not specify a --version, the "
49 | "bootstrap script and buildout gets the newest "
50 | "*final* versions of zc.buildout and its recipes and "
51 | "extensions for you. If you use this flag, "
52 | "bootstrap and buildout will get the newest releases "
53 | "even if they are alphas or betas."))
54 | parser.add_option("-c", "--config-file",
55 | help=("Specify the path to the buildout configuration "
56 | "file to be used."))
57 | parser.add_option("-f", "--find-links",
58 | help=("Specify a URL to search for buildout releases"))
59 | parser.add_option("--allow-site-packages",
60 | action="store_true", default=False,
61 | help=("Let bootstrap.py use existing site packages"))
62 | parser.add_option("--setuptools-version",
63 | help="use a specific setuptools version")
64 |
65 |
66 | options, args = parser.parse_args()
67 |
68 | ######################################################################
69 | # load/install setuptools
70 |
71 | try:
72 | if options.allow_site_packages:
73 | import setuptools
74 | import pkg_resources
75 | from urllib.request import urlopen
76 | except ImportError:
77 | from urllib2 import urlopen
78 |
79 | ez = {}
80 | exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
81 |
82 | if not options.allow_site_packages:
83 | # ez_setup imports site, which adds site packages
84 | # this will remove them from the path to ensure that incompatible versions
85 | # of setuptools are not in the path
86 | import site
87 | # inside a virtualenv, there is no 'getsitepackages'.
88 | # We can't remove these reliably
89 | if hasattr(site, 'getsitepackages'):
90 | for sitepackage_path in site.getsitepackages():
91 | sys.path[:] = [x for x in sys.path if sitepackage_path not in x]
92 |
93 | setup_args = dict(to_dir=tmpeggs, download_delay=0)
94 |
95 | if options.setuptools_version is not None:
96 | setup_args['version'] = options.setuptools_version
97 |
98 | ez['use_setuptools'](**setup_args)
99 | import setuptools
100 | import pkg_resources
101 |
102 | # This does not (always?) update the default working set. We will
103 | # do it.
104 | for path in sys.path:
105 | if path not in pkg_resources.working_set.entries:
106 | pkg_resources.working_set.add_entry(path)
107 |
108 | ######################################################################
109 | # Install buildout
110 |
111 | ws = pkg_resources.working_set
112 |
113 | cmd = [sys.executable, '-c',
114 | 'from setuptools.command.easy_install import main; main()',
115 | '-mZqNxd', tmpeggs]
116 |
117 | find_links = os.environ.get(
118 | 'bootstrap-testing-find-links',
119 | options.find_links or
120 | ('http://downloads.buildout.org/'
121 | if options.accept_buildout_test_releases else None)
122 | )
123 | if find_links:
124 | cmd.extend(['-f', find_links])
125 |
126 | setuptools_path = ws.find(
127 | pkg_resources.Requirement.parse('setuptools')).location
128 |
129 | requirement = 'zc.buildout'
130 | version = options.version
131 | if version is None and not options.accept_buildout_test_releases:
132 | # Figure out the most recent final version of zc.buildout.
133 | import setuptools.package_index
134 | _final_parts = '*final-', '*final'
135 |
136 | def _final_version(parsed_version):
137 | try:
138 | return not parsed_version.is_prerelease
139 | except AttributeError:
140 | # Older setuptools
141 | for part in parsed_version:
142 | if (part[:1] == '*') and (part not in _final_parts):
143 | return False
144 | return True
145 |
146 | index = setuptools.package_index.PackageIndex(
147 | search_path=[setuptools_path])
148 | if find_links:
149 | index.add_find_links((find_links,))
150 | req = pkg_resources.Requirement.parse(requirement)
151 | if index.obtain(req) is not None:
152 | best = []
153 | bestv = None
154 | for dist in index[req.project_name]:
155 | distv = dist.parsed_version
156 | if _final_version(distv):
157 | if bestv is None or distv > bestv:
158 | best = [dist]
159 | bestv = distv
160 | elif distv == bestv:
161 | best.append(dist)
162 | if best:
163 | best.sort()
164 | version = best[-1].version
165 | if version:
166 | requirement = '=='.join((requirement, version))
167 | cmd.append(requirement)
168 |
169 | import subprocess
170 | if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0:
171 | raise Exception(
172 | "Failed to execute command:\n%s" % repr(cmd)[1:-1])
173 |
174 | ######################################################################
175 | # Import and run buildout
176 |
177 | ws.add_entry(tmpeggs)
178 | ws.require(requirement)
179 | import zc.buildout.buildout
180 |
181 | if not [a for a in args if '=' not in a]:
182 | args.append('bootstrap')
183 |
184 | # if -c was provided, we push it back into args for buildout' main function
185 | if options.config_file is not None:
186 | args[0:0] = ['-c', options.config_file]
187 |
188 | zc.buildout.buildout.main(args)
189 | shutil.rmtree(tmpeggs)
190 |
--------------------------------------------------------------------------------
/buildout.cfg:
--------------------------------------------------------------------------------
1 | [buildout]
2 | extends = versions.cfg
3 | parts = demo
4 | test
5 | test-and-cover
6 | flake8
7 | coveralls
8 | evolution
9 | develop = .
10 | eggs = django
11 | django-app-namespace-template-loader
12 | show-picked-versions = true
13 |
14 | [demo]
15 | recipe = djangorecipe
16 | project = app_namespace.demo
17 | settings = settings
18 | eggs = ${buildout:eggs}
19 |
20 | [test]
21 | recipe = pbp.recipe.noserunner
22 | eggs = nose
23 | nose-sfd
24 | nose-progressive
25 | ${buildout:eggs}
26 | defaults = --with-progressive
27 | --with-sfd
28 | environment = testenv
29 |
30 | [test-and-cover]
31 | recipe = pbp.recipe.noserunner
32 | eggs = nose
33 | nose-sfd
34 | coverage
35 | ${buildout:eggs}
36 | defaults = --with-coverage
37 | --cover-package=app_namespace
38 | --cover-erase
39 | --with-sfd
40 | environment = testenv
41 |
42 | [flake8]
43 | recipe = zc.recipe.egg
44 | eggs = flake8
45 | flake8-import-order
46 | pep8-naming
47 |
48 | [coveralls]
49 | recipe = zc.recipe.egg
50 | eggs = python-coveralls
51 |
52 | [evolution]
53 | recipe = zc.recipe.egg
54 | eggs = buildout-versions-checker
55 | scripts = check-buildout-updates=evolve
56 | arguments = '-w --sorting alpha'
57 |
58 | [testenv]
59 | DJANGO_SETTINGS_MODULE = app_namespace.tests.settings
60 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [wheel]
2 | universal = 1
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """Setup script for django-app-namespace-template-loader"""
2 | import os
3 |
4 | from setuptools import find_packages
5 | from setuptools import setup
6 |
7 | __version__ = '0.4.1'
8 | __license__ = 'BSD License'
9 |
10 | __author__ = 'Fantomas42'
11 | __email__ = 'fantomas42@gmail.com'
12 |
13 | __url__ = 'https://github.com/Fantomas42/django-app-namespace-template-loader'
14 |
15 |
16 | setup(
17 | name='django-app-namespace-template-loader',
18 | version=__version__,
19 | zip_safe=False,
20 |
21 | packages=find_packages(exclude=['tests']),
22 | include_package_data=True,
23 |
24 | author=__author__,
25 | author_email=__email__,
26 | url=__url__,
27 |
28 | license=__license__,
29 | platforms='any',
30 | description='Template loader allowing you to both '
31 | 'extend and override a template at the same time. ',
32 | long_description=open(os.path.join('README.rst')).read(),
33 | keywords='django, template, loader',
34 | classifiers=[
35 | 'Framework :: Django',
36 | 'Environment :: Web Environment',
37 | 'Programming Language :: Python :: 2.7',
38 | 'Programming Language :: Python :: 3.3',
39 | 'Programming Language :: Python :: 3.4',
40 | 'Intended Audience :: Developers',
41 | 'Operating System :: OS Independent',
42 | 'License :: OSI Approved :: BSD License',
43 | 'Topic :: Software Development :: Libraries :: Python Modules'],
44 | install_requires=['six']
45 | )
46 |
--------------------------------------------------------------------------------
/versions.cfg:
--------------------------------------------------------------------------------
1 | [versions]
2 | blessings = 1.6
3 | buildout-versions-checker = 1.9.4
4 | configparser = 3.5.0
5 | coverage = 4.0.3
6 | django = 1.10.6
7 | djangorecipe = 2.2.1
8 | enum34 = 1.1.6
9 | flake8 = 3.3.0
10 | flake8-import-order = 0.12
11 | futures = 3.0.5
12 | mccabe = 0.6.1
13 | nose = 1.3.7
14 | nose-progressive = 1.5.1
15 | nose-sfd = 0.4
16 | pbp.recipe.noserunner = 0.2.6
17 | pep8-naming = 0.4.1
18 | pycodestyle = 2.3.1
19 | pyflakes = 1.5.0
20 | python-coveralls = 2.9.0
21 | pyyaml = 3.12
22 | requests = 2.13.0
23 | zc.buildout = 2.9.1
24 | zc.recipe.egg = 2.0.3
25 |
--------------------------------------------------------------------------------