kopia lustrzana https://github.com/wagtail/wagtail
Introduce new template fragment composition tags
rodzic
952edd84c7
commit
524cab82e3
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 Django’s 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 tag’s 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 template’s 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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue