liberating jingo from zamboni

This commit is contained in:
Jeff Balogh 2010-02-09 16:35:16 -08:00
commit 5caec33a4d
18 changed files with 567 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
docs/_build
.py[co]

27
LICENSE Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2010, Jeff Balogh.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of jingo nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3
MANIFEST.in Normal file
View File

@ -0,0 +1,3 @@
include LICENSE
include README.rst
prune examples

10
README.rst Normal file
View File

@ -0,0 +1,10 @@
=====
Jingo
=====
``jingo`` is an adapter for using
`Jinja2 <http://jinja.pocoo.org/2/documentation/>`_ templates within Django.
See the full docs at http://jbalogh.me/projects/jingo.

89
docs/Makefile Normal file
View File

@ -0,0 +1,89 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/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 $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/zamboni.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/zamboni.qhc"
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

30
docs/conf.py Normal file
View File

@ -0,0 +1,30 @@
import os
import sys
sys.path.append(os.path.abspath('..'))
import jingo
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
extensions = ['sphinx.ext.autodoc']
# General information about the project.
project = u'Jingo'
copyright = u'2010, The Zamboni Collective'
# 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.
#
# version: The short X.Y version.
# release: The full version, including alpha/beta/rc tags.
version = release = jingo.__version__
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build']

100
docs/index.rst Normal file
View File

@ -0,0 +1,100 @@
.. _jingo:
.. module:: jingo
A page about Jingo.
===================
Jingo is an adapter for using
`Jinja2 <http://jinja.pocoo.org/2/documentation/>`_ templates within Django.
Why are we already replacing the templates? AMO's current PHP templates let you
go hog-wild with logic in the templates, while Django is extremely restrictive.
Jinja loosens those restrictions somewhat, providing a more powerful engine with
the beauty of Django's templates. The tipping point for me was the verbosity of
doing L10n in Django templates.
Settings
--------
If you want to configure the Jinja environment, use ``JINJA_CONFIG`` in
``settings.py``. It can be a dict or a function that returns a dict. ::
JINJA_CONFIG = {'autoescape': False}
or ::
def JINJA_CONFIG():
return {'the_answer': 41 + 1}
Rendering
---------
At this point, Jingo only provides two shortcuts for rendering templates.
.. autofunction:: jingo.render
The basic usage is to pass an ``HttpRequest`` and a template name. All the
processors in ``settings.CONTEXT_PROCESSORS`` will be applied to the
context, just like when you use a ``RequestContext`` in Django. Any extra
keyword arguments are passed directly to ``http.HttpResponse``.
.. autofunction:: jingo.views.direct_to_template
Template Helpers
----------------
Instead of template tags, Jinja encourages you to add functions and filters to
the templating environment. In ``jingo``, we call these helpers. When the
Jinja environment is initialized, ``jingo`` will try to open a ``helpers.py``
file from every app in ``INSTALLED_APPS``. Two decorators are provided to ease
the environment extension:
.. function:: jingo.register.filter
Adds the decorated function to Jinja's filter library.
.. function:: jingo.register.function
Adds the decorated function to Jinja's global namespace.
.. highlight:: jinja
Default Helpers
~~~~~~~~~~~~~~~
Helpers are available in all templates automatically, without any extra
loading.
.. automodule:: jingo.helpers
:members:
Template Environment
--------------------
A single Jinja ``Environment`` is created for use in all templates. This is
available as ``jingo.env`` if you need to work with the ``Environment``.
Localization
------------
Since we all love L10n, let's see what it looks like in Jinja templates::
<h2>{{ _('Reviews for {0}')|f(addon.name) }}</h2>
The simple way is to use the familiar underscore and string within a ``{{ }}``
moustache block. ``f`` is an interpolation filter documented below. Sphinx
could create a link if I knew how to do that.
The other method uses Jinja's ``trans`` tag::
{% trans user=review.user|user_link, date=review.created|datetime %}
by {{ user }} on {{ date }}
{% endtrans %}
``trans`` is nice when you have a lot of text or want to inject some variables
directly. Both methods are useful, pick the one that makes you happy.

View File

View File

@ -0,0 +1 @@
JINJA_CONFIG = {}

36
fabfile.py vendored Normal file
View File

@ -0,0 +1,36 @@
"""
Creating standalone Django apps is a PITA because you're not in a project, so
you don't have a settings.py file. I can never remember to define
DJANGO_SETTINGS_MODULE, so I run these commands which get the right env
automatically.
"""
import functools
import os
from fabric.api import local, cd, env
from fabric.contrib.project import rsync_project
NAME = os.path.basename(os.path.dirname(__file__))
ROOT = os.path.abspath(os.path.dirname(__file__))
os.environ['DJANGO_SETTINGS_MODULE'] = '%s-project.settings' % NAME
os.environ['PYTHONPATH'] = os.pathsep.join([ROOT,
os.path.join(ROOT, 'examples')])
env.hosts = ['jbalogh.me']
local = functools.partial(local, capture=False)
def doc(kind='html'):
with cd('docs'):
local('make clean %s' % kind)
def test():
local('nosetests')
def updoc():
doc('dirhtml')
rsync_project('p/%s' % NAME, 'docs/_build/dirhtml/', delete=True)

94
jingo/__init__.py Normal file
View File

@ -0,0 +1,94 @@
"""Adapter for using Jinja2 with Django."""
from django import http
from django.conf import settings
from django.template.context import get_standard_processors
import jinja2
VERSION = (0, 3)
__version__ = '.'.join(map(str, VERSION))
def get_env():
"""Configure and return a jinja2 Environment."""
# Mimic Django's setup by loading templates from directories in
# TEMPLATE_DIRS and packages in INSTALLED_APPS.
x = ((jinja2.FileSystemLoader, settings.TEMPLATE_DIRS),
(jinja2.PackageLoader, settings.INSTALLED_APPS))
loaders = [loader(p) for loader, places in x for p in places]
opts = {'trim_blocks': True,
'extensions': ['jinja2.ext.i18n'],
'autoescape': True,
'auto_reload': settings.DEBUG,
'loader': jinja2.ChoiceLoader(loaders),
}
if hasattr(settings, 'JINJA_CONFIG'):
if hasattr(settings.JINJA_CONFIG, '__call__'):
config = settings.JINJA_CONFIG()
else:
config = settings.JINJA_CONFIG
opts.update(config)
e = jinja2.Environment(**opts)
# TODO: use real translations
e.install_null_translations()
return e
def render(request, template, context=None, **kwargs):
"""
Shortcut like Django's ``render_to_response``, but better.
Minimal usage, with only a request object and a template name::
return jingo.render(request, 'template.html')
With template context and keywords passed to
:class:`django.http.HttpResponse`::
return jingo.render(request, 'template.html',
{'some_var': 42}, status=209)
"""
if context is None:
context = {}
for processor in get_standard_processors():
context.update(processor(request))
rendered = env.get_template(template).render(**context)
return http.HttpResponse(rendered, **kwargs)
def load_helpers():
"""Try to import ``helpers.py`` from each app in INSTALLED_APPS."""
for app in settings.INSTALLED_APPS:
try:
__import__('%s.helpers' % app)
except ImportError:
pass
class Register(object):
"""Decorators to add filters and functions to the template Environment."""
def __init__(self, env):
self.env = env
def filter(self, f):
"""Adds the decorated function to Jinja's filter library."""
self.env.filters[f.__name__] = f
return f
def function(self, f):
"""Adds the decorated function to Jinja's global namespace."""
self.env.globals[f.__name__] = f
return f
env = get_env()
register = Register(env)
# Import down here after the env is initialized.
from . import helpers
load_helpers()

48
jingo/helpers.py Normal file
View File

@ -0,0 +1,48 @@
from django.utils.translation import ugettext as _
from django.template.defaulttags import CsrfTokenNode
import jinja2
from jingo import register
@register.function
@jinja2.contextfunction
def csrf(context):
return jinja2.Markup(CsrfTokenNode().render(context))
@register.filter
def f(string, *args, **kwargs):
"""
Uses ``str.format`` for string interpolation.
>>> {{ "{0} arguments and {x} arguments"|f('positional', x='keyword') }}
"positional arguments and keyword arguments"
"""
string = unicode(string)
return string.format(*args, **kwargs)
@register.filter
def nl2br(string):
"""Turn newlines into <br>."""
return jinja2.Markup('<br>'.join(jinja2.escape(string).splitlines()))
@register.filter
def datetime(t, format=_('%B %d, %Y')):
"""Call ``datetime.strftime`` with the given format string."""
return t.strftime(format)
@register.filter
def ifeq(a, b, text):
"""Return ``text`` if ``a == b``."""
return jinja2.Markup(text if a == b else '')
@register.filter
def class_selected(a, b):
"""Return ``'class="selected"'`` if ``a == b``."""
return ifeq(a, b, 'class="selected"')

View File

@ -0,0 +1,16 @@
from nose.tools import eq_
from mock import Mock, patch, sentinel
import jingo
@patch('jingo.env')
def test_render(mock_env):
mock_template = Mock()
mock_env.get_template.return_value = mock_template
response = jingo.render(Mock(), sentinel.template, status=32)
mock_env.get_template.assert_called_with(sentinel.template)
assert mock_template.render.called
eq_(response.status_code, 32)

View File

@ -0,0 +1,60 @@
"""Tests for the jingo's builtin helpers."""
from datetime import datetime
from nose.tools import eq_
import jingo
def render(s, context={}):
t = jingo.env.from_string(s)
return t.render(**context)
def test_f():
s = render('{{ "{0} : {z}"|f("a", z="b") }}')
eq_(s, 'a : b')
def test_nl2br():
text = "some\ntext\n\nwith\nnewlines"
s = render('{{ x|nl2br }}', {'x': text})
eq_(s, "some<br>text<br><br>with<br>newlines")
def test_datetime():
time = datetime(2009, 12, 25, 10, 11, 12)
s = render('{{ d|datetime }}', {'d': time})
eq_(s, 'December 25, 2009')
s = render('{{ d|datetime("%Y-%m-%d %H:%M:%S") }}', {'d': time})
eq_(s, '2009-12-25 10:11:12')
def test_ifeq():
eq_context = {'a': 1, 'b': 1}
neq_context = {'a': 1, 'b': 2}
s = render('{{ a|ifeq(b, "<b>something</b>") }}', eq_context)
eq_(s, '<b>something</b>')
s = render('{{ a|ifeq(b, "<b>something</b>") }}', neq_context)
eq_(s, '')
def test_class_selected():
eq_context = {'a': 1, 'b': 1}
neq_context = {'a': 1, 'b': 2}
s = render('{{ a|class_selected(b) }}', eq_context)
eq_(s, 'class="selected"')
s = render('{{ a|class_selected(b) }}', neq_context)
eq_(s, '')
def test_csrf():
s = render('{{ csrf() }}', {'csrf_token': 'fffuuu'})
eq_(s, "<div style='display:none'>"
"<input type='hidden' name='csrfmiddlewaretoken' value='fffuuu' />"
"</div>")

10
jingo/tests/test_views.py Normal file
View File

@ -0,0 +1,10 @@
from mock import patch, sentinel
import jingo.views
@patch('jingo.render')
def test_direct_to_template(mock_render):
request = sentinel.request
jingo.views.direct_to_template(request, 'base.html', x=1)
mock_render.assert_called_with(request, 'base.html', {'x': 1})

5
jingo/views.py Normal file
View File

@ -0,0 +1,5 @@
import jingo
def direct_to_template(request, template, **kwargs):
return jingo.render(request, template, kwargs)

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
# These are the reqs to build docs and run tests.
sphinx
jinja2
nose
mock
-e svn+http://code.djangoproject.com/svn/django/trunk@12335#egg=Django
fabric

29
setup.py Normal file
View File

@ -0,0 +1,29 @@
from setuptools import setup
setup(
name='jingo',
version='0.3',
description='An adapter for using Jinja2 templates with Django.',
long_description=open('README.rst').read(),
author='Jeff Balogh',
author_email='jbalogh@mozilla.com',
url='http://github.com/jbalogh/jingo',
license='BSD',
packages=['jingo'],
include_package_data=True,
zip_safe=False,
install_requires=['jinja2'],
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
# I don't know what exactly this means, but why not?
'Environment :: Web Environment :: Mozilla',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules',
]
)