kopia lustrzana https://github.com/wagtail/wagtail
Merge pull request #1755 from takeflight/feature/jinja2-template-functions
Add rudimentary Jinja2 template tag supportpull/1797/head
commit
4dc09dfb22
|
@ -12,3 +12,4 @@ Advanced topics
|
|||
privacy
|
||||
customisation/index
|
||||
third_party_tutorials
|
||||
jinja2
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
================
|
||||
|
|
1
tox.ini
1
tox.ini
|
@ -35,6 +35,7 @@ deps =
|
|||
pytz==2014.7
|
||||
Embedly
|
||||
Willow==0.2
|
||||
jinja2==2.8
|
||||
coverage
|
||||
|
||||
dj17: Django>=1.7.1,<1.8
|
||||
|
|
|
@ -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 + (
|
||||
|
|
|
@ -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
|
|
@ -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, '')
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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__)
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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)
|
Ładowanie…
Reference in New Issue