Introduce new template fragment composition tags

pull/8781/head
Thibaud Colas 2022-05-20 17:16:51 +01:00 zatwierdzone przez LB (Ben Johnston)
rodzic 952edd84c7
commit 524cab82e3
11 zmienionych plików z 290 dodań i 75 usunięć

Wyświetl plik

@ -23,7 +23,7 @@ jobs:
- run: pipenv run isort --check-only --diff .
- run: pipenv run black --target-version py37 --check --diff .
- run: git ls-files '*.html' | xargs pipenv run djhtml --check
- run: pipenv run curlylint --exclude '(dialog.html|end_dialog.html)' --parse-only wagtail
- run: pipenv run curlylint --parse-only wagtail
- run: pipenv run doc8 docs
- run: DATABASE_NAME=wagtail.db pipenv run python -u runtests.py

Wyświetl plik

@ -53,6 +53,7 @@ Changelog
* Added `WAGTAILADMIN_USER_PASSWORD_RESET_FORM` setting for overriding the admin password reset form (Michael Karamuth)
* Prefetch workflow states in edit page view to to avoid queries in other parts of the view/templates that need it (Tidiane Dia)
* Remove the edit link from edit bird in previews to avoid confusion (Sævar Öfjörð Magnússon)
* Introduce new template fragment and block level enclosure tags for easier template composition (Thibaud Colas)
* Fix: Typo in `ResumeWorkflowActionFormatter` message (Stefan Hammer)
* Fix: Throw a meaningful error when saving an image to an unrecognised image format (Christian Franke)
* Fix: Remove extra padding for headers with breadcrumbs on mobile viewport (Steven Steinwand)

Wyświetl plik

@ -21,7 +21,7 @@ lint-server:
black --target-version py37 --check --diff .
flake8
isort --check-only --diff .
curlylint --exclude '(dialog.html|end_dialog.html)' --parse-only wagtail
curlylint --parse-only wagtail
git ls-files '*.html' | xargs djhtml --check
lint-client:

Wyświetl plik

@ -60,6 +60,7 @@ When using a queryset to render a list of images, you can now use the `prefetch_
* Added `WAGTAILADMIN_USER_PASSWORD_RESET_FORM` setting for overriding the admin password reset form (Michael Karamuth)
* Prefetch workflow states in edit page view to to avoid queries in other parts of the view/templates that need it (Tidiane Dia)
* Remove the edit link from edit bird in previews to avoid confusion (Sævar Öfjörð Magnússon)
* Introduce new template fragment and block level enclosure tags for easier template composition (Thibaud Colas)
### Bug fixes

Wyświetl plik

@ -8,11 +8,15 @@
{% endblock %}
{% block content %}
{% fragment as header_title %}
{% block branding_welcome %}{% blocktrans trimmed %}Welcome to the {{ site_name }} Wagtail CMS{% endblocktrans %}{% endblock %}
{% endfragment %}
<header class="header merged header--home">
<div class="avatar"><img src="{% avatar_url user %}" alt="" /></div>
<div class="sm:w-ml-4">
<h1 class="header__title">{% block branding_welcome %}{% blocktrans trimmed %}Welcome to the {{ site_name }} Wagtail CMS{% endblocktrans %}{% endblock %}</h1>
<h1 class="header__title">{{ header_title }}</h1>
<div class="user-name">{{ user|user_display_name }}</div>
</div>
</header>

Wyświetl plik

@ -32,8 +32,8 @@
<p class="w-dialog__subtitle w-help-text">{{ subtitle }}</p>
{% endif %}
{% comment %}
This markup is intentionally left without closing div tags so that the contents can be populated with child elements between dialog and enddialog
For the end tags please see end-dialog.html
{% endcomment %}
{{ children }}
</div>
</div>
</div>
</template>

Wyświetl plik

@ -1,8 +0,0 @@
</div>
</div>
</div>
</template>
{% comment %}
This markup is used to close the end tags for dialog.html so that content can be nested between both tags like {% dialog %}{% enddialog %}
{% endcomment %}

Wyświetl plik

@ -0,0 +1,10 @@
{% load wagtailadmin_tags %}
<div class="help-block help-{{ status }}">
{% if status == 'info' %}
{% icon name='help' %}
{% else %}
{% icon name='warning' %}
{% endif %}
{{ children }}
</div>

Wyświetl plik

@ -8,6 +8,8 @@ from django.contrib.admin.utils import quote
from django.contrib.humanize.templatetags.humanize import intcomma, naturaltime
from django.contrib.messages.constants import DEFAULT_TAGS as MESSAGE_TAGS
from django.shortcuts import resolve_url as resolve_url_func
from django.template import Context
from django.template.base import token_kwargs
from django.template.defaultfilters import stringfilter
from django.templatetags.static import static
from django.urls import reverse
@ -859,61 +861,147 @@ def component(context, obj, fallback_render_method=False):
return obj.render_html(context)
@register.inclusion_tag("wagtailadmin/shared/dialog/dialog.html")
def dialog(
id,
title,
icon_name=None,
subtitle=None,
message_status=None,
message_heading=None,
message_description=None,
):
class FragmentNode(template.Node):
def __init__(self, nodelist, target_var):
self.nodelist = nodelist
self.target_var = target_var
def render(self, context):
fragment = self.nodelist.render(context) if self.nodelist else ""
context[self.target_var] = fragment
return ""
@register.tag(name="fragment")
def fragment(parser, token):
"""
Dialog tag - to be used with its corresponding {% enddialog %} tag with dialog content markup nested between
Store a template fragment as a variable.
Usage:
{% fragment as header_title %}
{% blocktrans trimmed %}Welcome to the {{ site_name }} Wagtail CMS{% endblocktrans %}
{% fragment %}
Copy-paste of slippers fragment template tag.
See https://github.com/mixxorz/slippers/blob/254c720e6bb02eb46ae07d104863fce41d4d3164/slippers/templatetags/slippers.py#L173.
"""
if not title:
raise ValueError("You must supply a title")
if not id:
raise ValueError("You must supply an id")
error_message = "The syntax for fragment is {% fragment as variable_name %}"
# Used for determining which icon the message will use
message_status_type = {
"info": {
"message_icon_name": "info-circle",
},
"warning": {
"message_icon_name": "warning",
},
"critical": {
"message_icon_name": "warning",
},
"success": {
"message_icon_name": "circle-check",
},
}
try:
tag_name, _, target_var = token.split_contents()
nodelist = parser.parse(("endfragment",))
parser.delete_first_token()
except ValueError:
if settings.DEBUG:
raise template.TemplateSyntaxError(error_message)
return ""
context = {
"id": id,
"title": title,
"icon_name": icon_name,
"subtitle": subtitle,
"message_heading": message_heading,
"message_description": message_description,
"message_status": message_status,
}
# If there is a message status then add the context for that message type
if message_status:
context.update(**message_status_type[message_status])
return context
return FragmentNode(nodelist, target_var)
# Closing tag for dialog tag {% enddialog %}
@register.inclusion_tag("wagtailadmin/shared/dialog/end_dialog.html")
def enddialog():
return
class BlockInclusionNode(template.Node):
"""
Create template-driven tags like Djangos inclusion_tag / InclusionNode, but for block-level tags.
Usage:
{% my_tag status="test" label="Alert" %}
Proceed with caution.
{% endmy_tag %}
Within `my_tag`s template, the template fragment will be accessible as the {{ children }} context variable.
The output can also be stored as a variable in the parent context:
{% my_tag status="test" label="Alert" as my_variable %}
Proceed with caution.
{% endmy_tag %}
Inspired by slippers Component Node.
See https://github.com/mixxorz/slippers/blob/254c720e6bb02eb46ae07d104863fce41d4d3164/slippers/templatetags/slippers.py#L47.
"""
def __init__(self, nodelist, template, extra_context, target_var=None):
self.nodelist = nodelist
self.template = template
self.extra_context = extra_context
self.target_var = target_var
def get_context_data(self, parent_context):
return parent_context
def render(self, context):
children = self.nodelist.render(context) if self.nodelist else ""
values = {
# Resolve the tags parameters within the current context.
key: value.resolve(context)
for key, value in self.extra_context.items()
}
t = context.template.engine.get_template(self.template)
# Add the `children` variable in the rendered templates context.
context_data = self.get_context_data({**values, "children": children})
output = t.render(Context(context_data, autoescape=context.autoescape))
if self.target_var:
context[self.target_var] = output
return ""
return output
@classmethod
def handle(cls, parser, token):
tag_name, *remaining_bits = token.split_contents()
nodelist = parser.parse((f"end{tag_name}",))
parser.delete_first_token()
extra_context = token_kwargs(remaining_bits, parser)
# Allow component fragment to be assigned to a variable
target_var = None
if len(remaining_bits) >= 2 and remaining_bits[-2] == "as":
target_var = remaining_bits[-1]
return cls(nodelist, cls.template, extra_context, target_var)
class DialogNode(BlockInclusionNode):
template = "wagtailadmin/shared/dialog/dialog.html"
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
if "title" not in context:
raise TypeError("You must supply a title")
if "id" not in context:
raise TypeError("You must supply an id")
# Used for determining which icon the message will use
message_icon_name = {
"info": "info-circle",
"warning": "warning",
"critical": "warning",
"success": "circle-check",
}
message_status = context.get("message_status")
# If there is a message status then determine which icon to use.
if message_status:
context["message_icon_name"] = message_icon_name[message_status]
return context
register.tag("dialog", DialogNode.handle)
class HelpBlockNode(BlockInclusionNode):
template = "wagtailadmin/shared/help_block.html"
register.tag("help_block", HelpBlockNode.handle)
# Button used to open dialogs

Wyświetl plik

@ -3,7 +3,7 @@ from datetime import timedelta
from unittest import mock
from django.conf import settings
from django.template import Context, Template
from django.template import Context, Template, TemplateSyntaxError
from django.test import TestCase
from django.test.utils import override_settings
from django.utils import timezone
@ -247,3 +247,125 @@ class TestInternationalisationTags(TestCase):
# check with an invalid id
with self.assertNumQueries(0):
self.assertIsNone(locale_label_from_id(self.locale_ids[-1] + 100), None)
class ComponentTest(TestCase):
def test_render_block_component(self):
template = """
{% load wagtailadmin_tags %}
{% help_block status="info" %}Proceed with caution{% endhelp_block %}
"""
expected = """
<div class="help-block help-info">
<svg aria-hidden="true" class="icon icon icon-help"><use href="#icon-help"></svg>
Proceed with caution
</div>
"""
self.assertHTMLEqual(expected, Template(template).render(Context()))
def test_render_nested(self):
template = """
{% load wagtailadmin_tags %}
{% help_block status="warning" %}
{% help_block status="info" %}Proceed with caution{% endhelp_block %}
{% endhelp_block %}
"""
expected = """
<div class="help-block help-warning">
<svg aria-hidden="true" class="icon icon icon-warning"><use href="#icon-warning"></svg>
<div class="help-block help-info">
<svg aria-hidden="true" class="icon icon icon-help"><use href="#icon-help"></svg>
Proceed with caution
</div>
</div>
"""
self.assertHTMLEqual(expected, Template(template).render(Context()))
def test_kwargs_with_filters(self):
template = """
{% load wagtailadmin_tags %}
{% help_block status="warning"|upper %}Proceed with caution{% endhelp_block %}
"""
expected = """
<div class="help-block help-WARNING">
<svg aria-hidden="true" class="icon icon icon-warning"><use href="#icon-warning"></svg>
Proceed with caution
</div>
"""
self.assertHTMLEqual(expected, Template(template).render(Context()))
def test_render_as_variable(self):
template = """
{% load wagtailadmin_tags %}
{% help_block status="info" as help %}Proceed with caution{% endhelp_block %}
<template>{{ help }}</template>
"""
expected = """
<template>
<div class="help-block help-info">
<svg aria-hidden="true" class="icon icon icon-help"><use href="#icon-help"></svg>
Proceed with caution
</div>
</template>
"""
self.assertHTMLEqual(expected, Template(template).render(Context()))
class FragmentTagTest(TestCase):
def test_basic(self):
context = Context({})
template = """
{% load wagtailadmin_tags %}
{% fragment as my_fragment %}
<p>Hello, World</p>
{% endfragment %}
Text coming after:
{{ my_fragment }}
"""
expected = """
Text coming after:
<p>Hello, World</p>
"""
self.assertHTMLEqual(expected, Template(template).render(context))
@override_settings(DEBUG=True)
def test_syntax_error(self):
template = """
{% load wagtailadmin_tags %}
{% fragment %}
<p>Hello, World</p>
{% endfragment %}
"""
with self.assertRaises(TemplateSyntaxError):
Template(template).render(Context())
def test_with_variables(self):
context = Context({"name": "jonathan wells"})
template = """
{% load wagtailadmin_tags %}
{% fragment as my_fragment %}
<p>Hello, {{ name|title }}</p>
{% endfragment %}
Text coming after:
{{ my_fragment }}
"""
expected = """
Text coming after:
<p>Hello, Jonathan Wells</p>
"""
self.assertHTMLEqual(expected, Template(template).render(context))

Wyświetl plik

@ -256,21 +256,18 @@
Help text is not to be confused with the messages that appear in a banner drop down from the top of the screen. Help text are permanent instructions, visible on every page view, that explain or warn about something.
</p>
<div class="help-block help-info">
{% icon name='help' %}
{% help_block status="info" %}
<p>This is help text that might be just for information, explaining what happens next, or drawing the user's attention to something they're about to do</p>
<p>It could be multiple lines</p>
</div>
{% endhelp_block %}
<p class="help-block help-warning">
{% icon name='warning' %}
{% help_block status="warning" %}
A warning message might be output in cases where a user's action could have serious consequences
</p>
{% endhelp_block %}
<div class="help-block help-critical">
{% icon name='warning' %}
{% help_block status="critical" %}
A critical message would probably be rare, in cases where a particularly brittle or dangerously destructive action could be performed and needs to be warned about.
</div>
{% endhelp_block %}
</section>