parent
d89a5313e5
commit
67e3b2d7cf
218
README.rst
218
README.rst
|
@ -1,208 +1,20 @@
|
||||||
Coffin: Jinja2 adapter for Django
|
Coffin: Jinja2 extensions for Django
|
||||||
---------------------------------
|
------------------------------------
|
||||||
|
|
||||||
|
This used to be a full-featured standalone adapter. With Django adding
|
||||||
|
support for other template backends, it's approach didn't make sense
|
||||||
|
anymore. Please use ``django_jinja`` instead - you won't regret it:
|
||||||
|
|
||||||
|
http://niwibe.github.io/django-jinja/
|
||||||
|
|
||||||
|
|
||||||
Supported Django template functionality
|
This module now is a lean collection of some Django tags that are
|
||||||
=======================================
|
not included in django-jinja, namely:
|
||||||
|
|
||||||
Coffin currently makes the following Django tags available in Jinja:
|
|
||||||
|
|
||||||
- {% cache %} - has currently an incompatibility: The second argument
|
|
||||||
(the fragment name) needs to be specified with surrounding quotes
|
|
||||||
if it is supposed to be a literal string, according to Jinja2 syntax.
|
|
||||||
It will otherwise be considered an identifer and resolved as a
|
|
||||||
variable.
|
|
||||||
|
|
||||||
- {% load %} - is actually a no-op in Coffin, since templatetag
|
|
||||||
libraries are always loaded. See also "Custom Filters and extensions".
|
|
||||||
|
|
||||||
|
- {% url %}
|
||||||
- {% spaceless %}
|
- {% spaceless %}
|
||||||
|
|
||||||
- {% url %} - additionally, a ``"view"|url()`` filter is also
|
|
||||||
available. Or use ``view|url()`` for Django 1.5 style lookups by the value of ``view``.
|
|
||||||
|
|
||||||
- {% with %}
|
- {% with %}
|
||||||
|
- {% load %} (as a noop)
|
||||||
- {% csrf_token %}
|
- {% get_media_prefix %}
|
||||||
|
- {% get_static_prefix %}
|
||||||
Django filters that are ported in Coffin:
|
- {% static %} (in a base variant, and with django.contrib.staticfiles support)
|
||||||
|
|
||||||
- date
|
|
||||||
- floatformat
|
|
||||||
- pluralize (expects an optional second parameter rather than the
|
|
||||||
comma syntax)
|
|
||||||
- time
|
|
||||||
- timesince
|
|
||||||
- timeuntil
|
|
||||||
- truncatewords
|
|
||||||
- truncatewords_html
|
|
||||||
|
|
||||||
Note that for the most part, you can simply use filters written for Django
|
|
||||||
directly in Coffin. For example, ``django.contrib.markup`` "just works" (tm).
|
|
||||||
|
|
||||||
The template-related functionality of the following contrib modules has
|
|
||||||
been ported in Coffin:
|
|
||||||
|
|
||||||
- ``coffin.contrib.syndication``.
|
|
||||||
|
|
||||||
Jinja 2's ``i18n`` extension is hooked up with Django, and a custom version
|
|
||||||
of makemessages supports string extraction from both Jinja2 and Django
|
|
||||||
templates. Just add 'coffin' to your INSTALLED_APPS and run makemessages as
|
|
||||||
usual, specifying additional Jinja extensions if necessary via the -e option.
|
|
||||||
|
|
||||||
Autoescape
|
|
||||||
==========
|
|
||||||
|
|
||||||
When using Auto Escape you will notice that marking something as a
|
|
||||||
Safestrings with Django will not affect the rendering in Jinja 2. To fix this
|
|
||||||
you can monkeypatch Django to produce Jinja 2 compatible Safestrings::
|
|
||||||
|
|
||||||
'''Monkeypatch Django to mimic Jinja2 behaviour'''
|
|
||||||
from django.utils import safestring
|
|
||||||
if not hasattr(safestring, '__html__'):
|
|
||||||
safestring.SafeString.__html__ = lambda self: str(self)
|
|
||||||
safestring.SafeUnicode.__html__ = lambda self: unicode(self)
|
|
||||||
|
|
||||||
Rendering
|
|
||||||
=========
|
|
||||||
|
|
||||||
Simply use the ``render_to_response`` replacement provided by coffin::
|
|
||||||
|
|
||||||
from coffin.shortcuts import render_to_response
|
|
||||||
render_to_response('template.html', {'var': 1})
|
|
||||||
|
|
||||||
This will render ``template.html`` using Jinja2, and returns a
|
|
||||||
``HttpResponse``.
|
|
||||||
|
|
||||||
|
|
||||||
404 and 500 handlers
|
|
||||||
====================
|
|
||||||
|
|
||||||
To have your HTTP 404 and 500 template rendered using Jinja, replace the
|
|
||||||
line::
|
|
||||||
|
|
||||||
from django.conf.urls.defaults import *
|
|
||||||
|
|
||||||
in your ``urls.py`` (it should be there by default), with::
|
|
||||||
|
|
||||||
from coffin.conf.urls import *
|
|
||||||
|
|
||||||
|
|
||||||
Custom filters and extensions
|
|
||||||
=============================
|
|
||||||
|
|
||||||
Coffin uses the same templatetag library approach as Django, meaning
|
|
||||||
your app has a ``templatetags`` directory, and each of it's modules
|
|
||||||
represents a "template library", providing new filters and tags.
|
|
||||||
|
|
||||||
A custom ``Library`` class in ``coffin.template.Library`` can be used
|
|
||||||
to register Jinja-specific components.
|
|
||||||
|
|
||||||
Coffin can automatically make your existing Django filters usable in
|
|
||||||
Jinja, but not your custom tags - you need to rewrite those as Jinja
|
|
||||||
extensions manually.
|
|
||||||
|
|
||||||
Example for a Jinja-enabled template library::
|
|
||||||
|
|
||||||
from coffin import template
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
register.filter('plenk', plenk) # Filter for both Django and Jinja
|
|
||||||
register.tag('foo', do_foo) # Django version of the tag
|
|
||||||
register.tag(FooExtension) # Jinja version of the tag
|
|
||||||
register.object(my_function_name) # A global function/object
|
|
||||||
register.test(my_test_name) # A test function
|
|
||||||
|
|
||||||
You may also define additional extensions, filters, tests and globals via your ``settings.py``::
|
|
||||||
|
|
||||||
JINJA2_FILTERS = (
|
|
||||||
'path.to.myfilter',
|
|
||||||
)
|
|
||||||
JINJA2_TESTS = {
|
|
||||||
'test_name': 'path.to.mytest',
|
|
||||||
}
|
|
||||||
JINJA2_EXTENSIONS = (
|
|
||||||
'jinja2.ext.do',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Other things of note
|
|
||||||
====================
|
|
||||||
|
|
||||||
When porting Django functionality, Coffin currently tries to avoid
|
|
||||||
Django's silent-errors approach, instead opting to be explicit. Django was
|
|
||||||
discussing the same thing before it's 1.0 release (*), but was constrained
|
|
||||||
by backwards-compatibility concerns. However, if you are converting your
|
|
||||||
templates anyway, it might be a good opportunity for this change.
|
|
||||||
|
|
||||||
(*) http://groups.google.com/group/django-developers/browse_thread/thread/f323338045ac2e5e
|
|
||||||
|
|
||||||
``coffin.template.loader`` is a port of ``django.template.loader`` and
|
|
||||||
comes with a Jinja2-enabled version of ``get_template()``.
|
|
||||||
|
|
||||||
``coffin.template.Template`` is a Jinja2-Template that supports the
|
|
||||||
Django render interface (being passed an instance of Context), and uses
|
|
||||||
Coffin's global Jinja2 environment.
|
|
||||||
|
|
||||||
``coffin.interop`` exposes functionality to manually convert Django
|
|
||||||
filters to Jinja2 and vice-versa. This is also what Coffin's ``Library``
|
|
||||||
object uses.
|
|
||||||
|
|
||||||
A Jinja2-enabled version of ``add_to_builtins`` can be found in the
|
|
||||||
``django.template`` namespace.
|
|
||||||
|
|
||||||
You may specify additional arguments to send to the ``Environment`` via ``JINJA2_ENVIRONMENT_OPTIONS``::
|
|
||||||
|
|
||||||
from jinja2 import StrictUndefined
|
|
||||||
JINJA2_ENVIRONMENT_OPTIONS = {
|
|
||||||
'autoescape': False,
|
|
||||||
'undefined': StrictUndefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
Things not supported by Coffin
|
|
||||||
==============================
|
|
||||||
|
|
||||||
These is an incomplete list things that Coffin does not yet and possibly
|
|
||||||
never will, requiring manual changes on your part:
|
|
||||||
|
|
||||||
- The ``slice`` filter works differently in Jinja2 and Django.
|
|
||||||
Replace it with Jinja's slice syntax: ``x[0:1]``.
|
|
||||||
|
|
||||||
- Jinja2's ``default`` filter by itself only tests the variable for
|
|
||||||
**existence**. To match Django's behaviour, you need to pass ``True``
|
|
||||||
as the second argument, so that it will also provide the default
|
|
||||||
value for things that are defined but evalute to ``False``
|
|
||||||
|
|
||||||
- Jinja2's loop variable is called ``loop``, but Django's ``forloop``.
|
|
||||||
|
|
||||||
- Implementing an equivalent to Django's cycle-tag might be difficult,
|
|
||||||
see also Django tickets #5908 and #7501. Jinja's own facilities
|
|
||||||
are the ``forloop.cycle()`` function and the global function
|
|
||||||
``cycler``.
|
|
||||||
|
|
||||||
- The ``add`` filter might not be worth being implemented. ``{{ x+y }}``
|
|
||||||
is a pretty basic feature of Jinja2, and could almost be lumped
|
|
||||||
together with the other Django->Jinja2 syntax changes.
|
|
||||||
|
|
||||||
- Django-type safe strings passed through the context are not converted
|
|
||||||
and therefore not recognized by Jinja2. For example, a notable place
|
|
||||||
were this would occur is the HTML generation of Django Forms.
|
|
||||||
|
|
||||||
- The {% autoescape %} tag is immensily difficult to port and currently
|
|
||||||
not supported.
|
|
||||||
|
|
||||||
- Literal strings from within a template are not automatically
|
|
||||||
considered "safe" by Jinja2, different from Django. According to
|
|
||||||
Armin Ronacher, this is a design limitation that will not be changed,
|
|
||||||
due to many Python builtin functions and methods, whichyou are free
|
|
||||||
to use in Jinja2, expecting raw, untainted strings and thus not being
|
|
||||||
able to work with Jinja2's ``Markup`` string.
|
|
||||||
|
|
||||||
|
|
||||||
Running the tests
|
|
||||||
====================
|
|
||||||
|
|
||||||
Use the nose framework:
|
|
||||||
|
|
||||||
http://somethingaboutorange.com/mrl/projects/nose/
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
clean:
|
|
||||||
@-rm -f *.pyc */*.pyc tags */tags
|
|
|
@ -1,44 +1,4 @@
|
||||||
"""
|
|
||||||
Coffin
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
`Coffin <http://www.github.com/coffin/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, 4, '0')
|
__version__ = (0, 4, '0')
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
import os
|
from common import *
|
||||||
|
from static import *
|
||||||
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()
|
|
||||||
|
|
464
coffin/common.py
464
coffin/common.py
|
@ -1,216 +1,292 @@
|
||||||
import os
|
from jinja2 import nodes
|
||||||
import warnings
|
from jinja2.ext import Extension
|
||||||
|
from jinja2.exceptions import TemplateSyntaxError
|
||||||
|
from jinja2 import Markup
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from django import dispatch
|
|
||||||
from jinja2 import Environment, loaders
|
|
||||||
from jinja2 import defaults as jinja2_defaults
|
|
||||||
from coffin.template import Library as CoffinLibrary
|
|
||||||
|
|
||||||
__all__ = ('env',)
|
class LoadExtension(Extension):
|
||||||
|
"""The load-tag is a no-op in Coffin. Instead, all template libraries
|
||||||
|
are always loaded.
|
||||||
|
|
||||||
env = None
|
Note: Supporting a functioning load-tag in Jinja is tough, though
|
||||||
|
theoretically possible. The trouble is activating new extensions while
|
||||||
_JINJA_I18N_EXTENSION_NAME = 'jinja2.ext.i18n'
|
parsing is ongoing. The ``Parser.extensions`` dict of the current
|
||||||
|
parser instance needs to be modified, but apparently the only way to
|
||||||
class CoffinEnvironment(Environment):
|
get access would be by hacking the stack.
|
||||||
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
|
|
||||||
)
|
|
||||||
# Note: all_ext already includes Jinja2's own builtins (with
|
|
||||||
# the proper priority), so we want to assign to these attributes.
|
|
||||||
self.filters = all_ext['filters'].copy()
|
|
||||||
self.filters.update(filters)
|
|
||||||
self.globals.update(all_ext['globals'])
|
|
||||||
self.globals.update(globals)
|
|
||||||
self.tests = all_ext['tests'].copy()
|
|
||||||
self.tests.update(tests)
|
|
||||||
for key, value in all_ext['attrs'].items():
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
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 = []
|
tags = set(['load'])
|
||||||
|
|
||||||
from coffin.template.loaders import jinja_loader_from_django_loader
|
def parse(self, parser):
|
||||||
from jinja2.loaders import BaseLoader as JinjaLoader
|
while not parser.stream.current.type == 'block_end':
|
||||||
|
parser.stream.next()
|
||||||
|
return []
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
_loaders = getattr(settings, 'JINJA2_TEMPLATE_LOADERS', settings.TEMPLATE_LOADERS)
|
"""class AutoescapeExtension(Extension):
|
||||||
for loader in _loaders:
|
""#"
|
||||||
if isinstance(loader, JinjaLoader):
|
Template to output works in three phases in Jinja2: parsing,
|
||||||
loaders.append(loader)
|
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'):
|
||||||
|
# Need to work around Jinja2 syntax here. Jinja by default acts
|
||||||
|
# like Python and concats subsequent strings. In this case
|
||||||
|
# though, we want {% url "app.views.post" "1" %} to be treated
|
||||||
|
# as view + argument, while still supporting
|
||||||
|
# {% url "app.views.post"|filter %}. Essentially, what we do is
|
||||||
|
# rather than let ``parser.parse_primary()`` deal with a "string"
|
||||||
|
# token, we do so ourselves, and let parse_expression() handle all
|
||||||
|
# other cases.
|
||||||
|
if stream.look().test('string'):
|
||||||
|
token = stream.next()
|
||||||
|
viewname = nodes.Const(token.value, lineno=token.lineno)
|
||||||
else:
|
else:
|
||||||
loader_name = args = None
|
viewname = parser.parse_expression()
|
||||||
if isinstance(loader, basestring):
|
else:
|
||||||
loader_name = loader
|
# parse valid tokens and manually build a string from them
|
||||||
|
bits = []
|
||||||
|
name_allowed = True
|
||||||
|
while True:
|
||||||
|
if stream.current.test_any('dot', 'sub', 'colon'):
|
||||||
|
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 = []
|
args = []
|
||||||
elif isinstance(loader, (tuple, list)):
|
kwargs = []
|
||||||
loader_name = loader[0]
|
while not stream.current.test_any('block_end', 'name:as'):
|
||||||
args = loader[1]
|
if args or kwargs:
|
||||||
|
stream.expect('comma')
|
||||||
if loader_name:
|
if stream.current.test('name') and stream.look().test('assign'):
|
||||||
loader_obj = jinja_loader_from_django_loader(loader_name, args)
|
key = nodes.Const(stream.next().value)
|
||||||
if loader_obj:
|
stream.skip()
|
||||||
loaders.append(loader_obj)
|
value = parser.parse_expression()
|
||||||
continue
|
kwargs.append(nodes.Pair(key, value, lineno=key.lineno))
|
||||||
|
|
||||||
warnings.warn('Cannot translate loader: %s' % 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, import_library, InvalidTemplateLibrary)
|
|
||||||
|
|
||||||
libs = []
|
|
||||||
for app in settings.INSTALLED_APPS:
|
|
||||||
ns = app + '.templatetags'
|
|
||||||
try:
|
|
||||||
path = __import__(ns, {}, {}, ['__file__']).__file__
|
|
||||||
path = os.path.dirname(path) # we now have the templatetags/ directory
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
for filename in os.listdir(path):
|
args.append(parser.parse_expression())
|
||||||
if filename == '__init__.py' or filename.startswith('.'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if filename.endswith('.py'):
|
def make_call_node(*kw):
|
||||||
try:
|
return self.call_method('_reverse', args=[
|
||||||
module = "%s.%s" % (ns, os.path.splitext(filename)[0])
|
viewname,
|
||||||
l = import_library(module)
|
nodes.List(args),
|
||||||
libs.append(l)
|
nodes.Dict(kwargs),
|
||||||
|
nodes.Name('_current_app', 'load'),
|
||||||
|
], kwargs=kw)
|
||||||
|
|
||||||
except InvalidTemplateLibrary:
|
# if an as-clause is specified, write the result to context...
|
||||||
pass
|
if stream.next_if('name:as'):
|
||||||
|
var = nodes.Name(stream.expect('name').value, 'store')
|
||||||
# In addition to loading application libraries, support a custom list
|
call_node = make_call_node(nodes.Keyword('fail',
|
||||||
for libname in getattr(settings, 'JINJA2_DJANGO_TEMPLATETAG_LIBRARIES', ()):
|
nodes.Const(False)))
|
||||||
libs.append(get_library(libname))
|
return nodes.Assign(var, call_node)
|
||||||
|
# ...otherwise print it out.
|
||||||
return libs
|
|
||||||
|
|
||||||
def _get_all_extensions(self):
|
|
||||||
from django.conf import settings
|
|
||||||
from django.template import builtins as django_builtins
|
|
||||||
from coffin.template import builtins as coffin_builtins
|
|
||||||
from django.core.urlresolvers import get_callable
|
|
||||||
|
|
||||||
# Note that for extensions, the order in which we load the libraries
|
|
||||||
# is not maintained (https://github.com/mitsuhiko/jinja2/issues#issue/3).
|
|
||||||
# Extensions support priorities, which should be used instead.
|
|
||||||
extensions, filters, globals, tests, attrs = [], {}, {}, {}, {}
|
|
||||||
def _load_lib(lib):
|
|
||||||
if not isinstance(lib, CoffinLibrary):
|
|
||||||
# If this is only a standard Django library,
|
|
||||||
# convert it. This will ensure that Django
|
|
||||||
# filters in that library are converted and
|
|
||||||
# made available in Jinja.
|
|
||||||
lib = CoffinLibrary.from_django(lib)
|
|
||||||
extensions.extend(getattr(lib, 'jinja2_extensions', []))
|
|
||||||
filters.update(getattr(lib, 'jinja2_filters', {}))
|
|
||||||
globals.update(getattr(lib, 'jinja2_globals', {}))
|
|
||||||
tests.update(getattr(lib, 'jinja2_tests', {}))
|
|
||||||
attrs.update(getattr(lib, 'jinja2_environment_attrs', {}))
|
|
||||||
|
|
||||||
# Start with Django's builtins; this give's us all of Django's
|
|
||||||
# filters courtasy of our interop layer.
|
|
||||||
for lib in django_builtins:
|
|
||||||
_load_lib(lib)
|
|
||||||
|
|
||||||
# The stuff Jinja2 comes with by default should override Django.
|
|
||||||
filters.update(jinja2_defaults.DEFAULT_FILTERS)
|
|
||||||
tests.update(jinja2_defaults.DEFAULT_TESTS)
|
|
||||||
globals.update(jinja2_defaults.DEFAULT_NAMESPACE)
|
|
||||||
|
|
||||||
# Our own set of builtins are next, overwriting Jinja2's.
|
|
||||||
for lib in coffin_builtins:
|
|
||||||
_load_lib(lib)
|
|
||||||
|
|
||||||
# Optionally, include the i18n extension.
|
|
||||||
if settings.USE_I18N:
|
|
||||||
extensions.append(_JINJA_I18N_EXTENSION_NAME)
|
|
||||||
|
|
||||||
# Next, add the globally defined extensions
|
|
||||||
extensions.extend(list(getattr(settings, 'JINJA2_EXTENSIONS', [])))
|
|
||||||
def from_setting(setting, values_must_be_callable = False):
|
|
||||||
retval = {}
|
|
||||||
setting = getattr(settings, setting, {})
|
|
||||||
if isinstance(setting, dict):
|
|
||||||
for key, value in setting.iteritems():
|
|
||||||
if values_must_be_callable and not callable(value):
|
|
||||||
value = get_callable(value)
|
|
||||||
retval[key] = value
|
|
||||||
else:
|
else:
|
||||||
for value in setting:
|
return nodes.Output([make_call_node()]).set_lineno(tag.lineno)
|
||||||
if values_must_be_callable and not callable(value):
|
|
||||||
value = get_callable(value)
|
|
||||||
retval[value.__name__] = value
|
|
||||||
return retval
|
|
||||||
|
|
||||||
tests.update(from_setting('JINJA2_TESTS', True))
|
@classmethod
|
||||||
filters.update(from_setting('JINJA2_FILTERS', True))
|
def _reverse(self, viewname, args, kwargs, current_app=None, fail=True):
|
||||||
globals.update(from_setting('JINJA2_GLOBALS'))
|
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||||
|
|
||||||
# Finally, add extensions defined in application's templatetag libraries
|
# Try to look up the URL twice: once given the view name,
|
||||||
for lib in self._get_templatelibs():
|
# and again relative to what we guess is the "main" app.
|
||||||
_load_lib(lib)
|
url = ''
|
||||||
|
urlconf=kwargs.pop('urlconf', None)
|
||||||
|
try:
|
||||||
|
url = reverse(viewname, urlconf=urlconf, args=args, kwargs=kwargs,
|
||||||
|
current_app=current_app)
|
||||||
|
except NoReverseMatch as ex:
|
||||||
|
projectname = settings.SETTINGS_MODULE.split('.')[0]
|
||||||
|
try:
|
||||||
|
url = reverse(projectname + '.' + viewname, urlconf=urlconf,
|
||||||
|
args=args, kwargs=kwargs)
|
||||||
|
except NoReverseMatch:
|
||||||
|
if fail:
|
||||||
|
raise ex
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
return dict(
|
return url
|
||||||
extensions=extensions,
|
|
||||||
filters=filters,
|
|
||||||
globals=globals,
|
|
||||||
tests=tests,
|
|
||||||
attrs=attrs,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_env():
|
|
||||||
|
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
|
||||||
"""
|
"""
|
||||||
:return: A Jinja2 environment singleton.
|
|
||||||
|
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)
|
||||||
|
# Use a local variable instead of a macro argument to alias
|
||||||
|
# the expression. This allows us to nest "with" statements.
|
||||||
|
body.insert(0, nodes.Assign(nodes.Name(name.value, 'store'), value))
|
||||||
|
return nodes.CallBlock(
|
||||||
|
self.call_method('_render_block'), [], [], body).\
|
||||||
|
set_lineno(lineno)
|
||||||
|
|
||||||
|
def _render_block(self, caller=None):
|
||||||
|
return caller()
|
||||||
|
|
||||||
|
|
||||||
|
class SpacelessExtension(Extension):
|
||||||
|
"""Removes whitespace between HTML tags, including tab and
|
||||||
|
newline characters.
|
||||||
|
|
||||||
|
Works exactly like Django's own tag.
|
||||||
"""
|
"""
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
kwargs = {
|
tags = set(['spaceless'])
|
||||||
'autoescape': True,
|
|
||||||
}
|
|
||||||
kwargs.update(getattr(settings, 'JINJA2_ENVIRONMENT_OPTIONS', {}))
|
|
||||||
|
|
||||||
result = CoffinEnvironment(**kwargs)
|
def parse(self, parser):
|
||||||
# Hook Jinja's i18n extension up to Django's translation backend
|
lineno = parser.stream.next().lineno
|
||||||
# if i18n is enabled; note that we differ here from Django, in that
|
body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
|
||||||
# Django always has it's i18n functionality available (that is, if
|
return nodes.CallBlock(
|
||||||
# enabled in a template via {% load %}), but uses a null backend if
|
self.call_method('_strip_spaces', [], [], None, None),
|
||||||
# the USE_I18N setting is disabled. Jinja2 provides something similar
|
[], [], body,
|
||||||
# (install_null_translations), but instead we are currently not
|
).set_lineno(lineno)
|
||||||
# enabling the extension at all when USE_I18N=False.
|
|
||||||
# While this is basically an incompatibility with Django, currently
|
|
||||||
# the i18n tags work completely differently anyway, so for now, I
|
|
||||||
# don't think it matters.
|
|
||||||
if settings.USE_I18N:
|
|
||||||
from django.utils import translation
|
|
||||||
result.install_gettext_translations(translation)
|
|
||||||
|
|
||||||
return result
|
def _strip_spaces(self, caller=None):
|
||||||
|
from django.utils.html import strip_spaces_between_tags
|
||||||
|
return strip_spaces_between_tags(caller().strip())
|
||||||
|
|
||||||
env = get_env()
|
# nicer import names
|
||||||
|
load = LoadExtension
|
||||||
|
url = URLExtension
|
||||||
|
with_ = WithExtension
|
||||||
|
spaceless = SpacelessExtension
|
||||||
|
|
||||||
|
|
||||||
|
# # I wish adding extensions in this way was supported
|
||||||
|
# try:
|
||||||
|
# from django_jinja import library
|
||||||
|
# except ImportError:
|
||||||
|
# pass
|
||||||
|
# else:
|
||||||
|
# library.tag(load)
|
||||||
|
# library.tag(url)
|
||||||
|
# library.tag(with_)
|
||||||
|
# library.tag(spaceless)
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
from django.conf.urls import *
|
|
||||||
|
|
||||||
handler404 = 'coffin.views.defaults.page_not_found'
|
|
||||||
handler500 = 'coffin.views.defaults.server_error'
|
|
|
@ -1 +0,0 @@
|
||||||
from django.contrib.auth.admin import *
|
|
|
@ -1 +0,0 @@
|
||||||
from django.contrib.auth.backends import *
|
|
|
@ -1 +0,0 @@
|
||||||
from django.contrib.auth.decorators import *
|
|
|
@ -1 +0,0 @@
|
||||||
from django.contrib.auth.forms import *
|
|
|
@ -1 +0,0 @@
|
||||||
from django.contrib.auth.handlers import *
|
|
|
@ -1 +0,0 @@
|
||||||
from django.contrib.auth.middleware import *
|
|
|
@ -1 +0,0 @@
|
||||||
from django.contrib.auth.models import *
|
|
|
@ -1 +0,0 @@
|
||||||
from django.contrib.auth.tokens import *
|
|
|
@ -1,6 +0,0 @@
|
||||||
import inspect
|
|
||||||
|
|
||||||
from django.contrib.auth import urls
|
|
||||||
|
|
||||||
exec inspect.getsource(urlpatterns)\
|
|
||||||
.replace('django.contrib.auth.views', 'coffin.contrib.auth.views')
|
|
|
@ -1,50 +0,0 @@
|
||||||
import inspect
|
|
||||||
|
|
||||||
from django.contrib.auth.views import *
|
|
||||||
|
|
||||||
# XXX: maybe approach this as importing the entire model, and doing string replacements
|
|
||||||
# on the template and shortcut import lines?
|
|
||||||
|
|
||||||
from coffin.shortcuts import render_to_response
|
|
||||||
from coffin.template import RequestContext, loader
|
|
||||||
|
|
||||||
exec inspect.getsource(logout)
|
|
||||||
exec inspect.getsource(password_change_done)
|
|
||||||
exec inspect.getsource(password_reset)
|
|
||||||
exec inspect.getsource(password_reset_confirm)
|
|
||||||
exec inspect.getsource(password_reset_done)
|
|
||||||
exec inspect.getsource(password_reset_complete)
|
|
||||||
|
|
||||||
exec inspect.getsource(password_change.view_func)
|
|
||||||
password_change = login_required(password_change)
|
|
||||||
|
|
||||||
# XXX: this function uses a decorator, which calls functools.wraps, which compiles the code
|
|
||||||
# thus we cannot inspect the source
|
|
||||||
def login(request, template_name='registration/login.html', redirect_field_name=REDIRECT_FIELD_NAME):
|
|
||||||
"Displays the login form and handles the login action."
|
|
||||||
redirect_to = request.REQUEST.get(redirect_field_name, '')
|
|
||||||
if request.method == "POST":
|
|
||||||
form = AuthenticationForm(data=request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
# Light security check -- make sure redirect_to isn't garbage.
|
|
||||||
if not redirect_to or '//' in redirect_to or ' ' in redirect_to:
|
|
||||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
|
||||||
from django.contrib.auth import login
|
|
||||||
login(request, form.get_user())
|
|
||||||
if request.session.test_cookie_worked():
|
|
||||||
request.session.delete_test_cookie()
|
|
||||||
return HttpResponseRedirect(redirect_to)
|
|
||||||
else:
|
|
||||||
form = AuthenticationForm(request)
|
|
||||||
request.session.set_test_cookie()
|
|
||||||
if Site._meta.installed:
|
|
||||||
current_site = Site.objects.get_current()
|
|
||||||
else:
|
|
||||||
current_site = RequestSite(request)
|
|
||||||
return render_to_response(template_name, {
|
|
||||||
'form': form,
|
|
||||||
redirect_field_name: redirect_to,
|
|
||||||
'site': current_site,
|
|
||||||
'site_name': current_site.name,
|
|
||||||
}, context_instance=RequestContext(request))
|
|
||||||
login = never_cache(login)
|
|
|
@ -1,2 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
from . import context
|
|
|
@ -1,2 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
from django.contrib.flatpages.admin import *
|
|
|
@ -1,29 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.flatpages.models import FlatPage
|
|
||||||
from coffin.common import env
|
|
||||||
|
|
||||||
|
|
||||||
def get_flatpages(starts_with=None, user=None, site_id=None):
|
|
||||||
"""
|
|
||||||
Context-function similar to get_flatpages tag in Django templates.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
<ul>
|
|
||||||
{% for page in get_flatpages(starts_with='/about/', user=user, site_id=site.pk) %}
|
|
||||||
<li><a href="{{ page.url }}">{{ page.title }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
"""
|
|
||||||
flatpages = FlatPage.objects.filter(sites__id=site_id or settings.SITE_ID)
|
|
||||||
|
|
||||||
if starts_with:
|
|
||||||
flatpages = flatpages.filter(url__startswith=starts_with)
|
|
||||||
|
|
||||||
if not user or not user.is_authenticated():
|
|
||||||
flatpages = flatpages.filter(registration_required=False)
|
|
||||||
|
|
||||||
return flatpages
|
|
||||||
|
|
||||||
env.globals['get_flatpages'] = get_flatpages
|
|
|
@ -1,6 +0,0 @@
|
||||||
import inspect
|
|
||||||
|
|
||||||
from django.contrib.flatpages.middleware import *
|
|
||||||
from coffin.contrib.flatpages.views import flatpage
|
|
||||||
|
|
||||||
exec inspect.getsource(FlatpageFallbackMiddleware)\
|
|
|
@ -1,54 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
from django.contrib.flatpages.models import FlatPage
|
|
||||||
from django.contrib.flatpages.views import DEFAULT_TEMPLATE
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.views.decorators.csrf import csrf_protect
|
|
||||||
|
|
||||||
from coffin.template import RequestContext, loader
|
|
||||||
|
|
||||||
# This view is called from FlatpageFallbackMiddleware.process_response
|
|
||||||
# when a 404 is raised, which often means CsrfViewMiddleware.process_view
|
|
||||||
# has not been called even if CsrfViewMiddleware is installed. So we need
|
|
||||||
# to use @csrf_protect, in case the template needs {% csrf_token %}.
|
|
||||||
@csrf_protect
|
|
||||||
def flatpage(request, url):
|
|
||||||
"""
|
|
||||||
Flat page view.
|
|
||||||
|
|
||||||
Models: `flatpages.flatpages`
|
|
||||||
Templates: Uses the template defined by the ``template_name`` field,
|
|
||||||
or `flatpages/default.html` if template_name is not defined.
|
|
||||||
Context:
|
|
||||||
flatpage
|
|
||||||
`flatpages.flatpages` object
|
|
||||||
"""
|
|
||||||
if not url.endswith('/') and settings.APPEND_SLASH:
|
|
||||||
return HttpResponseRedirect("%s/" % request.path)
|
|
||||||
if not url.startswith('/'):
|
|
||||||
url = "/" + url
|
|
||||||
f = get_object_or_404(FlatPage, url__exact=url, sites__id__exact=settings.SITE_ID)
|
|
||||||
# If registration is required for accessing this page, and the user isn't
|
|
||||||
# logged in, redirect to the login page.
|
|
||||||
if f.registration_required and not request.user.is_authenticated():
|
|
||||||
from django.contrib.auth.views import redirect_to_login
|
|
||||||
return redirect_to_login(request.path)
|
|
||||||
if f.template_name:
|
|
||||||
t = loader.select_template((f.template_name, DEFAULT_TEMPLATE))
|
|
||||||
else:
|
|
||||||
t = loader.get_template(DEFAULT_TEMPLATE)
|
|
||||||
|
|
||||||
# To avoid having to always use the "|safe" filter in flatpage templates,
|
|
||||||
# mark the title and content as already safe (since they are raw HTML
|
|
||||||
# content in the first place).
|
|
||||||
f.title = mark_safe(f.title)
|
|
||||||
f.content = mark_safe(f.content)
|
|
||||||
|
|
||||||
c = RequestContext(request, {
|
|
||||||
'flatpage': f,
|
|
||||||
})
|
|
||||||
response = HttpResponse(t.render(c))
|
|
||||||
return response
|
|
|
@ -1,47 +0,0 @@
|
||||||
# -*- 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 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',))
|
|
||||||
|
|
||||||
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):
|
|
||||||
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)
|
|
||||||
return template, template.filename
|
|
||||||
|
|
||||||
|
|
||||||
class FileSystemLoader(LoaderMixin, filesystem.Loader):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AppLoader(LoaderMixin, app_directories.Loader):
|
|
||||||
pass
|
|
|
@ -1,33 +0,0 @@
|
||||||
from django.contrib.syndication.feeds import * # merge modules
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from django.contrib.syndication.feeds import Feed as DjangoDeprecatedFeed
|
|
||||||
from django.contrib.syndication.views import Feed as DjangoNewFeed
|
|
||||||
from coffin.template import loader as coffin_loader
|
|
||||||
from django import VERSION as DJANGO_VERSION
|
|
||||||
|
|
||||||
|
|
||||||
class Feed(DjangoDeprecatedFeed):
|
|
||||||
"""Django changed the syndication framework in 1.2. This class
|
|
||||||
represents the old way, ported to Coffin. If you are using 1.2,
|
|
||||||
you should use the ``Feed`` class in
|
|
||||||
``coffin.contrib.syndication.views``.
|
|
||||||
|
|
||||||
See also there for some notes on what we are doing here.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_feed(self, *args, **kwargs):
|
|
||||||
if DJANGO_VERSION < (1,2):
|
|
||||||
parent_module = sys.modules[DjangoDeprecatedFeed.__module__]
|
|
||||||
else:
|
|
||||||
# In Django 1.2, our parent DjangoDeprecatedFeed class really
|
|
||||||
# inherits from DjangoNewFeed, so we need to patch the loader
|
|
||||||
# in a different module.
|
|
||||||
parent_module = sys.modules[DjangoNewFeed.__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
|
|
|
@ -1,36 +0,0 @@
|
||||||
from django.contrib.syndication.views import * # merge modules
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from django.contrib.syndication.views 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
|
|
|
@ -1,66 +0,0 @@
|
||||||
"""Jinja2's i18n functionality is not exactly the same as Django's.
|
|
||||||
In particular, the tags names and their syntax are different:
|
|
||||||
|
|
||||||
1. The Django ``trans`` tag is replaced by a _() global.
|
|
||||||
2. The Django ``blocktrans`` tag is called ``trans``.
|
|
||||||
|
|
||||||
(1) isn't an issue, since the whole ``makemessages`` process is based on
|
|
||||||
converting the template tags to ``_()`` calls. However, (2) means that
|
|
||||||
those Jinja2 ``trans`` tags will not be picked up my Django's
|
|
||||||
``makemessage`` command.
|
|
||||||
|
|
||||||
There aren't any nice solutions here. While Jinja2's i18n extension does
|
|
||||||
come with extraction capabilities built in, the code behind ``makemessages``
|
|
||||||
unfortunately isn't extensible, so we can:
|
|
||||||
|
|
||||||
* Duplicate the command + code behind it.
|
|
||||||
* Offer a separate command for Jinja2 extraction.
|
|
||||||
* Try to get Django to offer hooks into makemessages().
|
|
||||||
* Monkey-patch.
|
|
||||||
|
|
||||||
We are currently doing that last thing. It turns out there we are lucky
|
|
||||||
for once: It's simply a matter of extending two regular expressions.
|
|
||||||
Credit for the approach goes to:
|
|
||||||
http://stackoverflow.com/questions/2090717/getting-translation-strings-for-jinja2-templates-integrated-with-django-1-x
|
|
||||||
"""
|
|
||||||
|
|
||||||
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\s*-?$""")
|
|
||||||
trans_real.block_re = re.compile(
|
|
||||||
trans_real.block_re.pattern + '|' + r"""^-?\s*trans(?:\s+(?!'|")(?=.*?=.*?)|-?$)""")
|
|
||||||
trans_real.plural_re = re.compile(
|
|
||||||
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
|
|
|
@ -1,50 +0,0 @@
|
||||||
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', 'render')
|
|
||||||
|
|
||||||
|
|
||||||
# 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, **kwargs):
|
|
||||||
"""
|
|
||||||
: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, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
|
@ -3,15 +3,11 @@ try:
|
||||||
except ImportError: # Python 2
|
except ImportError: # Python 2
|
||||||
from urlparse import urljoin
|
from urlparse import urljoin
|
||||||
|
|
||||||
from coffin.template import Library
|
|
||||||
from jinja2.ext import Extension
|
from jinja2.ext import Extension
|
||||||
from jinja2 import nodes
|
from jinja2 import nodes
|
||||||
from django.utils.encoding import iri_to_uri
|
from django.utils.encoding import iri_to_uri
|
||||||
|
|
||||||
|
|
||||||
register = Library()
|
|
||||||
|
|
||||||
|
|
||||||
class PrefixExtension(Extension):
|
class PrefixExtension(Extension):
|
||||||
|
|
||||||
def parse(self, parser):
|
def parse(self, parser):
|
||||||
|
@ -119,12 +115,3 @@ class StaticExtension(PrefixExtension):
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_statc_url(cls, path):
|
def get_statc_url(cls, path):
|
||||||
return urljoin(PrefixExtension.get_uri_setting("STATIC_URL"), path)
|
return urljoin(PrefixExtension.get_uri_setting("STATIC_URL"), path)
|
||||||
|
|
||||||
|
|
||||||
register.tag(GetStaticPrefixExtension)
|
|
||||||
register.tag(GetMediaPrefixExtension)
|
|
||||||
register.tag(StaticExtension)
|
|
||||||
|
|
||||||
|
|
||||||
def static(path):
|
|
||||||
return StaticExtension.get_static_url(path)
|
|
|
@ -1,9 +1,5 @@
|
||||||
from coffin import template
|
|
||||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||||
from coffin.templatetags.static import StaticExtension
|
from coffin.static import StaticExtension
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
|
|
||||||
class StaticExtension(StaticExtension):
|
class StaticExtension(StaticExtension):
|
||||||
|
@ -29,10 +25,3 @@ class StaticExtension(StaticExtension):
|
||||||
def get_statc_url(cls, path):
|
def get_statc_url(cls, path):
|
||||||
return super(StaticExtension, cls).get_statc_url(
|
return super(StaticExtension, cls).get_statc_url(
|
||||||
staticfiles_storage.url(path))
|
staticfiles_storage.url(path))
|
||||||
|
|
||||||
|
|
||||||
register.tag(StaticExtension)
|
|
||||||
|
|
||||||
|
|
||||||
def static(path):
|
|
||||||
return StaticExtension.get_static_url(path)
|
|
|
@ -1,115 +0,0 @@
|
||||||
from django.template import (
|
|
||||||
Context as DjangoContext,
|
|
||||||
add_to_builtins as django_add_to_builtins,
|
|
||||||
import_library,
|
|
||||||
)
|
|
||||||
from jinja2 import Template as _Jinja2Template
|
|
||||||
from jinja2.runtime import Context as _Jinja2Context
|
|
||||||
|
|
||||||
# Merge with ``django.template``.
|
|
||||||
from django.template import __all__
|
|
||||||
from django.template import *
|
|
||||||
from django.template import Origin
|
|
||||||
from django.test import signals
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
"""
|
|
||||||
dict_ = {}
|
|
||||||
|
|
||||||
if isinstance(context, dict):
|
|
||||||
dict_ = context
|
|
||||||
|
|
||||||
if isinstance(context, DjangoContext):
|
|
||||||
dict_ = dict_from_django_context(context)
|
|
||||||
|
|
||||||
if isinstance(context, _Jinja2Context):
|
|
||||||
dict_ = context.get_all()
|
|
||||||
|
|
||||||
# It'd be nice to move this only into the test env
|
|
||||||
signals.template_rendered.send(sender=self, template=self, context=DjangoContext(dict_))
|
|
||||||
return super(Template, self).render(**dict_)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def origin(self):
|
|
||||||
return Origin(self.filename)
|
|
||||||
|
|
||||||
|
|
||||||
def dict_from_django_context(context):
|
|
||||||
"""Flattens a Django :class:`django.template.context.Context` object.
|
|
||||||
"""
|
|
||||||
dict_ = {}
|
|
||||||
# Jinja2 internally converts the context instance to a dictionary, thus
|
|
||||||
# we need to store the current_app attribute as a key/value pair.
|
|
||||||
dict_['_current_app'] = getattr(context, 'current_app', None)
|
|
||||||
|
|
||||||
# Newest dicts are up front, so update from oldest to newest.
|
|
||||||
for subcontext in reversed(list(context)):
|
|
||||||
dict_.update(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.
|
|
||||||
|
|
||||||
Once thing that is special about Coffin is that because {% load %}
|
|
||||||
is not supported in Coffin, *everything* it provides must be
|
|
||||||
registered through the builtins.
|
|
||||||
|
|
||||||
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(import_library(module_name))
|
|
||||||
django_add_to_builtins(module_name)
|
|
||||||
|
|
||||||
|
|
||||||
add_to_builtins('coffin.template.defaulttags')
|
|
||||||
add_to_builtins('coffin.template.defaultfilters')
|
|
||||||
add_to_builtins('coffin.templatetags.static')
|
|
||||||
|
|
|
@ -1,140 +0,0 @@
|
||||||
"""Coffin automatically makes Django's builtin filters available in Jinja2,
|
|
||||||
through an interop-layer.
|
|
||||||
|
|
||||||
However, Jinja 2 provides room to improve the syntax of some of the
|
|
||||||
filters. Those can be overridden here.
|
|
||||||
|
|
||||||
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
|
|
||||||
from jinja2 import filters
|
|
||||||
|
|
||||||
register = Library()
|
|
||||||
|
|
||||||
def url(view_name, *args, **kwargs):
|
|
||||||
"""This is an alternative to the {% url %} tag. It comes from a time
|
|
||||||
before Coffin had a port of the tag.
|
|
||||||
"""
|
|
||||||
from coffin.template.defaulttags import url
|
|
||||||
return url._reverse(view_name, args, kwargs)
|
|
||||||
register.jinja2_filter(url, jinja2_only=True)
|
|
||||||
register.object(url)
|
|
||||||
|
|
||||||
@register.jinja2_filter(jinja2_only=True)
|
|
||||||
def timesince(value, *arg):
|
|
||||||
if value is None or isinstance(value, Undefined):
|
|
||||||
return u''
|
|
||||||
from django.utils.timesince import timesince
|
|
||||||
return timesince(value, *arg)
|
|
||||||
|
|
||||||
@register.jinja2_filter(jinja2_only=True)
|
|
||||||
def timeuntil(value, *args):
|
|
||||||
if value is None or isinstance(value, Undefined):
|
|
||||||
return u''
|
|
||||||
from django.utils.timesince import timeuntil
|
|
||||||
return timeuntil(value, *args)
|
|
||||||
|
|
||||||
@register.jinja2_filter(jinja2_only=True)
|
|
||||||
def date(value, arg=None):
|
|
||||||
"""Formats a date according to the given format."""
|
|
||||||
if value is None or isinstance(value, Undefined):
|
|
||||||
return u''
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils import formats
|
|
||||||
from django.utils.dateformat import format
|
|
||||||
if arg is None:
|
|
||||||
arg = settings.DATE_FORMAT
|
|
||||||
try:
|
|
||||||
return formats.date_format(value, arg)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
return format(value, arg)
|
|
||||||
except AttributeError:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
@register.jinja2_filter(jinja2_only=True)
|
|
||||||
def time(value, arg=None):
|
|
||||||
"""Formats a time according to the given format."""
|
|
||||||
if value is None or isinstance(value, Undefined):
|
|
||||||
return u''
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils import formats
|
|
||||||
from django.utils.dateformat import time_format
|
|
||||||
if arg is None:
|
|
||||||
arg = settings.TIME_FORMAT
|
|
||||||
try:
|
|
||||||
return formats.time_format(value, arg)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
return time_format(value, arg)
|
|
||||||
except AttributeError:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
@register.jinja2_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.
|
|
||||||
try:
|
|
||||||
from django.utils.text import Truncator
|
|
||||||
except ImportError:
|
|
||||||
from django.utils.text import truncate_words # Django < 1.6
|
|
||||||
else:
|
|
||||||
truncate_words = lambda value, length: Truncator(value).words(length)
|
|
||||||
return truncate_words(value, int(length))
|
|
||||||
|
|
||||||
@register.jinja2_filter(jinja2_only=True)
|
|
||||||
def truncatewords_html(value, length):
|
|
||||||
try:
|
|
||||||
from django.utils.text import Truncator
|
|
||||||
except ImportError:
|
|
||||||
from django.utils.text import truncate_html_words # Django < 1.6
|
|
||||||
else:
|
|
||||||
truncate_html_words = lambda value, length: Truncator(value).words(length, html=True)
|
|
||||||
return truncate_html_words(value, int(length))
|
|
||||||
|
|
||||||
@register.jinja2_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.jinja2_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
|
|
||||||
|
|
||||||
@register.jinja2_filter(jinja2_only=True)
|
|
||||||
def default(value, default_value=u'', boolean=True):
|
|
||||||
"""Make the default filter, if used without arguments, behave like
|
|
||||||
Django's own version.
|
|
||||||
"""
|
|
||||||
return filters.do_default(value, default_value, boolean)
|
|
||||||
|
|
|
@ -1,411 +0,0 @@
|
||||||
from jinja2 import nodes
|
|
||||||
from jinja2.ext import Extension
|
|
||||||
from jinja2.exceptions import TemplateSyntaxError
|
|
||||||
from jinja2 import Markup
|
|
||||||
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'):
|
|
||||||
# Need to work around Jinja2 syntax here. Jinja by default acts
|
|
||||||
# like Python and concats subsequent strings. In this case
|
|
||||||
# though, we want {% url "app.views.post" "1" %} to be treated
|
|
||||||
# as view + argument, while still supporting
|
|
||||||
# {% url "app.views.post"|filter %}. Essentially, what we do is
|
|
||||||
# rather than let ``parser.parse_primary()`` deal with a "string"
|
|
||||||
# token, we do so ourselves, and let parse_expression() handle all
|
|
||||||
# other cases.
|
|
||||||
if stream.look().test('string'):
|
|
||||||
token = stream.next()
|
|
||||||
viewname = nodes.Const(token.value, lineno=token.lineno)
|
|
||||||
else:
|
|
||||||
viewname = parser.parse_expression()
|
|
||||||
else:
|
|
||||||
# parse valid tokens and manually build a string from them
|
|
||||||
bits = []
|
|
||||||
name_allowed = True
|
|
||||||
while True:
|
|
||||||
if stream.current.test_any('dot', 'sub', 'colon'):
|
|
||||||
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())
|
|
||||||
|
|
||||||
def make_call_node(*kw):
|
|
||||||
return self.call_method('_reverse', args=[
|
|
||||||
viewname,
|
|
||||||
nodes.List(args),
|
|
||||||
nodes.Dict(kwargs),
|
|
||||||
nodes.Name('_current_app', 'load'),
|
|
||||||
], 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, current_app=None, 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 = ''
|
|
||||||
urlconf=kwargs.pop('urlconf', None)
|
|
||||||
try:
|
|
||||||
url = reverse(viewname, urlconf=urlconf, args=args, kwargs=kwargs,
|
|
||||||
current_app=current_app)
|
|
||||||
except NoReverseMatch as ex:
|
|
||||||
projectname = settings.SETTINGS_MODULE.split('.')[0]
|
|
||||||
try:
|
|
||||||
url = reverse(projectname + '.' + viewname, urlconf=urlconf,
|
|
||||||
args=args, kwargs=kwargs)
|
|
||||||
except NoReverseMatch:
|
|
||||||
if fail:
|
|
||||||
raise ex
|
|
||||||
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)
|
|
||||||
# Use a local variable instead of a macro argument to alias
|
|
||||||
# the expression. This allows us to nest "with" statements.
|
|
||||||
body.insert(0, nodes.Assign(nodes.Name(name.value, 'store'), value))
|
|
||||||
return nodes.CallBlock(
|
|
||||||
self.call_method('_render_block'), [], [], body).\
|
|
||||||
set_lineno(lineno)
|
|
||||||
|
|
||||||
def _render_block(self, caller=None):
|
|
||||||
return caller()
|
|
||||||
|
|
||||||
|
|
||||||
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 hashlib import md5
|
|
||||||
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)
|
|
||||||
|
|
||||||
args_string = u':'.join([urlquote(v) for v in vary_on])
|
|
||||||
args_md5 = md5(args_string)
|
|
||||||
cache_key = 'template.cache.%s.%s' % (fragm_name, args_md5.hexdigest())
|
|
||||||
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 = set(['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())
|
|
||||||
|
|
||||||
|
|
||||||
class CsrfTokenExtension(Extension):
|
|
||||||
"""Jinja2-version of the ``csrf_token`` tag.
|
|
||||||
|
|
||||||
Adapted from a snippet by Jason Green:
|
|
||||||
http://www.djangosnippets.org/snippets/1847/
|
|
||||||
|
|
||||||
This tag is a bit stricter than the Django tag in that it doesn't
|
|
||||||
simply ignore any invalid arguments passed in.
|
|
||||||
"""
|
|
||||||
|
|
||||||
tags = set(['csrf_token'])
|
|
||||||
|
|
||||||
def parse(self, parser):
|
|
||||||
lineno = parser.stream.next().lineno
|
|
||||||
return nodes.Output([
|
|
||||||
self.call_method('_render', [nodes.Name('csrf_token', 'load')]),
|
|
||||||
]).set_lineno(lineno)
|
|
||||||
|
|
||||||
def _render(self, csrf_token):
|
|
||||||
from django.template.defaulttags import CsrfTokenNode
|
|
||||||
return Markup(CsrfTokenNode().render({'csrf_token': csrf_token}))
|
|
||||||
|
|
||||||
|
|
||||||
# nicer import names
|
|
||||||
load = LoadExtension
|
|
||||||
url = URLExtension
|
|
||||||
with_ = WithExtension
|
|
||||||
cache = CacheExtension
|
|
||||||
spaceless = SpacelessExtension
|
|
||||||
csrf_token = CsrfTokenExtension
|
|
||||||
|
|
||||||
register = Library()
|
|
||||||
register.tag(load)
|
|
||||||
register.tag(url)
|
|
||||||
register.tag(with_)
|
|
||||||
register.tag(cache)
|
|
||||||
register.tag(spaceless)
|
|
||||||
register.tag(csrf_token)
|
|
||||||
|
|
|
@ -1,252 +0,0 @@
|
||||||
from django.template import Library as DjangoLibrary, InvalidTemplateLibrary
|
|
||||||
from jinja2.ext import Extension as Jinja2Extension
|
|
||||||
import types
|
|
||||||
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_environment_attrs = {}
|
|
||||||
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.tags = copy(django_library.tags)
|
|
||||||
for name, func in django_library.filters.iteritems():
|
|
||||||
result._register_filter(name, func, type='django')
|
|
||||||
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, environment={}):
|
|
||||||
"""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 your extension needs to be configured by setting environment
|
|
||||||
attributes, you can can pass key-value pairs via ``environment``.
|
|
||||||
"""
|
|
||||||
if isinstance(name_or_node, type) and issubclass(name_or_node, Jinja2Extension):
|
|
||||||
if compile_function:
|
|
||||||
raise InvalidTemplateLibrary('"compile_function" argument not supported for Jinja2 extensions')
|
|
||||||
self.jinja2_extensions.append(name_or_node)
|
|
||||||
self.jinja2_environment_attrs.update(environment)
|
|
||||||
return name_or_node
|
|
||||||
else:
|
|
||||||
if environment:
|
|
||||||
raise InvalidTemplateLibrary('"environment" argument not supported for Django tags')
|
|
||||||
return super(Library, self).tag(name_or_node, compile_function)
|
|
||||||
|
|
||||||
def tag_function(self, func_or_node):
|
|
||||||
if not isinstance(func_or_node, types.FunctionType) and \
|
|
||||||
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, type=None, jinja2_only=None):
|
|
||||||
"""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.
|
|
||||||
|
|
||||||
If you know which template language the filter was written for,
|
|
||||||
you may want to specify type="django" or type="jinja2", to disable
|
|
||||||
the interop layer which in some cases might not be able to operate
|
|
||||||
entirely opaque. For example, Jinja 2 filters may not receive a
|
|
||||||
"Undefined" value if the interop layer is applied.
|
|
||||||
|
|
||||||
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, type=type, 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, type=type,
|
|
||||||
jinja2_only=jinja2_only)
|
|
||||||
return dec
|
|
||||||
elif name != None and filter_func != None:
|
|
||||||
# register.filter('somename', somefunc)
|
|
||||||
return self._register_filter(name, filter_func, type=type,
|
|
||||||
jinja2_only=jinja2_only)
|
|
||||||
else:
|
|
||||||
raise InvalidTemplateLibrary("Unsupported arguments to "
|
|
||||||
"Library.filter: (%r, %r)", (name, filter_func))
|
|
||||||
|
|
||||||
def jinja2_filter(self, *args, **kwargs):
|
|
||||||
"""Shortcut for filter(type='jinja2').
|
|
||||||
"""
|
|
||||||
kw = {'type': JINJA2}
|
|
||||||
kw.update(kwargs)
|
|
||||||
return self.filter(*args, **kw)
|
|
||||||
|
|
||||||
def _register_filter(self, name, func, type=None, jinja2_only=None):
|
|
||||||
assert type in (None, JINJA2, DJANGO,)
|
|
||||||
|
|
||||||
# The user might not specify the language the filter was written
|
|
||||||
# for, but sometimes we can auto detect it.
|
|
||||||
filter_type, can_be_ported = guess_filter_type(func)
|
|
||||||
assert not (filter_type and type) or filter_type == type, \
|
|
||||||
"guessed filter type (%s) not matching claimed type (%s)" % (
|
|
||||||
filter_type, type,
|
|
||||||
)
|
|
||||||
if not filter_type and type:
|
|
||||||
filter_type = type
|
|
||||||
|
|
||||||
if filter_type == JINJA2:
|
|
||||||
self.jinja2_filters[name] = func
|
|
||||||
if can_be_ported and not jinja2_only:
|
|
||||||
self.filters[name] = jinja2_filter_to_django(func)
|
|
||||||
return func
|
|
||||||
elif filter_type == DJANGO:
|
|
||||||
self.filters[name] = func
|
|
||||||
if not can_be_ported and jinja2_only:
|
|
||||||
raise ValueError('This filter cannot be ported to Jinja2.')
|
|
||||||
if can_be_ported:
|
|
||||||
self.jinja2_filters[name] = django_filter_to_jinja2(func)
|
|
||||||
return func
|
|
||||||
else:
|
|
||||||
django_func = jinja2_filter_to_django(func)
|
|
||||||
jinja2_func = django_filter_to_jinja2(func)
|
|
||||||
if jinja2_only:
|
|
||||||
self.jinja2_filters[name] = jinja2_func
|
|
||||||
return jinja2_func
|
|
||||||
else:
|
|
||||||
# register the filter with both engines
|
|
||||||
self.filters[name] = django_func
|
|
||||||
self.jinja2_filters[name] = jinja2_func
|
|
||||||
return (django_func, jinja2_func)
|
|
|
@ -1,69 +0,0 @@
|
||||||
"""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 django.template import TemplateDoesNotExist
|
|
||||||
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
|
|
||||||
try:
|
|
||||||
return env.get_template(template_name)
|
|
||||||
except TemplateNotFound:
|
|
||||||
raise TemplateDoesNotExist(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 TemplateDoesNotExist:
|
|
||||||
continue
|
|
||||||
# If we get here, none of the templates could be loaded
|
|
||||||
raise TemplateDoesNotExist(', '.join(template_name_list))
|
|
|
@ -1,91 +0,0 @@
|
||||||
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
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
if not match_loader.match(django_loader):
|
|
||||||
return None
|
|
||||||
for substr, func in _JINJA_LOADER_BY_DJANGO_SUBSTR.iteritems():
|
|
||||||
if substr in django_loader:
|
|
||||||
return func(*(args or []))
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
'AppLoader': _make_jinja_app_loader,
|
|
||||||
'FileSystemLoader': _make_jinja_filesystem_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]
|
|
|
@ -1,16 +0,0 @@
|
||||||
from coffin.template import loader
|
|
||||||
from django.template import response as django_response
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleTemplateResponse(django_response.SimpleTemplateResponse):
|
|
||||||
def resolve_template(self, template):
|
|
||||||
if isinstance(template, (list, tuple)):
|
|
||||||
return loader.select_template(template)
|
|
||||||
elif isinstance(template, basestring):
|
|
||||||
return loader.get_template(template)
|
|
||||||
else:
|
|
||||||
return template
|
|
||||||
|
|
||||||
class TemplateResponse(django_response.TemplateResponse,
|
|
||||||
SimpleTemplateResponse):
|
|
||||||
pass
|
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
|
@ -1,35 +0,0 @@
|
||||||
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)
|
|
|
@ -1,15 +0,0 @@
|
||||||
from django.views.generic import GenericViewError
|
|
||||||
try:
|
|
||||||
from django.views.generic.base import View, RedirectView
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
from coffin.views.generic.base import TemplateView
|
|
||||||
from coffin.views.generic.dates import (ArchiveIndexView, YearArchiveView,
|
|
||||||
MonthArchiveView, WeekArchiveView,
|
|
||||||
DayArchiveView, TodayArchiveView,
|
|
||||||
DateDetailView)
|
|
||||||
from coffin.views.generic.detail import DetailView
|
|
||||||
from coffin.views.generic.edit import (FormView, CreateView, UpdateView,
|
|
||||||
DeleteView)
|
|
||||||
from coffin.views.generic.list import ListView
|
|
|
@ -1,13 +0,0 @@
|
||||||
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.
|
|
||||||
"""
|
|
|
@ -1,7 +0,0 @@
|
||||||
from coffin.template import loader
|
|
||||||
from django.views.generic import create_update as _create_update
|
|
||||||
import functools
|
|
||||||
|
|
||||||
create_object = functools.partial(_create_update.create_object, template_loader=loader)
|
|
||||||
update_object = functools.partial(_create_update.update_object, template_loader=loader)
|
|
||||||
delete_object = functools.partial(_create_update.delete_object, template_loader=loader)
|
|
|
@ -1,13 +0,0 @@
|
||||||
from coffin.template import loader
|
|
||||||
from django.views.generic import date_based as _date_based
|
|
||||||
import functools
|
|
||||||
|
|
||||||
|
|
||||||
archive_index = functools.partial(_date_based.archive_index, template_loader=loader)
|
|
||||||
archive_year = functools.partial(_date_based.archive_year, template_loader=loader)
|
|
||||||
archive_month = functools.partial(_date_based.archive_month, template_loader=loader)
|
|
||||||
archive_week = functools.partial(_date_based.archive_week, template_loader=loader)
|
|
||||||
archive_daye = functools.partial(_date_based.archive_day, template_loader=loader)
|
|
||||||
archive_today = functools.partial(_date_based.archive_today, template_loader=loader)
|
|
||||||
|
|
||||||
object_detail = functools.partial(_date_based.object_detail, template_loader=loader)
|
|
|
@ -1,50 +0,0 @@
|
||||||
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'
|
|
|
@ -1,12 +0,0 @@
|
||||||
import django.views.generic.detail as _generic_detail
|
|
||||||
from coffin.views.generic.base import TemplateResponseMixin as JinjaTemplateResponseMixin
|
|
||||||
|
|
||||||
class SingleObjectTemplateResponseMixin(JinjaTemplateResponseMixin, _generic_detail.SingleObjectTemplateResponseMixin):
|
|
||||||
"""
|
|
||||||
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.
|
|
||||||
"""
|
|
|
@ -1,30 +0,0 @@
|
||||||
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'
|
|
|
@ -1,12 +0,0 @@
|
||||||
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.
|
|
||||||
"""
|
|
|
@ -1,6 +0,0 @@
|
||||||
from coffin.template import loader
|
|
||||||
from django.views.generic import list_detail as _list_detail
|
|
||||||
import functools
|
|
||||||
|
|
||||||
object_list = functools.partial(_list_detail.object_list, template_loader=loader)
|
|
||||||
object_detail = functools.partial(_list_detail.object_detail, template_loader=loader)
|
|
|
@ -1,6 +0,0 @@
|
||||||
import inspect
|
|
||||||
|
|
||||||
from django.views.generic.simple import *
|
|
||||||
from coffin.template import loader, RequestContext
|
|
||||||
|
|
||||||
exec inspect.getsource(direct_to_template)
|
|
|
@ -1,88 +0,0 @@
|
||||||
# Makefile for Sphinx documentation
|
|
||||||
#
|
|
||||||
|
|
||||||
# You can set these variables from the command line.
|
|
||||||
SPHINXOPTS =
|
|
||||||
SPHINXBUILD = sphinx-build
|
|
||||||
PAPER =
|
|
||||||
|
|
||||||
# Internal variables.
|
|
||||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
|
||||||
PAPEROPT_letter = -D latex_paper_size=letter
|
|
||||||
ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
|
||||||
|
|
||||||
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
|
|
||||||
|
|
||||||
help:
|
|
||||||
@echo "Please use \`make <target>' where <target> is one of"
|
|
||||||
@echo " html to make standalone HTML files"
|
|
||||||
@echo " dirhtml to make HTML files named index.html in directories"
|
|
||||||
@echo " pickle to make pickle files"
|
|
||||||
@echo " json to make JSON files"
|
|
||||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
|
||||||
@echo " qthelp to make HTML files and a qthelp project"
|
|
||||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
|
||||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
|
||||||
@echo " linkcheck to check all external links for integrity"
|
|
||||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
|
||||||
|
|
||||||
clean:
|
|
||||||
-rm -rf _build/*
|
|
||||||
|
|
||||||
html:
|
|
||||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML pages are in _build/html."
|
|
||||||
|
|
||||||
dirhtml:
|
|
||||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML pages are in _build/dirhtml."
|
|
||||||
|
|
||||||
pickle:
|
|
||||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can process the pickle files."
|
|
||||||
|
|
||||||
json:
|
|
||||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can process the JSON files."
|
|
||||||
|
|
||||||
htmlhelp:
|
|
||||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
|
||||||
".hhp project file in _build/htmlhelp."
|
|
||||||
|
|
||||||
qthelp:
|
|
||||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
|
||||||
".qhcp project file in _build/qthelp, like this:"
|
|
||||||
@echo "# qcollectiongenerator _build/qthelp/Coffin.qhcp"
|
|
||||||
@echo "To view the help file:"
|
|
||||||
@echo "# assistant -collectionFile _build/qthelp/Coffin.qhc"
|
|
||||||
|
|
||||||
latex:
|
|
||||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; the LaTeX files are in _build/latex."
|
|
||||||
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
|
|
||||||
"run these through (pdf)latex."
|
|
||||||
|
|
||||||
changes:
|
|
||||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes
|
|
||||||
@echo
|
|
||||||
@echo "The overview file is in _build/changes."
|
|
||||||
|
|
||||||
linkcheck:
|
|
||||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck
|
|
||||||
@echo
|
|
||||||
@echo "Link check complete; look for any errors in the above output " \
|
|
||||||
"or in _build/linkcheck/output.txt."
|
|
||||||
|
|
||||||
doctest:
|
|
||||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest
|
|
||||||
@echo "Testing of doctests in the sources finished, look at the " \
|
|
||||||
"results in _build/doctest/output.txt."
|
|
195
docs/conf.py
195
docs/conf.py
|
@ -1,195 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Coffin documentation build configuration file, created by
|
|
||||||
# sphinx-quickstart on Tue Sep 8 15:22:15 2009.
|
|
||||||
#
|
|
||||||
# This file is execfile()d with the current directory set to its containing dir.
|
|
||||||
#
|
|
||||||
# Note that not all possible configuration values are present in this
|
|
||||||
# autogenerated file.
|
|
||||||
#
|
|
||||||
# All configuration values have a default; values that are commented out
|
|
||||||
# serve to show the default.
|
|
||||||
|
|
||||||
import sys, os
|
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
||||||
#sys.path.append(os.path.abspath('.'))
|
|
||||||
|
|
||||||
# -- General configuration -----------------------------------------------------
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
|
||||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
|
||||||
extensions = []
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The encoding of source files.
|
|
||||||
#source_encoding = 'utf-8'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = u'Coffin'
|
|
||||||
copyright = u'2009, Christopher D. Leary'
|
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
|
||||||
# |version| and |release|, also used in various other places throughout the
|
|
||||||
# built documents.
|
|
||||||
|
|
||||||
import coffin
|
|
||||||
# The short X.Y version.
|
|
||||||
version = '.'.join(map(str, coffin.__version__))
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
|
||||||
release = '.'.join(map(str, coffin.__version__))
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
||||||
# for a list of supported languages.
|
|
||||||
#language = None
|
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
|
||||||
# non-false value, then it is used:
|
|
||||||
#today = ''
|
|
||||||
# Else, today_fmt is used as the format for a strftime call.
|
|
||||||
#today_fmt = '%B %d, %Y'
|
|
||||||
|
|
||||||
# List of documents that shouldn't be included in the build.
|
|
||||||
#unused_docs = []
|
|
||||||
|
|
||||||
# List of directories, relative to source directory, that shouldn't be searched
|
|
||||||
# for source files.
|
|
||||||
exclude_trees = ['_build']
|
|
||||||
|
|
||||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
|
||||||
#default_role = None
|
|
||||||
|
|
||||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
|
||||||
#add_function_parentheses = True
|
|
||||||
|
|
||||||
# If true, the current module name will be prepended to all description
|
|
||||||
# unit titles (such as .. function::).
|
|
||||||
#add_module_names = True
|
|
||||||
|
|
||||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
|
||||||
# output. They are ignored by default.
|
|
||||||
#show_authors = False
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
# A list of ignored prefixes for module index sorting.
|
|
||||||
#modindex_common_prefix = []
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
|
||||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
|
||||||
html_theme = 'default'
|
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
|
||||||
# further. For a list of options available for each theme, see the
|
|
||||||
# documentation.
|
|
||||||
#html_theme_options = {}
|
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
|
||||||
#html_theme_path = []
|
|
||||||
|
|
||||||
# The name for this set of Sphinx documents. If None, it defaults to
|
|
||||||
# "<project> v<release> documentation".
|
|
||||||
#html_title = None
|
|
||||||
|
|
||||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
|
||||||
#html_short_title = None
|
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top
|
|
||||||
# of the sidebar.
|
|
||||||
#html_logo = None
|
|
||||||
|
|
||||||
# The name of an image file (within the static path) to use as favicon of the
|
|
||||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
|
||||||
# pixels large.
|
|
||||||
#html_favicon = None
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
html_static_path = ['_static']
|
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
|
||||||
# using the given strftime format.
|
|
||||||
#html_last_updated_fmt = '%b %d, %Y'
|
|
||||||
|
|
||||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
|
||||||
# typographically correct entities.
|
|
||||||
#html_use_smartypants = True
|
|
||||||
|
|
||||||
# Custom sidebar templates, maps document names to template names.
|
|
||||||
#html_sidebars = {}
|
|
||||||
|
|
||||||
# Additional templates that should be rendered to pages, maps page names to
|
|
||||||
# template names.
|
|
||||||
#html_additional_pages = {}
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
#html_use_modindex = True
|
|
||||||
|
|
||||||
# If false, no index is generated.
|
|
||||||
#html_use_index = True
|
|
||||||
|
|
||||||
# If true, the index is split into individual pages for each letter.
|
|
||||||
#html_split_index = False
|
|
||||||
|
|
||||||
# If true, links to the reST sources are added to the pages.
|
|
||||||
#html_show_sourcelink = True
|
|
||||||
|
|
||||||
# If true, an OpenSearch description file will be output, and all pages will
|
|
||||||
# contain a <link> tag referring to it. The value of this option must be the
|
|
||||||
# base URL from which the finished HTML is served.
|
|
||||||
#html_use_opensearch = ''
|
|
||||||
|
|
||||||
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
|
|
||||||
#html_file_suffix = ''
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = 'Coffindoc'
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output --------------------------------------------------
|
|
||||||
|
|
||||||
# The paper size ('letter' or 'a4').
|
|
||||||
#latex_paper_size = 'letter'
|
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
|
||||||
#latex_font_size = '10pt'
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
|
||||||
latex_documents = [
|
|
||||||
('index', 'Coffin.tex', u'Coffin Documentation',
|
|
||||||
u'Christopher D. Leary', 'manual'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
|
||||||
# the title page.
|
|
||||||
#latex_logo = None
|
|
||||||
|
|
||||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
|
||||||
# not chapters.
|
|
||||||
#latex_use_parts = False
|
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
|
||||||
#latex_preamble = ''
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
#latex_appendices = []
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
#latex_use_modindex = True
|
|
|
@ -1,3 +0,0 @@
|
||||||
==========================
|
|
||||||
:mod:`coffin.contrib.auth`
|
|
||||||
==========================
|
|
|
@ -1,26 +0,0 @@
|
||||||
=====================
|
|
||||||
:mod:`coffin.contrib`
|
|
||||||
=====================
|
|
||||||
|
|
||||||
Coffin includes replacements for several Django contrib modules.
|
|
||||||
|
|
||||||
To use this, simply change your import line from::
|
|
||||||
|
|
||||||
from django.contrib.<module>
|
|
||||||
|
|
||||||
To the following::
|
|
||||||
|
|
||||||
from coffin.contrib.<module>
|
|
||||||
|
|
||||||
-----------------
|
|
||||||
Supported Modules
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
The following drop-in replacements are available:
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
auth
|
|
||||||
markup
|
|
||||||
syndication
|
|
|
@ -1,3 +0,0 @@
|
||||||
============================
|
|
||||||
:mod:`coffin.contrib.markup`
|
|
||||||
============================
|
|
|
@ -1,3 +0,0 @@
|
||||||
=================================
|
|
||||||
:mod:`coffin.contrib.syndication`
|
|
||||||
=================================
|
|
|
@ -1,18 +0,0 @@
|
||||||
Coffin Documentation
|
|
||||||
====================
|
|
||||||
|
|
||||||
Contents:
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
install
|
|
||||||
contrib/index
|
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
Install the package through PyPi::
|
|
||||||
|
|
||||||
easy_install Coffin
|
|
||||||
|
|
||||||
Or alternatively, get the source::
|
|
||||||
|
|
||||||
git clone git://github.com/dcramer/coffin.git
|
|
||||||
cd coffin
|
|
||||||
python setup.py install
|
|
||||||
|
|
||||||
Once installed, you will need to add Coffin to several places throughout your projects.
|
|
||||||
|
|
||||||
First, open up ``settings.py`` and add Coffin to your ``INSTALLED_APPS``::
|
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
|
||||||
'coffin',
|
|
||||||
...
|
|
||||||
)
|
|
||||||
|
|
||||||
The easiest way to enable Jinja2, rather than Django, is to change your import paths. For example, if we're using the ``render_to_response`` shortcut, we simply need to tweak our import line::
|
|
||||||
|
|
||||||
from django.shortcuts import render_to_response
|
|
||||||
|
|
||||||
To the following::
|
|
||||||
|
|
||||||
from coffin.shortcuts import render_to_response
|
|
||||||
|
|
||||||
Coffin includes drop in replacements for the following Django modules:
|
|
||||||
|
|
||||||
* :mod:`django.shortcuts`
|
|
||||||
* :mod:`django.views.generic.simple`
|
|
||||||
* :mod:`django.contrib.auth`
|
|
||||||
* :mod:`django.contrib.markup`
|
|
||||||
* :mod:`django.contrib.syndication`
|
|
||||||
* :mod:`django.template`
|
|
112
docs/make.bat
112
docs/make.bat
|
@ -1,112 +0,0 @@
|
||||||
@ECHO OFF
|
|
||||||
|
|
||||||
REM Command file for Sphinx documentation
|
|
||||||
|
|
||||||
set SPHINXBUILD=sphinx-build
|
|
||||||
set ALLSPHINXOPTS=-d _build/doctrees %SPHINXOPTS% .
|
|
||||||
if NOT "%PAPER%" == "" (
|
|
||||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "" goto help
|
|
||||||
|
|
||||||
if "%1" == "help" (
|
|
||||||
:help
|
|
||||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
|
||||||
echo. html to make standalone HTML files
|
|
||||||
echo. dirhtml to make HTML files named index.html in directories
|
|
||||||
echo. pickle to make pickle files
|
|
||||||
echo. json to make JSON files
|
|
||||||
echo. htmlhelp to make HTML files and a HTML help project
|
|
||||||
echo. qthelp to make HTML files and a qthelp project
|
|
||||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
|
||||||
echo. changes to make an overview over all changed/added/deprecated items
|
|
||||||
echo. linkcheck to check all external links for integrity
|
|
||||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "clean" (
|
|
||||||
for /d %%i in (_build\*) do rmdir /q /s %%i
|
|
||||||
del /q /s _build\*
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "html" (
|
|
||||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% _build/html
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The HTML pages are in _build/html.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "dirhtml" (
|
|
||||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% _build/dirhtml
|
|
||||||
echo.
|
|
||||||
echo.Build finished. The HTML pages are in _build/dirhtml.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "pickle" (
|
|
||||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% _build/pickle
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can process the pickle files.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "json" (
|
|
||||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% _build/json
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can process the JSON files.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "htmlhelp" (
|
|
||||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% _build/htmlhelp
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
|
||||||
.hhp project file in _build/htmlhelp.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "qthelp" (
|
|
||||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% _build/qthelp
|
|
||||||
echo.
|
|
||||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
|
||||||
.qhcp project file in _build/qthelp, like this:
|
|
||||||
echo.^> qcollectiongenerator _build\qthelp\Coffin.qhcp
|
|
||||||
echo.To view the help file:
|
|
||||||
echo.^> assistant -collectionFile _build\qthelp\Coffin.ghc
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "latex" (
|
|
||||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% _build/latex
|
|
||||||
echo.
|
|
||||||
echo.Build finished; the LaTeX files are in _build/latex.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "changes" (
|
|
||||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% _build/changes
|
|
||||||
echo.
|
|
||||||
echo.The overview file is in _build/changes.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "linkcheck" (
|
|
||||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% _build/linkcheck
|
|
||||||
echo.
|
|
||||||
echo.Link check complete; look for any errors in the above output ^
|
|
||||||
or in _build/linkcheck/output.txt.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
if "%1" == "doctest" (
|
|
||||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% _build/doctest
|
|
||||||
echo.
|
|
||||||
echo.Testing of doctests in the sources finished, look at the ^
|
|
||||||
results in _build/doctest/output.txt.
|
|
||||||
goto end
|
|
||||||
)
|
|
||||||
|
|
||||||
:end
|
|
|
@ -1,10 +0,0 @@
|
||||||
from os import path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 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)
|
|
|
@ -1,11 +0,0 @@
|
||||||
"""A Django library, but but auto-loaded via the templatetags/ directory.
|
|
||||||
|
|
||||||
Instead, to use it, it needs to be added to the builtins.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def foo(value):
|
|
||||||
return "{foo}"
|
|
||||||
|
|
||||||
from django.template import Library
|
|
||||||
register = Library()
|
|
||||||
register.filter('foo_django_builtin', foo)
|
|
|
@ -1,34 +0,0 @@
|
||||||
from coffin.contrib.syndication.feeds import Feed as OldFeed
|
|
||||||
|
|
||||||
|
|
||||||
class TestOldFeed(OldFeed):
|
|
||||||
title = 'Foo'
|
|
||||||
link = '/'
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return [1,2,3]
|
|
||||||
|
|
||||||
def item_link(self, item):
|
|
||||||
return '/item'
|
|
||||||
|
|
||||||
title_template = 'feeds_app/feed_title.html'
|
|
||||||
description_template = 'feeds_app/feed_description.html'
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from coffin.contrib.syndication.views import Feed as NewFeed
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
class TestNewFeed(NewFeed):
|
|
||||||
title = 'Foo'
|
|
||||||
link = '/'
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return [1,2,3]
|
|
||||||
|
|
||||||
def item_link(self, item):
|
|
||||||
return '/item'
|
|
||||||
|
|
||||||
title_template = 'feeds_app/feed_title.html'
|
|
||||||
description_template = 'feeds_app/feed_description.html'
|
|
|
@ -1,2 +0,0 @@
|
||||||
{# this syntax is not supported by Django #}
|
|
||||||
{{ "JINJA WAS HERE (DESCRIPTION): %s" % obj }}
|
|
|
@ -1,2 +0,0 @@
|
||||||
{# this syntax is not supported by Django #}
|
|
||||||
{{ "JINJA WAS HERE (TITLE): %s" % obj }}
|
|
|
@ -1,11 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
from django.core.management import execute_manager
|
|
||||||
try:
|
|
||||||
import settings # Assumed to be in the same directory.
|
|
||||||
except ImportError:
|
|
||||||
import sys
|
|
||||||
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
execute_manager(settings)
|
|
|
@ -1,21 +0,0 @@
|
||||||
from os import path
|
|
||||||
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
'default': {}
|
|
||||||
}
|
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
|
||||||
'templatelibs_app',
|
|
||||||
'feeds_app',
|
|
||||||
'urls_app',
|
|
||||||
)
|
|
||||||
|
|
||||||
TEMPLATE_LOADERS = (
|
|
||||||
'django.template.loaders.app_directories.load_template_source',
|
|
||||||
'django.template.loaders.filesystem.load_template_source',
|
|
||||||
)
|
|
||||||
|
|
||||||
TEMPLATE_DIRS = (path.join(path.dirname(__file__), 'templates'),)
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'urls'
|
|
|
@ -1,42 +0,0 @@
|
||||||
"""Register a number of portable filters (with a Coffin library object)
|
|
||||||
that require a compatibility layer to function correctly in both engines.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from jinja2 import Markup
|
|
||||||
from django.utils.safestring import mark_safe, mark_for_escaping
|
|
||||||
|
|
||||||
|
|
||||||
def needing_autoescape(value, autoescape=None):
|
|
||||||
return str(autoescape)
|
|
||||||
needing_autoescape.needs_autoescape = True
|
|
||||||
|
|
||||||
|
|
||||||
def jinja_safe_output(value):
|
|
||||||
return Markup(value)
|
|
||||||
|
|
||||||
def django_safe_output(value):
|
|
||||||
return mark_safe(value)
|
|
||||||
|
|
||||||
def unsafe_output(value):
|
|
||||||
return unicode(value)
|
|
||||||
|
|
||||||
|
|
||||||
def django_raw_output(value):
|
|
||||||
return value
|
|
||||||
|
|
||||||
def django_escape_output(value):
|
|
||||||
# Make sure the value is converted to unicode first, because otherwise,
|
|
||||||
# if it is already SafeData (for example, when coming from the template
|
|
||||||
# code), then mark_for_escaping would do nothing. We want to guarantee
|
|
||||||
# a EscapeData return value in this filter though.
|
|
||||||
return mark_for_escaping(unicode(value))
|
|
||||||
|
|
||||||
|
|
||||||
from coffin.template import Library
|
|
||||||
register = Library()
|
|
||||||
register.filter('needing_autoescape', needing_autoescape)
|
|
||||||
register.filter('jinja_safe_output', jinja_safe_output)
|
|
||||||
register.filter('django_safe_output', django_safe_output)
|
|
||||||
register.filter('django_raw_output', django_raw_output)
|
|
||||||
register.filter('unsafe_output', unsafe_output)
|
|
||||||
register.filter('django_escape_output', django_escape_output)
|
|
|
@ -1,9 +0,0 @@
|
||||||
"""Register a filter with a Django library object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def foo(value):
|
|
||||||
return "{foo}"
|
|
||||||
|
|
||||||
from django.template import Library
|
|
||||||
register = Library()
|
|
||||||
register.filter('foo_django', foo)
|
|
|
@ -1,15 +0,0 @@
|
||||||
"""Register a Django tag with a Coffin library object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.template import Node
|
|
||||||
|
|
||||||
class FooNode(Node):
|
|
||||||
def render(self, context):
|
|
||||||
return u'{foo}'
|
|
||||||
|
|
||||||
def do_foo(parser, token):
|
|
||||||
return FooNode()
|
|
||||||
|
|
||||||
from coffin.template import Library
|
|
||||||
register = Library()
|
|
||||||
register.tag('foo_coffin', do_foo)
|
|
|
@ -1,32 +0,0 @@
|
||||||
"""Register a Jinja2 extension with a Coffin library object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from jinja2.ext import Extension
|
|
||||||
from jinja2 import nodes
|
|
||||||
|
|
||||||
class FooExtension(Extension):
|
|
||||||
tags = set(['foo'])
|
|
||||||
|
|
||||||
def parse(self, parser):
|
|
||||||
parser.stream.next()
|
|
||||||
return nodes.Const('{foo}')
|
|
||||||
|
|
||||||
|
|
||||||
class FooWithConfigExtension(Extension):
|
|
||||||
tags = set(['foo_ex'])
|
|
||||||
|
|
||||||
def __init__(self, environment):
|
|
||||||
Extension.__init__(self, environment)
|
|
||||||
environment.extend(
|
|
||||||
foo_custom_output='foo',
|
|
||||||
)
|
|
||||||
|
|
||||||
def parse(self, parser):
|
|
||||||
parser.stream.next()
|
|
||||||
return nodes.Const('{%s}' % self.environment.foo_custom_output)
|
|
||||||
|
|
||||||
|
|
||||||
from coffin.template import Library
|
|
||||||
register = Library()
|
|
||||||
register.tag(FooExtension)
|
|
||||||
register.tag(FooWithConfigExtension, environment={'foo_custom_output': 'my_foo'})
|
|
|
@ -1,35 +0,0 @@
|
||||||
"""Register a number of non-portable, Jinja2-only filters with a Coffin
|
|
||||||
library object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from jinja2 import environmentfilter, contextfilter
|
|
||||||
|
|
||||||
@environmentfilter
|
|
||||||
def environment(environment, value):
|
|
||||||
return ""
|
|
||||||
|
|
||||||
@contextfilter
|
|
||||||
def context(context, value):
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def multiarg(value, arg1, arg2):
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def jinja_forced(value):
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def django_jinja_forced(value):
|
|
||||||
# a django filter that returns a django-safestring. It will *only*
|
|
||||||
# be added to jinja, and coffin will hopefully ensure the string
|
|
||||||
# stays safe.
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
return mark_safe(value)
|
|
||||||
|
|
||||||
|
|
||||||
from coffin.template import Library
|
|
||||||
register = Library()
|
|
||||||
register.filter('environment', environment)
|
|
||||||
register.filter('context', context)
|
|
||||||
register.filter('multiarg', multiarg)
|
|
||||||
register.filter('jinja_forced', jinja_forced, jinja2_only=True)
|
|
||||||
register.filter('django_jinja_forced', django_jinja_forced, jinja2_only=True)
|
|
|
@ -1,9 +0,0 @@
|
||||||
"""Register a Jinja2 global object with a Coffin library object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def hello_func(name):
|
|
||||||
return u"Hello %s" % name
|
|
||||||
|
|
||||||
from coffin.template import Library
|
|
||||||
register = Library()
|
|
||||||
register.object('hello', hello_func)
|
|
|
@ -1,9 +0,0 @@
|
||||||
"""Register a portable filter with a Coffin library object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def foo(value):
|
|
||||||
return "{foo}"
|
|
||||||
|
|
||||||
from coffin.template import Library
|
|
||||||
register = Library()
|
|
||||||
register.filter('foo', foo)
|
|
|
@ -1,2 +0,0 @@
|
||||||
{#- generic template for tests -#}
|
|
||||||
{{ x }}
|
|
|
@ -1,10 +0,0 @@
|
||||||
from django.conf.urls.defaults import *
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
|
||||||
(r'^url_test/', include('urls_app.urls')),
|
|
||||||
|
|
||||||
# These two are used to test that our url-tag implementation can
|
|
||||||
# deal with application namespaces / the "current app".
|
|
||||||
(r'^app/one/', include('urls_app.urls', app_name="testapp", namespace="testapp")), # default instance
|
|
||||||
(r'^app/two/', include('urls_app.urls', app_name="testapp", namespace="two")),
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
from django.conf.urls.defaults import *
|
|
||||||
|
|
||||||
urlpatterns = patterns('apps.urls_app',
|
|
||||||
# Test urls for testing reverse lookups
|
|
||||||
url(r'^$', 'views.index', name='the-index-view'),
|
|
||||||
(r'^sum/(?P<left>\d+),(?P<right>\d+)$', 'views.sum'),
|
|
||||||
)
|
|
|
@ -1,5 +0,0 @@
|
||||||
def index(r):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def sum(r):
|
|
||||||
pass
|
|
|
@ -1,31 +0,0 @@
|
||||||
from nose.plugins.skip import SkipTest
|
|
||||||
import django
|
|
||||||
|
|
||||||
|
|
||||||
class TestSyndication:
|
|
||||||
|
|
||||||
def test_old(self):
|
|
||||||
from django.http import HttpRequest
|
|
||||||
fake_request = HttpRequest()
|
|
||||||
fake_request.META['SERVER_NAME'] = 'foo'
|
|
||||||
fake_request.META['SERVER_PORT'] = 80
|
|
||||||
|
|
||||||
from apps.feeds_app.feeds import TestOldFeed
|
|
||||||
feedgen = TestOldFeed('', fake_request).get_feed(None)
|
|
||||||
s = feedgen.writeString('utf-8')
|
|
||||||
assert 'JINJA WAS HERE (TITLE)' in s
|
|
||||||
assert 'JINJA WAS HERE (DESCRIPTION)' in s
|
|
||||||
|
|
||||||
def test_new(self):
|
|
||||||
if django.VERSION < (1,2):
|
|
||||||
raise SkipTest()
|
|
||||||
|
|
||||||
from django.http import HttpRequest
|
|
||||||
fake_request = HttpRequest()
|
|
||||||
fake_request.META['SERVER_NAME'] = 'foo'
|
|
||||||
fake_request.META['SERVER_PORT'] = 80
|
|
||||||
|
|
||||||
from apps.feeds_app.feeds import TestNewFeed
|
|
||||||
response = TestNewFeed()(fake_request)
|
|
||||||
assert 'JINJA WAS HERE (TITLE)' in response.content
|
|
||||||
assert 'JINJA WAS HERE (DESCRIPTION)' in response.content
|
|
|
@ -97,18 +97,3 @@ def test_with():
|
||||||
|
|
||||||
assert env.from_string('{{ x }}{% with y as x %}{{ x }}{% endwith %}{{ x }}').render({'x': 'x', 'y': 'y'}) == 'xyx'
|
assert env.from_string('{{ x }}{% with y as x %}{{ x }}{% endwith %}{{ x }}').render({'x': 'x', 'y': 'y'}) == 'xyx'
|
||||||
|
|
||||||
|
|
||||||
def test_cache():
|
|
||||||
from coffin.template.defaulttags import CacheExtension
|
|
||||||
env = Environment(extensions=[CacheExtension])
|
|
||||||
|
|
||||||
x = 0
|
|
||||||
assert env.from_string('{%cache 500 "ab"%}{{x}}{%endcache%}').render({'x': x}) == '0'
|
|
||||||
# cache is used; Jinja2 expressions work
|
|
||||||
x += 1
|
|
||||||
assert env.from_string('{%cache 50*10 "a"+"b"%}{{x}}{%endcache%}').render({'x': x}) == '0'
|
|
||||||
# vary-arguments can be used
|
|
||||||
x += 1
|
|
||||||
assert env.from_string('{%cache 50*10 "ab" x "foo"%}{{x}}{%endcache%}').render({'x': x}) == '2'
|
|
||||||
x += 1
|
|
||||||
assert env.from_string('{%cache 50*10 "ab" x "foo"%}{{x}}{%endcache%}').render({'x': x}) == '3'
|
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
from datetime import datetime, date
|
|
||||||
from nose.tools import assert_raises
|
|
||||||
|
|
||||||
|
|
||||||
def r(s, context={}):
|
|
||||||
from coffin.common import env
|
|
||||||
return env.from_string(s).render(context)
|
|
||||||
|
|
||||||
|
|
||||||
def test_django_builtins_available():
|
|
||||||
"""Many filters have not been re-implemented specifically for
|
|
||||||
Coffin, but instead the Django version is used through an
|
|
||||||
interop-layer.
|
|
||||||
|
|
||||||
Make sure that those are properly made available in Jinja2.
|
|
||||||
"""
|
|
||||||
from coffin.template import defaultfilters
|
|
||||||
assert not hasattr(defaultfilters, 'get_digit') # has no port
|
|
||||||
assert r('{{ "23475"|get_digit("2") }}') == '7'
|
|
||||||
assert r('{{ unknown|get_digit("2") }}') == ''
|
|
||||||
|
|
||||||
|
|
||||||
def test_jinja2_builtins():
|
|
||||||
"""Ensure that the Jinja2 builtins are available, and take
|
|
||||||
precedence over the Django builtins (which we automatically convert
|
|
||||||
and install).
|
|
||||||
"""
|
|
||||||
# Django's default filter only accepts one argument.
|
|
||||||
assert r('{{ unknown|default("2", True) }}') == '2'
|
|
||||||
|
|
||||||
|
|
||||||
def test_url():
|
|
||||||
# project name is optional
|
|
||||||
assert r('{{ "urls_app.views.index"|url() }}') == '/url_test/'
|
|
||||||
assert r('{{ "apps.urls_app.views.index"|url() }}') == '/url_test/'
|
|
||||||
|
|
||||||
|
|
||||||
def test_default():
|
|
||||||
"""We make the Jinja2 default filter behave like Django's without
|
|
||||||
arguments, but still support Jinja2 extended syntax.
|
|
||||||
"""
|
|
||||||
assert r('{{ foo|default("default") }}') == 'default'
|
|
||||||
assert r('{{ foo|default("default") }}', {'foo': False}) == 'default'
|
|
||||||
assert r('{{ foo|default("default", False) }}', {'foo': False}) == 'False'
|
|
||||||
|
|
||||||
|
|
||||||
def test_pluralize():
|
|
||||||
assert r('vote{{ 0|pluralize }}') == 'votes'
|
|
||||||
assert r('vote{{ 1|pluralize }}') == 'vote'
|
|
||||||
assert r('class{{ 2|pluralize("es") }}') == 'classes'
|
|
||||||
assert r('cand{{ 0|pluralize("y", "ies") }}') == 'candies'
|
|
||||||
assert r('cand{{ 1|pluralize("y", "ies") }}') == 'candy'
|
|
||||||
assert r('cand{{ 2|pluralize("y", "ies") }}') == 'candies'
|
|
||||||
assert r('vote{{ [1,2,3]|pluralize }}') == 'votes'
|
|
||||||
assert r('anonyme{{ 0|pluralize("r", "") }}') == 'anonyme'
|
|
||||||
assert r('anonyme{{ 1|pluralize("r", "") }}') == 'anonymer'
|
|
||||||
assert r('vote{{ 1|pluralize }}') == 'vote'
|
|
||||||
assert_raises(TypeError, r, 'vote{{ x|pluralize }}', {'x': object()})
|
|
||||||
assert_raises(ValueError, r, 'vote{{ x|pluralize }}', {'x': 'foo'})
|
|
||||||
|
|
||||||
|
|
||||||
def test_floatformat():
|
|
||||||
assert r('{{ 1.3434|floatformat }}') == '1.3'
|
|
||||||
assert r('{{ 1.3511|floatformat }}') == '1.4'
|
|
||||||
assert r('{{ 1.3|floatformat(2) }}') == '1.30'
|
|
||||||
assert r('{{ 1.30|floatformat(-3) }}') == '1.300'
|
|
||||||
assert r('{{ 1.000|floatformat(3) }}') == '1.000'
|
|
||||||
assert r('{{ 1.000|floatformat(-3) }}') == '1'
|
|
||||||
assert_raises(ValueError, r, '{{ "foo"|floatformat(3) }}')
|
|
||||||
assert_raises(ValueError, r, '{{ 4.33|floatformat("foo") }}')
|
|
||||||
|
|
||||||
|
|
||||||
def test_date_stuff():
|
|
||||||
from coffin.common import env
|
|
||||||
assert r('a{{ d|date("Y") }}b', {'d': date(2007, 01, 01)}) == 'a2007b'
|
|
||||||
assert r('a{{ d|time("H") }}b', {'d': datetime(2007, 01, 01, 12, 01, 01)}) == 'a12b'
|
|
||||||
# TODO: timesince, timeuntil
|
|
||||||
|
|
||||||
# Make sure the date filters can handle unset values gracefully.
|
|
||||||
# While generally we'd like to be explicit instead of hiding errors,
|
|
||||||
# this is a particular case where it makes sense.
|
|
||||||
for f in ('date', 'time', 'timesince', 'timeuntil'):
|
|
||||||
assert r('a{{ d|%s }}b' % f) == 'ab'
|
|
||||||
assert r('a{{ d|%s }}b' % f, {'d': None}) == 'ab'
|
|
|
@ -1,46 +0,0 @@
|
||||||
"""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'
|
|
||||||
|
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
|
@ -1,136 +0,0 @@
|
||||||
"""Test the various features of our custom library object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from nose.tools import assert_raises
|
|
||||||
|
|
||||||
from jinja2 import TemplateAssertionError as Jinja2TemplateAssertionError
|
|
||||||
from django.template import Template, Context, \
|
|
||||||
TemplateSyntaxError as DjangoTemplateSyntaxError
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: It would be preferrable to split these tests into those checks
|
|
||||||
# which actually test the Library object, and those which test the assembly
|
|
||||||
# of the Environment instance. Testcode for the former could be written more
|
|
||||||
# cleanly by creating the library instances within the test code and
|
|
||||||
# registering them manually with the environment, rather than having to
|
|
||||||
# place them in fake Django apps in completely different files to have
|
|
||||||
# them loaded.
|
|
||||||
# The tests for the compatibility layer could also be factored out.
|
|
||||||
|
|
||||||
|
|
||||||
def test_nodes_and_extensions():
|
|
||||||
"""Test availability of registered nodes/extensions.
|
|
||||||
"""
|
|
||||||
from coffin.common import env
|
|
||||||
|
|
||||||
# Jinja2 extensions, loaded from a Coffin library
|
|
||||||
assert env.from_string('a{% foo %}b').render() == 'a{foo}b'
|
|
||||||
assert env.from_string('a{% foo_ex %}b').render() == 'a{my_foo}b'
|
|
||||||
|
|
||||||
# Django tags, loaded from a Coffin library
|
|
||||||
assert Template('{% load django_tags %}a{% foo_coffin %}b').render(Context()) == 'a{foo}b'
|
|
||||||
|
|
||||||
|
|
||||||
def test_objects():
|
|
||||||
"""For coffin, global objects can be registered.
|
|
||||||
"""
|
|
||||||
from coffin.common import env
|
|
||||||
|
|
||||||
# Jinja2 global objects, loaded from a Coffin library
|
|
||||||
assert env.from_string('{{ hello("John") }}').render() == 'Hello John'
|
|
||||||
|
|
||||||
|
|
||||||
def test_filters():
|
|
||||||
"""Test availability of registered filters.
|
|
||||||
"""
|
|
||||||
from coffin.common import env
|
|
||||||
|
|
||||||
# Filter registered with a Coffin library is available in Django and Jinja2
|
|
||||||
assert env.from_string('a{{ "b"|foo }}c').render() == 'a{foo}c'
|
|
||||||
assert Template('{% load portable_filters %}a{{ "b"|foo }}c').render(Context()) == 'a{foo}c'
|
|
||||||
|
|
||||||
# Filter registered with a Django library is also available in Jinja2
|
|
||||||
Template('{% load django_library %}{{ "b"|foo_django }}').render(Context())
|
|
||||||
assert env.from_string('a{{ "b"|foo }}c').render() == 'a{foo}c'
|
|
||||||
|
|
||||||
# Some filters, while registered with a Coffin library, are only
|
|
||||||
# available in Jinja2:
|
|
||||||
# - when using @environmentfilter
|
|
||||||
env.from_string('{{ "b"|environment }}')
|
|
||||||
assert_raises(Exception, Template, '{% load jinja2_filters %}{{ "b"|environment }}')
|
|
||||||
# - when using @contextfilter
|
|
||||||
env.from_string('{{ "b"|context }}')
|
|
||||||
assert_raises(Exception, Template, '{% load jinja2_filters %}{{ "b"|context }}')
|
|
||||||
# - when requiring more than one argument
|
|
||||||
env.from_string('{{ "b"|multiarg(1,2) }}')
|
|
||||||
assert_raises(Exception, Template, '{% load jinja2_filters %}{{ "b"|multiarg }}')
|
|
||||||
# - when Jinja2-exclusivity is explicitly requested
|
|
||||||
env.from_string('{{ "b"|jinja_forced }}')
|
|
||||||
assert_raises(Exception, Template, '{% load jinja2_filters %}{{ "b"|jinja_forced }}')
|
|
||||||
# [bug] Jinja2-exclusivity caused the compatibility layer to be not
|
|
||||||
# applied, causing for example safe strings to be escaped.
|
|
||||||
assert env.from_string('{{ "><"|django_jinja_forced }}').render() == '><'
|
|
||||||
|
|
||||||
|
|
||||||
def test_env_builtins_django():
|
|
||||||
"""Test that when the environment is assembled, Django libraries which
|
|
||||||
are included in the list of builtins are properly supported.
|
|
||||||
"""
|
|
||||||
from coffin.common import get_env
|
|
||||||
from coffin.template import add_to_builtins
|
|
||||||
add_to_builtins('apps.django_lib')
|
|
||||||
assert get_env().from_string('a{{ "b"|foo_django_builtin }}c').render() == 'a{foo}c'
|
|
||||||
|
|
||||||
|
|
||||||
def test_filter_compat_safestrings():
|
|
||||||
"""Test filter compatibility layer with respect to safe strings.
|
|
||||||
"""
|
|
||||||
from coffin.common import env
|
|
||||||
env.autoescape = True
|
|
||||||
|
|
||||||
# Jinja-style safe output strings are considered "safe" by both engines
|
|
||||||
assert env.from_string('{{ "<b>"|jinja_safe_output }}').render() == '<b>'
|
|
||||||
# TODO: The below actually works regardless of our converting between
|
|
||||||
# the same string types: Jinja's Markup() strings are actually immune
|
|
||||||
# to Django's escape() attempt, since they have a custom version of
|
|
||||||
# replace() that operates on an already escaped version.
|
|
||||||
assert Template('{% load compat_filters %}{{ "<b>"|jinja_safe_output }}').render(Context()) == '<b>'
|
|
||||||
|
|
||||||
# Unsafe, unmarked output strings are considered "unsafe" by both engines
|
|
||||||
assert env.from_string('{{ "<b>"|unsafe_output }}').render() == '<b>'
|
|
||||||
assert Template('{% load compat_filters %}{{ "<b>"|unsafe_output }}').render(Context()) == '<b>'
|
|
||||||
|
|
||||||
# Django-style safe output strings are considered "safe" by both engines
|
|
||||||
assert env.from_string('{{ "<b>"|django_safe_output }}').render() == '<b>'
|
|
||||||
assert Template('{% load compat_filters %}{{ "<b>"|django_safe_output }}').render(Context()) == '<b>'
|
|
||||||
|
|
||||||
|
|
||||||
def test_filter_compat_escapetrings():
|
|
||||||
"""Test filter compatibility layer with respect to strings flagged as
|
|
||||||
"wanted for escaping".
|
|
||||||
"""
|
|
||||||
from coffin.common import env
|
|
||||||
env.autoescape = False
|
|
||||||
|
|
||||||
# Django-style "force escaping" works in both engines
|
|
||||||
assert env.from_string('{{ "<b>"|django_escape_output }}').render() == '<b>'
|
|
||||||
assert Template('{% load compat_filters %}{{ "<b>"|django_escape_output }}').render(Context()) == '<b>'
|
|
||||||
|
|
||||||
|
|
||||||
def test_filter_compat_other():
|
|
||||||
"""Test other features of the filter compatibility layer.
|
|
||||||
"""
|
|
||||||
# A Django filter with @needs_autoescape works in Jinja2
|
|
||||||
from coffin.common import env
|
|
||||||
env.autoescape = True
|
|
||||||
assert env.from_string('{{ "b"|needing_autoescape }}').render() == 'True'
|
|
||||||
env.autoescape = False
|
|
||||||
assert env.from_string('{{ "b"|needing_autoescape }}').render() == 'False'
|
|
||||||
|
|
||||||
# [bug] @needs_autoescape also (still) works correctly in Django
|
|
||||||
assert Template('{% load compat_filters %}{{ "b"|needing_autoescape }}').render(Context()) == 'True'
|
|
||||||
|
|
||||||
# The Django filters can handle "Undefined" values
|
|
||||||
assert env.from_string('{{ doesnotexist|django_raw_output }}').render() == ''
|
|
||||||
|
|
||||||
# TODO: test @stringfilter
|
|
|
@ -1,5 +0,0 @@
|
||||||
def test_render():
|
|
||||||
"""Test the render shortcut."""
|
|
||||||
from coffin.shortcuts import render
|
|
||||||
response = render(None, 'render-x.html', {'x': 'foo'})
|
|
||||||
assert response.content == 'foo'
|
|
|
@ -1,56 +0,0 @@
|
||||||
"""Tests for ``coffin.template``.
|
|
||||||
|
|
||||||
``coffin.template.library``, ``coffin.template.defaultfilters`` and
|
|
||||||
``coffin.template.defaulttags`` have their own test modules.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_template_class():
|
|
||||||
from coffin.template import Template
|
|
||||||
from coffin.common import env
|
|
||||||
|
|
||||||
# initializing a template directly uses Coffin's Jinja
|
|
||||||
# environment - we know it does if our tags are available.
|
|
||||||
t = Template('{% spaceless %}{{ ""|truncatewords }}{% endspaceless %}')
|
|
||||||
assert t.environment == env
|
|
||||||
|
|
||||||
# render can accept a Django context object
|
|
||||||
from django.template import Context
|
|
||||||
c = Context()
|
|
||||||
c.update({'x': '1'}) # update does a push
|
|
||||||
c.update({'y': '2'})
|
|
||||||
assert Template('{{x}};{{y}}').render(c) == '1;2'
|
|
||||||
|
|
||||||
# [bug] render can handle nested Context objects
|
|
||||||
c1 = Context(); c2 = Context(); c3 = Context()
|
|
||||||
c3['foo'] = 'bar'
|
|
||||||
c2.update(c3)
|
|
||||||
c1.update(c2)
|
|
||||||
assert Template('{{foo}}').render(c1) == 'bar'
|
|
||||||
|
|
||||||
# There is a "origin" attribute for Django compatibility
|
|
||||||
assert Template('{{foo}}').origin.name == '<template>'
|
|
||||||
|
|
||||||
|
|
||||||
def test_render_to_string():
|
|
||||||
# [bug] Test that the values given directly do overwrite does that
|
|
||||||
# are already exist in the given context_instance. Due to a bug this
|
|
||||||
# was previously not the case.
|
|
||||||
from django.template import Context
|
|
||||||
from coffin.template.loader import render_to_string
|
|
||||||
assert render_to_string('render-x.html', {'x': 'new'},
|
|
||||||
context_instance=Context({'x': 'old'})) == 'new'
|
|
||||||
|
|
||||||
# [bug] Test that the values from context_instance actually make it
|
|
||||||
# into the template.
|
|
||||||
assert render_to_string('render-x.html',
|
|
||||||
context_instance=Context({'x': 'foo'})) == 'foo'
|
|
||||||
|
|
||||||
# [bug] Call without the optional ``context_instance`` argument works
|
|
||||||
assert render_to_string('render-x.html', {'x': 'foo'}) == 'foo'
|
|
||||||
|
|
||||||
# ``dictionary`` argument may be a Context instance
|
|
||||||
assert render_to_string('render-x.html', Context({'x': 'foo'})) == 'foo'
|
|
||||||
|
|
||||||
# [bug] Both ``dictionary`` and ``context_instance`` may be
|
|
||||||
# Context objects
|
|
||||||
assert render_to_string('render-x.html', Context({'x': 'foo'}), context_instance=Context()) == 'foo'
|
|
|
@ -1,3 +0,0 @@
|
||||||
def test_import():
|
|
||||||
# [bug] make sure the coffin.views module is importable.
|
|
||||||
from coffin import views
|
|
Loading…
Reference in New Issue