kopia lustrzana https://github.com/wagtail/wagtail
Adds the include_block template tag (#2786)
Update render and render_basic methods on Block to take a context kwarg Update TableBlock to support passing extra context to render Implement render_as_block on BoundBlock, StreamValue and StructValue. Collectively, these are the objects encountered during template rendering which typically render a block template when output inside {{ ... }} tags. Implementing render_as_block allows us to do the same thing, but passing a template context as well. Implement include_block tag Support extra context vars on include_block via 'with foo=bar' Support 'only' flag on include_block tag, to omit the parent context Update StreamField documentation to cover the include_block tag Rewrite 'BoundBlocks and values' docs based on the include_block tag Add tests for blocks with legacy render / render_basic methods Any bits of StreamField infrastructure that attempt to call render or render_basic on a block with a 'context' kwarg, should (for now) also work on blocks that don't accept the context kwarg, but output a RemovedInWagtail18Warning. Explicitly test whether render / render_basic will accept a 'context' kwarg This avoids unexpected behaviour when the method legitimately accepts a context kwarg, but happens to throw an unrelated TypeError - in this situation, the final output (or error diagnostics) will behave as if the context was never passed, making debugging difficult. See https://github.com/torchbox/wagtail/pull/2786#discussion_r69563984pull/2786/merge
rodzic
098433a724
commit
dbc4c9b28e
|
@ -89,3 +89,30 @@ should be updated to:
|
|||
.. code-block:: python
|
||||
|
||||
bases=(models.Model, wagtail.wagtailsearch.index.Indexed),
|
||||
|
||||
``render`` and ``render_basic`` methods on StreamField blocks now accept a ``context`` keyword argument
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``render`` and ``render_basic`` methods on ``wagtail.wagtailcore.blocks.Block`` have been updated to accept an optional ``context`` keyword argument, a template context to use when rendering the block. If you have defined any custom StreamField blocks that override either of these methods, the method signature now needs to be updated to include this keyword argument:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyBlock(Block):
|
||||
|
||||
def render(self, value):
|
||||
...
|
||||
|
||||
def render_basic(self, value):
|
||||
...
|
||||
|
||||
should now become:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyBlock(Block):
|
||||
|
||||
def render(self, value, context=None):
|
||||
...
|
||||
|
||||
def render_basic(self, value, context=None):
|
||||
...
|
||||
|
|
|
@ -391,20 +391,24 @@ Since ``StreamField`` accepts an instance of ``StreamBlock`` as a parameter, in
|
|||
Template rendering
|
||||
------------------
|
||||
|
||||
The simplest way to render the contents of a StreamField into your template is to output it as a variable, like any other field:
|
||||
StreamField provides an HTML representation for the stream content as a whole, as well as for each individual block. To include this HTML into your page, use the ``{% include_block %}`` tag:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ page.body }}
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
{% include_block page.body %}
|
||||
|
||||
|
||||
This will render each block of the stream in turn, wrapped in a ``<div class="block-my_block_name">`` element (where ``my_block_name`` is the block name given in the StreamField definition). If you wish to provide your own HTML markup, you can instead iterate over the field's value to access each block in turn:
|
||||
In the default rendering, each block of the stream is wrapped in a ``<div class="block-my_block_name">`` element (where ``my_block_name`` is the block name given in the StreamField definition). If you wish to provide your own HTML markup, you can instead iterate over the field's value, and invoke ``{% include_block %}`` on each block in turn:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
<article>
|
||||
{% for block in page.body %}
|
||||
<section>{{ block }}</section>
|
||||
<section>{% include_block block %}</section>
|
||||
{% endfor %}
|
||||
</article>
|
||||
|
||||
|
@ -413,22 +417,22 @@ For more control over the rendering of specific block types, each block object p
|
|||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
<article>
|
||||
{% for block in page.body %}
|
||||
{% if block.block_type == 'heading' %}
|
||||
<h1>{{ block.value }}</h1>
|
||||
{% else %}
|
||||
<section class="block-{{ block.block_type }}">
|
||||
{{ block }}
|
||||
{% include_block block %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</article>
|
||||
|
||||
|
||||
Each block type provides its own front-end HTML rendering mechanism, and this is used for the output of ``{{ block }}``. For most simple block types, such as CharBlock, this will simply output the field's value, but others will provide their own HTML markup. For example, a ListBlock will output the list of child blocks as a ``<ul>`` element (with each child wrapped in an ``<li>`` element and rendered using the child block's own HTML rendering).
|
||||
|
||||
To override this with your own custom HTML rendering, you can pass a ``template`` argument to the block, giving the filename of a template file to be rendered. This is particularly useful for custom block types derived from StructBlock, as the default StructBlock rendering is simple and somewhat generic:
|
||||
By default, each block is rendered using simple, minimal HTML markup, or no markup at all. For example, a CharBlock value is rendered as plain text, while a ListBlock outputs its child blocks in a `<ul>` wrapper. To override this with your own custom HTML rendering, you can pass a ``template`` argument to the block, giving the filename of a template file to be rendered. This is particularly useful for custom block types derived from StructBlock:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -471,10 +475,82 @@ Within the template, the block value is accessible as the variable ``value``:
|
|||
{{ value.biography }}
|
||||
</div>
|
||||
|
||||
Since ``first_name``, ``surname``, ``photo`` and ``biography`` are defined as blocks in their own right, this could also be written as:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% load wagtailcore_tags wagtailimages_tags %}
|
||||
|
||||
<div class="person">
|
||||
{% image value.photo width-400 %}
|
||||
<h2>{% include_block value.first_name %} {% include_block value.surname %}</h2>
|
||||
{% include_block value.biography %}
|
||||
</div>
|
||||
|
||||
Writing ``{{ my_block }}`` is roughly equivalent to ``{% include_block my_block %}``, but the short form is more restrictive, as it does not pass variables from the calling template such as ``request`` or ``page``; for this reason, it is recommended that you only use it for simple values that do not render HTML of their own. For example, if our PersonBlock used the template:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% load wagtailimages_tags %}
|
||||
|
||||
<div class="person">
|
||||
{% image value.photo width-400 %}
|
||||
<h2>{{ value.first_name }} {{ value.surname }}</h2>
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
<a href="#">Contact this person</a>
|
||||
{% endif %}
|
||||
|
||||
{{ value.biography }}
|
||||
</div>
|
||||
|
||||
then the ``request.user.is_authenticated`` test would not work correctly when rendering the block through a ``{{ ... }}`` tag:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{# Incorrect: #}
|
||||
|
||||
{% for block in page.body %}
|
||||
{% if block.block_type == 'person' %}
|
||||
<div>
|
||||
{{ block }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{# Correct: #}
|
||||
|
||||
{% for block in page.body %}
|
||||
{% if block.block_type == 'person' %}
|
||||
<div>
|
||||
{% include_block block %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
Like Django's ``{% include %}`` tag, ``{% include_block %}`` also allows passing additional variables to the included template, through the syntax ``{% include_block my_block with foo="bar" %}``:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{# In page template: #}
|
||||
|
||||
{% for block in page.body %}
|
||||
{% if block.block_type == 'person' %}
|
||||
{% include_block block with classname="important" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{# In PersonBlock template: #}
|
||||
|
||||
<div class="{{ classname }}">
|
||||
...
|
||||
</div>
|
||||
|
||||
The syntax ``{% include_block my_block with foo="bar" only %}`` is also supported, to specify that no variables from the parent template other than ``foo`` will be passed to the child template.
|
||||
|
||||
.. _streamfield_get_context:
|
||||
|
||||
To pass additional context variables to the template, block subclasses can override the ``get_context`` method:
|
||||
As well as passing variables from the parent template, block subclasses can pass additional template variables of their own by overriding the ``get_context`` method:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -499,7 +575,7 @@ In this example, the variable ``is_happening_today`` will be made available with
|
|||
BoundBlocks and values
|
||||
----------------------
|
||||
|
||||
As you've seen above, it's possible to assign a particular template for rendering a block. This can be done on any block type (not just StructBlocks), but there are some extra details to be aware of. Consider the following block definition:
|
||||
All block types, not just StructBlock, accept a ``template`` parameter to determine how they will be rendered on a page. However, for blocks that handle basic Python data types, such as ``CharBlock`` and ``IntegerBlock``, there are some limitations on where the template will take effect, since those built-in types (``str``, ``int`` and so on) cannot be 'taught' about their template rendering. As an example of this, consider the following block definition:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -526,15 +602,21 @@ This gives us a block that behaves as an ordinary text field, but wraps its outp
|
|||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
{% for block in page.body %}
|
||||
{% if block.block_type == 'heading' %}
|
||||
{{ block }} {# This block will output its own <h1>...</h1> tags. #}
|
||||
{% include_block block %} {# This block will output its own <h1>...</h1> tags. #}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
This is a powerful feature, but it involves some complexity behind the scenes to make it work. Effectively, HeadingBlock has a double identity - logically it represents a plain Python string value, but in circumstances such as this it needs to yield a 'magic' object that knows its own custom HTML representation. This 'magic' object is an instance of ``BoundBlock`` - an object that represents the pairing of a value and its block definition. (Django developers may recognise this as the same principle behind ``BoundField`` in Django's forms framework.)
|
||||
This kind of arrangement - a value that supposedly represents a plain text string, but has its own custom HTML representation when output on a template - would normally be a very messy thing to achieve in Python, but it works here because the items you get when iterating over a StreamField are not actually the 'native' values of the blocks. Instead, each item is returned as an instance of ``BoundBlock`` - an object that represents the pairing of a value and its block definition. By keeping track of the block definition, a ``BoundBlock`` always knows which template to render. To get to the underlying value - in this case, the text content of the heading - you would need to access ``block.value``. Indeed, if you were to output ``{% include_block block.value %}`` on the page, you would find that it renders as plain text, without the ``<h1>`` tags.
|
||||
|
||||
Most of the time, you won't need to worry about whether you're dealing with a plain value or a BoundBlock; you can trust Wagtail to do the right thing. However, there are certain cases where the distinction becomes important. For example, consider the following setup:
|
||||
(More precisely, the items returned when iterating over a StreamField are instances of a class ``StreamChild``, which provides the ``block_type`` property as well as ``value``.)
|
||||
|
||||
Experienced Django developers may find it helpful to compare this to the ``BoundField`` class in Django's forms framework, which represents the pairing of a form field value with its corresponding form field definition, and therefore knows how to render the value as an HTML form field.
|
||||
|
||||
Most of the time, you won't need to worry about these internal details; Wagtail will use the template rendering wherever you would expect it to. However, there are certain cases where the illusion isn't quite complete - namely, when accessing children of a ``ListBlock`` or ``StructBlock``. In these cases, there is no ``BoundBlock`` wrapper, and so the item cannot be relied upon to know its own template rendering. For example, consider the following setup, where our ``HeadingBlock`` is a child of a StructBlock:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -546,65 +628,40 @@ Most of the time, you won't need to worry about whether you're dealing with a pl
|
|||
class Meta:
|
||||
template = 'blocks/event.html'
|
||||
|
||||
where ``blocks/event.html`` is:
|
||||
In ``blocks/event.html``:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
|
||||
{{ value.heading }}
|
||||
- {{ value.description }}
|
||||
{% include_block value.heading %}
|
||||
- {% include_block value.description %}
|
||||
</div>
|
||||
|
||||
In this case, ``value.heading`` returns the plain string value, because if this weren't the case, the comparison in ``{% if value.heading == 'Party!' %}`` would never succeed. This in turn means that ``{{ value.heading }}`` renders as the plain string, without the ``<h1>`` tags.
|
||||
|
||||
Interactions between BoundBlocks and plain values work according to the following rules:
|
||||
|
||||
1. When iterating over the value of a StreamField or StreamBlock (as in ``{% for block in page.body %}``), you will get back a sequence of BoundBlocks.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This means that ``{{ block }}`` will always render using the block's own template, if one is supplied. More specifically, these ``block`` objects will be instances of StreamChild, which additionally provides the ``block_type`` property.
|
||||
|
||||
2. If you have a BoundBlock instance, you can access the plain value as ``block.value``.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For example, if you had a particular page template where you wanted HeadingBlock to display as ``<h2>`` rather than ``<h1>``, you could write:
|
||||
In this case, ``value.heading`` returns the plain string value rather than a ``BoundBlock``; this is necessary because otherwise the comparison in ``{% if value.heading == 'Party!' %}`` would never succeed. This in turn means that ``{% include_block value.heading %}`` renders as the plain string, without the ``<h1>`` tags. To get the HTML rendering, you need to explicitly access the ``BoundBlock`` instance through ``value.bound_blocks.heading``:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% for block in page.body %}
|
||||
{% if block.block_type == 'heading' %}
|
||||
<h2>{{ block.value }}</h2>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
3. Accessing a child of a StructBlock (as in ``value.heading``) will return a plain value; to retrieve the BoundBlock instead, use ``value.bound_blocks.heading``.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This ensures that template tags such as ``{% if value.heading == 'Party!' %}`` and ``{% image value.photo fill-320x200 %}`` work as expected. The event template above could be rewritten as follows to access the HeadingBlock content as a BoundBlock and use its own HTML representation (with ``<h1>`` tags included):
|
||||
|
||||
.. code-block:: html+django
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
|
||||
{{ value.bound_block.heading }}
|
||||
{{ value.description }}
|
||||
{% include_block value.bound_blocks.heading %}
|
||||
- {% include_block value.description %}
|
||||
</div>
|
||||
|
||||
However, in this case it's probably more readable to make the ``<h1>`` tag explicit in the EventBlock's template:
|
||||
In practice, it would probably be more natural and readable to make the ``<h1>`` tag explicit in the EventBlock's template:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
|
||||
<h1>{{ value.heading }}</h1>
|
||||
{{ value.description }}
|
||||
- {% include_block value.description %}
|
||||
</div>
|
||||
|
||||
4. The value of a ListBlock is a plain Python list; iterating over it returns plain child values.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
5. StructBlock and StreamBlock values always know how to render their own templates, even if you only have the plain value rather than the BoundBlock.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is possible because the HTML rendering behaviour of these blocks does not interfere with their main role as a container for data - there's no "double identity" as there is for blocks like CharBlock. For example, if a StructBlock is nested in another StructBlock, as in:
|
||||
This limitation does not apply to StructBlock and StreamBlock values as children of a StructBlock, because Wagtail implements these as complex objects that know their own template rendering, even when not wrapped in a ``BoundBlock``. For example, if a StructBlock is nested in another StructBlock, as in:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -617,7 +674,15 @@ This is possible because the HTML rendering behaviour of these blocks does not i
|
|||
('photo', ImageChooserBlock()),
|
||||
], template='blocks/speaker.html')
|
||||
|
||||
then writing ``{{ value.guest_speaker }}`` within the EventBlock's template will use the template rendering from ``blocks/speaker.html`` for that field.
|
||||
then ``{% include_block value.guest_speaker %}`` within the EventBlock's template will pick up the template rendering from ``blocks/speaker.html`` as intended.
|
||||
|
||||
In summary, interactions between BoundBlocks and plain values work according to the following rules:
|
||||
|
||||
1. When iterating over the value of a StreamField or StreamBlock (as in ``{% for block in page.body %}``), you will get back a sequence of BoundBlocks.
|
||||
2. If you have a BoundBlock instance, you can access the plain value as ``block.value``.
|
||||
3. Accessing a child of a StructBlock (as in ``value.heading``) will return a plain value; to retrieve the BoundBlock instead, use ``value.bound_blocks.heading``.
|
||||
4. The value of a ListBlock is a plain Python list; iterating over it returns plain child values.
|
||||
5. StructBlock and StreamBlock values always know how to render their own templates, even if you only have the plain value rather than the BoundBlock.
|
||||
|
||||
|
||||
.. _custom_editing_interfaces_for_structblock:
|
||||
|
|
|
@ -74,22 +74,28 @@ class TableBlock(FieldBlock):
|
|||
def is_html_renderer(self):
|
||||
return self.table_options['renderer'] == 'html'
|
||||
|
||||
def render(self, value):
|
||||
def render(self, value, context=None):
|
||||
template = getattr(self.meta, 'template', None)
|
||||
if template and value:
|
||||
table_header = value['data'][0] if value.get('data', None) and len(value['data']) > 0 and value.get('first_row_is_table_header', False) else None
|
||||
first_col_is_header = value.get('first_col_is_header', False)
|
||||
context = {
|
||||
|
||||
if context is None:
|
||||
new_context = {}
|
||||
else:
|
||||
new_context = dict(context)
|
||||
|
||||
new_context.update({
|
||||
'self': value,
|
||||
self.TEMPLATE_VAR: value,
|
||||
'table_header': table_header,
|
||||
'first_col_is_header': first_col_is_header,
|
||||
'html_renderer': self.is_html_renderer(),
|
||||
'data': value['data'][1:] if table_header else value.get('data', [])
|
||||
}
|
||||
return render_to_string(template, context)
|
||||
})
|
||||
return render_to_string(template, new_context)
|
||||
else:
|
||||
return self.render_basic(value)
|
||||
return self.render_basic(value, context=context)
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
|
|
|
@ -84,7 +84,6 @@ class TestTableBlock(TestTableBlockRenderingBase):
|
|||
self.assertHTMLEqual(result, expected)
|
||||
self.assertIn('Test 2', result)
|
||||
|
||||
|
||||
def test_render_empty_table(self):
|
||||
"""
|
||||
An empty table should render okay.
|
||||
|
@ -93,7 +92,6 @@ class TestTableBlock(TestTableBlockRenderingBase):
|
|||
result = block.render(self.default_value)
|
||||
self.assertHTMLEqual(result, self.default_expected)
|
||||
|
||||
|
||||
def test_do_not_render_html(self):
|
||||
"""
|
||||
Ensure that raw html doesn't render
|
||||
|
@ -109,7 +107,6 @@ class TestTableBlock(TestTableBlockRenderingBase):
|
|||
result = block.render(value)
|
||||
self.assertHTMLEqual(result, expected)
|
||||
|
||||
|
||||
def test_row_headers(self):
|
||||
"""
|
||||
Ensure that row headers are properly rendered.
|
||||
|
@ -134,7 +131,6 @@ class TestTableBlock(TestTableBlockRenderingBase):
|
|||
result = block.render(value)
|
||||
self.assertHTMLEqual(result, expected)
|
||||
|
||||
|
||||
def test_row_and_column_headers(self):
|
||||
"""
|
||||
Test row and column headers at the same time.
|
||||
|
@ -147,7 +143,6 @@ class TestTableBlock(TestTableBlockRenderingBase):
|
|||
result = block.render(value)
|
||||
self.assertHTMLEqual(result, expected)
|
||||
|
||||
|
||||
def test_value_for_and_from_form(self):
|
||||
"""
|
||||
Make sure we get back good json and make
|
||||
|
@ -162,7 +157,6 @@ class TestTableBlock(TestTableBlockRenderingBase):
|
|||
self.assertJSONEqual(expected_json, returned_json)
|
||||
self.assertEqual(block.value_from_form(returned_json), value)
|
||||
|
||||
|
||||
def test_is_html_renderer(self):
|
||||
"""
|
||||
Test that settings flow through correctly to
|
||||
|
@ -177,3 +171,19 @@ class TestTableBlock(TestTableBlockRenderingBase):
|
|||
new_options['renderer'] = 'html'
|
||||
block2 = TableBlock(table_options=new_options)
|
||||
self.assertEqual(block2.is_html_renderer(), True)
|
||||
|
||||
def test_render_with_extra_context(self):
|
||||
"""
|
||||
Test that extra context variables passed in block.render are passed through
|
||||
to the template.
|
||||
"""
|
||||
block = TableBlock(template="tests/blocks/table_block_with_caption.html")
|
||||
|
||||
value = {'first_row_is_table_header': False, 'first_col_is_header': False,
|
||||
'data': [['Test 1', 'Test 2', 'Test 3'], [None, None, None],
|
||||
[None, None, None]]}
|
||||
result = block.render(value, context={
|
||||
'caption': "A fascinating table."
|
||||
})
|
||||
self.assertIn("Test 1", result)
|
||||
self.assertIn("<div>A fascinating table.</div>", result)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<h1{% if language %} lang="{{ language }}"{% endif %}{% if classname %} class="{{ classname }}"{% endif %}>{{ value }}</h1>
|
|
@ -0,0 +1,3 @@
|
|||
{% load wagtailcore_tags %}
|
||||
|
||||
<body>{% include_block test_block with classname="IMPORTANT"|lower only %}</body>
|
|
@ -0,0 +1,3 @@
|
|||
{% load wagtailcore_tags %}
|
||||
|
||||
<body>{% include_block test_block %}</body>
|
|
@ -0,0 +1,3 @@
|
|||
{% load wagtailcore_tags %}
|
||||
|
||||
<body>{% include_block test_block|default:999 %}</body>
|
|
@ -0,0 +1,3 @@
|
|||
{% load wagtailcore_tags %}
|
||||
|
||||
<body>{% include_block test_block with classname="IMPORTANT"|lower %}</body>
|
|
@ -1 +1 @@
|
|||
<h1>{{ value.title }}</h1>{{ value.bound_blocks.body.render }}
|
||||
<h1{% if language %} lang="{{ language }}"{% endif %}>{{ value.title }}</h1>{{ value.bound_blocks.body.render }}
|
|
@ -0,0 +1,5 @@
|
|||
{% load wagtailcore_tags %}
|
||||
|
||||
{% for block in value %}
|
||||
<div class="{{ block.block_type }}"{% if language %} lang="{{ language }}"{% endif %}>{% include_block block %}</div>
|
||||
{% endfor %}
|
|
@ -0,0 +1,2 @@
|
|||
{% include "table_block/blocks/table.html" %}
|
||||
<div>{{ caption }}</div>
|
|
@ -1,6 +1,9 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import collections
|
||||
import inspect
|
||||
import sys
|
||||
import warnings
|
||||
from importlib import import_module
|
||||
|
||||
from django import forms
|
||||
|
@ -12,6 +15,8 @@ from django.utils.encoding import force_text, python_2_unicode_compatible
|
|||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import capfirst
|
||||
|
||||
from wagtail.utils.deprecation import RemovedInWagtail18Warning
|
||||
|
||||
# unicode_literals ensures that any render / __str__ methods returning HTML via calls to mark_safe / format_html
|
||||
# return a SafeText, not SafeBytes; necessary so that it doesn't get re-encoded when the template engine
|
||||
# calls force_text, which would cause it to lose its 'safe' flag
|
||||
|
@ -20,6 +25,24 @@ from django.utils.text import capfirst
|
|||
__all__ = ['BaseBlock', 'Block', 'BoundBlock', 'DeclarativeSubBlocksMetaclass', 'BlockWidget', 'BlockField']
|
||||
|
||||
|
||||
def accepts_context(func):
|
||||
"""
|
||||
Helper function used by _render_with_context and _render_basic_with_context. Return true
|
||||
if the callable 'func' accepts a 'context' keyword argument
|
||||
"""
|
||||
if sys.version_info >= (3, 3):
|
||||
signature = inspect.signature(func)
|
||||
try:
|
||||
signature.bind_partial(context=None)
|
||||
return True
|
||||
except TypeError:
|
||||
return False
|
||||
else:
|
||||
# Fall back on inspect.getargspec, available on Python 2.7 but deprecated since 3.5
|
||||
argspec = inspect.getargspec(func)
|
||||
return ('context' in argspec.args) or (argspec.keywords is not None)
|
||||
|
||||
|
||||
# =========================================
|
||||
# Top-level superclasses and helper objects
|
||||
# =========================================
|
||||
|
@ -204,24 +227,100 @@ class Block(six.with_metaclass(BaseBlock, object)):
|
|||
return value
|
||||
|
||||
def get_context(self, value):
|
||||
"""
|
||||
Return a dict of context variables (derived from the block value, or otherwise)
|
||||
to be added to the template context when rendering this value through a template.
|
||||
"""
|
||||
return {
|
||||
'self': value,
|
||||
self.TEMPLATE_VAR: value,
|
||||
}
|
||||
|
||||
def render(self, value):
|
||||
def _render_with_context(self, value, context=None):
|
||||
"""
|
||||
Temporary hack to accommodate Block subclasses created for Wagtail <1.6 which
|
||||
have overridden `render` with the type signature:
|
||||
def render(self, value)
|
||||
and will therefore fail when passed a `context` kwarg.
|
||||
|
||||
In Wagtail 1.8, when support for context-less `render` methods is dropped,
|
||||
this method will be deleted (and calls to it replaced with a direct call to `render`).
|
||||
"""
|
||||
if accepts_context(self.render):
|
||||
# this render method can receive a 'context' kwarg, so we're good
|
||||
return self.render(value, context=context)
|
||||
else:
|
||||
# this render method needs updating for Wagtail >=1.6 -
|
||||
# output a deprecation warning
|
||||
|
||||
# find the specific parent class that defines `render` by stepping through the MRO,
|
||||
# falling back on type(self) if it can't be found for some reason
|
||||
class_with_render_method = next(
|
||||
(cls for cls in type(self).__mro__ if 'render' in cls.__dict__),
|
||||
type(self)
|
||||
)
|
||||
|
||||
warnings.warn(
|
||||
"The render method on %s needs to be updated to accept an optional 'context' "
|
||||
"keyword argument" % class_with_render_method,
|
||||
category=RemovedInWagtail18Warning
|
||||
)
|
||||
|
||||
# fall back on a call to 'render' without the context kwarg
|
||||
return self.render(value)
|
||||
|
||||
def render(self, value, context=None):
|
||||
"""
|
||||
Return a text rendering of 'value', suitable for display on templates. By default, this will
|
||||
use a template if a 'template' property is specified on the block, and fall back on render_basic
|
||||
otherwise.
|
||||
use a template (with the passed context, supplemented by the result of get_context) if a
|
||||
'template' property is specified on the block, and fall back on render_basic otherwise.
|
||||
"""
|
||||
template = getattr(self.meta, 'template', None)
|
||||
if template:
|
||||
return render_to_string(template, self.get_context(value))
|
||||
if not template:
|
||||
return self._render_basic_with_context(value, context=context)
|
||||
|
||||
if context is None:
|
||||
new_context = self.get_context(value)
|
||||
else:
|
||||
new_context = dict(context)
|
||||
new_context.update(self.get_context(value))
|
||||
|
||||
return render_to_string(template, new_context)
|
||||
|
||||
def _render_basic_with_context(self, value, context=None):
|
||||
"""
|
||||
Temporary hack to accommodate Block subclasses created for Wagtail <1.6 which
|
||||
have overridden `render_basic` with the type signature:
|
||||
def render_basic(self, value)
|
||||
and will therefore fail when passed a `context` kwarg.
|
||||
|
||||
In Wagtail 1.8, when support for context-less `render_basic` methods is dropped,
|
||||
this method will be deleted (and calls to it replaced with a direct call to `render_basic`).
|
||||
"""
|
||||
if accepts_context(self.render_basic):
|
||||
# this render_basic method can receive a 'context' kwarg, so we're good
|
||||
return self.render_basic(value, context=context)
|
||||
else:
|
||||
# this render_basic method needs updating for Wagtail >=1.6 -
|
||||
# output a deprecation warning
|
||||
|
||||
# find the specific parent class that defines `render_basic` by stepping through the MRO,
|
||||
# falling back on type(self) if it can't be found for some reason
|
||||
class_with_render_basic_method = next(
|
||||
(cls for cls in type(self).__mro__ if 'render_basic' in cls.__dict__),
|
||||
type(self)
|
||||
)
|
||||
|
||||
warnings.warn(
|
||||
"The render_basic method on %s needs to be updated to accept an optional 'context' "
|
||||
"keyword argument" % class_with_render_basic_method,
|
||||
category=RemovedInWagtail18Warning
|
||||
)
|
||||
|
||||
# fall back on a call to 'render_basic' without the context kwarg
|
||||
return self.render_basic(value)
|
||||
|
||||
def render_basic(self, value):
|
||||
def render_basic(self, value, context=None):
|
||||
"""
|
||||
Return a text rendering of 'value', suitable for display on templates. render() will fall back on
|
||||
this if the block does not define a 'template' property.
|
||||
|
@ -387,8 +486,18 @@ class BoundBlock(object):
|
|||
def render_form(self):
|
||||
return self.block.render_form(self.value, self.prefix, errors=self.errors)
|
||||
|
||||
def render(self):
|
||||
return self.block.render(self.value)
|
||||
def render(self, context=None):
|
||||
return self.block._render_with_context(self.value, context=context)
|
||||
|
||||
def render_as_block(self, context=None):
|
||||
"""
|
||||
Alias for render; the include_block tag will specifically check for the presence of a method
|
||||
with this name. (This is because {% include_block %} is just as likely to be invoked on a bare
|
||||
value as a BoundBlock. If we looked for a `render` method instead, we'd run the risk of finding
|
||||
an unrelated method that just happened to have that name - for example, when called on a
|
||||
PageChooserBlock it could end up calling page.render.
|
||||
"""
|
||||
return self.block._render_with_context(self.value, context=context)
|
||||
|
||||
def id_for_label(self):
|
||||
return self.block.id_for_label(self.prefix)
|
||||
|
|
|
@ -536,7 +536,7 @@ class PageChooserBlock(ChooserBlock):
|
|||
from wagtail.wagtailadmin.widgets import AdminPageChooser
|
||||
return AdminPageChooser(can_choose_root=self.can_choose_root)
|
||||
|
||||
def render_basic(self, value):
|
||||
def render_basic(self, value, context=None):
|
||||
if value:
|
||||
return format_html('<a href="{0}">{1}</a>', value.url, value.title)
|
||||
else:
|
||||
|
|
|
@ -140,10 +140,13 @@ class ListBlock(Block):
|
|||
for item in value
|
||||
]
|
||||
|
||||
def render_basic(self, value):
|
||||
def render_basic(self, value, context=None):
|
||||
children = format_html_join(
|
||||
'\n', '<li>{0}</li>',
|
||||
[(self.child_block.render(child_value),) for child_value in value]
|
||||
[
|
||||
(self.child_block._render_with_context(child_value, context=context),)
|
||||
for child_value in value
|
||||
]
|
||||
)
|
||||
return format_html("<ul>{0}</ul>", children)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.forms.utils import ErrorList
|
|||
from django.template.loader import render_to_string
|
||||
# Must be imported from Django so we get the new implementation of with_metaclass
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.html import format_html_join
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
@ -205,10 +205,13 @@ class BaseStreamBlock(Block):
|
|||
for child in value # child is a BoundBlock instance
|
||||
]
|
||||
|
||||
def render_basic(self, value):
|
||||
def render_basic(self, value, context=None):
|
||||
return format_html_join(
|
||||
'\n', '<div class="block-{1}">{0}</div>',
|
||||
[(force_text(child), child.block_type) for child in value]
|
||||
[
|
||||
(child.render(context=context), child.block_type)
|
||||
for child in value
|
||||
]
|
||||
)
|
||||
|
||||
def get_searchable_content(self, value):
|
||||
|
@ -342,6 +345,9 @@ class StreamValue(collections.Sequence):
|
|||
def __repr__(self):
|
||||
return repr(list(self))
|
||||
|
||||
def render_as_block(self, context=None):
|
||||
return self.stream_block.render(self, context=context)
|
||||
|
||||
def __html__(self):
|
||||
return self.stream_block.render(self)
|
||||
|
||||
|
|
|
@ -186,6 +186,9 @@ class StructValue(collections.OrderedDict):
|
|||
def __str__(self):
|
||||
return self.block.render(self)
|
||||
|
||||
def render_as_block(self, context=None):
|
||||
return self.block.render(self, context=context)
|
||||
|
||||
@cached_property
|
||||
def bound_blocks(self):
|
||||
return collections.OrderedDict([
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django import template
|
||||
from django.template.defaulttags import token_kwargs
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from wagtail.wagtailcore import __version__
|
||||
|
@ -46,3 +48,63 @@ def richtext(value):
|
|||
html = expand_db_html(value)
|
||||
|
||||
return mark_safe('<div class="rich-text">' + html + '</div>')
|
||||
|
||||
|
||||
class IncludeBlockNode(template.Node):
|
||||
def __init__(self, block_var, extra_context, use_parent_context):
|
||||
self.block_var = block_var
|
||||
self.extra_context = extra_context
|
||||
self.use_parent_context = use_parent_context
|
||||
|
||||
def render(self, context):
|
||||
try:
|
||||
value = self.block_var.resolve(context)
|
||||
except template.VariableDoesNotExist:
|
||||
return ''
|
||||
|
||||
if hasattr(value, 'render_as_block'):
|
||||
if self.use_parent_context:
|
||||
new_context = context.flatten()
|
||||
else:
|
||||
new_context = {}
|
||||
|
||||
if self.extra_context:
|
||||
for var_name, var_value in self.extra_context.items():
|
||||
new_context[var_name] = var_value.resolve(context)
|
||||
|
||||
return value.render_as_block(context=new_context)
|
||||
else:
|
||||
return force_text(value)
|
||||
|
||||
|
||||
@register.tag
|
||||
def include_block(parser, token):
|
||||
"""
|
||||
Render the passed item of StreamField content, passing the current template context
|
||||
if there's an identifiable way of doing so (i.e. if it has a `render_as_block` method).
|
||||
"""
|
||||
tokens = token.split_contents()
|
||||
|
||||
try:
|
||||
tag_name = tokens.pop(0)
|
||||
block_var_token = tokens.pop(0)
|
||||
except IndexError:
|
||||
raise template.TemplateSyntaxError("%r tag requires at least one argument" % tag_name)
|
||||
|
||||
block_var = parser.compile_filter(block_var_token)
|
||||
|
||||
if tokens and tokens[0] == 'with':
|
||||
tokens.pop(0)
|
||||
extra_context = token_kwargs(tokens, parser)
|
||||
else:
|
||||
extra_context = None
|
||||
|
||||
use_parent_context = True
|
||||
if tokens and tokens[0] == 'only':
|
||||
tokens.pop(0)
|
||||
use_parent_context = False
|
||||
|
||||
if tokens:
|
||||
raise template.TemplateSyntaxError("Unexpected argument to %r tag: %r" % (tag_name, tokens[0]))
|
||||
|
||||
return IncludeBlockNode(block_var, extra_context, use_parent_context)
|
||||
|
|
|
@ -4,17 +4,20 @@ from __future__ import absolute_import, unicode_literals
|
|||
import base64
|
||||
import collections
|
||||
import unittest
|
||||
import warnings
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms.utils import ErrorList
|
||||
from django.template.loader import render_to_string
|
||||
from django.test import SimpleTestCase, TestCase
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
|
||||
from wagtail.tests.testapp.blocks import LinkBlock as CustomLinkBlock
|
||||
from wagtail.tests.testapp.blocks import SectionBlock
|
||||
from wagtail.utils.deprecation import RemovedInWagtail18Warning
|
||||
from wagtail.wagtailcore import blocks
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.wagtailcore.rich_text import RichText
|
||||
|
@ -31,6 +34,24 @@ class FooStreamBlock(blocks.StreamBlock):
|
|||
return value
|
||||
|
||||
|
||||
class LegacyRenderMethodBlock(blocks.CharBlock):
|
||||
"""
|
||||
A block with a render method that doesn't accept a 'context' kwarg.
|
||||
Support for these will be dropped in Wagtail 1.8
|
||||
"""
|
||||
def render(self, value):
|
||||
return str(value).upper()
|
||||
|
||||
|
||||
class LegacyRenderBasicMethodBlock(blocks.CharBlock):
|
||||
"""
|
||||
A block with a render_basic method that doesn't accept a 'context' kwarg.
|
||||
Support for these will be dropped in Wagtail 1.8
|
||||
"""
|
||||
def render_basic(self, value):
|
||||
return str(value).upper()
|
||||
|
||||
|
||||
class TestFieldBlock(unittest.TestCase):
|
||||
def test_charfield_render(self):
|
||||
block = blocks.CharBlock()
|
||||
|
@ -38,6 +59,20 @@ class TestFieldBlock(unittest.TestCase):
|
|||
|
||||
self.assertEqual(html, "Hello world!")
|
||||
|
||||
def test_charfield_render_with_template(self):
|
||||
block = blocks.CharBlock(template='tests/blocks/heading_block.html')
|
||||
html = block.render("Hello world!")
|
||||
|
||||
self.assertEqual(html, '<h1>Hello world!</h1>')
|
||||
|
||||
def test_charfield_render_with_template_with_extra_context(self):
|
||||
block = blocks.CharBlock(template='tests/blocks/heading_block.html')
|
||||
html = block.render("Bonjour le monde!", context={
|
||||
'language': 'fr',
|
||||
})
|
||||
|
||||
self.assertEqual(html, '<h1 lang="fr">Bonjour le monde!</h1>')
|
||||
|
||||
def test_charfield_render_form(self):
|
||||
block = blocks.CharBlock()
|
||||
html = block.render_form("Hello world!")
|
||||
|
@ -148,6 +183,24 @@ class TestFieldBlock(unittest.TestCase):
|
|||
self.assertIn('pretty.css', ''.join(block.all_media().render_css()))
|
||||
self.assertIn('animations.js', ''.join(block.all_media().render_js()))
|
||||
|
||||
def test_legacy_render_basic(self):
|
||||
"""
|
||||
LegacyRenderBasicMethodBlock defines a render_basic method that doesn't accept
|
||||
a 'context' kwarg. Calling 'render' should gracefully handle this and return
|
||||
the result of calling render_basic(value) (i.e. without passing context), but
|
||||
generate a RemovedInWagtail18Warning.
|
||||
"""
|
||||
block = LegacyRenderBasicMethodBlock()
|
||||
|
||||
with warnings.catch_warnings(record=True) as ws:
|
||||
warnings.simplefilter('always')
|
||||
|
||||
result = block.render('hello')
|
||||
|
||||
self.assertEqual(result, 'HELLO')
|
||||
self.assertEqual(len(ws), 1)
|
||||
self.assertIs(ws[0].category, RemovedInWagtail18Warning)
|
||||
|
||||
|
||||
class TestIntegerBlock(unittest.TestCase):
|
||||
def test_type(self):
|
||||
|
@ -1041,6 +1094,31 @@ class TestStructBlock(SimpleTestCase):
|
|||
result = block.render(value)
|
||||
self.assertEqual(result, """<h1>Hello</h1><div class="rich-text"><i>italic</i> world</div>""")
|
||||
|
||||
def test_render_block_with_extra_context(self):
|
||||
block = SectionBlock()
|
||||
value = block.to_python({'title': 'Bonjour', 'body': 'monde <i>italique</i>'})
|
||||
result = block.render(value, context={'language': 'fr'})
|
||||
self.assertEqual(result, """<h1 lang="fr">Bonjour</h1><div class="rich-text">monde <i>italique</i></div>""")
|
||||
|
||||
def test_render_structvalue(self):
|
||||
"""
|
||||
The string representation of a StructValue should use the block's template
|
||||
"""
|
||||
block = SectionBlock()
|
||||
value = block.to_python({'title': 'Hello', 'body': '<i>italic</i> world'})
|
||||
result = str(value)
|
||||
self.assertEqual(result, """<h1>Hello</h1><div class="rich-text"><i>italic</i> world</div>""")
|
||||
|
||||
# value.render_as_block() should be equivalent to str(value)
|
||||
result = value.render_as_block()
|
||||
self.assertEqual(result, """<h1>Hello</h1><div class="rich-text"><i>italic</i> world</div>""")
|
||||
|
||||
def test_render_structvalue_with_extra_context(self):
|
||||
block = SectionBlock()
|
||||
value = block.to_python({'title': 'Bonjour', 'body': 'monde <i>italique</i>'})
|
||||
result = value.render_as_block(context={'language': 'fr'})
|
||||
self.assertEqual(result, """<h1 lang="fr">Bonjour</h1><div class="rich-text">monde <i>italique</i></div>""")
|
||||
|
||||
|
||||
class TestListBlock(unittest.TestCase):
|
||||
def test_initialise_with_class(self):
|
||||
|
@ -1084,6 +1162,53 @@ class TestListBlock(unittest.TestCase):
|
|||
self.assertIn('<li>', html)
|
||||
self.assertIn('</li>', html)
|
||||
|
||||
def test_render_calls_block_render_on_children(self):
|
||||
"""
|
||||
The default rendering of a ListBlock should invoke the block's render method
|
||||
on each child, rather than just outputting the child value as a string.
|
||||
"""
|
||||
block = blocks.ListBlock(
|
||||
blocks.CharBlock(template='tests/blocks/heading_block.html')
|
||||
)
|
||||
html = block.render(["Hello world!", "Goodbye world!"])
|
||||
|
||||
self.assertIn('<h1>Hello world!</h1>', html)
|
||||
self.assertIn('<h1>Goodbye world!</h1>', html)
|
||||
|
||||
def test_render_passes_context_to_children(self):
|
||||
"""
|
||||
Template context passed to the render method should be passed on
|
||||
to the render method of the child block.
|
||||
"""
|
||||
block = blocks.ListBlock(
|
||||
blocks.CharBlock(template='tests/blocks/heading_block.html')
|
||||
)
|
||||
html = block.render(["Bonjour le monde!", "Au revoir le monde!"], context={
|
||||
'language': 'fr',
|
||||
})
|
||||
|
||||
self.assertIn('<h1 lang="fr">Bonjour le monde!</h1>', html)
|
||||
self.assertIn('<h1 lang="fr">Au revoir le monde!</h1>', html)
|
||||
|
||||
def test_child_with_legacy_render(self):
|
||||
"""
|
||||
If the child block has a legacy 'render' method that doesn't accept a 'context'
|
||||
kwarg, ListBlock.render should use the result of calling render(child_value), but
|
||||
generate a RemovedInWagtail18Warning.
|
||||
"""
|
||||
block = blocks.ListBlock(LegacyRenderBasicMethodBlock())
|
||||
|
||||
with warnings.catch_warnings(record=True) as ws:
|
||||
warnings.simplefilter('always')
|
||||
|
||||
result = block.render(['hello', 'world'])
|
||||
|
||||
self.assertIn('<li>HELLO</li>', result)
|
||||
self.assertIn('<li>WORLD</li>', result)
|
||||
self.assertEqual(len(ws), 2)
|
||||
self.assertIs(ws[0].category, RemovedInWagtail18Warning)
|
||||
self.assertIs(ws[1].category, RemovedInWagtail18Warning)
|
||||
|
||||
def render_form(self):
|
||||
class LinkBlock(blocks.StructBlock):
|
||||
title = blocks.CharBlock()
|
||||
|
@ -1426,6 +1551,117 @@ class TestStreamBlock(SimpleTestCase):
|
|||
self.assertNotIn('Hello', html)
|
||||
self.assertIn('<div class="block-paragraph"><div class="rich-text">My first paragraph</div></div>', html)
|
||||
|
||||
def test_render_calls_block_render_on_children(self):
|
||||
"""
|
||||
The default rendering of a StreamBlock should invoke the block's render method
|
||||
on each child, rather than just outputting the child value as a string.
|
||||
"""
|
||||
block = blocks.StreamBlock([
|
||||
('heading', blocks.CharBlock(template='tests/blocks/heading_block.html')),
|
||||
('paragraph', blocks.CharBlock()),
|
||||
])
|
||||
value = block.to_python([
|
||||
{'type': 'heading', 'value': 'Hello'}
|
||||
])
|
||||
html = block.render(value)
|
||||
self.assertIn('<div class="block-heading"><h1>Hello</h1></div>', html)
|
||||
|
||||
# calling render_as_block() on value (a StreamValue instance)
|
||||
# should be equivalent to block.render(value)
|
||||
html = value.render_as_block()
|
||||
self.assertIn('<div class="block-heading"><h1>Hello</h1></div>', html)
|
||||
|
||||
def test_render_child_with_legacy_render_method(self):
|
||||
"""
|
||||
StreamBlock should gracefully handle child blocks with a legacy 'render'
|
||||
method (one which doesn't accept a 'context' kwarg), but output a
|
||||
RemovedInWagtail18Warning
|
||||
"""
|
||||
block = blocks.StreamBlock([
|
||||
('heading', LegacyRenderMethodBlock()),
|
||||
('paragraph', blocks.CharBlock()),
|
||||
])
|
||||
value = block.to_python([
|
||||
{'type': 'heading', 'value': 'Hello'}
|
||||
])
|
||||
with warnings.catch_warnings(record=True) as ws:
|
||||
warnings.simplefilter('always')
|
||||
|
||||
result = block.render(value)
|
||||
|
||||
self.assertIn('<div class="block-heading">HELLO</div>', result)
|
||||
self.assertEqual(len(ws), 1)
|
||||
self.assertIs(ws[0].category, RemovedInWagtail18Warning)
|
||||
|
||||
# calling render_as_block() on value (a StreamValue instance)
|
||||
# should be equivalent to block.render(value)
|
||||
with warnings.catch_warnings(record=True) as ws:
|
||||
warnings.simplefilter('always')
|
||||
|
||||
result = value.render_as_block()
|
||||
|
||||
self.assertIn('<div class="block-heading">HELLO</div>', result)
|
||||
self.assertEqual(len(ws), 1)
|
||||
self.assertIs(ws[0].category, RemovedInWagtail18Warning)
|
||||
|
||||
def test_render_passes_context_to_children(self):
|
||||
block = blocks.StreamBlock([
|
||||
('heading', blocks.CharBlock(template='tests/blocks/heading_block.html')),
|
||||
('paragraph', blocks.CharBlock()),
|
||||
])
|
||||
value = block.to_python([
|
||||
{'type': 'heading', 'value': 'Bonjour'}
|
||||
])
|
||||
html = block.render(value, context={
|
||||
'language': 'fr',
|
||||
})
|
||||
self.assertIn('<div class="block-heading"><h1 lang="fr">Bonjour</h1></div>', html)
|
||||
|
||||
# calling render_as_block(context=foo) on value (a StreamValue instance)
|
||||
# should be equivalent to block.render(value, context=foo)
|
||||
html = value.render_as_block(context={
|
||||
'language': 'fr',
|
||||
})
|
||||
self.assertIn('<div class="block-heading"><h1 lang="fr">Bonjour</h1></div>', html)
|
||||
|
||||
def test_render_on_stream_child_uses_child_template(self):
|
||||
"""
|
||||
Accessing a child element of the stream (giving a StreamChild object) and rendering it
|
||||
should use the block template, not just render the value's string representation
|
||||
"""
|
||||
block = blocks.StreamBlock([
|
||||
('heading', blocks.CharBlock(template='tests/blocks/heading_block.html')),
|
||||
('paragraph', blocks.CharBlock()),
|
||||
])
|
||||
value = block.to_python([
|
||||
{'type': 'heading', 'value': 'Hello'}
|
||||
])
|
||||
html = value[0].render()
|
||||
self.assertEqual('<h1>Hello</h1>', html)
|
||||
|
||||
# StreamChild.__str__ should do the same
|
||||
html = str(value[0])
|
||||
self.assertEqual('<h1>Hello</h1>', html)
|
||||
|
||||
# and so should StreamChild.render_as_block
|
||||
html = value[0].render_as_block()
|
||||
self.assertEqual('<h1>Hello</h1>', html)
|
||||
|
||||
def test_can_pass_context_to_stream_child_template(self):
|
||||
block = blocks.StreamBlock([
|
||||
('heading', blocks.CharBlock(template='tests/blocks/heading_block.html')),
|
||||
('paragraph', blocks.CharBlock()),
|
||||
])
|
||||
value = block.to_python([
|
||||
{'type': 'heading', 'value': 'Bonjour'}
|
||||
])
|
||||
html = value[0].render(context={'language': 'fr'})
|
||||
self.assertEqual('<h1 lang="fr">Bonjour</h1>', html)
|
||||
|
||||
# the same functionality should be available through the alias `render_as_block`
|
||||
html = value[0].render_as_block(context={'language': 'fr'})
|
||||
self.assertEqual('<h1 lang="fr">Bonjour</h1>', html)
|
||||
|
||||
def render_form(self):
|
||||
class ArticleBlock(blocks.StreamBlock):
|
||||
heading = blocks.CharBlock()
|
||||
|
@ -1950,3 +2186,117 @@ class TestTemplateRendering(TestCase):
|
|||
|
||||
self.assertIn('data-prefix="my-link-block"', result)
|
||||
self.assertIn('<p>Hello from get_form_context!</p>', result)
|
||||
|
||||
|
||||
class TestIncludeBlockTag(TestCase):
|
||||
def test_include_block_tag_with_boundblock(self):
|
||||
"""
|
||||
The include_block tag should be able to render a BoundBlock's template
|
||||
while keeping the parent template's context
|
||||
"""
|
||||
block = blocks.CharBlock(template='tests/blocks/heading_block.html')
|
||||
bound_block = block.bind('bonjour')
|
||||
|
||||
result = render_to_string('tests/blocks/include_block_test.html', {
|
||||
'test_block': bound_block,
|
||||
'language': 'fr',
|
||||
})
|
||||
self.assertIn('<body><h1 lang="fr">bonjour</h1></body>', result)
|
||||
|
||||
def test_include_block_tag_with_structvalue(self):
|
||||
"""
|
||||
The include_block tag should be able to render a StructValue's template
|
||||
while keeping the parent template's context
|
||||
"""
|
||||
block = SectionBlock()
|
||||
struct_value = block.to_python({'title': 'Bonjour', 'body': 'monde <i>italique</i>'})
|
||||
|
||||
result = render_to_string('tests/blocks/include_block_test.html', {
|
||||
'test_block': struct_value,
|
||||
'language': 'fr',
|
||||
})
|
||||
|
||||
self.assertIn(
|
||||
"""<body><h1 lang="fr">Bonjour</h1><div class="rich-text">monde <i>italique</i></div></body>""",
|
||||
result
|
||||
)
|
||||
|
||||
def test_include_block_tag_with_streamvalue(self):
|
||||
"""
|
||||
The include_block tag should be able to render a StreamValue's template
|
||||
while keeping the parent template's context
|
||||
"""
|
||||
block = blocks.StreamBlock([
|
||||
('heading', blocks.CharBlock(template='tests/blocks/heading_block.html')),
|
||||
('paragraph', blocks.CharBlock()),
|
||||
], template='tests/blocks/stream_with_language.html')
|
||||
|
||||
stream_value = block.to_python([
|
||||
{'type': 'heading', 'value': 'Bonjour'}
|
||||
])
|
||||
|
||||
result = render_to_string('tests/blocks/include_block_test.html', {
|
||||
'test_block': stream_value,
|
||||
'language': 'fr',
|
||||
})
|
||||
|
||||
self.assertIn('<div class="heading" lang="fr"><h1 lang="fr">Bonjour</h1></div>', result)
|
||||
|
||||
def test_include_block_tag_with_plain_value(self):
|
||||
"""
|
||||
The include_block tag should be able to render a value without a render_as_block method
|
||||
by just rendering it as a string
|
||||
"""
|
||||
result = render_to_string('tests/blocks/include_block_test.html', {
|
||||
'test_block': 42,
|
||||
})
|
||||
|
||||
self.assertIn('<body>42</body>', result)
|
||||
|
||||
def test_include_block_tag_with_filtered_value(self):
|
||||
"""
|
||||
The block parameter on include_block tag should support complex values including filters,
|
||||
e.g. {% include_block foo|default:123 %}
|
||||
"""
|
||||
block = blocks.CharBlock(template='tests/blocks/heading_block.html')
|
||||
bound_block = block.bind('bonjour')
|
||||
|
||||
result = render_to_string('tests/blocks/include_block_test_with_filter.html', {
|
||||
'test_block': bound_block,
|
||||
'language': 'fr',
|
||||
})
|
||||
self.assertIn('<body><h1 lang="fr">bonjour</h1></body>', result)
|
||||
|
||||
result = render_to_string('tests/blocks/include_block_test_with_filter.html', {
|
||||
'test_block': None,
|
||||
'language': 'fr',
|
||||
})
|
||||
self.assertIn('<body>999</body>', result)
|
||||
|
||||
def test_include_block_tag_with_extra_context(self):
|
||||
"""
|
||||
Test that it's possible to pass extra context on an include_block tag using
|
||||
{% include_block foo with classname="bar" %}
|
||||
"""
|
||||
block = blocks.CharBlock(template='tests/blocks/heading_block.html')
|
||||
bound_block = block.bind('bonjour')
|
||||
|
||||
result = render_to_string('tests/blocks/include_block_with_test.html', {
|
||||
'test_block': bound_block,
|
||||
'language': 'fr',
|
||||
})
|
||||
self.assertIn('<body><h1 lang="fr" class="important">bonjour</h1></body>', result)
|
||||
|
||||
def test_include_block_tag_with_only_flag(self):
|
||||
"""
|
||||
A tag such as {% include_block foo with classname="bar" only %}
|
||||
should not inherit the parent context
|
||||
"""
|
||||
block = blocks.CharBlock(template='tests/blocks/heading_block.html')
|
||||
bound_block = block.bind('bonjour')
|
||||
|
||||
result = render_to_string('tests/blocks/include_block_only_test.html', {
|
||||
'test_block': bound_block,
|
||||
'language': 'fr',
|
||||
})
|
||||
self.assertIn('<body><h1 class="important">bonjour</h1></body>', result)
|
||||
|
|
|
@ -17,7 +17,7 @@ class DocumentChooserBlock(ChooserBlock):
|
|||
from wagtail.wagtaildocs.widgets import AdminDocumentChooser
|
||||
return AdminDocumentChooser
|
||||
|
||||
def render_basic(self, value):
|
||||
def render_basic(self, value, context=None):
|
||||
if value:
|
||||
return format_html('<a href="{0}">{1}</a>', value.url, value.title)
|
||||
else:
|
||||
|
|
|
@ -18,7 +18,7 @@ class ImageChooserBlock(ChooserBlock):
|
|||
from wagtail.wagtailimages.widgets import AdminImageChooser
|
||||
return AdminImageChooser
|
||||
|
||||
def render_basic(self, value):
|
||||
def render_basic(self, value, context=None):
|
||||
if value:
|
||||
return get_rendition_or_not_found(value, 'original').img_tag()
|
||||
else:
|
||||
|
|
Ładowanie…
Reference in New Issue