From 19586c0e2a2eec01f9b8d70a7d7499b7d9f8c5c9 Mon Sep 17 00:00:00 2001 From: CBWhiz Date: Fri, 23 Sep 2011 11:34:32 -0400 Subject: [PATCH 01/18] Add support for settings.JINJA2_TEMPLATE_LOADERS attribute --- coffin/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coffin/common.py b/coffin/common.py index be09adf..1b5959e 100644 --- a/coffin/common.py +++ b/coffin/common.py @@ -50,7 +50,8 @@ def _get_loaders(self): from coffin.template.loaders import jinja_loader_from_django_loader from django.conf import settings - for loader in settings.TEMPLATE_LOADERS: + _loaders = getattr(settings, 'JINJA2_TEMPLATE_LOADERS', settings.TEMPLATE_LOADERS) + for loader in _loaders: if isinstance(loader, basestring): loader_obj = jinja_loader_from_django_loader(loader) if loader_obj: From 178a806838f9089651e8bf3c1f468fd174282f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Sat, 22 Oct 2011 09:29:20 -0700 Subject: [PATCH 02/18] Fix URL to Github repository (Thanks slacy). --- coffin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coffin/__init__.py b/coffin/__init__.py index c076b00..5ee7725 100644 --- a/coffin/__init__.py +++ b/coffin/__init__.py @@ -2,7 +2,7 @@ Coffin ~~~~~~ -`Coffin ` is a package that resolves the +`Coffin ` is a package that resolves the impedance mismatch between `Django ` and `Jinja2 ` through various adapters. The aim is to use Coffin as a drop-in replacement for Django's template system to whatever extent is From 005a592c00ffa2be357db029f787ab44d00a45c4 Mon Sep 17 00:00:00 2001 From: Kirill Zaitsev Date: Fri, 16 Dec 2011 01:49:48 +0400 Subject: [PATCH 03/18] wrappers for django 1.3 class-based views --- coffin/views/decorators.py | 9 +++++++++ coffin/views/generic/base.py | 6 ++++++ coffin/views/generic/date.py | 14 ++++++++++++++ coffin/views/generic/detail.py | 6 ++++++ coffin/views/generic/edit.py | 8 ++++++++ coffin/views/generic/list.py | 6 ++++++ 6 files changed, 49 insertions(+) create mode 100644 coffin/views/decorators.py create mode 100644 coffin/views/generic/base.py create mode 100644 coffin/views/generic/date.py create mode 100644 coffin/views/generic/detail.py create mode 100644 coffin/views/generic/edit.py create mode 100644 coffin/views/generic/list.py diff --git a/coffin/views/decorators.py b/coffin/views/decorators.py new file mode 100644 index 0000000..cbfbacc --- /dev/null +++ b/coffin/views/decorators.py @@ -0,0 +1,9 @@ +from coffin.template.response import TemplateResponse + +def template_response(cls): + """ + A decorator to enforce class_based generic views + to use coffin TemplateResponse + """ + cls.response_class = TemplateResponse + return cls diff --git a/coffin/views/generic/base.py b/coffin/views/generic/base.py new file mode 100644 index 0000000..00990d9 --- /dev/null +++ b/coffin/views/generic/base.py @@ -0,0 +1,6 @@ +from django.views.generic.base import RedirectView, TemplateView as _TemplateView +from coffin.views.decorators import template_response + +__all__ = ['RedirectView', 'TemplateView'] + +TemplateView = template_response(_TemplateView) diff --git a/coffin/views/generic/date.py b/coffin/views/generic/date.py new file mode 100644 index 0000000..ba072d3 --- /dev/null +++ b/coffin/views/generic/date.py @@ -0,0 +1,14 @@ +from django.views.generic.dates import ArchiveIndexView as _ArchiveIndexView, YearArchiveIndexView as _YearArchiveIndexView +from django.views.generic.dates import MonthArchiveIndexView as _MonthArchiveIndexView, WeekArchiveIndexView as _WeekArchiveIndexView +from django.views.generic.dates import DayArchiveIndexView as _DayArchiveIndexView, TodayArchiveIndexView as _TodayArchiveIndexView +from django.views.generic.dates import DateDetailView as _DateDetailView +from coffin.views.decorators import template_response + +__all__ = ['ArchiveIndexView', 'YearArchiveIndexView', 'MonthArchiveIndexView', 'WeekArchiveIndexView', + 'DayArchiveIndexView', 'TodayArchiveIndexView', 'DateDetailView'] + +ArchiveIndexView, YearArchiveIndexView, MonthArchiveIndexView, WeekArchiveIndexView, + DayArchiveIndexView, TodayArchiveIndexView, DateDetailView = map(template_response, + (_ArchiveIndexView, _YearArchiveIndexView, _MonthArchiveIndexView, _WeekArchiveIndexView, + _DayArchiveIndexView, _TodayArchiveIndexView, _DateDetailView)) + diff --git a/coffin/views/generic/detail.py b/coffin/views/generic/detail.py new file mode 100644 index 0000000..3e2873f --- /dev/null +++ b/coffin/views/generic/detail.py @@ -0,0 +1,6 @@ +from django.views.generic.detail import DetailView as _DetailView +from coffin.views.decorators import template_response + +__all__ = ['DetailView'] + +DetailView = template_response(_DetailView) diff --git a/coffin/views/generic/edit.py b/coffin/views/generic/edit.py new file mode 100644 index 0000000..6597d90 --- /dev/null +++ b/coffin/views/generic/edit.py @@ -0,0 +1,8 @@ +from django.views.generic.edit import CreateView as _CreateView, UpdateView as _UpdateView, DeleteView as _DeleteView +from coffin.views.decorators import template_response + +__all__ = ['CreateView', 'UpdateView', 'DeleteView'] + +CreateView, UpdateView, DeleteView = map(template_response, + (_CreateView, _UpdateView, _DeleteView)) + diff --git a/coffin/views/generic/list.py b/coffin/views/generic/list.py new file mode 100644 index 0000000..a99af4e --- /dev/null +++ b/coffin/views/generic/list.py @@ -0,0 +1,6 @@ +from django.views.generic.list import ListView as _ListView +from coffin.views.decorators import template_response + +__all__ = ['ListView'] + +ListView = template_response(_ListView) From 0e2592fc0b78faa89e13c6587607e3d8553190e2 Mon Sep 17 00:00:00 2001 From: Kirill Zaitsev Date: Tue, 27 Dec 2011 17:36:31 +0400 Subject: [PATCH 04/18] SyntaxError fix --- coffin/views/generic/date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coffin/views/generic/date.py b/coffin/views/generic/date.py index ba072d3..b892e78 100644 --- a/coffin/views/generic/date.py +++ b/coffin/views/generic/date.py @@ -7,7 +7,7 @@ __all__ = ['ArchiveIndexView', 'YearArchiveIndexView', 'MonthArchiveIndexView', 'WeekArchiveIndexView', 'DayArchiveIndexView', 'TodayArchiveIndexView', 'DateDetailView'] -ArchiveIndexView, YearArchiveIndexView, MonthArchiveIndexView, WeekArchiveIndexView, +ArchiveIndexView, YearArchiveIndexView, MonthArchiveIndexView, WeekArchiveIndexView,\ DayArchiveIndexView, TodayArchiveIndexView, DateDetailView = map(template_response, (_ArchiveIndexView, _YearArchiveIndexView, _MonthArchiveIndexView, _WeekArchiveIndexView, _DayArchiveIndexView, _TodayArchiveIndexView, _DateDetailView)) From e325ad3b2f2639c25dd6a564f21b87ef23324954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 7 Feb 2012 23:22:03 +0100 Subject: [PATCH 05/18] Do not override globals provided by extensions. Fixes #26, closes #27. --- coffin/common.py | 2 +- tests/__init__.py | 13 +++++++------ tests/test_env.py | 11 +++++++++++ 3 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 tests/test_env.py diff --git a/coffin/common.py b/coffin/common.py index be09adf..69ddf49 100644 --- a/coffin/common.py +++ b/coffin/common.py @@ -28,7 +28,7 @@ def __init__(self, filters={}, globals={}, tests={}, loader=None, extensions=[], # the proper priority), so we want to assign to these attributes. self.filters = all_ext['filters'].copy() self.filters.update(filters) - self.globals = all_ext['globals'].copy() + self.globals.update(all_ext['globals']) self.globals.update(globals) self.tests = all_ext['tests'].copy() self.tests.update(tests) diff --git a/tests/__init__.py b/tests/__init__.py index 3d156c1..ef49302 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,9 +1,10 @@ from os import path import sys -def setup_package(): - # setup Django with our test demo project - sys.path.insert(0, path.join(path.dirname(__file__), 'res', 'apps')) - from django.core.management import setup_environ - import settings - setup_environ(settings) +# Setup Django with our test demo project. We need to do this in global +# module code rather than setup_package(), because we want it to run +# before any module-wide imports in any of the test modules. +sys.path.insert(0, path.join(path.dirname(__file__), 'res', 'apps')) +from django.core.management import setup_environ +import settings +setup_environ(settings) diff --git a/tests/test_env.py b/tests/test_env.py new file mode 100644 index 0000000..4bbaaf4 --- /dev/null +++ b/tests/test_env.py @@ -0,0 +1,11 @@ +"""Test construction of the implicitly provided JinjaEnvironment, +in the common.py module. +""" + +from coffin.common import get_env +from django.test.utils import override_settings + + +def test_i18n(): + with override_settings(USE_I18N=True): + assert get_env().from_string('{{ _("test") }}').render() == 'test' From aaea5d2514ffbba858572804639d192785fa1326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Tue, 7 Feb 2012 23:43:08 +0100 Subject: [PATCH 06/18] Implement the Django 1.3 render() shortcut. Closes #21. --- coffin/shortcuts/__init__.py | 28 +++++++++++++++++++++++++++- tests/test_shortcuts.py | 5 +++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tests/test_shortcuts.py diff --git a/coffin/shortcuts/__init__.py b/coffin/shortcuts/__init__.py index c882dfc..1fc41fd 100644 --- a/coffin/shortcuts/__init__.py +++ b/coffin/shortcuts/__init__.py @@ -5,7 +5,7 @@ from django.shortcuts import * -__all__ = ('render_to_string', 'render_to_response',) +__all__ = ('render_to_string', 'render_to_response', 'render') # Is within ``template.loader`` as per Django specification - @@ -23,3 +23,29 @@ def render_to_response(template_name, dictionary=None, context_instance=None, """ rendered = render_to_string(template_name, dictionary, context_instance) return HttpResponse(rendered, mimetype=mimetype) + + +def render(request, *args, **kwargs): + """ + Returns a HttpResponse whose content is filled with the result of calling + coffin.template.loader.render_to_string() with the passed arguments. + Uses a RequestContext by default. + """ + httpresponse_kwargs = { + 'content_type': kwargs.pop('content_type', None), + 'status': kwargs.pop('status', None), + } + + if 'context_instance' in kwargs: + context_instance = kwargs.pop('context_instance') + if kwargs.get('current_app', None): + raise ValueError('If you provide a context_instance you must ' + 'set its current_app before calling render()') + else: + current_app = kwargs.pop('current_app', None) + context_instance = RequestContext(request, current_app=current_app) + + kwargs['context_instance'] = context_instance + + return HttpResponse(render_to_string(*args, **kwargs), + **httpresponse_kwargs) diff --git a/tests/test_shortcuts.py b/tests/test_shortcuts.py new file mode 100644 index 0000000..c954742 --- /dev/null +++ b/tests/test_shortcuts.py @@ -0,0 +1,5 @@ +def test_render(): + """Test the render shortcut.""" + from coffin.shortcuts import render + response = render(None, 'render-x.html', {'x': 'foo'}) + assert response.content == 'foo' From 3edda75018a14404936be8763e4c57921c55bade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Wed, 8 Feb 2012 12:27:31 +0100 Subject: [PATCH 07/18] More precisely identify Django loaders. --- coffin/template/loaders.py | 2 ++ tests/test_env.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/coffin/template/loaders.py b/coffin/template/loaders.py index cb42fd5..1206bda 100644 --- a/coffin/template/loaders.py +++ b/coffin/template/loaders.py @@ -9,6 +9,8 @@ def jinja_loader_from_django_loader(django_loader): :return: The similarly-behaving Jinja loader, or None if a similar loader could not be found. """ + if not django_loader.startswith('django.'): + return None for substr, func in _JINJA_LOADER_BY_DJANGO_SUBSTR.iteritems(): if substr in django_loader: return func() diff --git a/tests/test_env.py b/tests/test_env.py index 4bbaaf4..3466cbf 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -9,3 +9,17 @@ def test_i18n(): with override_settings(USE_I18N=True): assert get_env().from_string('{{ _("test") }}').render() == 'test' + + +def test_django_loader_replace(): + from coffin.template.loaders import jinja_loader_from_django_loader + from jinja2 import loaders + + # Test replacement of filesystem loader + l = jinja_loader_from_django_loader('django.template.loaders.filesystem.Loader') + assert isinstance(l, loaders.FileSystemLoader) + + # Since we don't do exact matches for the loader string, make sure we + # are not replacing loaders that are outside the Django namespace. + l = jinja_loader_from_django_loader('djangoaddon.template.loaders.filesystem.Loader') + assert not isinstance(l, loaders.FileSystemLoader) From bf1fc6afad9a36d633049203dd583c6277e95252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Wed, 8 Feb 2012 13:54:19 +0100 Subject: [PATCH 08/18] Support Django cached loader. Closes #23. --- coffin/common.py | 25 +++++++++++++------ coffin/template/loaders.py | 50 ++++++++++++++++++++++++++++++++++++-- tests/test_env.py | 45 +++++++++++++++++++++++++--------- 3 files changed, 99 insertions(+), 21 deletions(-) diff --git a/coffin/common.py b/coffin/common.py index 6dd7ed2..c8b273e 100644 --- a/coffin/common.py +++ b/coffin/common.py @@ -48,18 +48,29 @@ def _get_loaders(self): loaders = [] from coffin.template.loaders import jinja_loader_from_django_loader + from jinja2.loaders import BaseLoader as JinjaLoader from django.conf import settings _loaders = getattr(settings, 'JINJA2_TEMPLATE_LOADERS', settings.TEMPLATE_LOADERS) for loader in _loaders: - if isinstance(loader, basestring): - loader_obj = jinja_loader_from_django_loader(loader) - if loader_obj: - loaders.append(loader_obj) - else: - warnings.warn('Cannot translate loader: %s' % loader) - else: # It's assumed to be a Jinja2 loader instance. + if isinstance(loader, JinjaLoader): loaders.append(loader) + else: + loader_name = args = None + if isinstance(loader, basestring): + loader_name = loader + args = [] + elif isinstance(loader, (tuple, list)): + loader_name = loader[0] + args = loader[1] + + if loader_name: + loader_obj = jinja_loader_from_django_loader(loader_name, args) + if loader_obj: + loaders.append(loader_obj) + continue + + warnings.warn('Cannot translate loader: %s' % loader) return loaders diff --git a/coffin/template/loaders.py b/coffin/template/loaders.py index 1206bda..1c81d85 100644 --- a/coffin/template/loaders.py +++ b/coffin/template/loaders.py @@ -1,7 +1,7 @@ from jinja2 import loaders -def jinja_loader_from_django_loader(django_loader): +def jinja_loader_from_django_loader(django_loader, args=None): """Attempts to make a conversion from the given Django loader to an similarly-behaving Jinja loader. @@ -13,7 +13,7 @@ def jinja_loader_from_django_loader(django_loader): return None for substr, func in _JINJA_LOADER_BY_DJANGO_SUBSTR.iteritems(): if substr in django_loader: - return func() + return func(*(args or [])) return None @@ -33,8 +33,54 @@ def _make_jinja_filesystem_loader(): return loaders.FileSystemLoader(settings.TEMPLATE_DIRS) +def _make_jinja_cached_loader(*loaders): + """Makes a loader for Jinja which acts like + :mod:`django.template.loaders.cached`. + """ + return JinjaCachedLoader( + [jinja_loader_from_django_loader(l) for l in loaders]) + + # Determine loaders from Django's conf. _JINJA_LOADER_BY_DJANGO_SUBSTR = { # {substr: callable, ...} 'app_directories': _make_jinja_app_loader, 'filesystem': _make_jinja_filesystem_loader, + 'cached': _make_jinja_cached_loader, } + + +class JinjaCachedLoader(loaders.BaseLoader): + """A "sort of" port of of Django's "cached" template loader + to Jinja 2. It exists primarily to support Django's full + TEMPLATE_LOADERS syntax. + + However, note that it does not behave exactly like Django's cached + loader: Rather than caching the compiled template, it only caches + the template source, and recompiles the template every time. This is + due to the way the Jinja2/Coffin loader setup works: The ChoiceLoader, + which Coffin uses at the root to select from any of the configured + loaders, calls the ``get_source`` method of each loader directly, + bypassing ``load``. Our loader can therefore only hook into the process + BEFORE template compilation. + Caching the compiled templates by implementing ``load`` would only + work if this loader instance were the root loader. See also the comments + in Jinja2's BaseLoader class. + + Note that Jinja2 has an environment-wide bytecode cache (i.e. it caches + compiled templates), that can function alongside with this class. + + Note further that Jinja2 has an environment-wide template cache (via the + ``auto_reload`` environment option), which duplicate the functionality + of this class entirely, and should be preferred when possible. + """ + + def __init__(self, subloaders): + self.loader = loaders.ChoiceLoader(subloaders) + self.template_cache = {} + + def get_source(self, environment, template): + key = (environment, template) + if key not in self.template_cache: + result = self.loader.get_source(environment, template) + self.template_cache[key] = result + return self.template_cache[key] diff --git a/tests/test_env.py b/tests/test_env.py index 3466cbf..744a89b 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -11,15 +11,36 @@ def test_i18n(): assert get_env().from_string('{{ _("test") }}').render() == 'test' -def test_django_loader_replace(): - from coffin.template.loaders import jinja_loader_from_django_loader - from jinja2 import loaders - - # Test replacement of filesystem loader - l = jinja_loader_from_django_loader('django.template.loaders.filesystem.Loader') - assert isinstance(l, loaders.FileSystemLoader) - - # Since we don't do exact matches for the loader string, make sure we - # are not replacing loaders that are outside the Django namespace. - l = jinja_loader_from_django_loader('djangoaddon.template.loaders.filesystem.Loader') - assert not isinstance(l, loaders.FileSystemLoader) +class TestLoaders: + + def test_django_loader_replace(self): + from coffin.template.loaders import jinja_loader_from_django_loader + from jinja2 import loaders + + # Test replacement of filesystem loader + l = jinja_loader_from_django_loader('django.template.loaders.filesystem.Loader') + assert isinstance(l, loaders.FileSystemLoader) + + # Since we don't do exact matches for the loader string, make sure we + # are not replacing loaders that are outside the Django namespace. + l = jinja_loader_from_django_loader('djangoaddon.template.loaders.filesystem.Loader') + assert not isinstance(l, loaders.FileSystemLoader) + + def test_cached_loader(self): + from jinja2 import loaders + + with override_settings(TEMPLATE_LOADERS=[ + ('django.template.loaders.cached.Loader', ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + )),]): + env = get_env() + assert len(env.loader.loaders) == 1 + cached_loader = get_env().loader.loaders[0] + assert hasattr(cached_loader, 'template_cache') + assert len(cached_loader.loader.loaders) == 2 + assert isinstance(cached_loader.loader.loaders[0], loaders.FileSystemLoader) + + # the cached loader can find a template too. + assert env.loader.load(env, 'render-x.html').render({'x': 'foo'}) == 'foo' + From 6124a4db30b3380831e4d662c4b358d646b88e54 Mon Sep 17 00:00:00 2001 From: akx Date: Tue, 29 May 2012 19:25:56 +0300 Subject: [PATCH 09/18] Non-callable globals + better Django ttag support Adds better support for non-callable globals (such as class instances, strings, etc.). Also adds JINJA2_DJANGO_TEMPLATETAG_LIBRARIES, which can be an iterable of templatetag library names. Use for `tz`, for instance. --- coffin/common.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/coffin/common.py b/coffin/common.py index c8b273e..32b509c 100644 --- a/coffin/common.py +++ b/coffin/common.py @@ -150,23 +150,35 @@ def _load_lib(lib): # Next, add the globally defined extensions extensions.extend(list(getattr(settings, 'JINJA2_EXTENSIONS', []))) - def from_setting(setting): + def from_setting(setting, values_must_be_callable = False): retval = {} setting = getattr(settings, setting, {}) if isinstance(setting, dict): for key, value in setting.iteritems(): - retval[key] = callable(value) and value or get_callable(value) + if values_must_be_callable and not callable(value): + value = get_callable(value) + retval[key] = value else: for value in setting: - value = callable(value) and value or get_callable(value) + if values_must_be_callable and not callable(value): + value = get_callable(value) retval[value.__name__] = value return retval - filters.update(from_setting('JINJA2_FILTERS')) + + tests.update(from_setting('JINJA2_TESTS', True)) + filters.update(from_setting('JINJA2_FILTERS', True)) globals.update(from_setting('JINJA2_GLOBALS')) - tests.update(from_setting('JINJA2_TESTS')) + # Finally, add extensions defined in application's templatetag libraries - for lib in self._get_templatelibs(): + libraries = self._get_templatelibs() + + # Load custom libraries. + from django.template import get_library + for libname in getattr(settings, 'JINJA2_DJANGO_TEMPLATETAG_LIBRARIES', ()): + libraries.append(get_library(libname)) + + for lib in libraries: _load_lib(lib) attrs.update(getattr(lib, 'jinja2_environment_attrs', {})) From 7ec1c216a943bceddee4a662a1b1da1069f91231 Mon Sep 17 00:00:00 2001 From: akx Date: Tue, 29 May 2012 22:06:40 +0300 Subject: [PATCH 10/18] Loader wrapper A loader wrapper for *.jinja files. Allows any old Django view (django.contrib.auth.views.* for instance) to use Coffin for its templates. --- coffin/contrib/loader.py | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 coffin/contrib/loader.py diff --git a/coffin/contrib/loader.py b/coffin/contrib/loader.py new file mode 100644 index 0000000..e957cb2 --- /dev/null +++ b/coffin/contrib/loader.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +A Django template loader wrapper for Coffin that intercepts +requests for "*.jinja" templates, rendering them with Coffin +instead of Django templates. + +Usage: + +TEMPLATE_LOADERS = ( + 'coffin.contrib.loader.AppLoader', + 'coffin.contrib.loader.FileSystemLoader', +) + +""" + +from coffin.common import env +from django.conf import settings +from django.template.loaders import app_directories, filesystem + + +JINJA2_DEFAULT_TEMPLATE_EXTENSION = getattr(settings, + 'JINJA2_DEFAULT_TEMPLATE_EXTENSION', '.jinja') + + +class LoaderMixin(object): + is_usable = True + + def load_template(self, template_name, template_dirs=None): + if not template_name.endswith(JINJA2_DEFAULT_TEMPLATE_EXTENSION): + return super(LoaderMixin, self).load_template(template_name, + template_dirs) + template = env.get_template(template_name) + return template, template.filename + + +class FileSystemLoader(LoaderMixin, filesystem.Loader): + pass + + +class AppLoader(LoaderMixin, app_directories.Loader): + pass From fc1028fb03fe493ecfd6be7093630607e0cb2b46 Mon Sep 17 00:00:00 2001 From: Dmitry Panteleev Date: Mon, 16 Jul 2012 12:21:29 +0400 Subject: [PATCH 11/18] Fixed problems with generic.dates views Added Jinja template response mixins These mixins are used according to https://docs.djangoproject.com/en/dev/topics/class-based-views Added documentation to coffin generic views --- coffin/views/generic/base.py | 19 +++++++++---- coffin/views/generic/date.py | 14 ---------- coffin/views/generic/dates.py | 50 ++++++++++++++++++++++++++++++++++ coffin/views/generic/detail.py | 18 ++++++++---- coffin/views/generic/edit.py | 38 ++++++++++++++++++++------ coffin/views/generic/list.py | 18 ++++++++---- 6 files changed, 117 insertions(+), 40 deletions(-) delete mode 100644 coffin/views/generic/date.py create mode 100644 coffin/views/generic/dates.py diff --git a/coffin/views/generic/base.py b/coffin/views/generic/base.py index 00990d9..ad2a370 100644 --- a/coffin/views/generic/base.py +++ b/coffin/views/generic/base.py @@ -1,6 +1,13 @@ -from django.views.generic.base import RedirectView, TemplateView as _TemplateView -from coffin.views.decorators import template_response - -__all__ = ['RedirectView', 'TemplateView'] - -TemplateView = template_response(_TemplateView) +import django.views.generic.base as _generic_base +from coffin.template.response import TemplateResponse as JinjaTemplateResponse + +class TemplateResponseMixin(_generic_base.TemplateResponseMixin): + """ + A mixin that can be used to render a template using Jinja. + """ + response_class = JinjaTemplateResponse + +class TemplateView(TemplateResponseMixin, _generic_base.TemplateView): + """ + A view that renders a template using Jinja. + """ \ No newline at end of file diff --git a/coffin/views/generic/date.py b/coffin/views/generic/date.py deleted file mode 100644 index b892e78..0000000 --- a/coffin/views/generic/date.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.views.generic.dates import ArchiveIndexView as _ArchiveIndexView, YearArchiveIndexView as _YearArchiveIndexView -from django.views.generic.dates import MonthArchiveIndexView as _MonthArchiveIndexView, WeekArchiveIndexView as _WeekArchiveIndexView -from django.views.generic.dates import DayArchiveIndexView as _DayArchiveIndexView, TodayArchiveIndexView as _TodayArchiveIndexView -from django.views.generic.dates import DateDetailView as _DateDetailView -from coffin.views.decorators import template_response - -__all__ = ['ArchiveIndexView', 'YearArchiveIndexView', 'MonthArchiveIndexView', 'WeekArchiveIndexView', - 'DayArchiveIndexView', 'TodayArchiveIndexView', 'DateDetailView'] - -ArchiveIndexView, YearArchiveIndexView, MonthArchiveIndexView, WeekArchiveIndexView,\ - DayArchiveIndexView, TodayArchiveIndexView, DateDetailView = map(template_response, - (_ArchiveIndexView, _YearArchiveIndexView, _MonthArchiveIndexView, _WeekArchiveIndexView, - _DayArchiveIndexView, _TodayArchiveIndexView, _DateDetailView)) - diff --git a/coffin/views/generic/dates.py b/coffin/views/generic/dates.py new file mode 100644 index 0000000..265bc32 --- /dev/null +++ b/coffin/views/generic/dates.py @@ -0,0 +1,50 @@ +from coffin.views.generic.detail import SingleObjectTemplateResponseMixin +from coffin.views.generic.list import MultipleObjectTemplateResponseMixin +import django.views.generic.dates as _generic_dates + +class ArchiveIndexView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseArchiveIndexView): + """ + Equivalent of django generic view ArchiveIndexView, but uses Jinja template renderer. + """ + template_name_suffix = '_archive' + + +class YearArchiveView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseYearArchiveView): + """ + Equivalent of django generic view YearArchiveView, but uses Jinja template renderer. + """ + template_name_suffix = '_archive_year' + + +class MonthArchiveView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseMonthArchiveView): + """ + Equivalent of django generic view MonthArchiveView, but uses Jinja template renderer. + """ + template_name_suffix = '_archive_month' + + +class WeekArchiveView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseWeekArchiveView): + """ + Equivalent of django generic view WeekArchiveView, but uses Jinja template renderer. + """ + template_name_suffix = '_archive_week' + + +class DayArchiveView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseDayArchiveView): + """ + Equivalent of django generic view DayArchiveView, but uses Jinja template renderer. + """ + template_name_suffix = "_archive_day" + +class TodayArchiveView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseTodayArchiveView): + """ + Equivalent of django generic view TodayArchiveView, but uses Jinja template renderer. + """ + template_name_suffix = "_archive_day" + + +class DateDetailView(SingleObjectTemplateResponseMixin, _generic_dates.BaseDateDetailView): + """ + Equivalent of django generic view DateDetailView, but uses Jinja template renderer. + """ + template_name_suffix = '_detail' \ No newline at end of file diff --git a/coffin/views/generic/detail.py b/coffin/views/generic/detail.py index 3e2873f..aaeb80e 100644 --- a/coffin/views/generic/detail.py +++ b/coffin/views/generic/detail.py @@ -1,6 +1,12 @@ -from django.views.generic.detail import DetailView as _DetailView -from coffin.views.decorators import template_response - -__all__ = ['DetailView'] - -DetailView = template_response(_DetailView) +import django.views.generic.detail as _generic_detail +from coffin.views.generic.base import TemplateResponseMixin as JinjaTemplateResponseMixin + +class SingleObjectTemplateResponseMixin(JinjaTemplateResponseMixin, _generic_detail.TemplateResponseMixin): + """ + Equivalent of django mixin SingleObjectTemplateResponseMixin, but uses Jinja template renderer. + """ + +class DetailView(SingleObjectTemplateResponseMixin, _generic_detail.BaseDetailView): + """ + Equivalent of django generic view DetailView, but uses Jinja template renderer. + """ diff --git a/coffin/views/generic/edit.py b/coffin/views/generic/edit.py index 6597d90..e3f415c 100644 --- a/coffin/views/generic/edit.py +++ b/coffin/views/generic/edit.py @@ -1,8 +1,30 @@ -from django.views.generic.edit import CreateView as _CreateView, UpdateView as _UpdateView, DeleteView as _DeleteView -from coffin.views.decorators import template_response - -__all__ = ['CreateView', 'UpdateView', 'DeleteView'] - -CreateView, UpdateView, DeleteView = map(template_response, - (_CreateView, _UpdateView, _DeleteView)) - +from coffin.views.generic.base import TemplateResponseMixin +from coffin.views.generic.detail import SingleObjectTemplateResponseMixin +import django.views.generic.edit as _generic_edit + + +class FormView(TemplateResponseMixin, _generic_edit.BaseFormView): + """ + Equivalent of django generic view FormView, but uses Jinja template renderer. + """ + + +class CreateView(SingleObjectTemplateResponseMixin, _generic_edit.BaseCreateView): + """ + Equivalent of django generic view CreateView, but uses Jinja template renderer. + """ + template_name_suffix = '_form' + + +class UpdateView(SingleObjectTemplateResponseMixin, _generic_edit.BaseUpdateView): + """ + Equivalent of django generic view UpdateView, but uses Jinja template renderer. + """ + template_name_suffix = '_form' + + +class DeleteView(SingleObjectTemplateResponseMixin, _generic_edit.BaseDeleteView): + """ + Equivalent of django generic view DeleteView, but uses Jinja template renderer. + """ + template_name_suffix = '_confirm_delete' diff --git a/coffin/views/generic/list.py b/coffin/views/generic/list.py index a99af4e..4f36295 100644 --- a/coffin/views/generic/list.py +++ b/coffin/views/generic/list.py @@ -1,6 +1,12 @@ -from django.views.generic.list import ListView as _ListView -from coffin.views.decorators import template_response - -__all__ = ['ListView'] - -ListView = template_response(_ListView) +import django.views.generic.list as _generic_list +from coffin.views.generic.base import TemplateResponseMixin as JinjaTemplateResponseMixin + +class MultipleObjectTemplateResponseMixin(JinjaTemplateResponseMixin, _generic_list.MultipleObjectTemplateResponseMixin): + """ + Equivalent of django mixin MultipleObjectTemplateResponseMixin, but uses Jinja template renderer. + """ + +class ListView(MultipleObjectTemplateResponseMixin, _generic_list.BaseListView): + """ + Equivalent of django generic view ListView, but uses Jinja template renderer. + """ From 295c076e6b5f3ee09498e2b027c332e7148e43b6 Mon Sep 17 00:00:00 2001 From: dpantele Date: Mon, 16 Jul 2012 16:55:53 +0400 Subject: [PATCH 12/18] Fixed Copy-Paste typo --- coffin/views/generic/detail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coffin/views/generic/detail.py b/coffin/views/generic/detail.py index aaeb80e..b5bd079 100644 --- a/coffin/views/generic/detail.py +++ b/coffin/views/generic/detail.py @@ -1,7 +1,7 @@ import django.views.generic.detail as _generic_detail from coffin.views.generic.base import TemplateResponseMixin as JinjaTemplateResponseMixin -class SingleObjectTemplateResponseMixin(JinjaTemplateResponseMixin, _generic_detail.TemplateResponseMixin): +class SingleObjectTemplateResponseMixin(JinjaTemplateResponseMixin, _generic_detail.SingleObjectTemplateResponseMixin): """ Equivalent of django mixin SingleObjectTemplateResponseMixin, but uses Jinja template renderer. """ From 8993259a4ea1e780ad839d627804dff8e5a79cc1 Mon Sep 17 00:00:00 2001 From: Dmitry Panteleev Date: Mon, 6 Aug 2012 12:48:41 +0400 Subject: [PATCH 13/18] makemessages: Support whitespace stripping tags. Apparently, makemessages didn't work when trans or pluralize used Jinja2's {%- syntax to strip whitespace. Closes #37. --- coffin/management/commands/makemessages.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/coffin/management/commands/makemessages.py b/coffin/management/commands/makemessages.py index 93ba3ee..68b1a2f 100644 --- a/coffin/management/commands/makemessages.py +++ b/coffin/management/commands/makemessages.py @@ -27,24 +27,40 @@ import re from django.core.management.commands import makemessages from django.utils.translation import trans_real +from django.template import BLOCK_TAG_START, BLOCK_TAG_END +strip_whitespace_right = re.compile(r"(%s-?\s*(trans|pluralize).*?-%s)\s+" % (BLOCK_TAG_START, BLOCK_TAG_END), re.U) +strip_whitespace_left = re.compile(r"\s+(%s-\s*(endtrans|pluralize).*?-?%s)" % (BLOCK_TAG_START, BLOCK_TAG_END), re.U) + +def strip_whitespaces(src): + src = strip_whitespace_left.sub(r'\1', src) + src = strip_whitespace_right.sub(r'\1', src) + return src class Command(makemessages.Command): def handle(self, *args, **options): old_endblock_re = trans_real.endblock_re old_block_re = trans_real.block_re + old_templatize = trans_real.templatize # Extend the regular expressions that are used to detect # translation blocks with an "OR jinja-syntax" clause. trans_real.endblock_re = re.compile( - trans_real.endblock_re.pattern + '|' + r"""^\s*endtrans$""") + trans_real.endblock_re.pattern + '|' + r"""^-?\s*endtrans\s*-?$""") trans_real.block_re = re.compile( - trans_real.block_re.pattern + '|' + r"""^\s*trans(?:\s+(?!'|")(?=.*?=.*?)|$)""") + trans_real.block_re.pattern + '|' + r"""^-?\s*trans(?:\s+(?!'|")(?=.*?=.*?)|-?$)""") trans_real.plural_re = re.compile( - trans_real.plural_re.pattern + '|' + r"""^\s*pluralize(?:\s+.+|$)""") + trans_real.plural_re.pattern + '|' + r"""^-?\s*pluralize(?:\s+.+|-?$)""") + + def my_templatize(src, origin=None): + new_src = strip_whitespaces(src) + return old_templatize(new_src, origin) + + trans_real.templatize = my_templatize try: super(Command, self).handle(*args, **options) finally: trans_real.endblock_re = old_endblock_re trans_real.block_re = old_block_re + trans_real.templatize = old_templatize From 95e2ba932fde115e587413f6f2389cd37d1c4f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsdo=CC=88rfer?= Date: Sat, 29 Sep 2012 18:05:38 +0200 Subject: [PATCH 14/18] Set version number for release. --- coffin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coffin/__init__.py b/coffin/__init__.py index 5ee7725..421b461 100644 --- a/coffin/__init__.py +++ b/coffin/__init__.py @@ -14,7 +14,7 @@ __all__ = ('__version__', '__build__', '__docformat__', 'get_revision') -__version__ = (0, 3, '7', 'dev') +__version__ = (0, 3, '7') __docformat__ = 'restructuredtext en' import os From 41bc35a53201cf6e293300be20f64a024ad65119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsdo=CC=88rfer?= Date: Sat, 29 Sep 2012 18:11:04 +0200 Subject: [PATCH 15/18] Update CHANGES file before new release. --- CHANGES | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index cf39fb9..59c3931 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,11 @@ +0.3.7 (2012-09-29) + - Support for Django 1.3 class-based views (Kirill Zaitsev, + Dmitry Panteleev) + - Loader for *.jinja files (Aarni Koskela) + - makemessages command now supports whitespace stripping tags + (Dmitry Panteleev) + - Now supports Django's CachedLoader. + 0.3.6 (2011-09-09) - - Re-release of 0.3.5, containing the correct repository + - Re-release of 0.3.5, containing the correct repository state this time. From acd0c583e3a4a5f4d002fe4d139fc70b23bab9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsdo=CC=88rfer?= Date: Sat, 29 Sep 2012 18:11:54 +0200 Subject: [PATCH 16/18] Inc version number for development branch. --- coffin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coffin/__init__.py b/coffin/__init__.py index 421b461..0352a04 100644 --- a/coffin/__init__.py +++ b/coffin/__init__.py @@ -14,7 +14,7 @@ __all__ = ('__version__', '__build__', '__docformat__', 'get_revision') -__version__ = (0, 3, '7') +__version__ = (0, 3, '8', 'dev') __docformat__ = 'restructuredtext en' import os From 43bc309608fea328ffaaef765786e65513bee003 Mon Sep 17 00:00:00 2001 From: Vladislav Poluhin Date: Fri, 2 Nov 2012 15:27:02 +0800 Subject: [PATCH 17/18] closes #40 -- fix jinja_loader_from_django_loader --- coffin/template/loaders.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/coffin/template/loaders.py b/coffin/template/loaders.py index 1c81d85..70ca295 100644 --- a/coffin/template/loaders.py +++ b/coffin/template/loaders.py @@ -1,5 +1,8 @@ +import re from jinja2 import loaders +match_loader = re.compile(r'^(django|coffin)\.') + def jinja_loader_from_django_loader(django_loader, args=None): """Attempts to make a conversion from the given Django loader to an @@ -9,7 +12,7 @@ def jinja_loader_from_django_loader(django_loader, args=None): :return: The similarly-behaving Jinja loader, or None if a similar loader could not be found. """ - if not django_loader.startswith('django.'): + if not match_loader.match(django_loader): return None for substr, func in _JINJA_LOADER_BY_DJANGO_SUBSTR.iteritems(): if substr in django_loader: @@ -46,6 +49,8 @@ def _make_jinja_cached_loader(*loaders): 'app_directories': _make_jinja_app_loader, 'filesystem': _make_jinja_filesystem_loader, 'cached': _make_jinja_cached_loader, + 'AppLoader': _make_jinja_app_loader, + 'FileSystemLoader': _make_jinja_filesystem_loader, } From ca6644c81bb0fc305113bbda0916a098ccf08703 Mon Sep 17 00:00:00 2001 From: Vladislav Poluhin Date: Tue, 13 Nov 2012 10:31:24 +0800 Subject: [PATCH 18/18] JINJA2_DEFAULT_TEMPLATE_EXTENSION now supports multiple values --- coffin/contrib/loader.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/coffin/contrib/loader.py b/coffin/contrib/loader.py index e957cb2..4a29631 100644 --- a/coffin/contrib/loader.py +++ b/coffin/contrib/loader.py @@ -13,20 +13,26 @@ """ +from os.path import splitext from coffin.common import env from django.conf import settings from django.template.loaders import app_directories, filesystem JINJA2_DEFAULT_TEMPLATE_EXTENSION = getattr(settings, - 'JINJA2_DEFAULT_TEMPLATE_EXTENSION', '.jinja') + 'JINJA2_DEFAULT_TEMPLATE_EXTENSION', ('.jinja',)) + +if isinstance(JINJA2_DEFAULT_TEMPLATE_EXTENSION, basestring): + JINJA2_DEFAULT_TEMPLATE_EXTENSION = (JINJA2_DEFAULT_TEMPLATE_EXTENSION,) class LoaderMixin(object): is_usable = True def load_template(self, template_name, template_dirs=None): - if not template_name.endswith(JINJA2_DEFAULT_TEMPLATE_EXTENSION): + extension = splitext(template_name)[1] + + if not extension in JINJA2_DEFAULT_TEMPLATE_EXTENSION: return super(LoaderMixin, self).load_template(template_name, template_dirs) template = env.get_template(template_name)