summaryrefslogtreecommitdiffstats
path: root/coffin
diff options
context:
space:
mode:
Diffstat (limited to 'coffin')
-rw-r--r--coffin/.___init__.pybin0 -> 187 bytes
-rw-r--r--coffin/._common.pybin0 -> 187 bytes
-rw-r--r--coffin/._interop.pybin0 -> 184 bytes
-rw-r--r--coffin/__init__.py44
-rw-r--r--coffin/common.py148
-rw-r--r--coffin/conf/.___init__.pybin0 -> 184 bytes
-rw-r--r--coffin/conf/__init__.py0
-rw-r--r--coffin/conf/urls/__init__.py0
-rw-r--r--coffin/conf/urls/defaults.py4
-rw-r--r--coffin/contrib/__init__.py0
-rw-r--r--coffin/contrib/markup/__init__.py0
-rw-r--r--coffin/contrib/markup/models.py0
-rw-r--r--coffin/contrib/markup/templatetags/__init__.py0
-rw-r--r--coffin/contrib/markup/templatetags/markup.py15
-rw-r--r--coffin/contrib/syndication/__init__.py0
-rw-r--r--coffin/contrib/syndication/feeds.py36
-rw-r--r--coffin/interop.py120
-rw-r--r--coffin/shortcuts/.___init__.pybin0 -> 187 bytes
-rw-r--r--coffin/shortcuts/__init__.py25
-rw-r--r--coffin/template/.___init__.pybin0 -> 187 bytes
-rw-r--r--coffin/template/._defaultfilters.pybin0 -> 185 bytes
-rw-r--r--coffin/template/._defaulttags.pybin0 -> 184 bytes
-rw-r--r--coffin/template/._library.pybin0 -> 187 bytes
-rw-r--r--coffin/template/._loader.pybin0 -> 187 bytes
-rw-r--r--coffin/template/._loaders.pybin0 -> 184 bytes
-rw-r--r--coffin/template/__init__.py93
-rw-r--r--coffin/template/defaultfilters.py99
-rw-r--r--coffin/template/defaulttags.py364
-rw-r--r--coffin/template/library.py215
-rw-r--r--coffin/template/loader.py66
-rw-r--r--coffin/template/loaders.py38
-rw-r--r--coffin/views/__init__.py0
-rw-r--r--coffin/views/defaults.py35
-rw-r--r--coffin/views/generic/._simple.pybin0 -> 185 bytes
-rw-r--r--coffin/views/generic/__init__.py1
-rw-r--r--coffin/views/generic/simple.py6
36 files changed, 1309 insertions, 0 deletions
diff --git a/coffin/.___init__.py b/coffin/.___init__.py
new file mode 100644
index 00000000..b44b5fae
--- /dev/null
+++ b/coffin/.___init__.py
Binary files differ
diff --git a/coffin/._common.py b/coffin/._common.py
new file mode 100644
index 00000000..c83780af
--- /dev/null
+++ b/coffin/._common.py
Binary files differ
diff --git a/coffin/._interop.py b/coffin/._interop.py
new file mode 100644
index 00000000..94121d1f
--- /dev/null
+++ b/coffin/._interop.py
Binary files differ
diff --git a/coffin/__init__.py b/coffin/__init__.py
new file mode 100644
index 00000000..df89bdd6
--- /dev/null
+++ b/coffin/__init__.py
@@ -0,0 +1,44 @@
+"""
+Coffin
+~~~~~~
+
+`Coffin <http://www.github.com/dcramer/coffin>` is a package that resolves the
+impedance mismatch between `Django <http://www.djangoproject.com/>` and `Jinja2
+<http://jinja.pocoo.org/2/>` through various adapters. The aim is to use Coffin
+as a drop-in replacement for Django's template system to whatever extent is
+reasonable.
+
+:copyright: 2008 by Christopher D. Leary
+:license: BSD, see LICENSE for more details.
+"""
+
+
+__all__ = ('__version__', '__build__', '__docformat__', 'get_revision')
+__version__ = (0, 3)
+__docformat__ = 'restructuredtext en'
+
+import os
+
+def _get_git_revision(path):
+ revision_file = os.path.join(path, 'refs', 'heads', 'master')
+ if not os.path.exists(revision_file):
+ return None
+ fh = open(revision_file, 'r')
+ try:
+ return fh.read()
+ finally:
+ fh.close()
+
+def get_revision():
+ """
+ :returns: Revision number of this branch/checkout, if available. None if
+ no revision number can be determined.
+ """
+ package_dir = os.path.dirname(__file__)
+ checkout_dir = os.path.normpath(os.path.join(package_dir, '..'))
+ path = os.path.join(checkout_dir, '.git')
+ if os.path.exists(path):
+ return _get_git_revision(path)
+ return None
+
+__build__ = get_revision()
diff --git a/coffin/common.py b/coffin/common.py
new file mode 100644
index 00000000..2e381ff3
--- /dev/null
+++ b/coffin/common.py
@@ -0,0 +1,148 @@
+import os
+import warnings
+
+from django import dispatch
+from jinja2 import Environment, loaders
+
+__all__ = ('env', 'need_env')
+
+env = None
+
+_JINJA_I18N_EXTENSION_NAME = 'jinja2.ext.i18n'
+
+# TODO: This should be documented (as even I'm not sure where it's use-case is)
+need_env = dispatch.Signal(providing_args=['arguments', 'loaders',
+ 'filters', 'extensions',
+ 'globals', 'tests'])
+
+class CoffinEnvironment(Environment):
+ def __init__(self, filters={}, globals={}, tests={}, loader=None, extensions=[], **kwargs):
+ if not loader:
+ loader = loaders.ChoiceLoader(self._get_loaders())
+ all_ext = self._get_all_extensions()
+
+ extensions.extend(all_ext['extensions'])
+ super(CoffinEnvironment, self).__init__(extensions=extensions, loader=loader, **kwargs)
+ self.filters.update(filters)
+ self.filters.update(all_ext['filters'])
+ self.globals.update(globals)
+ self.globals.update(all_ext['globals'])
+ self.tests.update(tests)
+ self.tests.update(all_ext['tests'])
+
+ from coffin.template import Template as CoffinTemplate
+ self.template_class = CoffinTemplate
+
+ def _get_loaders(self):
+ """Tries to translate each template loader given in the Django settings
+ (:mod:`django.settings`) to a similarly-behaving Jinja loader.
+ Warns if a similar loader cannot be found.
+ Allows for Jinja2 loader instances to be placed in the template loader
+ settings.
+ """
+ loaders = []
+
+ from coffin.template.loaders import jinja_loader_from_django_loader
+
+ from django.conf import settings
+ for loader in settings.TEMPLATE_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.
+ loaders.append(loader)
+ return loaders
+
+
+ def _get_templatelibs(self):
+ """Return an iterable of template ``Library`` instances.
+
+ Since we cannot support the {% load %} tag in Jinja, we have to
+ register all libraries globally.
+ """
+ from django.conf import settings
+ from django.template import get_library, InvalidTemplateLibrary
+
+ libs = []
+ for a in settings.INSTALLED_APPS:
+ try:
+ path = __import__(a + '.templatetags', {}, {}, ['__file__']).__file__
+ path = os.path.dirname(path) # we now have the templatetags/ directory
+ except ImportError:
+ pass
+ else:
+ for f in os.listdir(path):
+ if f == '__init__.py':
+ continue
+ if f.endswith('.py'):
+ try:
+ # TODO: will need updating when #6587 lands
+ libs.append(get_library(
+ "django.templatetags.%s" % os.path.splitext(f)[0]))
+ except InvalidTemplateLibrary:
+ pass
+ return libs
+
+ def _get_all_extensions(self):
+ from django.conf import settings
+ from coffin.template import builtins
+ from django.core.urlresolvers import get_callable
+
+ extensions, filters, globals, tests = [], {}, {}, {}
+
+ # start with our builtins
+ for lib in builtins:
+ extensions.extend(getattr(lib, 'jinja2_extensions', []))
+ filters.update(getattr(lib, 'jinja2_filters', {}))
+ globals.update(getattr(lib, 'jinja2_globals', {}))
+ tests.update(getattr(lib, 'jinja2_tests', {}))
+
+ if settings.USE_I18N:
+ extensions.append(_JINJA_I18N_EXTENSION_NAME)
+
+ # add the globally defined extension list
+ extensions.extend(list(getattr(settings, 'JINJA2_EXTENSIONS', [])))
+
+ def from_setting(setting):
+ retval = {}
+ setting = getattr(settings, setting, {})
+ if isinstance(setting, dict):
+ for key, value in setting.iteritems():
+ retval[user] = callable(value) and value or get_callable(value)
+ else:
+ for value in setting:
+ value = callable(value) and value or get_callable(value)
+ retval[value.__name__] = value
+ return retval
+
+ filters.update(from_setting('JINJA2_FILTERS'))
+ globals.update(from_setting('JINJA2_GLOBALS'))
+ tests.update(from_setting('JINJA2_TESTS'))
+
+ # add extensions defined in application's templatetag libraries
+ for lib in self._get_templatelibs():
+ extensions.extend(getattr(lib, 'jinja2_extensions', []))
+ filters.update(getattr(lib, 'jinja2_filters', {}))
+ globals.update(getattr(lib, 'jinja2_globals', {}))
+ tests.update(getattr(lib, 'jinja2_tests', {}))
+
+ return dict(
+ extensions=extensions,
+ filters=filters,
+ globals=globals,
+ tests=tests,
+ )
+
+def get_env():
+ """
+ :return: A Jinja2 environment singleton.
+ """
+ # need_env.send(sender=Environment, arguments=arguments,
+ # loaders=loaders_, extensions=extensions,
+ # filters=filters, tests=tests, globals=globals)
+ return CoffinEnvironment(autoescape=True)
+
+env = get_env()
diff --git a/coffin/conf/.___init__.py b/coffin/conf/.___init__.py
new file mode 100644
index 00000000..c6b47018
--- /dev/null
+++ b/coffin/conf/.___init__.py
Binary files differ
diff --git a/coffin/conf/__init__.py b/coffin/conf/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/conf/__init__.py
diff --git a/coffin/conf/urls/__init__.py b/coffin/conf/urls/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/conf/urls/__init__.py
diff --git a/coffin/conf/urls/defaults.py b/coffin/conf/urls/defaults.py
new file mode 100644
index 00000000..3049e2b9
--- /dev/null
+++ b/coffin/conf/urls/defaults.py
@@ -0,0 +1,4 @@
+from django.conf.urls.defaults import *
+
+handler404 = 'coffin.views.defaults.page_not_found'
+handler500 = 'coffin.views.defaults.server_error' \ No newline at end of file
diff --git a/coffin/contrib/__init__.py b/coffin/contrib/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/contrib/__init__.py
diff --git a/coffin/contrib/markup/__init__.py b/coffin/contrib/markup/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/contrib/markup/__init__.py
diff --git a/coffin/contrib/markup/models.py b/coffin/contrib/markup/models.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/contrib/markup/models.py
diff --git a/coffin/contrib/markup/templatetags/__init__.py b/coffin/contrib/markup/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/contrib/markup/templatetags/__init__.py
diff --git a/coffin/contrib/markup/templatetags/markup.py b/coffin/contrib/markup/templatetags/markup.py
new file mode 100644
index 00000000..0d6b92f9
--- /dev/null
+++ b/coffin/contrib/markup/templatetags/markup.py
@@ -0,0 +1,15 @@
+"""Makes the template filters from the ``django.contrib.markup`` app
+available to both the Jinja2 and Django engines.
+
+In other words, adding ``coffin.contrib.markup`` to your INSTALLED_APPS
+setting will enable the markup filters not only through Coffin, but
+also through the default Django template system.
+"""
+
+from coffin.template import Library as CoffinLibrary
+from django.contrib.markup.templatetags.markup import register
+
+
+# Convert Django's Library into a Coffin Library object, which will
+# make sure the filters are correctly ported to Jinja2.
+register = CoffinLibrary.from_django(register) \ No newline at end of file
diff --git a/coffin/contrib/syndication/__init__.py b/coffin/contrib/syndication/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/contrib/syndication/__init__.py
diff --git a/coffin/contrib/syndication/feeds.py b/coffin/contrib/syndication/feeds.py
new file mode 100644
index 00000000..f8f0701f
--- /dev/null
+++ b/coffin/contrib/syndication/feeds.py
@@ -0,0 +1,36 @@
+from django.contrib.syndication.feeds import * # merge modules
+
+import sys
+from django.contrib.syndication.feeds import Feed as DjangoFeed
+from coffin.template import loader as coffin_loader
+
+
+class Feed(DjangoFeed):
+ """A ``Feed`` implementation that renders it's title and
+ description templates using Jinja2.
+
+ Unfortunately, Django's base ``Feed`` class is not very extensible
+ in this respect at all. For a real solution, we'd have to essentially
+ have to duplicate the whole class. So for now, we use this terrible
+ non-thread safe hack.
+
+ Another, somewhat crazy option would be:
+ * Render the templates ourselves through Jinja2 (possible
+ introduce new attributes to avoid having to rewrite the
+ existing ones).
+ * Make the rendered result available to Django/the superclass by
+ using a custom template loader using a prefix, say
+ "feed:<myproject.app.views.MyFeed>". The loader would simply
+ return the Jinja-rendered template (escaped), the Django template
+ mechanism would find no nodes and just pass the output through.
+ Possible even worse than this though.
+ """
+
+ def get_feed(self, *args, **kwargs):
+ parent_module = sys.modules[DjangoFeed.__module__]
+ old_loader = parent_module.loader
+ parent_module.loader = coffin_loader
+ try:
+ return super(Feed, self).get_feed(*args, **kwargs)
+ finally:
+ parent_module.loader = old_loader \ No newline at end of file
diff --git a/coffin/interop.py b/coffin/interop.py
new file mode 100644
index 00000000..54120004
--- /dev/null
+++ b/coffin/interop.py
@@ -0,0 +1,120 @@
+"""Compatibility functions between Jinja2 and Django.
+
+General notes:
+
+ - The Django ``stringfilter`` decorator is supported, but should not be
+ used when writing filters specifically for Jinja: It will lose the
+ attributes attached to the filter function by Jinja's
+ ``environmentfilter`` and ``contextfilter`` decorators, when used
+ in the wrong order.
+
+ Maybe coffin should provide a custom version of stringfilter.
+
+ - While transparently converting filters between Django and Jinja works
+ for the most part, there is an issue with Django's
+ ``mark_for_escaping``, as Jinja does not support a similar mechanism.
+ Instead, for Jinja, we escape such strings immediately (whereas Django
+ defers it to the template engine).
+"""
+
+import inspect
+from django.utils.safestring import SafeUnicode, SafeData, EscapeData
+from jinja2 import Markup, environmentfilter
+
+
+__all__ = (
+ 'DJANGO', 'JINJA2',
+ 'django_filter_to_jinja2',
+ 'jinja2_filter_to_django',
+ 'guess_filter_type',)
+
+
+DJANGO = 'django'
+JINJA2 = 'jinja2'
+
+
+def django_filter_to_jinja2(filter_func):
+ """
+ Note: Due to the way this function is used by
+ ``coffin.template.Library``, it needs to be able to handle native
+ Jinja2 filters and pass them through unmodified. This necessity
+ stems from the fact that it is not always possible to determine
+ the type of a filter.
+
+ TODO: Django's "func.is_safe" is not yet handled
+ """
+ def _convert(v):
+ if isinstance(v, SafeData):
+ return Markup(v)
+ if isinstance(v, EscapeData):
+ return Markup.escape(v) # not 100% equivalent, see mod docs
+ return v
+ def conversion_wrapper(*args, **kwargs):
+ result = filter_func(*args, **kwargs)
+ return _convert(result)
+ # Jinja2 supports a similar machanism to Django's
+ # ``needs_autoescape`` filters: environment filters. We can
+ # thus support Django filters that use it in Jinja2 with just
+ # a little bit of argument rewriting.
+ if hasattr(filter_func, 'needs_autoescape'):
+ @environmentfilter
+ def autoescape_wrapper(environment, *args, **kwargs):
+ kwargs['autoescape'] = environment.autoescape
+ return conversion_wrapper(*args, **kwargs)
+ return autoescape_wrapper
+ else:
+ return conversion_wrapper
+
+
+def jinja2_filter_to_django(filter_func):
+ """
+ Note: Due to the way this function is used by
+ ``coffin.template.Library``, it needs to be able to handle native
+ Django filters and pass them through unmodified. This necessity
+ stems from the fact that it is not always possible to determine
+ the type of a filter.
+ """
+ if guess_filter_type(filter_func)[0] == DJANGO:
+ return filter_func
+ def _convert(v):
+ # TODO: for now, this is not even necessary: Markup strings have
+ # a custom replace() method that is immume to Django's escape()
+ # attempts.
+ #if isinstance(v, Markup):
+ # return SafeUnicode(v) # jinja is always unicode
+ # ... Jinja does not have a EscapeData equivalent
+ return v
+ def wrapped(value, *args, **kwargs):
+ result = filter_func(value, *args, **kwargs)
+ return _convert(result)
+ return wrapped
+
+
+def guess_filter_type(filter_func):
+ """Returns a 2-tuple of (type, can_be_ported).
+
+ ``type`` is one of DJANGO, JINJA2, or ``False`` if the type can
+ not be determined.
+
+ ``can_be_ported`` is ``True`` if we believe the filter could be
+ ported to the other engine, respectively, or ``False`` if we know
+ it can't.
+
+ TODO: May not yet use all possible clues, e.g. decorators like
+ ``stringfilter``.
+ TOOD: Needs tests.
+ """
+ if hasattr(filter_func, 'contextfilter') or \
+ hasattr(filter_func, 'environmentfilter'):
+ return JINJA2, False
+
+ args = inspect.getargspec(filter_func)
+ if len(args[0]) - (len(args[3]) if args[3] else 0) > 2:
+ return JINJA2, False
+
+ if hasattr(filter_func, 'needs_autoescape'):
+ return DJANGO, True
+
+ # Looks like your run of the mill Python function, which are
+ # easily convertible in either direction.
+ return False, True \ No newline at end of file
diff --git a/coffin/shortcuts/.___init__.py b/coffin/shortcuts/.___init__.py
new file mode 100644
index 00000000..c21832ed
--- /dev/null
+++ b/coffin/shortcuts/.___init__.py
Binary files differ
diff --git a/coffin/shortcuts/__init__.py b/coffin/shortcuts/__init__.py
new file mode 100644
index 00000000..c882dfcd
--- /dev/null
+++ b/coffin/shortcuts/__init__.py
@@ -0,0 +1,25 @@
+from django.http import HttpResponse
+
+# Merge with original namespace so user
+# doesn't have to import twice.
+from django.shortcuts import *
+
+
+__all__ = ('render_to_string', 'render_to_response',)
+
+
+# Is within ``template.loader`` as per Django specification -
+# but I think it fits very well here.
+from coffin.template.loader import render_to_string
+
+
+def render_to_response(template_name, dictionary=None, context_instance=None,
+ mimetype=None):
+ """
+ :param template_name: Filename of the template to get or a sequence of
+ filenames to try, in order.
+ :param dictionary: Rendering context for the template.
+ :returns: A response object with the evaluated template as a payload.
+ """
+ rendered = render_to_string(template_name, dictionary, context_instance)
+ return HttpResponse(rendered, mimetype=mimetype)
diff --git a/coffin/template/.___init__.py b/coffin/template/.___init__.py
new file mode 100644
index 00000000..87947898
--- /dev/null
+++ b/coffin/template/.___init__.py
Binary files differ
diff --git a/coffin/template/._defaultfilters.py b/coffin/template/._defaultfilters.py
new file mode 100644
index 00000000..8fd7d746
--- /dev/null
+++ b/coffin/template/._defaultfilters.py
Binary files differ
diff --git a/coffin/template/._defaulttags.py b/coffin/template/._defaulttags.py
new file mode 100644
index 00000000..db9dcec7
--- /dev/null
+++ b/coffin/template/._defaulttags.py
Binary files differ
diff --git a/coffin/template/._library.py b/coffin/template/._library.py
new file mode 100644
index 00000000..18fe8260
--- /dev/null
+++ b/coffin/template/._library.py
Binary files differ
diff --git a/coffin/template/._loader.py b/coffin/template/._loader.py
new file mode 100644
index 00000000..3b699201
--- /dev/null
+++ b/coffin/template/._loader.py
Binary files differ
diff --git a/coffin/template/._loaders.py b/coffin/template/._loaders.py
new file mode 100644
index 00000000..e17a06d5
--- /dev/null
+++ b/coffin/template/._loaders.py
Binary files differ
diff --git a/coffin/template/__init__.py b/coffin/template/__init__.py
new file mode 100644
index 00000000..b487c8f5
--- /dev/null
+++ b/coffin/template/__init__.py
@@ -0,0 +1,93 @@
+from django.template import (
+ Context as DjangoContext,
+ add_to_builtins as django_add_to_builtins,
+ get_library)
+from jinja2 import Template as _Jinja2Template
+
+# Merge with ``django.template``.
+from django.template import __all__
+from django.template import *
+
+# Override default library class with ours
+from library import *
+
+
+class Template(_Jinja2Template):
+ """Fixes the incompabilites between Jinja2's template class and
+ Django's.
+
+ The end result should be a class that renders Jinja2 templates but
+ is compatible with the interface specfied by Django.
+
+ This includes flattening a ``Context`` instance passed to render
+ and making sure that this class will automatically use the global
+ coffin environment.
+ """
+
+ def __new__(cls, template_string, origin=None, name=None):
+ # We accept the "origin" and "name" arguments, but discard them
+ # right away - Jinja's Template class (apparently) stores no
+ # equivalent information.
+ from coffin.common import env
+
+ return env.from_string(template_string, template_class=cls)
+
+ def __iter__(self):
+ # TODO: Django allows iterating over the templates nodes. Should
+ # be parse ourself and iterate over the AST?
+ raise NotImplementedError()
+
+ def render(self, context=None):
+ """Differs from Django's own render() slightly in that makes the
+ ``context`` parameter optional. We try to strike a middle ground
+ here between implementing Django's interface while still supporting
+ Jinja's own call syntax as well.
+ """
+ if context is None:
+ context = {}
+ else:
+ context = dict_from_django_context(context)
+ assert isinstance(context, dict) # Required for **-operator.
+ return super(Template, self).render(**context)
+
+
+def dict_from_django_context(context):
+ """Flattens a Django :class:`django.template.context.Context` object.
+ """
+ if not isinstance(context, DjangoContext):
+ return context
+ else:
+ dict_ = {}
+ # Newest dicts are up front, so update from oldest to newest.
+ for subcontext in reversed(list(context)):
+ dict_.update(dict_from_django_context(subcontext))
+ return dict_
+
+
+# libraries to load by default for a new environment
+builtins = []
+
+
+def add_to_builtins(module_name):
+ """Add the given module to both Coffin's list of default template
+ libraries as well as Django's. This makes sense, since Coffin
+ libs are compatible with Django libraries.
+
+ You can still use Django's own ``add_to_builtins`` to register
+ directly with Django and bypass Coffin.
+
+ TODO: Allow passing path to (or reference of) extensions and
+ filters directly. This would make it easier to use this function
+ with 3rd party Jinja extensions that do not know about Coffin and
+ thus will not provide a Library object.
+
+ XXX/TODO: Why do we need our own custom list of builtins? Our
+ Library object is compatible, remember!? We can just add them
+ directly to Django's own list of builtins.
+ """
+ builtins.append(get_library(module_name))
+ django_add_to_builtins(module_name)
+
+
+add_to_builtins('coffin.template.defaulttags')
+add_to_builtins('coffin.template.defaultfilters') \ No newline at end of file
diff --git a/coffin/template/defaultfilters.py b/coffin/template/defaultfilters.py
new file mode 100644
index 00000000..c566a7d2
--- /dev/null
+++ b/coffin/template/defaultfilters.py
@@ -0,0 +1,99 @@
+"""Jinja2-ports of many of Django's default filters.
+
+TODO: Most of the filters in here need to be updated for autoescaping.
+"""
+
+from coffin.template import Library
+from jinja2.runtime import Undefined
+# from jinja2 import Markup
+
+register = Library()
+
+@register.filter(jinja2_only=True)
+def url(view_name, *args, **kwargs):
+ from coffin.template.defaulttags import url
+ return url._reverse(view_name, args, kwargs)
+
+@register.filter(jinja2_only=True)
+def timesince(value, arg=None):
+ if value is None or isinstance(value, Undefined):
+ return u''
+ from django.utils.timesince import timesince
+ if arg:
+ return timesince(value, arg)
+ return timesince(value)
+
+@register.filter(jinja2_only=True)
+def timeuntil(value, arg=None):
+ if value is None or isinstance(value, Undefined):
+ return u''
+ from django.utils.timesince import timeuntil
+ return timeuntil(date, arg)
+
+@register.filter(jinja2_only=True)
+def date(value, arg=None):
+ if value is None or isinstance(value, Undefined):
+ return u''
+ from django.conf import settings
+ from django.utils.dateformat import format
+ if arg is None:
+ arg = settings.DATE_FORMAT
+ return format(value, arg)
+
+@register.filter(jinja2_only=True)
+def time(value, arg=None):
+ if value is None or isinstance(value, Undefined):
+ return u''
+ from django.conf import settings
+ from django.utils.dateformat import time_format
+ if arg is None:
+ arg = settings.TIME_FORMAT
+ return time_format(value, arg)
+
+@register.filter(jinja2_only=True)
+def truncatewords(value, length):
+ # Jinja2 has it's own ``truncate`` filter that supports word
+ # boundaries and more stuff, but cannot deal with HTML.
+ from django.utils.text import truncate_words
+ return truncate_words(value, int(length))
+
+@register.filter(jinja2_only=True)
+def truncatewords_html(value, length):
+ from django.utils.text import truncate_html_words
+ return truncate_html_words(value, int(length))
+
+@register.filter(jinja2_only=True)
+def pluralize(value, s1='s', s2=None):
+ """Like Django's pluralize-filter, but instead of using an optional
+ comma to separate singular and plural suffixes, it uses two distinct
+ parameters.
+
+ It also is less forgiving if applied to values that do not allow
+ making a decision between singular and plural.
+ """
+ if s2 is not None:
+ singular_suffix, plural_suffix = s1, s2
+ else:
+ plural_suffix = s1
+ singular_suffix = ''
+
+ try:
+ if int(value) != 1:
+ return plural_suffix
+ except TypeError: # not a string or a number; maybe it's a list?
+ if len(value) != 1:
+ return plural_suffix
+ return singular_suffix
+
+@register.filter(jinja2_only=True)
+def floatformat(value, arg=-1):
+ """Builds on top of Django's own version, but adds strict error
+ checking, staying with the philosophy.
+ """
+ from django.template.defaultfilters import floatformat
+ from coffin.interop import django_filter_to_jinja2
+ arg = int(arg) # raise exception
+ result = django_filter_to_jinja2(floatformat)(value, arg)
+ if result == '': # django couldn't handle the value
+ raise ValueError(value)
+ return result \ No newline at end of file
diff --git a/coffin/template/defaulttags.py b/coffin/template/defaulttags.py
new file mode 100644
index 00000000..b9994257
--- /dev/null
+++ b/coffin/template/defaulttags.py
@@ -0,0 +1,364 @@
+from jinja2 import nodes
+from jinja2.ext import Extension
+from jinja2.exceptions import TemplateSyntaxError
+from django.conf import settings
+from coffin.template import Library
+
+
+class LoadExtension(Extension):
+ """The load-tag is a no-op in Coffin. Instead, all template libraries
+ are always loaded.
+
+ Note: Supporting a functioning load-tag in Jinja is tough, though
+ theoretically possible. The trouble is activating new extensions while
+ parsing is ongoing. The ``Parser.extensions`` dict of the current
+ parser instance needs to be modified, but apparently the only way to
+ get access would be by hacking the stack.
+ """
+
+ tags = set(['load'])
+
+ def parse(self, parser):
+ while not parser.stream.current.type == 'block_end':
+ parser.stream.next()
+ return []
+
+
+"""class AutoescapeExtension(Extension):
+ ""#"
+ Template to output works in three phases in Jinja2: parsing,
+ generation (compilation, AST-traversal), and rendering (execution).
+
+ Unfortunatly, the environment ``autoescape`` option comes into effect
+ during traversal, the part where we happen to have basically no control
+ over as an extension. It determines whether output is wrapped in
+ ``escape()`` calls.
+
+ Solutions that could possibly work:
+
+ * This extension could preprocess it's childnodes and wrap
+ everything output related inside the appropriate
+ ``Markup()`` or escape() call.
+
+ * We could use the ``preprocess`` hook to insert the
+ appropriate ``|safe`` and ``|escape`` filters on a
+ string-basis. This is very unlikely to work well.
+
+ There's also the issue of inheritance and just generally the nesting
+ of autoescape-tags to consider.
+
+ Other things of note:
+
+ * We can access ``parser.environment``, but that would only
+ affect the **parsing** of our child nodes.
+
+ * In the commented-out code below we are trying to affect the
+ autoescape setting during rendering. As noted, this could be
+ necessary for rare border cases where custom extension use
+ the autoescape attribute.
+
+ Both the above things would break Environment thread-safety though!
+
+ Overall, it's not looking to good for this extension.
+ ""#"
+
+ tags = ['autoescape']
+
+ def parse(self, parser):
+ lineno = parser.stream.next().lineno
+
+ old_autoescape = parser.environment.autoescape
+ parser.environment.autoescape = True
+ try:
+ body = parser.parse_statements(
+ ['name:endautoescape'], drop_needle=True)
+ finally:
+ parser.environment.autoescape = old_autoescape
+
+ # Not sure yet if the code below is necessary - it changes
+ # environment.autoescape during template rendering. If for example
+ # a CallBlock function accesses ``environment.autoescape``, it
+ # presumably is.
+ # This also should use try-finally though, which Jinja's API
+ # doesn't support either. We could fake that as well by using
+ # InternalNames that output the necessary indentation and keywords,
+ # but at this point it starts to get really messy.
+ #
+ # TODO: Actually, there's ``nodes.EnvironmentAttribute``.
+ #ae_setting = object.__new__(nodes.InternalName)
+ #nodes.Node.__init__(ae_setting, 'environment.autoescape', lineno=lineno)
+ #temp = parser.free_identifier()
+ #body.insert(0, nodes.Assign(temp, ae_setting))
+ #body.insert(1, nodes.Assign(ae_setting, nodes.Const(True)))
+ #body.insert(len(body), nodes.Assign(ae_setting, temp))
+ return body
+"""
+
+
+class URLExtension(Extension):
+ """Returns an absolute URL matching given view with its parameters.
+
+ This is a way to define links that aren't tied to a particular URL
+ configuration::
+
+ {% url path.to.some_view arg1,arg2,name1=value1 %}
+
+ Known differences to Django's url-Tag:
+
+ - In Django, the view name may contain any non-space character.
+ Since Jinja's lexer does not identify whitespace to us, only
+ characters that make up valid identifers, plus dots and hyphens
+ are allowed. Note that identifers in Jinja 2 may not contain
+ non-ascii characters.
+
+ As an alternative, you may specifify the view as a string,
+ which bypasses all these restrictions. It further allows you
+ to apply filters:
+
+ {% url "меткаda.some-view"|afilter %}
+ """
+
+ tags = set(['url'])
+
+ def parse(self, parser):
+ stream = parser.stream
+
+ tag = stream.next()
+
+ # get view name
+ if stream.current.test('string'):
+ viewname = parser.parse_primary()
+ else:
+ # parse valid tokens and manually build a string from them
+ bits = []
+ name_allowed = True
+ while True:
+ if stream.current.test_any('dot', 'sub'):
+ bits.append(stream.next())
+ name_allowed = True
+ elif stream.current.test('name') and name_allowed:
+ bits.append(stream.next())
+ name_allowed = False
+ else:
+ break
+ viewname = nodes.Const("".join([b.value for b in bits]))
+ if not bits:
+ raise TemplateSyntaxError("'%s' requires path to view" %
+ tag.value, tag.lineno)
+
+ # get arguments
+ args = []
+ kwargs = []
+ while not stream.current.test_any('block_end', 'name:as'):
+ if args or kwargs:
+ stream.expect('comma')
+ if stream.current.test('name') and stream.look().test('assign'):
+ key = nodes.Const(stream.next().value)
+ stream.skip()
+ value = parser.parse_expression()
+ kwargs.append(nodes.Pair(key, value, lineno=key.lineno))
+ else:
+ args.append(parser.parse_expression())
+
+ make_call_node = lambda *kw: \
+ self.call_method('_reverse',
+ args=[viewname, nodes.List(args), nodes.Dict(kwargs)],
+ kwargs=kw)
+
+ # if an as-clause is specified, write the result to context...
+ if stream.next_if('name:as'):
+ var = nodes.Name(stream.expect('name').value, 'store')
+ call_node = make_call_node(nodes.Keyword('fail', nodes.Const(False)))
+ return nodes.Assign(var, call_node)
+ # ...otherwise print it out.
+ else:
+ return nodes.Output([make_call_node()]).set_lineno(tag.lineno)
+
+ @classmethod
+ def _reverse(self, viewname, args, kwargs, fail=True):
+ from django.core.urlresolvers import reverse, NoReverseMatch
+
+ # Try to look up the URL twice: once given the view name,
+ # and again relative to what we guess is the "main" app.
+ url = ''
+ try:
+ url = reverse(viewname, args=args, kwargs=kwargs)
+ except NoReverseMatch:
+ projectname = settings.SETTINGS_MODULE.split('.')[0]
+ try:
+ url = reverse(projectname + '.' + viewname,
+ args=args, kwargs=kwargs)
+ except NoReverseMatch:
+ if fail:
+ raise
+ else:
+ return ''
+
+ return url
+
+
+class WithExtension(Extension):
+ """Adds a value to the context (inside this block) for caching and
+ easy access, just like the Django-version does.
+
+ For example::
+
+ {% with person.some_sql_method as total %}
+ {{ total }} object{{ total|pluralize }}
+ {% endwith %}
+
+ TODO: The new Scope node introduced in Jinja2 6334c1eade73 (the 2.2
+ dev version) would help here, but we don't want to rely on that yet.
+ See also:
+ http://dev.pocoo.org/projects/jinja/browser/tests/test_ext.py
+ http://dev.pocoo.org/projects/jinja/ticket/331
+ http://dev.pocoo.org/projects/jinja/ticket/329
+ """
+
+ tags = set(['with'])
+
+ def parse(self, parser):
+ lineno = parser.stream.next().lineno
+
+ value = parser.parse_expression()
+ parser.stream.expect('name:as')
+ name = parser.stream.expect('name')
+
+ body = parser.parse_statements(['name:endwith'], drop_needle=True)
+ return nodes.CallBlock(
+ self.call_method('_render_block', args=[value]),
+ [nodes.Name(name.value, 'store')], [], body).\
+ set_lineno(lineno)
+
+ def _render_block(self, value, caller=None):
+ return caller(value)
+
+
+class CacheExtension(Extension):
+ """Exactly like Django's own tag, but supports full Jinja2
+ expressiveness for all arguments.
+
+ {% cache gettimeout()*2 "foo"+options.cachename %}
+ ...
+ {% endcache %}
+
+ This actually means that there is a considerable incompatibility
+ to Django: In Django, the second argument is simply a name, but
+ interpreted as a literal string. This tag, with Jinja2 stronger
+ emphasis on consistent syntax, requires you to actually specify the
+ quotes around the name to make it a string. Otherwise, allowing
+ Jinja2 expressions would be very hard to impossible (one could use
+ a lookahead to see if the name is followed by an operator, and
+ evaluate it as an expression if so, or read it as a string if not.
+ TODO: This may not be the right choice. Supporting expressions
+ here is probably not very important, so compatibility should maybe
+ prevail. Unfortunately, it is actually pretty hard to be compatibly
+ in all cases, simply because Django's per-character parser will
+ just eat everything until the next whitespace and consider it part
+ of the fragment name, while we have to work token-based: ``x*2``
+ would actually be considered ``"x*2"`` in Django, while Jinja2
+ would give us three tokens: ``x``, ``*``, ``2``.
+
+ General Syntax:
+
+ {% cache [expire_time] [fragment_name] [var1] [var2] .. %}
+ .. some expensive processing ..
+ {% endcache %}
+
+ Available by default (does not need to be loaded).
+
+ Partly based on the ``FragmentCacheExtension`` from the Jinja2 docs.
+
+ TODO: Should there be scoping issues with the internal dummy macro
+ limited access to certain outer variables in some cases, there is a
+ different way to write this. Generated code would look like this:
+
+ internal_name = environment.extensions['..']._get_cache_value():
+ if internal_name is not None:
+ yield internal_name
+ else:
+ internal_name = "" # or maybe use [] and append() for performance
+ internalname += "..."
+ internalname += "..."
+ internalname += "..."
+ environment.extensions['..']._set_cache_value(internalname):
+ yield internalname
+
+ In other words, instead of using a CallBlock which uses a local
+ function and calls into python, we have to separate calls into
+ python, but put the if-else logic itself into the compiled template.
+ """
+
+ tags = set(['cache'])
+
+ def parse(self, parser):
+ lineno = parser.stream.next().lineno
+
+ expire_time = parser.parse_expression()
+ fragment_name = parser.parse_expression()
+ vary_on = []
+ while not parser.stream.current.test('block_end'):
+ vary_on.append(parser.parse_expression())
+
+ body = parser.parse_statements(['name:endcache'], drop_needle=True)
+
+ return nodes.CallBlock(
+ self.call_method('_cache_support',
+ [expire_time, fragment_name,
+ nodes.List(vary_on), nodes.Const(lineno)]),
+ [], [], body).set_lineno(lineno)
+
+ def _cache_support(self, expire_time, fragm_name, vary_on, lineno, caller):
+ from django.core.cache import cache # delay depending in settings
+ from django.utils.http import urlquote
+
+ try:
+ expire_time = int(expire_time)
+ except (ValueError, TypeError):
+ raise TemplateSyntaxError('"%s" tag got a non-integer '
+ 'timeout value: %r' % (list(self.tags)[0], expire_time), lineno)
+
+ cache_key = u':'.join([fragm_name] + [urlquote(v) for v in vary_on])
+ value = cache.get(cache_key)
+ if value is None:
+ value = caller()
+ cache.set(cache_key, value, expire_time)
+ return value
+
+
+class SpacelessExtension(Extension):
+ """Removes whitespace between HTML tags, including tab and
+ newline characters.
+
+ Works exactly like Django's own tag.
+ """
+
+ tags = ['spaceless']
+
+ def parse(self, parser):
+ lineno = parser.stream.next().lineno
+ body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
+ return nodes.CallBlock(
+ self.call_method('_strip_spaces', [], [], None, None),
+ [], [], body
+ ).set_lineno(lineno)
+
+ def _strip_spaces(self, caller=None):
+ from django.utils.html import strip_spaces_between_tags
+ return strip_spaces_between_tags(caller().strip())
+
+
+# nicer import names
+load = LoadExtension
+url = URLExtension
+with_ = WithExtension
+cache = CacheExtension
+spaceless = SpacelessExtension
+
+
+register = Library()
+register.tag(load)
+register.tag(url)
+register.tag(with_)
+register.tag(cache)
+register.tag(spaceless) \ No newline at end of file
diff --git a/coffin/template/library.py b/coffin/template/library.py
new file mode 100644
index 00000000..8e80edc5
--- /dev/null
+++ b/coffin/template/library.py
@@ -0,0 +1,215 @@
+from django.template import Library as DjangoLibrary, InvalidTemplateLibrary
+from jinja2.ext import Extension as Jinja2Extension
+from coffin.interop import (
+ DJANGO, JINJA2,
+ guess_filter_type, jinja2_filter_to_django, django_filter_to_jinja2)
+
+
+__all__ = ['Library']
+
+
+class Library(DjangoLibrary):
+ """Version of the Django ``Library`` class that can handle both
+ Django template engine tags and filters, as well as Jinja2
+ extensions and filters.
+
+ Tries to present a common registration interface to the extension
+ author, but provides both template engines with only those
+ components they can support.
+
+ Since custom Django tags and Jinja2 extensions are two completely
+ different beasts, they are handled completely separately. You can
+ register custom Django tags as usual, for example:
+
+ register.tag('current_time', do_current_time)
+
+ Or register a Jinja2 extension like this:
+
+ register.tag(CurrentTimeNode)
+
+ Filters, on the other hand, work similarily in both engines, and
+ for the most one can't tell whether a filter function was written
+ for Django or Jinja2. A compatibility layer is used to make to
+ make the filters you register usuable with both engines:
+
+ register.filter('cut', cut)
+
+ However, some of the more powerful filters just won't work in
+ Django, for example if more than one argument is required, or if
+ context- or environmentfilters are used. If ``cut`` in the above
+ example where such an extended filter, it would only be registered
+ with Jinja.
+
+ See also the module documentation for ``coffin.interop`` for
+ information on some of the limitations of this conversion.
+
+ TODO: Jinja versions of the ``simple_tag`` and ``inclusion_tag``
+ helpers would be nice, though since custom tags are not needed as
+ often in Jinja, this is not urgent.
+ """
+
+ def __init__(self):
+ super(Library, self).__init__()
+ self.jinja2_filters = {}
+ self.jinja2_extensions = []
+ self.jinja2_globals = {}
+ self.jinja2_tests = {}
+
+ @classmethod
+ def from_django(cls, django_library):
+ """Create a Coffin library object from a Django library.
+
+ Specifically, this ensures that filters already registered
+ with the Django library are also made available to Jinja,
+ where applicable.
+ """
+ from copy import copy
+ result = cls()
+ result.filters = copy(django_library.filters)
+ result.tags = copy(django_library.tags)
+ for name, func in result.filters.iteritems():
+ result._register_filter(name, func, jinja2_only=True)
+ return result
+
+ def test(self, name=None, func=None):
+ def inner(f):
+ name = getattr(f, "_decorated_function", f).__name__
+ self.jinja2_tests[name] = f
+ return f
+ if name == None and func == None:
+ # @register.test()
+ return inner
+ elif func == None:
+ if (callable(name)):
+ # register.test()
+ return inner(name)
+ else:
+ # @register.test('somename') or @register.test(name='somename')
+ def dec(func):
+ return self.test(name, func)
+ return dec
+ elif name != None and func != None:
+ # register.filter('somename', somefunc)
+ self.jinja2_tests[name] = func
+ return func
+ else:
+ raise InvalidTemplateLibrary("Unsupported arguments to "
+ "Library.test: (%r, %r)", (name, func))
+
+ def object(self, name=None, func=None):
+ def inner(f):
+ name = getattr(f, "_decorated_function", f).__name__
+ self.jinja2_globals[name] = f
+ return f
+ if name == None and func == None:
+ # @register.object()
+ return inner
+ elif func == None:
+ if (callable(name)):
+ # register.object()
+ return inner(name)
+ else:
+ # @register.object('somename') or @register.object(name='somename')
+ def dec(func):
+ return self.object(name, func)
+ return dec
+ elif name != None and func != None:
+ # register.object('somename', somefunc)
+ self.jinja2_globals[name] = func
+ return func
+ else:
+ raise InvalidTemplateLibrary("Unsupported arguments to "
+ "Library.object: (%r, %r)", (name, func))
+
+ def tag(self, name_or_node=None, compile_function=None):
+ """Register a Django template tag (1) or Jinja 2 extension (2).
+
+ For (1), supports the same invocation syntax as the original
+ Django version, including use as a decorator.
+
+ For (2), since Jinja 2 extensions are classes (which can't be
+ decorated), and have the tag name effectively built in, only the
+ following syntax is supported:
+
+ register.tag(MyJinjaExtensionNode)
+ """
+ if isinstance(name_or_node, Jinja2Extension):
+ if compile_function:
+ raise InvalidTemplateLibrary('"compile_function" argument not supported for Jinja2 extensions')
+ self.jinja2_extensions.append(name_or_node)
+ return name_or_node
+ else:
+ return super(Library, self).tag(name_or_node, compile_function)
+
+ def tag_function(self, func_or_node):
+ if issubclass(func_or_node, Jinja2Extension):
+ self.jinja2_extensions.append(func_or_node)
+ return func_or_node
+ else:
+ return super(Library, self).tag_function(func_or_node)
+
+ def filter(self, name=None, filter_func=None, jinja2_only=False):
+ """Register a filter with both the Django and Jinja2 template
+ engines, if possible - or only Jinja2, if ``jinja2_only`` is
+ specified. ``jinja2_only`` does not affect conversion of the
+ filter if neccessary.
+
+ Implements a compatibility layer to handle the different
+ auto-escaping approaches transparently. Extended Jinja2 filter
+ features like environment- and contextfilters are however not
+ supported in Django. Such filters will only be registered with
+ Jinja.
+
+ Supports the same invocation syntax as the original Django
+ version, including use as a decorator.
+
+ If the function is supposed to return the registered filter
+ (by example of the superclass implementation), but has
+ registered multiple filters, a tuple of all filters is
+ returned.
+ """
+ def filter_function(f):
+ return self._register_filter(
+ getattr(f, "_decorated_function", f).__name__,
+ f, jinja2_only=jinja2_only)
+ if name == None and filter_func == None:
+ # @register.filter()
+ return filter_function
+ elif filter_func == None:
+ if (callable(name)):
+ # @register.filter
+ return filter_function(name)
+ else:
+ # @register.filter('somename') or @register.filter(name='somename')
+ def dec(func):
+ return self.filter(name, func, jinja2_only=jinja2_only)
+ return dec
+ elif name != None and filter_func != None:
+ # register.filter('somename', somefunc)
+ return self._register_filter(name, filter_func,
+ jinja2_only=jinja2_only)
+ else:
+ raise InvalidTemplateLibrary("Unsupported arguments to "
+ "Library.filter: (%r, %r)", (name, filter_func))
+
+ def _register_filter(self, name, func, jinja2_only=None):
+ filter_type, can_be_ported = guess_filter_type(func)
+ if filter_type == JINJA2 and not can_be_ported:
+ self.jinja2_filters[name] = func
+ return func
+ elif filter_type == DJANGO and not can_be_ported:
+ if jinja2_only:
+ raise ValueError('This filter cannot be ported to Jinja2.')
+ self.filters[name] = func
+ return func
+ elif jinja2_only:
+ func = django_filter_to_jinja2(func)
+ self.jinja2_filters[name] = func
+ return func
+ else:
+ # register the filter with both engines
+ django_func = jinja2_filter_to_django(func)
+ jinja2_func = django_filter_to_jinja2(func)
+ self.filters[name] = django_func
+ self.jinja2_filters[name] = jinja2_func
+ return (django_func, jinja2_func)
diff --git a/coffin/template/loader.py b/coffin/template/loader.py
new file mode 100644
index 00000000..1f2bbb1f
--- /dev/null
+++ b/coffin/template/loader.py
@@ -0,0 +1,66 @@
+"""Replacement for ``django.template.loader`` that uses Jinja 2.
+
+The module provides a generic way to load templates from an arbitrary
+backend storage (e.g. filesystem, database).
+"""
+
+from coffin.template import Template as CoffinTemplate
+from jinja2 import TemplateNotFound
+
+
+def find_template_source(name, dirs=None):
+ # This is Django's most basic loading function through which
+ # all template retrievals go. Not sure if Jinja 2 publishes
+ # an equivalent, but no matter, it mostly for internal use
+ # anyway - developers will want to start with
+ # ``get_template()`` or ``get_template_from_string`` anyway.
+ raise NotImplementedError()
+
+
+def get_template(template_name):
+ # Jinja will handle this for us, and env also initializes
+ # the loader backends the first time it is called.
+ from coffin.common import env
+ return env.get_template(template_name)
+
+
+def get_template_from_string(source):
+ """
+ Does not support then ``name`` and ``origin`` parameters from
+ the Django version.
+ """
+ from coffin.common import env
+ return env.from_string(source)
+
+
+def render_to_string(template_name, dictionary=None, context_instance=None):
+ """Loads the given ``template_name`` and renders it with the given
+ dictionary as context. The ``template_name`` may be a string to load
+ a single template using ``get_template``, or it may be a tuple to use
+ ``select_template`` to find one of the templates in the list.
+
+ ``dictionary`` may also be Django ``Context`` object.
+
+ Returns a string.
+ """
+ dictionary = dictionary or {}
+ if isinstance(template_name, (list, tuple)):
+ template = select_template(template_name)
+ else:
+ template = get_template(template_name)
+ if context_instance:
+ context_instance.update(dictionary)
+ else:
+ context_instance = dictionary
+ return template.render(context_instance)
+
+
+def select_template(template_name_list):
+ "Given a list of template names, returns the first that can be loaded."
+ for template_name in template_name_list:
+ try:
+ return get_template(template_name)
+ except TemplateNotFound:
+ continue
+ # If we get here, none of the templates could be loaded
+ raise TemplateNotFound(', '.join(template_name_list))
diff --git a/coffin/template/loaders.py b/coffin/template/loaders.py
new file mode 100644
index 00000000..cb42fd5d
--- /dev/null
+++ b/coffin/template/loaders.py
@@ -0,0 +1,38 @@
+from jinja2 import loaders
+
+
+def jinja_loader_from_django_loader(django_loader):
+ """Attempts to make a conversion from the given Django loader to an
+ similarly-behaving Jinja loader.
+
+ :param django_loader: Django loader module string.
+ :return: The similarly-behaving Jinja loader, or None if a similar loader
+ could not be found.
+ """
+ for substr, func in _JINJA_LOADER_BY_DJANGO_SUBSTR.iteritems():
+ if substr in django_loader:
+ return func()
+ return None
+
+
+def _make_jinja_app_loader():
+ """Makes an 'app loader' for Jinja which acts like
+ :mod:`django.template.loaders.app_directories`.
+ """
+ from django.template.loaders.app_directories import app_template_dirs
+ return loaders.FileSystemLoader(app_template_dirs)
+
+
+def _make_jinja_filesystem_loader():
+ """Makes a 'filesystem loader' for Jinja which acts like
+ :mod:`django.template.loaders.filesystem`.
+ """
+ from django.conf import settings
+ return loaders.FileSystemLoader(settings.TEMPLATE_DIRS)
+
+
+# Determine loaders from Django's conf.
+_JINJA_LOADER_BY_DJANGO_SUBSTR = { # {substr: callable, ...}
+ 'app_directories': _make_jinja_app_loader,
+ 'filesystem': _make_jinja_filesystem_loader,
+}
diff --git a/coffin/views/__init__.py b/coffin/views/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/coffin/views/__init__.py
diff --git a/coffin/views/defaults.py b/coffin/views/defaults.py
new file mode 100644
index 00000000..4386a49b
--- /dev/null
+++ b/coffin/views/defaults.py
@@ -0,0 +1,35 @@
+from django import http
+from django.template import Context, RequestContext
+from coffin.template.loader import render_to_string
+
+
+__all__ = ('page_not_found', 'server_error', 'shortcut')
+
+
+# no Jinja version for this needed
+from django.views.defaults import shortcut
+
+
+def page_not_found(request, template_name='404.html'):
+ """
+ Default 404 handler.
+
+ Templates: `404.html`
+ Context:
+ request_path
+ The path of the requested URL (e.g., '/app/pages/bad_page/')
+ """
+ content = render_to_string(template_name,
+ RequestContext(request, {'request_path': request.path}))
+ return http.HttpResponseNotFound(content)
+
+
+def server_error(request, template_name='500.html'):
+ """
+ 500 error handler.
+
+ Templates: `500.html`
+ Context: None
+ """
+ content = render_to_string(template_name, Context({}))
+ return http.HttpResponseServerError(content)
diff --git a/coffin/views/generic/._simple.py b/coffin/views/generic/._simple.py
new file mode 100644
index 00000000..a1bb18de
--- /dev/null
+++ b/coffin/views/generic/._simple.py
Binary files differ
diff --git a/coffin/views/generic/__init__.py b/coffin/views/generic/__init__.py
new file mode 100644
index 00000000..7ea80e4c
--- /dev/null
+++ b/coffin/views/generic/__init__.py
@@ -0,0 +1 @@
+from django.views.generic import *
diff --git a/coffin/views/generic/simple.py b/coffin/views/generic/simple.py
new file mode 100644
index 00000000..ff7678b2
--- /dev/null
+++ b/coffin/views/generic/simple.py
@@ -0,0 +1,6 @@
+import inspect
+
+from django.views.generic.simple import *
+from coffin.template import loader, RequestContext
+
+exec inspect.getsource(direct_to_template)