Merge pull request #1755 from takeflight/feature/jinja2-template-functions

Add rudimentary Jinja2 template tag support
pull/1797/head
Karl Hobley 2015-10-06 22:00:47 +01:00
commit 4dc09dfb22
14 zmienionych plików z 444 dodań i 3 usunięć

Wyświetl plik

@ -12,3 +12,4 @@ Advanced topics
privacy
customisation/index
third_party_tutorials
jinja2

Wyświetl plik

@ -0,0 +1,101 @@
.. _jinja2:
=======================
Jinja2 template support
=======================
Wagtail supports Jinja2 templating for all front end features. More information on each of the template tags below can be found in the :ref:`writing_templates` documentation.
Configuring Django
==================
Django needs to be configured to support Jinja2 templates. As the Wagtail admin is written using regular Django templates, Django has to be configured to use both templating engines. Wagtail supports the Jinja2 backend that ships with Django 1.8 and above. Add the following configuration to the ``TEMPLATES`` setting for your app:
.. code-block:: python
TEMPLATES = [
# ...
{
'BACKEND': 'django.template.backends.jinja2.Jinja2',
'APP_DIRS': True,
'OPTIONS': {
'extensions': [
'wagtail.wagtailcore.templatetags.jinja2.core',
'wagtail.wagtailadmin.templatetags.jinja2.userbar',
'wagtail.wagtailimages.templatetags.jinja2.images',
],
},
}
]
Jinja templates must be placed in a ``jinja2/`` directory in your app. The template for an ``EventPage`` model in an ``events`` app should be created at ``events/jinja2/events/event_page.html``.
By default, the Jinja environment does not have any Django functions or filters. The Django documentation has more information on `configuring Jinja for Django <https://docs.djangoproject.com/en/1.8/topics/templates/#django.template.backends.jinja2.Jinja2>`_.
``self`` in templates
=====================
In Django templates, ``self`` is used to refer to the current page, stream block, or field panel. In Jinja, ``self`` is reserved for internal use. When writing Jinja templates, use ``page`` to refer to pages, ``value`` for stream blocks, and ``field_panel`` for field panels.
Template functions & filters
============================
``pageurl()``
~~~~~~~~~~~~~
Generate a URL for a Page instance:
.. code-block:: html+jinja
<a href="{{ pageurl(page.more_information) }}">More information</a>
See :ref:`pageurl_tag` for more information
``slugurl()``
~~~~~~~~~~~~~
Generate a URL for a Page with a slug:
.. code-block:: html+jinja
<a href="{{ pageurl("about") }}">About us</a>
See :ref:`slugurl_tag` for more information
``image()``
~~~~~~~~~~~
Resize an image, and print an ``<img>`` tag:
.. code-block:: html+jinja
{# Print an image tag #}
{{ image(page.header_image, "fill-1024x200", class="header-image") }}
{# Resize an image #}
{% set background=image(page.background_image, "max-1024x1024") %}
<div class="wrapper" style="background-image: url({{ background.url }});">
See :ref:`image_tag` for more information
``|richtext``
~~~~~~~~~~~~~
Transform Wagtail's internal HTML representation, expanding internal references to pages and images.
.. code-block:: html+jinja
{{ page.body|richtext }}
See :ref:`rich-text-filter` for more information
``wagtailuserbar()``
~~~~~~~~~~~~~~~~~~~~
Output the Wagtail contextual flyout menu for editing pages from the front end
.. code-block:: html+jinja
{{ wagtailuserbar() }}
See :ref:`wagtailuserbar_tag` for more information

Wyświetl plik

@ -1,3 +1,5 @@
.. _writing_templates:
=================
Writing templates
=================
@ -80,7 +82,6 @@ In addition to Django's standard tags and filters, Wagtail provides some of its
Images (tag)
~~~~~~~~~~~~
The ``image`` tag inserts an XHTML-compatible ``img`` element into the page, setting its ``src``, ``width``, ``height`` and ``alt``. See also :ref:`image_tag_alt`.
The syntax for the tag is thus::
@ -147,6 +148,8 @@ Wagtail embeds and images are included at their full width, which may overflow t
Internal links (tag)
~~~~~~~~~~~~~~~~~~~~
.. _pageurl_tag:
``pageurl``
-----------
@ -158,8 +161,10 @@ Takes a Page object and returns a relative URL (``/foo/bar/``) if within the sam
...
<a href="{% pageurl self.blog_page %}">
slugurl
--------
.. _slugurl_tag:
``slugurl``
------------
Takes any ``slug`` as defined in a page's "Promote" tab and returns the URL for the matching Page. Like ``pageurl``, will try to provide a relative link if possible, but will default to an absolute link if on a different site. This is most useful when creating shared page furniture e.g top level navigation or site-wide links.
@ -186,6 +191,7 @@ Used to load anything from your static files directory. Use of this tag avoids r
Notice that the full path name is not required and the path snippet you enter only need begin with the parent app's directory name.
.. _wagtailuserbar_tag:
Wagtail User Bar
================

Wyświetl plik

@ -35,6 +35,7 @@ deps =
pytz==2014.7
Embedly
Willow==0.2
jinja2==2.8
coverage
dj17: Django>=1.7.1,<1.8

Wyświetl plik

@ -56,6 +56,17 @@ if django.VERSION >= (1, 8):
],
},
},
{
'BACKEND': 'django.template.backends.jinja2.Jinja2',
'APP_DIRS': True,
'OPTIONS': {
'extensions': [
'wagtail.wagtailcore.templatetags.jinja2.core',
'wagtail.wagtailadmin.templatetags.jinja2.userbar',
'wagtail.wagtailimages.templatetags.jinja2.images',
],
},
},
]
else:
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + (

Wyświetl plik

@ -0,0 +1,19 @@
from __future__ import absolute_import
import jinja2
from jinja2.ext import Extension
from .wagtailuserbar import wagtailuserbar
class WagtailUserbarExtension(Extension):
def __init__(self, environment):
super(WagtailUserbarExtension, self).__init__(environment)
self.environment.globals.update({
'wagtailuserbar': jinja2.contextfunction(wagtailuserbar),
})
# Nicer import names
userbar = WagtailUserbarExtension

Wyświetl plik

@ -0,0 +1,52 @@
from __future__ import absolute_import, unicode_literals
import unittest
import django
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.test import TestCase
from wagtail.wagtailcore.models import Page, PAGE_TEMPLATE_VAR, Site
@unittest.skipIf(django.VERSION < (1, 8), 'Multiple engines only supported in Django>=1.8')
class TestCoreJinja(TestCase):
def setUp(self):
# This does not exist on Django<1.8
from django.template import engines
self.engine = engines['jinja2']
self.user = get_user_model().objects.create_superuser(username='test', email='test@email.com', password='password')
self.homepage = Page.objects.get(id=2)
def render(self, string, context=None, request_context=True):
if context is None:
context = {}
template = self.engine.from_string(string)
return template.render(context)
def dummy_request(self, user=None):
site = Site.objects.get(is_default_site=True)
request = self.client.get('/')
request.site = site
request.user = user or AnonymousUser()
return request
def test_userbar(self):
content = self.render('{{ wagtailuserbar() }}', {
PAGE_TEMPLATE_VAR: self.homepage,
'request': self.dummy_request(self.user)})
self.assertIn("<!-- Wagtail user bar embed code -->", content)
def test_userbar_anonymous_user(self):
content = self.render('{{ wagtailuserbar() }}', {
PAGE_TEMPLATE_VAR: self.homepage,
'request': self.dummy_request()})
# Make sure nothing was rendered
self.assertEqual(content, '')

Wyświetl plik

@ -307,5 +307,8 @@ class StreamValue(collections.Sequence):
def __repr__(self):
return repr(list(self))
def __html__(self):
return self.__str__()
def __str__(self):
return self.stream_block.render(self)

Wyświetl plik

@ -0,0 +1,24 @@
from __future__ import absolute_import
import jinja2
from jinja2.ext import Extension
from .wagtailcore_tags import pageurl, richtext, slugurl, wagtail_version
class WagtailCoreExtension(Extension):
def __init__(self, environment):
super(WagtailCoreExtension, self).__init__(environment)
self.environment.globals.update({
'pageurl': jinja2.contextfunction(pageurl),
'slugurl': jinja2.contextfunction(slugurl),
'wagtail_version': wagtail_version,
})
self.environment.filters.update({
'richtext': richtext,
})
# Nicer import names
core = WagtailCoreExtension

Wyświetl plik

@ -0,0 +1,55 @@
from __future__ import absolute_import, unicode_literals
import unittest
import django
from django.test import TestCase
from wagtail.wagtailcore import __version__
from wagtail.wagtailcore.models import Page, Site
@unittest.skipIf(django.VERSION < (1, 8), 'Multiple engines only supported in Django>=1.8')
class TestCoreJinja(TestCase):
def setUp(self):
# This does not exist on Django<1.8
from django.template import engines
self.engine = engines['jinja2']
def render(self, string, context=None, request_context=True):
if context is None:
context = {}
# Add a request to the template, to simulate a RequestContext
if request_context:
site = Site.objects.get(is_default_site=True)
request = self.client.get('/test/', HTTP_HOST=site.hostname)
request.site = site
context['request'] = request
template = self.engine.from_string(string)
return template.render(context)
def test_richtext(self):
richtext = '<p>Merry <a linktype="page" id="2">Christmas</a>!</p>'
self.assertEqual(
self.render('{{ text|richtext }}', {'text': richtext}),
'<div class="rich-text"><p>Merry <a href="/">Christmas</a>!</p></div>')
def test_pageurl(self):
page = Page.objects.get(pk=2)
self.assertEqual(
self.render('{{ pageurl(page) }}', {'page': page}),
page.url)
def test_slugurl(self):
page = Page.objects.get(pk=2)
self.assertEqual(
self.render('{{ slugurl(page.slug) }}', {'page': page}),
page.url)
def test_wagtail_version(self):
self.assertEqual(
self.render('{{ wagtail_version() }}'),
__version__)

Wyświetl plik

@ -1,8 +1,13 @@
import json
import unittest
import django
from django.apps import apps
from django.test import TestCase
from django.db import models
from django.template import Template, Context
from django.utils.safestring import SafeText
from django.utils.six import text_type
from wagtail.tests.testapp.models import StreamModel
from wagtail.wagtailcore import blocks
@ -128,3 +133,56 @@ class TestStreamValueAccess(TestCase):
self.assertEqual(len(fetched_body), 1)
self.assertIsInstance(fetched_body[0].value, RichText)
self.assertEqual(fetched_body[0].value.source, "<h2>hello world</h2>")
class TestStreamFieldRenderingBase(TestCase):
def setUp(self):
self.image = Image.objects.create(
title='Test image',
file=get_test_image_file())
self.instance = StreamModel.objects.create(body=json.dumps([
{'type': 'rich_text', 'value': '<p>Rich text</p>'},
{'type': 'image', 'value': self.image.pk},
{'type': 'text', 'value': 'Hello, World!'}]))
img_tag = self.image.get_rendition('original').img_tag()
self.expected = ''.join([
'<div class="block-rich_text"><div class="rich-text"><p>Rich text</p></div></div>',
'<div class="block-image">{}</div>'.format(img_tag),
'<div class="block-text">Hello, World!</div>',
])
class TestStreamFieldRendering(TestStreamFieldRenderingBase):
def test_to_string(self):
rendered = text_type(self.instance.body)
self.assertHTMLEqual(rendered, self.expected)
self.assertIsInstance(rendered, SafeText)
class TestStreamFieldDjangoRendering(TestStreamFieldRenderingBase):
def render(self, string, context):
return Template(string).render(Context(context))
def test_render(self):
rendered = self.render('{{ instance.body }}', {
'instance': self.instance})
self.assertHTMLEqual(rendered, self.expected)
@unittest.skipIf(django.VERSION < (1, 8), 'Multiple engines only supported in Django>=1.8')
class TestStreamFieldJinjaRendering(TestStreamFieldRenderingBase):
def setUp(self):
# This does not exist on Django<1.8
super(TestStreamFieldJinjaRendering, self).setUp()
from django.template import engines
self.engine = engines['jinja2']
def render(self, string, context):
return self.engine.from_string(string).render(context)
def test_render(self):
rendered = self.render('{{ instance.body }}', {
'instance': self.instance})
self.assertHTMLEqual(rendered, self.expected)

Wyświetl plik

@ -454,6 +454,9 @@ class AbstractRendition(models.Model):
else:
return mark_safe('<img %s>' % self.attrs)
def __html__(self):
return self.img_tag()
class Meta:
abstract = True

Wyświetl plik

@ -0,0 +1,41 @@
from __future__ import absolute_import
from jinja2.ext import Extension
from wagtail.wagtailimages.models import SourceImageIOError
def image(image, filterspec, **attrs):
if not image:
return ''
try:
rendition = image.get_rendition(filterspec)
except SourceImageIOError:
# It's fairly routine for people to pull down remote databases to their
# local dev versions without retrieving the corresponding image files.
# In such a case, we would get a SourceImageIOError at the point where we try to
# create the resized version of a non-existent image. Since this is a
# bit catastrophic for a missing image, we'll substitute a dummy
# Rendition object so that we just output a broken link instead.
Rendition = image.renditions.model # pick up any custom Image / Rendition classes that may be in use
rendition = Rendition(image=image, width=0, height=0)
rendition.file.name = 'not-found'
if attrs:
return rendition.img_tag(attrs)
else:
return rendition
class WagtailImagesExtension(Extension):
def __init__(self, environment):
super(WagtailImagesExtension, self).__init__(environment)
self.environment.globals.update({
'image': image,
})
# Nicer import names
images = WagtailImagesExtension

Wyświetl plik

@ -0,0 +1,66 @@
from __future__ import absolute_import, unicode_literals
import os
import unittest
import django
from django.conf import settings
from django.test import TestCase
from wagtail.wagtailcore.models import Site
from .utils import get_test_image_file, Image
@unittest.skipIf(django.VERSION < (1, 8), 'Multiple engines only supported in Django>=1.8')
class TestImagesJinja(TestCase):
def setUp(self):
# This does not exist on Django<1.8
from django.template import engines
self.engine = engines['jinja2']
self.image = Image.objects.create(
title="Test image",
file=get_test_image_file(),
)
def render(self, string, context=None, request_context=True):
if context is None:
context = {}
# Add a request to the template, to simulate a RequestContext
if request_context:
site = Site.objects.get(is_default_site=True)
request = self.client.get('/test/', HTTP_HOST=site.hostname)
request.site = site
context['request'] = request
template = self.engine.from_string(string)
return template.render(context)
def get_image_filename(self, image, filterspec):
"""
Get the generated filename for a resized image
"""
name, ext = os.path.splitext(os.path.basename(image.file.name))
return '{}images/{}.{}{}'.format(
settings.MEDIA_URL, name, filterspec, ext)
def test_image(self):
self.assertHTMLEqual(
self.render('{{ image(myimage, "width-200") }}', {'myimage': self.image}),
'<img alt="Test image" src="{}" width="200" height="150">'.format(
self.get_image_filename(self.image, "width-200")))
def test_image_attributes(self):
self.assertHTMLEqual(
self.render('{{ image(myimage, "width-200", class="test") }}', {'myimage': self.image}),
'<img alt="Test image" src="{}" width="200" height="150" class="test">'.format(
self.get_image_filename(self.image, "width-200")))
def test_image_assignment(self):
template = ('{% set background=image(myimage, "width-200") %}'
'width: {{ background.width }}, url: {{ background.url }}')
output = ('width: 200, url: ' + self.get_image_filename(self.image, "width-200"))
self.assertHTMLEqual(self.render(template, {'myimage': self.image}), output)