├── djanjinja_test ├── __init__.py ├── cache │ ├── __init__.py │ ├── urls.py │ ├── views.py │ └── tests.py ├── simple │ ├── __init__.py │ ├── urls.py │ ├── views.py │ └── tests.py ├── generic │ ├── __init__.py │ ├── urls.py │ └── tests.py ├── shortcuts │ ├── __init__.py │ ├── urls.py │ ├── views.py │ └── tests.py ├── templates │ ├── 500.html │ ├── plain.txt │ ├── 404.html │ ├── context.txt │ ├── req_context.txt │ ├── cache_global.txt │ ├── middleware.txt │ └── cache_fragment.txt ├── urls.py ├── manage.py └── settings.py ├── .hgignore ├── djanjinja ├── extensions │ ├── __init__.py │ └── cache.py ├── bundles │ ├── cache.py │ ├── __init__.py │ ├── humanize.py │ ├── site.py │ └── csrf.py ├── __init__.py ├── handlers.py ├── middleware.py ├── generic.py ├── bccache.py ├── views.py ├── loader.py └── environment.py ├── .hgtags ├── setup.py ├── UNLICENSE ├── pavement.py ├── pylintrc └── README.md /djanjinja_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /djanjinja_test/cache/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /djanjinja_test/simple/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /djanjinja_test/generic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /djanjinja_test/shortcuts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /djanjinja_test/templates/500.html: -------------------------------------------------------------------------------- 1 | ERROR OCCURRED. -------------------------------------------------------------------------------- /djanjinja_test/templates/plain.txt: -------------------------------------------------------------------------------- 1 | Hello, World! -------------------------------------------------------------------------------- /djanjinja_test/templates/404.html: -------------------------------------------------------------------------------- 1 | NOT FOUND: {{ request_path }} -------------------------------------------------------------------------------- /djanjinja_test/templates/context.txt: -------------------------------------------------------------------------------- 1 | a = {{ a }}; b = {{ b }} -------------------------------------------------------------------------------- /djanjinja_test/templates/req_context.txt: -------------------------------------------------------------------------------- 1 | user.is_anonymous() => {{ user.is_anonymous() }} -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | .DS_Store 4 | *.pyc 5 | build 6 | dist 7 | MANIFEST 8 | *.egg-info 9 | -------------------------------------------------------------------------------- /djanjinja_test/templates/cache_global.txt: -------------------------------------------------------------------------------- 1 | {% do cache.set('key', 'value') %} 2 | {{ cache.get('key') }} -------------------------------------------------------------------------------- /djanjinja_test/templates/middleware.txt: -------------------------------------------------------------------------------- 1 | {% if user.is_anonymous() %}anonymous{% endif %}, a{{ a }}, b{{ b }} -------------------------------------------------------------------------------- /djanjinja_test/templates/cache_fragment.txt: -------------------------------------------------------------------------------- 1 | {% cache "fragment_caching", 3600 %} 2 | {{ call_state.call() }} 3 | {% endcache %} -------------------------------------------------------------------------------- /djanjinja/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Django-specific Jinja2 extensions. 5 | 6 | This module contains extensions to Jinja2 which will aid integration with 7 | Django projects and apps. 8 | """ -------------------------------------------------------------------------------- /djanjinja_test/cache/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls.defaults import patterns, url 4 | 5 | 6 | urlpatterns = patterns('djanjinja_test.cache.views', 7 | url(r'^global/$', 'global_', name='cache-global'), 8 | ) 9 | -------------------------------------------------------------------------------- /djanjinja/bundles/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Add the Django cache object to the global template variables.""" 4 | 5 | from django.core import cache 6 | 7 | from djanjinja.loader import Bundle 8 | 9 | 10 | bundle = Bundle() 11 | bundle.globals['cache'] = cache.cache -------------------------------------------------------------------------------- /djanjinja/bundles/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A number of useful bundles, including some Django ports. 3 | 4 | ``djanjinja.bundles`` contains a number of bundles, using the standard 5 | interface which DjanJinja expects. Some of these bundles are simple ports of 6 | existing Django functionality, others are DjanJinja-specific. 7 | """ 8 | 9 | __all__ = ['cache', 'humanize', 'site'] -------------------------------------------------------------------------------- /djanjinja_test/simple/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls.defaults import patterns, url 4 | 5 | 6 | urlpatterns = patterns('djanjinja_test.simple.views', 7 | url(r'^plain/$', 'plain', name='simple-plain'), 8 | url(r'^context/$', 'context', name='simple-context'), 9 | url(r'^req_context/$', 'req_context', name='simple-req_context'), 10 | ) 11 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 0dd95006cad6ef14ef49582ac45bd6d0db218e42 v0.6 2 | 0dd95006cad6ef14ef49582ac45bd6d0db218e42 v0.6 3 | 0000000000000000000000000000000000000000 v0.6 4 | 0000000000000000000000000000000000000000 v0.6 5 | 713d942dbf8fa34606d5dc522a2a7315ffe33233 v0.6 6 | 9a4519f39fc2741acf9abcaf5e69ee39aab220fd v0.6 7 | 0ad994ff3795afd2b74301977c1b8957849d23df v0.7 8 | 951c6bc15ca2f8bcf4d643dcb204134b9d554773 v0.8 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from distutils.core import setup 4 | 5 | 6 | setup( 7 | name='DjanJinja', 8 | version='0.8', 9 | description='Easy Django and Jinja2 integration.', 10 | packages=['djanjinja', 'djanjinja.bundles', 'djanjinja.extensions'], 11 | url='http://bitbucket.org/zacharyvoase/djanjinja/', 12 | 13 | author='Zachary Voase', 14 | author_email='zacharyvoase@me.com', 15 | ) 16 | -------------------------------------------------------------------------------- /djanjinja_test/shortcuts/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls.defaults import patterns, url 4 | 5 | 6 | urlpatterns = patterns('djanjinja_test.shortcuts.views', 7 | url(r'^plain/$', 'plain', name='shortcuts-plain'), 8 | url(r'^context/$', 'context', name='shortcuts-context'), 9 | url(r'^req_context/$', 'req_context', name='shortcuts-req_context'), 10 | url(r'^middleware/$', 'middleware', name='shortcuts-middleware'), 11 | ) -------------------------------------------------------------------------------- /djanjinja_test/cache/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.http import HttpResponse 4 | 5 | import djanjinja 6 | 7 | 8 | local_env = djanjinja.get_env().copy() 9 | local_env.load('djanjinja', 'cache') 10 | 11 | 12 | def global_(request): 13 | """Renders a template which uses the global cache object.""" 14 | 15 | template = local_env.get_template('cache_global.txt') 16 | content = template.render().strip() 17 | return HttpResponse(content=content) 18 | -------------------------------------------------------------------------------- /djanjinja/bundles/humanize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Port of django.contrib.humanize to Jinja2.""" 4 | 5 | from django.contrib.humanize.templatetags import humanize 6 | from djanjinja.loader import Bundle 7 | 8 | 9 | bundle = Bundle() 10 | # All of the humanize filters are plain functions too, and Django's way of 11 | # storing filters is very similar; we just update our bundle's `filters` 12 | # attribute using humanize's register. 13 | bundle.filters.update(humanize.register.filters) 14 | -------------------------------------------------------------------------------- /djanjinja_test/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls.defaults import include, patterns 4 | 5 | 6 | urlpatterns = patterns('', 7 | (r'^simple/', include('djanjinja_test.simple.urls')), 8 | (r'^shortcuts/', include('djanjinja_test.shortcuts.urls')), 9 | (r'^generic/', include('djanjinja_test.generic.urls')), 10 | (r'^cache/', include('djanjinja_test.cache.urls')), 11 | ) 12 | 13 | 14 | handler404 = 'djanjinja.handlers.page_not_found' 15 | handler500 = 'djanjinja.handlers.server_error' -------------------------------------------------------------------------------- /djanjinja_test/generic/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls.defaults import patterns, url 4 | 5 | 6 | urlpatterns = patterns('djanjinja.generic', 7 | url(r'^plain/$', 'direct_to_template', {'template': 'plain.txt'}, 8 | name='generic-plain'), 9 | url(r'^context/$', 'direct_to_template', 10 | {'template': 'context.txt', 'extra_context': {'a': 1, 'b': 2}}, 11 | name='generic-context'), 12 | url(r'^req_context/$', 'direct_to_template', 13 | {'template': 'req_context.txt'}, name='generic-req_context') 14 | ) -------------------------------------------------------------------------------- /djanjinja_test/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from django.core.management import execute_manager 4 | 5 | try: 6 | # pylint: disable-msg=W0403 7 | import settings # Assumed to be in the same directory. 8 | except ImportError: 9 | import sys 10 | sys.stderr.write( 11 | """Error: Can't find the file 'settings.py' in the directory containing %r. \ 12 | It appears you've customized things.\nYou'll have to run django-admin.py, \ 13 | passing it your settings module.\n(If the file settings.py does indeed exist, \ 14 | it's causing an ImportError somehow.)\n""" % __file__) 15 | sys.exit(1) 16 | 17 | if __name__ == "__main__": 18 | execute_manager(settings) 19 | -------------------------------------------------------------------------------- /djanjinja/bundles/site.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Contains definitions for working with your Django site itself.""" 4 | 5 | from djanjinja.loader import Bundle 6 | 7 | 8 | bundle = Bundle() 9 | 10 | 11 | @bundle.function 12 | def url(name, *args, **kwargs): 13 | """A simple wrapper around ``django.core.urlresolvers.reverse``.""" 14 | 15 | from django.core.urlresolvers import reverse 16 | return reverse(name, args=args, kwargs=kwargs) 17 | 18 | 19 | @bundle.function 20 | def setting(name, default=None): 21 | """Get the value of a particular setting, defaulting to ``None``.""" 22 | 23 | from django.conf import settings 24 | return getattr(settings, name, default) -------------------------------------------------------------------------------- /djanjinja/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """djanjinja - A reusable Django app to use Jinja2 templates from Django.""" 4 | 5 | from djanjinja import environment 6 | from djanjinja.environment import is_safe 7 | 8 | 9 | __all__ = [ 10 | 'bccache', 11 | 'bundles', 12 | 'environment', 13 | 'extensions', 14 | 'generic', 15 | 'handlers', 16 | 'loader', 17 | 'middleware', 18 | 'views', 19 | ] 20 | 21 | __version__ = '0.8' 22 | 23 | 24 | def get_environment(): 25 | """Return the template environment, bootstrapping if necessary.""" 26 | 27 | if not environment.TEMPLATE_ENVIRONMENT: 28 | environment.bootstrap() 29 | return environment.TEMPLATE_ENVIRONMENT 30 | 31 | # Shorthand alias for `get_environment()`. 32 | get_env = get_environment 33 | 34 | 35 | def get_template(template_name): 36 | """Return the specified template.""" 37 | 38 | return get_env().get_template(template_name) 39 | -------------------------------------------------------------------------------- /djanjinja_test/generic/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Tests for views which render templates using the generic views.""" 4 | 5 | from django.test import TestCase 6 | 7 | 8 | PLAIN_RESPONSE = 'Hello, World!' 9 | CONTEXT_RESPONSE = 'a = 1; b = 2' 10 | REQ_CONTEXT_RESPONSE = 'user.is_anonymous() => True' 11 | 12 | 13 | class GenericTest(TestCase): 14 | 15 | def test_plain(self): 16 | response = self.client.get('/generic/plain/') 17 | self.assertEqual(response.status_code, 200) 18 | self.assertEqual(response.content, PLAIN_RESPONSE) 19 | 20 | def test_context(self): 21 | response = self.client.get('/generic/context/') 22 | self.assertEqual(response.status_code, 200) 23 | self.assertEqual(response.content, CONTEXT_RESPONSE) 24 | 25 | def test_req_context(self): 26 | response = self.client.get('/generic/req_context/') 27 | self.assertEqual(response.status_code, 200) 28 | self.assertEqual(response.content, REQ_CONTEXT_RESPONSE) 29 | -------------------------------------------------------------------------------- /djanjinja_test/shortcuts/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Views which use DjanJinja shortcut functions to render templates.""" 4 | 5 | from django.template import RequestContext 6 | 7 | from djanjinja.views import context_to_dict, render_to_response 8 | 9 | 10 | def plain(request): 11 | """Renders a template with no context directly to a response.""" 12 | 13 | return render_to_response('plain.txt') 14 | 15 | 16 | def context(request): 17 | """Renders a template with a context directly to a response.""" 18 | 19 | return render_to_response('context.txt', {'a': 1, 'b': 2}) 20 | 21 | 22 | def req_context(request): 23 | """Renders a template with a ``RequestContext`` directly to a response.""" 24 | 25 | return render_to_response('req_context.txt', 26 | context_to_dict(RequestContext(request))) 27 | 28 | 29 | def middleware(request): 30 | """Renders a template with ``request.Context`` using middleware.""" 31 | 32 | return request.Context({'a': 1, 'b': 2}).render_response('middleware.txt') -------------------------------------------------------------------------------- /djanjinja_test/simple/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.http import HttpResponse 4 | from django.template import RequestContext 5 | 6 | import djanjinja 7 | from djanjinja.views import context_to_dict 8 | 9 | 10 | def plain(request): 11 | """Renders a template with no context and returns it in a response.""" 12 | 13 | template = djanjinja.get_template('plain.txt') 14 | content = template.render() 15 | return HttpResponse(content=content) 16 | 17 | 18 | def context(request): 19 | """Renders a template with a context and returns it in a response.""" 20 | 21 | template = djanjinja.get_template('context.txt') 22 | content = template.render({'a': 1, 'b': 2}) 23 | return HttpResponse(content=content) 24 | 25 | 26 | def req_context(request): 27 | """Renders a template with a ``RequestContext`` and returns a repsonse.""" 28 | 29 | template = djanjinja.get_template('req_context.txt') 30 | content = template.render(context_to_dict(RequestContext(request))) 31 | return HttpResponse(content=content) 32 | -------------------------------------------------------------------------------- /djanjinja/handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Replacement for the default Django 404/500 exception handlers.""" 4 | 5 | from djanjinja.views import RequestContext, render_to_response 6 | 7 | 8 | def page_not_found(request, template_name='404.html'): 9 | 10 | """ 11 | 404 (page not found) handler which uses Jinja2 to render the template. 12 | 13 | The default template is ``404.html``, and its context will contain 14 | ``request_path`` (the path of the requested URL) and any additional 15 | parameters provided by the registered context processors (this view uses 16 | ``RequestContext``). 17 | """ 18 | 19 | context = RequestContext(request, {'request_path': request.path}) 20 | response = context.render_response(template_name) 21 | response.status_code = 404 22 | return response 23 | 24 | 25 | def server_error(request, template_name='500.html'): 26 | 27 | """ 28 | 500 (server error) handler which uses Jinja2 to render the template. 29 | 30 | The default template is ``500.html``, and it will be rendered with a 31 | completely empty context. This is to prevent further exceptions from being 32 | raised. 33 | """ 34 | 35 | return render_to_response(template_name) 36 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /djanjinja_test/shortcuts/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Tests for views which render templates using DjanJinja shortcuts.""" 4 | 5 | from django.test import TestCase 6 | 7 | 8 | PLAIN_RESPONSE = 'Hello, World!' 9 | CONTEXT_RESPONSE = 'a = 1; b = 2' 10 | REQ_CONTEXT_RESPONSE = 'user.is_anonymous() => True' 11 | MIDDLEWARE_RESPONSE = 'anonymous, a1, b2' 12 | 13 | 14 | class ShortcutsTest(TestCase): 15 | 16 | def test_plain(self): 17 | response = self.client.get('/shortcuts/plain/') 18 | self.assertEqual(response.status_code, 200) 19 | self.assertEqual(response.content, PLAIN_RESPONSE) 20 | 21 | def test_context(self): 22 | response = self.client.get('/shortcuts/context/') 23 | self.assertEqual(response.status_code, 200) 24 | self.assertEqual(response.content, CONTEXT_RESPONSE) 25 | 26 | def test_req_context(self): 27 | response = self.client.get('/shortcuts/req_context/') 28 | self.assertEqual(response.status_code, 200) 29 | self.assertEqual(response.content, REQ_CONTEXT_RESPONSE) 30 | 31 | def test_middleware(self): 32 | response = self.client.get('/shortcuts/middleware/') 33 | self.assertEqual(response.status_code, 200) 34 | self.assertEqual(response.content, MIDDLEWARE_RESPONSE) -------------------------------------------------------------------------------- /djanjinja/middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | djanjinja.middleware - Helpful middleware for using Jinja2 from Django. 5 | 6 | This module contains middleware which helps you use Jinja2 from within your 7 | views. At the moment it only contains ``RequestContextMiddleware``, but may 8 | expand in future. 9 | """ 10 | 11 | from djanjinja.views import RequestContext 12 | 13 | 14 | class RequestContextMiddleware(object): 15 | 16 | """Attach a special ``RequestContext`` class to each request object.""" 17 | 18 | @staticmethod 19 | def process_request(request): 20 | 21 | """ 22 | Attach a special ``RequestContext`` subclass to each request object. 23 | 24 | This is the only method in the ``RequestContextMiddleware`` Django 25 | middleware class. It attaches a ``RequestContext`` subclass to each 26 | request as the ``Context`` attribute. This subclass has the request 27 | object pre-specified, so you only need to use ``request.Context()`` to 28 | make instances of ``django.template.RequestContext``. 29 | 30 | Consult the documentation for ``djanjinja.views.RequestContext`` for 31 | more information. 32 | """ 33 | 34 | request.Context = RequestContext.with_request(request) 35 | -------------------------------------------------------------------------------- /djanjinja_test/simple/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Tests for simple views which render templates.""" 4 | 5 | from django.test import TestCase 6 | 7 | 8 | PLAIN_RESPONSE = 'Hello, World!' 9 | CONTEXT_RESPONSE = 'a = 1; b = 2' 10 | REQ_CONTEXT_RESPONSE = 'user.is_anonymous() => True' 11 | NOT_FOUND_RESPONSE = 'NOT FOUND: /this/does/not/exist/' 12 | SERVER_ERROR_RESPONSE = 'ERROR OCCURRED.' 13 | 14 | 15 | class SimpleTest(TestCase): 16 | 17 | def test_plain(self): 18 | response = self.client.get('/simple/plain/') 19 | self.assertEqual(response.status_code, 200) 20 | self.assertEqual(response.content, PLAIN_RESPONSE) 21 | 22 | def test_context(self): 23 | response = self.client.get('/simple/context/') 24 | self.assertEqual(response.status_code, 200) 25 | self.assertEqual(response.content, CONTEXT_RESPONSE) 26 | 27 | def test_req_context(self): 28 | response = self.client.get('/simple/req_context/') 29 | self.assertEqual(response.status_code, 200) 30 | self.assertEqual(response.content, REQ_CONTEXT_RESPONSE) 31 | 32 | def test_404(self): 33 | response = self.client.get('/this/does/not/exist/') 34 | self.assertEqual(response.status_code, 404) 35 | self.assertEqual(response.content, NOT_FOUND_RESPONSE) 36 | -------------------------------------------------------------------------------- /pavement.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from paver.easy import * 4 | 5 | 6 | @task 7 | def lint(options): 8 | """Run PyLint on the ``djanjinja`` and ``djanjinja_test`` directories.""" 9 | 10 | import os 11 | 12 | os.environ['DJANGO_SETTINGS_MODULE'] = 'djanjinja_test.settings' 13 | rcfile = path(__file__).abspath().dirname() / 'pylintrc' 14 | 15 | run_pylint('djanjinja', rcfile=rcfile) 16 | run_pylint('djanjinja_test', rcfile=rcfile, disable_msg='C0111') 17 | 18 | 19 | def run_pylint(directory, **options): 20 | """Run PyLint on a given directory, with some command-line options.""" 21 | 22 | from pylint import lint 23 | 24 | rcfile = options.pop('rcfile', None) 25 | if not rcfile: 26 | if (path(__file__).abspath().dirname() / 'pylintrc').exists(): 27 | rcfile = path(__file__).abspath().dirname() / 'pylintrc' 28 | if rcfile: 29 | options['rcfile'] = rcfile 30 | 31 | arguments = [] 32 | for option, value in options.items(): 33 | arguments.append('--%s=%s' % (option.replace('_', '-'), value)) 34 | arguments.append(directory) 35 | 36 | message = 'pylint ' + ' '.join( 37 | argument.replace(' ', r'\ ') for argument in arguments) 38 | 39 | try: 40 | dry(message, lint.Run, arguments) 41 | except SystemExit, exc: 42 | if exc.args[0] != 0: 43 | raise BuildFailure('PyLint returned with a non-zero exit code.') 44 | -------------------------------------------------------------------------------- /djanjinja/bundles/csrf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Add a helper to the context for rendering CSRF tokens.""" 4 | 5 | import django 6 | from django.core.exceptions import ImproperlyConfigured 7 | from djanjinja.loader import Bundle 8 | 9 | 10 | bundle = Bundle() 11 | 12 | if django.VERSION >= (1, 2): # CSRF changed in Django v1.2+ 13 | @bundle.ctxfunction 14 | def csrf_token(context): 15 | token = context.get('csrf_token', None) 16 | if token: 17 | if token == 'NOTPROVIDED': 18 | return Markup(u'') 19 | return Markup( 20 | u'
' 21 | u'' 22 | u'
' % (token,)) 23 | 24 | if 'django.core.context_processors.csrf' in settings.TEMPLATE_CONTEXT_PROCESSORS: 25 | raise ImproperlyConfigured( 26 | "csrf_token() was used in a template, but a CSRF token was not " 27 | "present in the context. This is usually caused by not using " 28 | "RequestContext.") 29 | else: 30 | raise ImproperlyConfigured( 31 | "csrf_token() was used in a template, but a CSRF token was not " 32 | "present in the context. You need to add " 33 | "'django.core.context_processors.csrf' to the " 34 | "TEMPLATE_CONTEXT_PROCESSORS setting, and use RequestContext.") 35 | -------------------------------------------------------------------------------- /djanjinja_test/cache/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Tests for views which render templates which use the caching extras.""" 4 | 5 | from django.test import TestCase 6 | 7 | import djanjinja 8 | 9 | 10 | CACHE_GLOBAL_RESPONSE = u'value' 11 | 12 | 13 | class CacheTest(TestCase): 14 | 15 | def test_global(self): 16 | response = self.client.get('/cache/global/') 17 | self.assertEqual(response.status_code, 200) 18 | self.assertEqual(response.content, CACHE_GLOBAL_RESPONSE) 19 | 20 | def test_fragment(self): 21 | """Tests the fragment caching extension.""" 22 | 23 | # A singleton which stores the number of times its `call()` method has 24 | # been called. 25 | # pylint: disable-msg=C0103 26 | class call_state(object): 27 | called = 0 28 | 29 | @classmethod 30 | def call(cls): 31 | cls.called += 1 32 | 33 | template = djanjinja.get_template('cache_fragment.txt') 34 | # In the beginning, `called` will be 0. 35 | self.assertEqual(call_state.called, 0) 36 | 37 | # After rendering once, `called` will be 1. 38 | template.render({'call_state': call_state}) 39 | self.assertEqual(call_state.called, 1) 40 | 41 | # If fragment caching is working correctly, the output of the previous 42 | # render should be stored and the `call()` method should not be called 43 | # again. 44 | template.render({'call_state': call_state}) 45 | self.assertEqual(call_state.called, 1) -------------------------------------------------------------------------------- /djanjinja/generic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Generic views for Django which use Jinja2 instead. 5 | 6 | At the moment this module only contains ``direct_to_template``. Eventually it 7 | may house some more useful generic views, which have been made to use Jinja2 8 | instead of Django's built-in template language. 9 | """ 10 | 11 | import mimetypes 12 | 13 | from djanjinja.middleware import RequestContextMiddleware 14 | from djanjinja.views import DEFAULT_CONTENT_TYPE 15 | 16 | 17 | def direct_to_template(request, template=None, extra_context=None, 18 | mimetype=None, *args, **kwargs): 19 | 20 | """ 21 | A generic view, similar to that of the same name provided by Django. 22 | 23 | Please consult the documentation for Django's 24 | ``django.views.generic.simple.direct_to_template`` generic view. This 25 | function exports an identical calling signature, only it uses the Jinja2 26 | templating system instead. 27 | """ 28 | 29 | # Ensure the request has a `Context` attribute. This means the middleware 30 | # does not have to be installed. 31 | if not hasattr(request, 'Context'): 32 | RequestContextMiddleware.process_request(request) 33 | 34 | # Build the context, optionally accepting additional context values. 35 | context = request.Context(dict=(extra_context or {})) 36 | 37 | # Build the `params` variable from the parameters passed into the view 38 | # from the URLconf. 39 | params = kwargs.copy() 40 | for i, value in enumerate(args): 41 | params[i] = value 42 | context['params'] = params 43 | 44 | # Ensure the mimetype is sensible; if not provided, it will be inferred 45 | # from the name of the template. If that fails, fall back to the default. 46 | if not mimetype: 47 | mimetype = mimetypes.guess_type(template)[0] or DEFAULT_CONTENT_TYPE 48 | 49 | return context.render_response(template, mimetype=mimetype) 50 | -------------------------------------------------------------------------------- /djanjinja/bccache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """A Jinja2 bytecode cache which uses the Django caching framework.""" 4 | 5 | import jinja2 6 | 7 | 8 | class B64CacheClient(object): 9 | 10 | """ 11 | A wrapper for the Django cache client which Base64-encodes everything. 12 | 13 | This wrapper is needed to use the Django cache with Jinja2. Because Django 14 | tries to store/retrieve everything as Unicode, it makes it impossible to 15 | store binary data. Since Jinja2 uses marshal to store bytecode, we need to 16 | Base64-encode the binary data and then we can send that into and get that 17 | out of the Django cache. 18 | """ 19 | 20 | def __init__(self, cache): 21 | self.cache = cache 22 | 23 | def get(self, key): 24 | """Fetch a key from the cache, base64-decoding the result.""" 25 | data = self.cache.get(key) 26 | if data is not None: 27 | return data.decode('base64') 28 | 29 | def set(self, key, value, timeout=None): 30 | """Set a value in the cache, performing base64 encoding beforehand.""" 31 | if timeout is not None: 32 | self.cache.set(key, value.encode('base64'), timeout) 33 | else: 34 | self.cache.set(key, value.encode('base64')) 35 | 36 | 37 | def get_cache(): 38 | """Get a Jinja2 bytecode cache which uses the configured Django cache.""" 39 | 40 | from django.conf import settings 41 | from django.core import cache 42 | 43 | cache_backend = cache.parse_backend_uri(settings.CACHE_BACKEND)[0] 44 | memcached_client = None 45 | 46 | if cache_backend == 'memcached': 47 | # We can get the actual memcached client object itself. This will 48 | # avoid the Django problem of storing binary data (as Django tries to 49 | # coerce everything to Unicode). 50 | 51 | # Here, we look for either `cache.cache._cache` or 52 | # `cache.cache._client`; I believe there is some discrepancy between 53 | # different versions of Django and where they put this. 54 | memcached_client = getattr( 55 | cache.cache, '_cache', getattr(cache.cache, '_client', None)) 56 | 57 | memcached_client = memcached_client or B64CacheClient(cache.cache) 58 | 59 | return jinja2.MemcachedBytecodeCache(memcached_client) 60 | -------------------------------------------------------------------------------- /djanjinja_test/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for djanjinja_test project. 2 | 3 | import os 4 | 5 | PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) 6 | 7 | DEBUG = True 8 | TEMPLATE_DEBUG = DEBUG 9 | 10 | ADMINS = ( 11 | ('Zachary Voase', 'zacharyvoase@me.com'), 12 | ) 13 | 14 | MANAGERS = ADMINS 15 | 16 | DATABASE_ENGINE = 'sqlite3' 17 | DATABASE_NAME = 'dev.db' 18 | DATABASE_USER = '' 19 | DATABASE_PASSWORD = '' 20 | DATABASE_HOST = '' 21 | DATABASE_PORT = '' 22 | 23 | # Local time zone for this installation. Choices can be found here: 24 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 25 | # although not all choices may be available on all operating systems. 26 | # If running in a Windows environment this must be set to the same as your 27 | # system time zone. 28 | TIME_ZONE = 'Europe/Madrid' 29 | 30 | # Language code for this installation. All choices can be found here: 31 | # http://www.i18nguy.com/unicode/language-identifiers.html 32 | LANGUAGE_CODE = 'en-gb' 33 | 34 | SITE_ID = 1 35 | 36 | # If you set this to False, Django will make some optimizations so as not 37 | # to load the internationalization machinery. 38 | USE_I18N = True 39 | 40 | # Absolute path to the directory that holds media. 41 | # Example: "/home/media/media.lawrence.com/" 42 | MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media') 43 | 44 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 45 | # trailing slash if there is a path component (optional in other cases). 46 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 47 | MEDIA_URL = '/media/' 48 | 49 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 50 | # trailing slash. 51 | # Examples: "http://foo.com/media/", "/media/". 52 | ADMIN_MEDIA_PREFIX = '/media/' 53 | 54 | # Make this unique, and don't share it with anybody. 55 | SECRET_KEY = 'vbz@n)b!xw+@qb5dqiblc&m%7u7u%r$b+qv7emz6ck^945q2!0' 56 | 57 | # List of callables that know how to import templates from various sources. 58 | TEMPLATE_LOADERS = ( 59 | 'django.template.loaders.filesystem.load_template_source', 60 | 'django.template.loaders.app_directories.load_template_source', 61 | ) 62 | 63 | MIDDLEWARE_CLASSES = ( 64 | 'django.middleware.common.CommonMiddleware', 65 | 'django.contrib.sessions.middleware.SessionMiddleware', 66 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 67 | 'djanjinja.middleware.RequestContextMiddleware', 68 | ) 69 | 70 | ROOT_URLCONF = 'djanjinja_test.urls' 71 | 72 | TEMPLATE_DIRS = ( 73 | os.path.join(PROJECT_ROOT, 'templates'), 74 | ) 75 | 76 | # Third-party apps 77 | INSTALLED_APPS = ( 78 | 'django.contrib.auth', 79 | 'django.contrib.contenttypes', 80 | 'django.contrib.sessions', 81 | 'django.contrib.sites', 82 | 'djanjinja', 83 | ) 84 | 85 | # Local apps 86 | INSTALLED_APPS += ( 87 | 'djanjinja_test.simple', 88 | 'djanjinja_test.shortcuts', 89 | 'djanjinja_test.generic', 90 | 'djanjinja_test.cache', 91 | ) 92 | 93 | 94 | DJANJINJA_BUNDLES = ( 95 | 'djanjinja.cache', 96 | 'djanjinja.humanize', 97 | 'djanjinja.site', 98 | ) -------------------------------------------------------------------------------- /djanjinja/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | djanjinja.views - Utilities to help write Django views which use Jinja2. 5 | 6 | This module contains several functions and classes to make it easier to write 7 | views which use Jinja2. It replaces a couple of the most common Django 8 | template-rendering shortcuts, and features an extended ``RequestContext``. 9 | """ 10 | 11 | from functools import partial 12 | 13 | from django import template 14 | from django.conf import settings 15 | from django.http import HttpResponse 16 | 17 | from djanjinja import get_env 18 | 19 | 20 | DEFAULT_CONTENT_TYPE = getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html') 21 | 22 | 23 | class RequestContext(template.RequestContext): 24 | 25 | """A ``RequestContext`` with a pre-specified request attribute.""" 26 | 27 | request = None 28 | 29 | def __init__(self, *args, **kwargs): 30 | # If the class has a `request` attribute which is not `None`, use that 31 | # to initialize the `RequestContext`. 32 | if self.request is not None: 33 | super(RequestContext, self).__init__( 34 | self.request, *args, **kwargs) 35 | else: 36 | # Otherwise, just act as if the normal ``RequestContext`` 37 | # constructor was called. 38 | super(RequestContext, self).__init__(*args, **kwargs) 39 | 40 | @classmethod 41 | def with_request(cls, request): 42 | """Return a `RequestContext` subclass for a specified request.""" 43 | 44 | # Subclasses `RequestContext` with a value for `request`, so that it 45 | # does not need it as an explicit request argument for initialization. 46 | return type( 47 | cls.__name__, (cls,), 48 | {'request': request, '__module__': cls.__module__}) 49 | 50 | def render_string(self, filename): 51 | """Render a given template name to a string, using this context.""" 52 | 53 | return render_to_string(filename, context=self) 54 | 55 | def render_response(self, filename, mimetype=DEFAULT_CONTENT_TYPE): 56 | """Render a given template name to a response, using this context.""" 57 | 58 | return render_to_response(filename, context=self, mimetype=mimetype) 59 | 60 | 61 | def context_to_dict(context): 62 | """Flattens a Django context into a single dictionary.""" 63 | 64 | if not isinstance(context, template.Context): 65 | return context 66 | 67 | dict_out = {} 68 | 69 | # This helps us handle the order of dictionaries in the context. By 70 | # default, the most recent (and therefore most significant/important) 71 | # sub-dictionaries are at the front of the list. This means that variables 72 | # defined later on need to be processed last, hence the use of the 73 | # `reversed()` built-in. 74 | for sub_dict in reversed(context.dicts): 75 | dict_out.update(sub_dict) 76 | return dict_out 77 | 78 | 79 | def render_to_string(filename, context=None, environment=None): 80 | """Renders a given template name to a string.""" 81 | 82 | if context is None: 83 | context = {} 84 | 85 | if environment is None: 86 | environment = get_env() 87 | 88 | return environment.get_template(filename).render( 89 | context_to_dict(context)) 90 | 91 | 92 | def render_to_response(filename, context=None, mimetype=DEFAULT_CONTENT_TYPE, 93 | environment=None): 94 | """Renders a given template name to a ``django.http.HttpResponse``.""" 95 | 96 | return HttpResponse( 97 | render_to_string(filename, context=context, environment=environment), 98 | mimetype=mimetype) 99 | 100 | 101 | def shortcuts_for_environment(environment): 102 | """Returns shortcuts pre-configured for a given environment.""" 103 | 104 | return ( 105 | partial(render_to_string, environment=environment), 106 | partial(render_to_response, environment=environment)) 107 | -------------------------------------------------------------------------------- /djanjinja/extensions/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | A Jinja2 template tag for fragment caching. 5 | 6 | This extension, copied mainly from the Jinja2 documentation on extensions, 7 | adds a ``{% cache %}`` tag which permits fragment caching, much like that in 8 | the native Django templating language. 9 | 10 | Usage is as follows: 11 | 12 | {% cache "cache_key", 3600 %} 13 | ... 14 | {% endcache %} 15 | 16 | This will cache the fragment between ``cache`` and ``endcache``, using the 17 | cache key ``"cache_key"`` and with a timeout of 3600 seconds. 18 | 19 | More complex cache keys can be specified by passing in a sequence (such as a 20 | list or tuple) of items. The entire sequence must be 'dumpable' using the 21 | standard ``marshal`` module in Python. For example: 22 | 23 | {% cache ("article", article.id), 3600 %} 24 | ... 25 | {% endcache %} 26 | 27 | To generate the key, the tuple is marshalled, the SHA1 hash of the resulting 28 | string is taken and base64-encoded, with newlines and padding stripped, and 29 | this is appended to the string ``jinja_frag_``. For more information, consult 30 | the code (located in ``djanjinja/extensions/cache.py``). 31 | """ 32 | 33 | import hashlib 34 | import marshal 35 | 36 | from jinja2 import nodes 37 | from jinja2.ext import Extension 38 | 39 | 40 | class CacheExtension(Extension): 41 | 42 | """Fragment caching using the Django cache system.""" 43 | 44 | tags = set(['cache']) 45 | cache_key_format = 'jinja_frag_%(hash)s' 46 | 47 | def __init__(self, environment): 48 | super(CacheExtension, self).__init__(environment) 49 | 50 | # Extend the environment with the default cache key prefix. 51 | environment.extend(cache_key_format=self.cache_key_format) 52 | 53 | def parse(self, parser): 54 | """Parse a fragment cache block in a Jinja2 template.""" 55 | 56 | # The first parsed token will be 'cache', so we ignore that but keep 57 | # the line number to give to nodes we create later. 58 | lineno = parser.stream.next().lineno 59 | 60 | # This should be the cache key. 61 | args = [parser.parse_expression()] 62 | 63 | # This will check to see if the user provided a timeout parameter 64 | # (which would be separated by a comma). 65 | if parser.stream.skip_if('comma'): 66 | args.append(parser.parse_expression()) 67 | else: 68 | args.append(nodes.Const(None)) 69 | # Here, we parse up to {% endcache %} and drop the needle, which will 70 | # be the `endcache` tag itself. 71 | body = parser.parse_statements(['name:endcache'], drop_needle=True) 72 | 73 | # Now return a `CallBlock` node which calls the `_cache` method on the 74 | # extension. 75 | return nodes.CallBlock( 76 | self.call_method('_cache', args), [], [], body 77 | ).set_lineno(lineno) 78 | 79 | def _cache(self, parameters, timeout, caller): 80 | """Helper method for fragment caching.""" 81 | 82 | # This is lazily loaded so that it can be set up without Django. If 83 | # you try to use it without Django, it will just render the fragment 84 | # as usual. 85 | try: 86 | from django.core import cache 87 | except ImportError: 88 | # `caller()` will render whatever is between {% cache %} and 89 | # {% endcache %}. 90 | return caller() 91 | 92 | key = self._generate_key(parameters) 93 | 94 | # If the fragment is cached, return it. Otherwise, render it, set the 95 | # key in the cache, and return it. 96 | retrieved_value = cache.cache.get(key) 97 | if retrieved_value is not None: 98 | return retrieved_value 99 | 100 | value = caller() 101 | cache.cache.set(key, value, timeout) 102 | return value 103 | 104 | def _generate_key(self, parameters): 105 | """Generate a cache key from some parameters (maybe a sequence).""" 106 | 107 | # Marshal => Hash => Prefix should generate a unique key for each 108 | # set of parameters which is the same for equal parameters. 109 | # Essentially, this is a 1:1 mapping. 110 | serialized = marshal.dumps(parameters) 111 | digest = (hashlib.sha1(serialized) 112 | .digest() 113 | .encode('base64') 114 | .rstrip('\r\n=')) 115 | 116 | return self.environment.cache_key_format % {'hash': digest} -------------------------------------------------------------------------------- /djanjinja/loader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Utilities for loading definitions from reusable Django apps.""" 4 | 5 | import copy 6 | 7 | from django.core.exceptions import ImproperlyConfigured 8 | from django.utils.importlib import import_module 9 | 10 | from djanjinja import get_env 11 | from djanjinja.environment import Environment 12 | 13 | 14 | class Bundle(object): 15 | 16 | """ 17 | Store a bunch of tests, filters and functions from a single app. 18 | 19 | Instances of the ``Bundle`` class store tests, filters and functions in an 20 | internal register that can then be lazily merged with the Jinja2 21 | environment later on. It is used when apps want to define a number of 22 | utilities for lazy loading from template code, typically using the 23 | ``{% load %}`` template tag defined in djanjinja.extensions.bundles. 24 | 25 | Essentially, a bundle is just an environment without the templating and 26 | loading parts. They can be pushed onto another environment, which will 27 | return a new environment with a new set of filters, globals and tests but 28 | all the other attributes from the original environment. 29 | """ 30 | 31 | TYPES = ( 32 | 'test', 33 | 'filter', 'ctxfilter', 'envfilter', 34 | 'function', 'ctxfunction', 'envfunction' 35 | ) 36 | 37 | # Set these attributes now, to prevent pylint from flagging errors later. 38 | test = None 39 | filter = None 40 | ctxfilter = None 41 | envfilter = None 42 | function = None 43 | ctxfunction = None 44 | envfunction = None 45 | 46 | def __init__(self): 47 | self.filters = {} 48 | self.globals = {} 49 | self.tests = {} 50 | 51 | def merge_into(self, environment=None): 52 | """Push this bundle onto the environment, returning a new env.""" 53 | 54 | if environment is None: 55 | environment = get_env() 56 | 57 | for attr in ['globals', 'filters', 'tests']: 58 | getattr(environment, attr).update(getattr(self, attr)) 59 | 60 | return environment 61 | 62 | 63 | # Dynamically create decorators for each type. 64 | for type in TYPES: 65 | # We get the attribute from the `Environment` class itself because it 66 | # will be an unbound method at this point. 67 | # `im_func` is the underlying function definition. Within this 68 | # definition, `self` has no special meaning, so by copying it here 69 | # we can essentially repurpose the same method for this class. It's 70 | # actually similar to a mixin. 71 | vars()[type] = copy.copy(getattr(Environment, type).im_func) 72 | 73 | # Clean up ``type`` from the namespace. 74 | del type 75 | 76 | 77 | def get_bundle(app_label, bundle_name): 78 | 79 | """ 80 | Loads the bundle with a given name for a specific app. 81 | 82 | First, we import the app. Then, we look in the ``bundles`` sub-module for 83 | the specified bundle name. If the given name is a top-level attribute of 84 | ``bundles``, we use that. Otherwise, we try to import a submodule of 85 | ``bundles`` with that name and look for a ``bundle`` attribute in that 86 | submodule. 87 | 88 | Note that this function only retrieves the bundle; it does not insert it 89 | into the environment. To do this in one step, use the ``load()`` function 90 | in this module. 91 | """ 92 | 93 | from django.conf import settings 94 | 95 | # Load the app (this is a plain Python module). 96 | app, app_name = None, '' 97 | for full_app_name in settings.INSTALLED_APPS: 98 | if app_label in (full_app_name, full_app_name.split('.')[-1]): 99 | app = import_module(full_app_name) 100 | app_name = full_app_name 101 | break 102 | if not (app and app_name): 103 | raise ImproperlyConfigured( 104 | 'App with label %r not found' % (app_label,)) 105 | 106 | # Try to find the bundles sub-module. Having this separate allows us to 107 | # provide a more detailed exception message. 108 | try: 109 | bundles = import_module('.bundles', package=app_name) 110 | except ImportError: 111 | raise ImproperlyConfigured( 112 | 'App %r has no `bundles` module' % (app_name,)) 113 | 114 | # Now load the specified bundle name. First we look to see if it is a top- 115 | # level attribute of the bundles module: 116 | if hasattr(bundles, bundle_name): 117 | bundle = getattr(bundles, bundle_name) 118 | if isinstance(bundle, Bundle): 119 | return bundle 120 | 121 | try: 122 | bundle_mod = import_module( 123 | '.' + bundle_name, package=(app_name + '.bundles')) 124 | except ImportError: 125 | raise ImproperlyConfigured( 126 | 'Could not find bundle %r in app %r' % (bundle_name, app_name)) 127 | 128 | if hasattr(bundle_mod, 'bundle'): 129 | return getattr(bundle_mod, 'bundle') 130 | raise ImproperlyConfigured( 131 | "Module '%s.bundles.%s' has no `bundle` attribute" % ( 132 | app_name, bundle_name)) 133 | 134 | 135 | def load(app_label, bundle_name, environment=None, reload=False): 136 | """Load a specified bundle into an/the environment.""" 137 | 138 | if environment is None: 139 | environment = get_env() 140 | 141 | bundle = get_bundle(app_label, bundle_name) 142 | if (bundle not in environment.loaded_bundles) or reload: 143 | bundle.merge_into(environment) 144 | environment.loaded_bundles.add(bundle) 145 | return bundle -------------------------------------------------------------------------------- /djanjinja/environment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Django-compatible Jinja2 Environment and helpers. 5 | 6 | This module contains a subclass of ``jinja2.Environment`` and several other 7 | functions which help use Jinja2 from within your Django projects. 8 | """ 9 | 10 | from functools import wraps 11 | try: 12 | set 13 | except NameError: 14 | from sets import Set as set 15 | 16 | import jinja2 17 | 18 | 19 | TEMPLATE_ENVIRONMENT = None 20 | 21 | 22 | class Environment(jinja2.Environment): 23 | 24 | """An environment with decorators for filters, functions and tests.""" 25 | 26 | def __init__(self, *args, **kwargs): 27 | super(Environment, self).__init__(*args, **kwargs) 28 | # Add a `set()` attribute which stores the loaded bundles. 29 | self.loaded_bundles = set() 30 | 31 | def load(self, app_label, bundle_name, reload=False): 32 | """Load the specified bundle into this environment.""" 33 | 34 | from djanjinja import loader 35 | 36 | # Returns the loaded bundle. 37 | return loader.load( 38 | app_label, bundle_name, environment=self, reload=reload) 39 | 40 | def copy(self): 41 | """Create a copy of the environment.""" 42 | 43 | copy = self.overlay() 44 | for attr in ['loaded_bundles', 'globals', 'filters', 'tests']: 45 | setattr(copy, attr, getattr(self, attr).copy()) 46 | 47 | return copy 48 | 49 | # pylint: disable-msg=C0111 50 | def adder(attribute, wrapper, name, docstring): 51 | 52 | """ 53 | Generate decorator methods for adding filters, functions and tests. 54 | 55 | Note that this function is not a method; it is deleted before the end 56 | of the class definition and is only used to generate the decorator 57 | methods. It helps to remove a lot of boilerplate. 58 | """ 59 | 60 | def adder_(self, function, name=None): 61 | """Add the function to the environment with wrappers, etc.""" 62 | 63 | key = name or function.__name__ 64 | value = wrapper and wrapper(function) or function 65 | getattr(self, attribute)[key] = value 66 | return function 67 | 68 | def decorator(self, *args, **kwargs): 69 | """Boilerplate which allows both normal calling and decoration.""" 70 | 71 | def wrapper(*args): 72 | return adder_(self, *args, **kwargs) 73 | if args: 74 | return wrapper(*args) 75 | return wrapper 76 | 77 | decorator.__name__ = name 78 | decorator.__doc__ = docstring 79 | return decorator 80 | 81 | ## Simple 82 | 83 | filter = adder('filters', None, 'filter', 84 | 'Decorate a function as a simple template filter.') 85 | 86 | test = adder('tests', None, 'test', 87 | 'Decorate a function as a simple template test.') 88 | 89 | function = adder('globals', None, 'function', 90 | 'Decorate a function as a simple global template function.') 91 | 92 | ## Environment 93 | # Note that environment- and context-tests are not supported by Jinja2. 94 | 95 | envfilter = adder('filters', jinja2.environmentfilter, 'envfilter', 96 | 'Decorate a function as an environment filter.') 97 | 98 | envfunction = adder('globals', jinja2.environmentfunction, 'envfunction', 99 | 'Decorate a function as a global environment function.') 100 | 101 | ## Context 102 | 103 | ctxfilter = adder('filters', jinja2.contextfilter, 'ctxfilter', 104 | 'Decorate a function as a context filter.') 105 | 106 | ctxfunction = adder('globals', jinja2.contextfunction, 'ctxfunction', 107 | 'Decorate a function as a global context function.') 108 | 109 | # Clean up the namespace. Also, without this, `type` will try to convert 110 | # `adder()` into a method. Which it most certainly is not. 111 | del adder 112 | 113 | 114 | def get_template_source(name): 115 | 116 | """ 117 | Interface with Django to load the source for a given template name. 118 | 119 | This function is a simple wrapper around 120 | ``django.template.loader.find_template_source()`` to support the behaviour 121 | expected by the ``jinja2.FunctionLoader`` loader class. It requires Django 122 | to be configured (i.e. the settings need to be loaded). 123 | """ 124 | 125 | from django.template import loader 126 | # `loader.find_template_source()` returns a 2-tuple of the source and a 127 | # `LoaderOrigin` object. 128 | source = loader.find_template_source(name)[0] 129 | 130 | # `jinja2.FunctionLoader` expects a triple of the source of the template, 131 | # the name used to load it, and a 0-ary callable which will return whether 132 | # or not the template needs to be reloaded. The callable will only ever be 133 | # called if auto-reload is on. In this case, we'll just assume that the 134 | # template does need to be reloaded. 135 | return (source, name, lambda: False) 136 | 137 | 138 | def bootstrap(): 139 | """Load the TEMPLATE_ENVIRONMENT global variable.""" 140 | 141 | from django.conf import settings 142 | if not settings.configured: 143 | # At least this will make it work, even if it's using the defaults. 144 | settings.configure() 145 | 146 | from djanjinja import bccache 147 | from djanjinja.extensions.cache import CacheExtension 148 | 149 | # Get the bytecode cache object. 150 | bytecode_cache = bccache.get_cache() 151 | 152 | default_extensions = set([ 153 | 'jinja2.ext.do', 'jinja2.ext.loopcontrols', CacheExtension]) 154 | if getattr(settings, 'USE_I18N', False): 155 | default_extensions.add('jinja2.ext.i18n') 156 | autoescape = getattr(settings, 'JINJA_AUTOESCAPE', False) 157 | if autoescape: 158 | default_extensions.add('jinja2.ext.autoescape') 159 | 160 | extensions = list(set(getattr(settings, 'JINJA_EXTENSIONS', [])).union(default_extensions)) 161 | 162 | # Set up global `TEMPLATE_ENVIRONMENT` variable. 163 | global TEMPLATE_ENVIRONMENT 164 | 165 | TEMPLATE_ENVIRONMENT = Environment( 166 | loader=jinja2.FunctionLoader(get_template_source), 167 | auto_reload=getattr(settings, 'DEBUG', True), autoescape=autoescape, 168 | bytecode_cache=bytecode_cache, extensions=extensions) 169 | 170 | if getattr(settings, 'USE_I18N', False): 171 | # The `django.utils.translation` module behaves like a singleton of 172 | # `gettext.GNUTranslations`, since it exports all the necessary 173 | # methods. 174 | from django.utils import translation 175 | # pylint: disable-msg=E1101 176 | TEMPLATE_ENVIRONMENT.install_gettext_translations(translation) 177 | 178 | bundles = getattr(settings, 'DJANJINJA_BUNDLES', []) 179 | for bundle_specifier in bundles: 180 | app_label, bundle_name = bundle_specifier.rsplit('.', 1) 181 | TEMPLATE_ENVIRONMENT.load(app_label, bundle_name) 182 | 183 | 184 | def is_safe(function): 185 | """Decorator which declares that a function returns safe markup.""" 186 | 187 | @wraps(function) 188 | def safe_wrapper(*args, **kwargs): 189 | """Wraps the output of the function as safe markup.""" 190 | 191 | # All this wrapper does is to wrap the output of the function with 192 | # `jinja2.Markup`, which declares to the template that the string is 193 | # safe. 194 | result = function(*args, **kwargs) 195 | if not isinstance(result, jinja2.Markup): 196 | return jinja2.Markup(result) 197 | return result 198 | 199 | return safe_wrapper 200 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | # lint Python modules using external checkers. 2 | # 3 | # This is the main checker controlling the other ones and the reports 4 | # generation. It is itself both a raw checker and an astng checker in order 5 | # to: 6 | # * handle message activation / deactivation at the module level 7 | # * handle some basic but necessary stats'data (number of classes, methods...) 8 | # 9 | [MASTER] 10 | 11 | # Specify a configuration file. 12 | #rcfile= 13 | 14 | # Python code to execute, usually for sys.path manipulation such as 15 | # pygtk.require(). 16 | #init-hook= 17 | 18 | # Profiled execution. 19 | profile=no 20 | 21 | # Add to the black list. It should be a base name, not a 22 | # path. You may set this option multiple times. 23 | ignore=CVS 24 | 25 | # Pickle collected data for later comparisons. 26 | persistent=yes 27 | 28 | # Set the cache size for astng objects. 29 | cache-size=500 30 | 31 | # List of plugins (as comma separated values of python modules names) to load, 32 | # usually to register additional checkers. 33 | load-plugins= 34 | 35 | 36 | [MESSAGES CONTROL] 37 | 38 | # Enable only checker(s) with the given id(s). This option conflicts with the 39 | # disable-checker option 40 | #enable-checker= 41 | 42 | # Enable all checker(s) except those with the given id(s). This option 43 | # conflicts with the enable-checker option 44 | #disable-checker= 45 | 46 | # Enable all messages in the listed categories (IRCWEF). 47 | #enable-msg-cat= 48 | 49 | # Disable all messages in the listed categories (IRCWEF). 50 | disable-msg-cat=I 51 | 52 | # Enable the message(s) with the given id(s). 53 | #enable-msg= 54 | 55 | # Disable the message(s) with the given id(s). 56 | disable-msg=E0213,R0401,R0903,R0904,W0142,W0603,W0613,W0622 57 | 58 | 59 | [REPORTS] 60 | 61 | # Set the output format. Available formats are text, parseable, colorized, msvs 62 | # (visual studio) and html 63 | output-format=text 64 | 65 | # Include message's id in output 66 | include-ids=yes 67 | 68 | # Put messages in a separate file for each module / package specified on the 69 | # command line instead of printing them on stdout. Reports (if any) will be 70 | # written in a file name "pylint_global.[txt|html]". 71 | files-output=no 72 | 73 | # Tells wether to display a full report or only the messages 74 | reports=yes 75 | 76 | # Python expression which should return a note less than 10 (10 is the highest 77 | # note). You have access to the variables errors warning, statement which 78 | # respectivly contain the number of errors / warnings messages and the total 79 | # number of statements analyzed. This is used by the global evaluation report 80 | # (R0004). 81 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 82 | 83 | # Add a comment according to your evaluation note. This is used by the global 84 | # evaluation report (R0004). 85 | comment=no 86 | 87 | # Enable the report(s) with the given id(s). 88 | #enable-report= 89 | 90 | # Disable the report(s) with the given id(s). 91 | #disable-report= 92 | 93 | 94 | # checks for : 95 | # * doc strings 96 | # * modules / classes / functions / methods / arguments / variables name 97 | # * number of arguments, local variables, branchs, returns and statements in 98 | # functions, methods 99 | # * required module attributes 100 | # * dangerous default values as arguments 101 | # * redefinition of function / method / class 102 | # * uses of the global statement 103 | # 104 | [BASIC] 105 | 106 | # Required attributes for module, separated by a comma 107 | required-attributes= 108 | 109 | # Regular expression which should only match functions or classes name which do 110 | # not require a docstring 111 | no-docstring-rgx=__.*__ 112 | 113 | # Regular expression which should only match correct module names 114 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 115 | 116 | # Regular expression which should only match correct module level names 117 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|([a-z_][a-z0-9_]{2,30}))$ 118 | 119 | # Regular expression which should only match correct class names 120 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 121 | 122 | # Regular expression which should only match correct function names 123 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 124 | 125 | # Regular expression which should only match correct method names 126 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 127 | 128 | # Regular expression which should only match correct instance attribute names 129 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 130 | 131 | # Regular expression which should only match correct argument names 132 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 133 | 134 | # Regular expression which should only match correct variable names 135 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 136 | 137 | # Regular expression which should only match correct list comprehension / 138 | # generator expression variable names 139 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 140 | 141 | # Good variable names which should always be accepted, separated by a comma 142 | good-names=i,j,k,ex,Run,_ 143 | 144 | # Bad variable names which should always be refused, separated by a comma 145 | bad-names=foo,bar,baz,toto,tutu,tata 146 | 147 | # List of builtins function names that should not be used, separated by a comma 148 | bad-functions=apply,input 149 | 150 | 151 | # try to find bugs in the code using type inference 152 | # 153 | [TYPECHECK] 154 | 155 | # Tells wether missing members accessed in mixin class should be ignored. A 156 | # mixin class is detected if its name ends with "mixin" (case insensitive). 157 | ignore-mixin-members=yes 158 | 159 | # List of classes names for which member attributes should not be checked 160 | # (useful for classes with attributes dynamically set). 161 | ignored-classes=SQLObject 162 | 163 | # When zope mode is activated, add a predefined set of Zope acquired attributes 164 | # to generated-members. 165 | zope=no 166 | 167 | # List of members which are set dynamically and missed by pylint inference 168 | # system, and so shouldn't trigger E0201 when accessed. 169 | generated-members=REQUEST,acl_users,aq_parent 170 | 171 | 172 | # checks for 173 | # * unused variables / imports 174 | # * undefined variables 175 | # * redefinition of variable from builtins or from an outer scope 176 | # * use of variable before assigment 177 | # 178 | [VARIABLES] 179 | 180 | # Tells wether we should check for unused import in __init__ files. 181 | init-import=no 182 | 183 | # A regular expression matching names used for dummy variables (i.e. not used). 184 | dummy-variables-rgx=_|dummy 185 | 186 | # List of additional names supposed to be defined in builtins. Remember that 187 | # you should avoid to define new builtins when possible. 188 | additional-builtins= 189 | 190 | 191 | # checks for : 192 | # * methods without self as first argument 193 | # * overridden methods signature 194 | # * access only to existant members via self 195 | # * attributes not defined in the __init__ method 196 | # * supported interfaces implementation 197 | # * unreachable code 198 | # 199 | [CLASSES] 200 | 201 | # List of interface methods to ignore, separated by a comma. This is used for 202 | # instance to not check methods defines in Zope's Interface base class. 203 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 204 | 205 | # List of method names used to declare (i.e. assign) instance attributes. 206 | defining-attr-methods=__init__,__new__,setUp 207 | 208 | 209 | # checks for sign of poor/misdesign: 210 | # * number of methods, attributes, local variables... 211 | # * size, complexity of functions, methods 212 | # 213 | [DESIGN] 214 | 215 | # Maximum number of arguments for function / method 216 | max-args=5 217 | 218 | # Maximum number of locals for function / method body 219 | max-locals=15 220 | 221 | # Maximum number of return / yield for function / method body 222 | max-returns=6 223 | 224 | # Maximum number of branch for function / method body 225 | max-branchs=12 226 | 227 | # Maximum number of statements in function / method body 228 | max-statements=50 229 | 230 | # Maximum number of parents for a class (see R0901). 231 | max-parents=7 232 | 233 | # Maximum number of attributes for a class (see R0902). 234 | max-attributes=7 235 | 236 | # Minimum number of public methods for a class (see R0903). 237 | min-public-methods=2 238 | 239 | # Maximum number of public methods for a class (see R0904). 240 | max-public-methods=20 241 | 242 | 243 | # checks for 244 | # * external modules dependencies 245 | # * relative / wildcard imports 246 | # * cyclic imports 247 | # * uses of deprecated modules 248 | # 249 | [IMPORTS] 250 | 251 | # Deprecated modules which should not be used, separated by a comma 252 | deprecated-modules=regsub,string,TERMIOS,Bastion,rexec 253 | 254 | # Create a graph of every (i.e. internal and external) dependencies in the 255 | # given file (report R0402 must not be disabled) 256 | import-graph= 257 | 258 | # Create a graph of external dependencies in the given file (report R0402 must 259 | # not be disabled) 260 | ext-import-graph= 261 | 262 | # Create a graph of internal dependencies in the given file (report R0402 must 263 | # not be disabled) 264 | int-import-graph= 265 | 266 | 267 | # checks for : 268 | # * unauthorized constructions 269 | # * strict indentation 270 | # * line length 271 | # * use of <> instead of != 272 | # 273 | [FORMAT] 274 | 275 | # Maximum number of characters on a single line. 276 | max-line-length=80 277 | 278 | # Maximum number of lines in a module 279 | max-module-lines=1000 280 | 281 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 282 | # tab). 283 | indent-string=' ' 284 | 285 | 286 | # checks for: 287 | # * warning notes in the code like FIXME, XXX 288 | # * PEP 263: source code with non ascii character but no encoding declaration 289 | # 290 | [MISCELLANEOUS] 291 | 292 | # List of note tags to take in consideration, separated by a comma. 293 | notes=FIXME,XXX,TODO 294 | 295 | 296 | # checks for similarities and duplicated code. This computation may be 297 | # memory / CPU intensive, so you should disable it if you experiments some 298 | # problems. 299 | # 300 | [SIMILARITIES] 301 | 302 | # Minimum lines number of a similarity. 303 | min-similarity-lines=4 304 | 305 | # Ignore comments when computing similarities. 306 | ignore-comments=yes 307 | 308 | # Ignore docstrings when computing similarities. 309 | ignore-docstrings=yes 310 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DjanJinja 2 | 3 | DjanJinja: the sound you make when you've got peanut butter stuck to the roof of 4 | your mouth. Incidentally, it also happens to be the name of a new re-usable 5 | Django app. This one, in fact. 6 | 7 | DjanJinja exists to help you leverage the power of 8 | [Jinja2](http://jinja.pocoo.org/2/) templates in your Django projects. It's 9 | simple to get started. 10 | 11 | ## Installing and Using DjanJinja 12 | 13 | ### Installing DjanJinja 14 | 15 | 1. Install DjanJinja using `easy_install djanjinja`, `pip install djanjinja`, or 16 | by grabbing a copy of the Mercurial repo and running `python setup.py 17 | install`. 18 | 2. Add `'djanjinja'` to your `INSTALLED_APPS` list. 19 | 3. (Optionally) add `'djanjinja.middleware.RequestContextMiddleware'` to your 20 | `MIDDLEWARE_CLASSES` list. 21 | 22 | It’s assumed that you have fairly recent versions of Jinja2 and Django 23 | installed. The author recommends the *most recent* stable versions of each, 24 | which should be installable via PyPI (i.e. a simple `easy_install` or `pip 25 | install`). A lot of people have their own ways of installing things, so the 26 | author hasn’t put any explicit requirements in the `setup.py` file. DjanJinja 27 | just expects to find `jinja2` and `django` on the import path. 28 | 29 | ### Using DjanJinja 30 | 31 | * Instead of using `django.shortcuts.render_to_response`, use one of the 32 | Jinja2-based functions provided. 33 | * Instead of using the Django loaders to load templates, get them from the 34 | Jinja2 environment created for you by DjanJinja. 35 | * Instead of using Django’s provided generic views, use those contained within 36 | `djanjinja.generic` (at the moment the only one is `direct_to_template()`). 37 | 38 | ## Shortcut Functions 39 | 40 | DjanJinja provides you with two shortcut functions for rendering templates, 41 | `render_to_response` and `render_to_string`. These are very similar to those 42 | provided by Django in the `django.shortcuts` module, except they use Jinja2 43 | instead of the Django templating system. To use them from your views, just do 44 | `from djanjinja.views import render_to_response, render_to_string` at the top of 45 | your views module. 46 | 47 | ## Bundles 48 | 49 | A Jinja2 environment can contain additional filters, tests and global variables 50 | which will be available in all templates rendered through that environment. 51 | Since individual Django apps will have their own set of things to add to the 52 | environment, DjanJinja adds the concept of ‘bundles’; small objects containing 53 | some global variables, filters and tests. Each app may define any number of 54 | these bundles which can then be loaded as required. 55 | 56 | ### Defining Bundles 57 | 58 | It’s relatively easy to define a bundle; an example is shown below: 59 | 60 | from djanjinja.loader import Bundle 61 | 62 | foo = Bundle() 63 | foo.globals['myvar'] = 12345 64 | 65 | @foo.envfilter 66 | def myenvfilter(environment, value): 67 | pass # do something here... 68 | 69 | @foo.ctxfunction 70 | def mycontextfunction(context, value): 71 | pass # do something here... 72 | 73 | Here we define a bundle called `foo`, with a global variable of `myvar` 74 | containing the value `12345`, an environment filter and a context function (for 75 | more information on each of these please consult the Jinja2 documentation). The 76 | `Bundle` class also supplies these handy decorators (the full list can be found 77 | as `djanjinja.loader.Bundle.TYPES`) to define various components. 78 | 79 | DjanJinja expects to find bundles in a `bundles` submodule of your Django app. 80 | You can lay things out in one of two ways: 81 | 82 | * Add a file called `bundles.py` to your app, and within this define multiple 83 | `Bundle` instances. 84 | * Add a package called `bundles` to your app (i.e. a `bundles` directory 85 | containing an empty file called `__init__.py`), and within this define 86 | submodules for each of your bundles. Each submodule should have a top-level 87 | `bundle` variable which is an instance of the `Bundle` class. 88 | 89 | You can actually mix and match these; you could add some bundle instances to the 90 | `bundles/__init__.py` file with different names, in addition to having the 91 | submodules. These are loaded lazily, so DjanJinja sees no real difference. It 92 | doesn’t scour the `bundles` module for definitions, it just loads what you ask 93 | it to. 94 | 95 | ### Addressing Bundles 96 | 97 | In order to use the functions, filters and tests defined in a bundle, you first 98 | have to load it into the environment. Bundles are specified in two parts: the 99 | ‘app label’ and the ‘bundle name’. The app label is simply the name of the app 100 | which contains it. For example, it may be `django.contrib.auth`, or simply 101 | `auth`, since you may just give the last part of the full name and DjanJinja 102 | will figure it out from looking at the `INSTALLED_APPS` setting. 103 | 104 | If a bundle is defined within a `bundles.py` or a `bundles/__init__.py` file, 105 | then the bundle name will be the name in the module with which it was defined. 106 | For example: 107 | 108 | # in the file `myapp/bundles.py` 109 | foo = Bundle() 110 | foo.globals['myvar'] = 12345 111 | 112 | In this case, the app label will be `myapp`, and the bundle name will be `foo`. 113 | If the bundles are defined in submodules, then the bundle name will be the name 114 | of the submodule. 115 | 116 | ### Loading Bundles 117 | 118 | In order to load any bundles into the global Jinja2 environment, you need to specify a 119 | `DJANJINJA_BUNDLES` setting in your `settings.py` file. This is a list or tuple 120 | of bundle specifiers in an `'app_label.bundle_name'` format. For example: 121 | 122 | DJANJINJA_BUNDLES = ( 123 | 'djanjinja.cache', 124 | 'djanjinja.humanize', 125 | 'djanjinja.site', 126 | ) 127 | 128 | You can also add bundles to the environment programmatically. This is useful 129 | when: 130 | 131 | * Your app needs to do some initial setup before a bundle is loaded. 132 | * Your app relies on a particular bundle being present in the global environment 133 | anyway, and you don’t want the user to have to add the bundle to 134 | `DJANJINJA_BUNDLES` manually. 135 | * Your app needs to load bundles dynamically. 136 | * Your app wants to use a bundle locally, not globally. 137 | 138 | You can load a bundle into an environment like this: 139 | 140 | import djanjinja 141 | env = djanjinja.get_env() 142 | env.load('app_label', 'bundle_name', reload=False) 143 | 144 | This will load the bundle into the environment, passing through if it’s already 145 | loaded. If you specify `reload=True`, you can make it reload a bundle even if 146 | it’s been loaded. 147 | 148 | You should put this code somewhere where it will get executed when you want it 149 | to. If you want it to be executed immediately, as Django starts up, put it in 150 | `myapp/__init__.py`. 151 | 152 | You can also use a bundle within only one app, by using a local environment 153 | copied from the global environment. Here’s how you might do this: 154 | 155 | import djanjinja 156 | global_env = djanjinja.get_env() 157 | local_env = global_env.copy() 158 | local_env.load('app_label', 'bundle_name') 159 | 160 | You'd then use that local environment later on in your code. For example, the 161 | above code might be in `myapp/__init__.py`; so your views might look like this: 162 | 163 | from django.http import HttpResponse 164 | from myapp import local_env 165 | 166 | def view(request): 167 | template = local_env.get_template('template_name.html') 168 | data = template.render({'key1': 'value1', 'key2': 'value2'}) 169 | return HttpResponse(content=data) 170 | 171 | Of course, you’re probably going to want to use the usual shortcuts (i.e. 172 | `render_to_response()` and `render_to_string()`). You can build these easily 173 | using `djanjinja.views.shortcuts_for_environment()`: 174 | 175 | from djanjinja.views import shortcuts_for_environment 176 | 177 | render_to_response, render_to_string = shortcuts_for_environment(local_env) 178 | 179 | Do this at the top of your `views.py` file, and then you can use the generated 180 | functions throughout all of your views. 181 | 182 | ### Caveats and Limitations 183 | 184 | Jinja2 does not yet support scoped filters and tests; as a result of this, the 185 | contents of bundles specified in `DJANJINJA_BUNDLES` will be loaded into the global environment. It is important 186 | to make sure that definitions in your bundle do not override those in another 187 | bundle. This is especially important with threaded web applications, as multiple 188 | bundles overriding one another could cause unpredictable behavior in the 189 | templates. 190 | 191 | ### Included Bundles 192 | 193 | DjanJinja provides three bundles already which either replace Django 194 | counterparts or add some useful functionality to your Jinja2 templates: 195 | 196 | * `djanjinja.cache`: Loading this bundle will add a global `cache` object to the 197 | environment; this is the Django cache, and allows you to carry out caching 198 | operations from within your templates (such as `cache.get(key)`, et cetera). 199 | 200 | * `djanjinja.humanize`: This will add all of the filters contained within the 201 | `django.contrib.humanize` app; consult the official Django docs for more 202 | information on the filters provided. 203 | 204 | * `djanjinja.site`: This will add two functions to the global environment: 205 | `url`, and `setting`. The former acts like Django’s template tag, by reversing 206 | URLconf names and views into URLs, but because Jinja2 supports a richer 207 | syntax, it can be used via `{{ url(name, *args, **kwargs) }}` instead. 208 | `setting` attempts to resolve a setting name into a value, returning an 209 | optional default instead (i.e. `setting('MEDIA_URL', '/media')`). 210 | 211 | ## Extensions 212 | 213 | Jinja2 supports the concept of *environment extensions*; these are non-trivial 214 | plugins which enhance the Jinja2 templating engine itself. By default, the 215 | environment is configured with the `do` statement and the loop controls (i.e. 216 | `break` and `continue`), but if you want to add extensions to the environment 217 | then you can do so with the `JINJA_EXTENSIONS` setting. Just add this to your 218 | `settings.py` file: 219 | 220 | JINJA_EXTENSIONS = ( 221 | 'jinja2.ext.i18n', # i18n Extension 222 | ... 223 | ) 224 | 225 | For all the extensions you wish to load. This will be passed in directly to the 226 | `jinja2.Environment` constructor. 227 | 228 | If you have set `USE_I18N = True` in your settings file, then DjanJinja will 229 | automatically initialize the i18n machinery for the Jinja2 environment, loading 230 | your Django translations during the bootstrapping process. For more information 231 | on how to use the Jinja2 i18n extension, please consult the Jinja2 232 | documentation. 233 | 234 | ### Autoescaping 235 | 236 | To enable autoescaping, just add `JINJA_AUTOESCAPE = True` to your settings 237 | file. This will add the extension and set up the Jinja2 environment correctly. 238 | 239 | ### Cache 240 | 241 | DjanJinja also provides an extension for fragment caching using the Django cache 242 | system. The code for this borrows heavily from the example in the Jinja2 243 | documentation, but with a few extras thrown in. You can use the extension like 244 | this: 245 | 246 | {% cache (parameter1, param2, param3), timeout %} 247 | ... 248 | {% endcache %} 249 | 250 | The tuple of parameters is used to generate the cache key for this fragment. You 251 | can place any object here, so long as it is suitable for serialization by the 252 | standard library `marshal` module. The cache key for the fragment is generated 253 | by marshaling the parameters, hashing them and then using the digest with a 254 | prefix as the key. This allows you to specify cached fragments which vary 255 | depending on multiple variables. The timeout is optional, and should be given in 256 | seconds. 257 | 258 | ## 404 and 500 Handlers 259 | 260 | Your project’s URLconf must specify two variables—`handler404` and `handler500`—which give the name of a Django view to be processed in the event of a 404 "Not Found" and a 500 "Server Error" response respectively. These are set to a default which uses the Django templating system to render a response from templates called `404.html` and `500.html`. If you were to use the Jinja2 templating system instead, you will be able to define richer error pages, and your error pages will be able to inherit from and extend other Jinja2 master templates on the template path. 261 | 262 | It’s relatively simple to set Django up to do this. Simply override the handler variables like so from your `urls.py` file: 263 | 264 | handler404 = 'djanjinja.handlers.page_not_found' 265 | handler500 = 'djanjinja.handlers.server_error' 266 | 267 | ## `RequestContext` 268 | 269 | One of Django's most useful features is the `RequestContext` class, which allows 270 | you to specify several context processors which each add some information to the 271 | context before templates are rendered. Luckily, this feature is 272 | template-agnostic, and is therefore fully compatible with DjanJinja. 273 | 274 | However, DjanJinja also provides you with some very helpful shortcuts for using 275 | request contexts. Usually, without DjanJinja, you would use them like this: 276 | 277 | from django.shortcuts import render_to_response 278 | from django.template import RequestContext 279 | 280 | def myview(request): 281 | context = {'foo': bar, 'spam': eggs} 282 | return render_to_response('template_name.html', 283 | context, context_instance=RequestContext()) 284 | 285 | To be honest,this doesn't look very much like a 'shortcut' at all. For this 286 | reason, DjanJinja contains a subclass of `RequestContext` specialised for 287 | Jinja2, which is used like this: 288 | 289 | from djanjinja.views import RequestContext 290 | 291 | def myview(request): 292 | context = RequestContext(request, {'foo': bar, 'spam': eggs}) 293 | return context.render_response('template_name.html') 294 | 295 | This code is much more concise, but loses none of the flexibility of the 296 | previous example. The main changes made are the addition of `render_response` 297 | and `render_string` methods to the context object itself. This is highly 298 | specialised to rendering Jinja2 templates, so it may not be a very reusable 299 | approach (indeed, other code which does not use Jinja2 will need to use the full 300 | Django syntax), but it works for the problem domain it was designed for. 301 | 302 | ## Middleware 303 | 304 | One important thing to note from before is that each time a `RequestContext` 305 | instance is constructed, it is necessary to explicitly pass the request. In 306 | object-oriented programming, and Python expecially, when we have functions to 307 | which we must always pass an object of a certain type, it makes sense to make 308 | that function a *method* of the type. When that function is not, in fact, a 309 | function, but a constructor, this seems more difficult. However, thanks to a 310 | feature of Python known as metaprogramming, we can do this very easily. Because 311 | it's not exactly obvious how to do so, DjanJinja includes a special piece of 312 | middleware which can help make your code a lot shorter yet *still* retain all 313 | the functionality and flexibility of the previous two examples. 314 | 315 | To use this middleware, simply add 316 | `'djanjinja.middleware.RequestContextMiddleware'` to your `MIDDLEWARE_CLASSES` 317 | list in the settings module of your project. Then, you can write view code like 318 | this: 319 | 320 | def myview(request): 321 | return request.Context({'foo': bar, 'spam': eggs}).render_response( 322 | 'template_name.html') 323 | 324 | As you can see, we've greatly reduced the verbosity of the previous code, but 325 | it's still obvious what this code does. The middleware attaches a `Context` 326 | attribute to each request object. This attribute is in fact a fully-fledged 327 | Python class, which may itself be subclassed and modified later on. When 328 | constructed, it behaves almost exactly the same as the usual `RequestContext`, 329 | only it uses the request object to which it has been attached, so you don't have 330 | to pass it in to the constructor every time. 331 | 332 | ## Template Loading 333 | 334 | DjanJinja hooks directly into the Django template loader machinery to load 335 | templates. This means you can mix Jinja2 templates freely with Django templates, 336 | in your `TEMPLATE_DIRS` and your applications, and render each type 337 | independently and seamlessly. If you want more information on how it actually 338 | works, please consult the `djanjinja/environment.py` file. 339 | 340 | ## (Un)license 341 | 342 | This is free and unencumbered software released into the public domain. 343 | 344 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 345 | software, either in source code form or as a compiled binary, for any purpose, 346 | commercial or non-commercial, and by any means. 347 | 348 | In jurisdictions that recognize copyright laws, the author or authors of this 349 | software dedicate any and all copyright interest in the software to the public 350 | domain. We make this dedication for the benefit of the public at large and to 351 | the detriment of our heirs and successors. We intend this dedication to be an 352 | overt act of relinquishment in perpetuity of all present and future rights to 353 | this software under copyright law. 354 | 355 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 356 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 357 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE 358 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 359 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 360 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 361 | 362 | For more information, please refer to 363 | --------------------------------------------------------------------------------