Add mechanism for previewing StreamField blocks

pull/12700/head
Sage Abdullah 2024-12-16 16:46:29 +00:00
rodzic 7c0712cc38
commit 3bad585b74
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: EB1A33CC51CC0217
7 zmienionych plików z 362 dodań i 2 usunięć

Wyświetl plik

@ -0,0 +1,28 @@
{% load wagtailcore_tags i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% get_current_language_bidi as LANGUAGE_BIDI %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}">
<head>
{% block head %}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<meta name="robots" content="noindex" />
<title>{{ page_title }}</title>
{% block css %}
{% endblock %}
{% endblock %}
</head>
<body>
<main>
{% block content %}
{% include_block bound_block %}
{% endblock %}
</main>
{% block js %}
{% endblock %}
</body>
</html>

Wyświetl plik

@ -0,0 +1,228 @@
from django.contrib.auth.models import Permission
from django.http import HttpRequest
from django.test import TestCase
from django.urls import reverse
from django.utils.http import urlencode
from wagtail import blocks
from wagtail.test.utils import WagtailTestUtils
class TestStreamFieldBlockPreviewView(WagtailTestUtils, TestCase):
def get(self, block):
return self.client.get(
reverse("wagtailadmin_block_preview"),
{"id": block.definition_prefix},
)
def setUp(self):
self.user = self.login()
def test_simple(self):
block = blocks.CharBlock(
label="Single-line text",
description="A single line of text",
preview_value="Hello, world!",
)
response = self.get(block)
self.assertEqual(response.status_code, 200)
soup = self.get_soup(response.content)
html = soup.select_one("html")
self.assertIsNotNone(html)
self.assertEqual(html["lang"], "en")
self.assertEqual(html["dir"], "ltr")
color_scheme = soup.select_one("meta[name=color-scheme]")
self.assertIsNotNone(color_scheme)
self.assertEqual(color_scheme["content"], "dark light")
robots = soup.select_one("meta[name=robots]")
self.assertIsNotNone(robots)
self.assertEqual(robots["content"], "noindex")
title = soup.select_one("title")
self.assertIsNotNone(title)
self.assertEqual(title.text.strip(), "Preview for Single-line text (CharBlock)")
main = soup.select_one("main")
self.assertIsNotNone(main)
self.assertEqual(main.text.strip(), "Hello, world!")
def test_nonexisting_block(self):
response = self.client.get(reverse("wagtailadmin_block_preview"))
self.assertEqual(response.status_code, 404)
response = self.client.get(
reverse("wagtailadmin_block_preview"),
{"id": "nonexisting"},
)
self.assertEqual(response.status_code, 404)
def test_no_admin_permission(self):
self.user.is_superuser = False
self.user.save()
block = blocks.CharBlock()
response = self.get(block)
self.assertRedirects(
response,
reverse("wagtailadmin_login")
+ "?"
+ urlencode({"next": response.wsgi_request.get_full_path()}),
)
def test_minimal_permission(self):
self.user.is_superuser = False
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin",
codename="access_admin",
)
)
self.user.save()
block = blocks.CharBlock()
response = self.get(block)
self.assertEqual(response.status_code, 200)
def test_no_preview_value_no_default(self):
block = blocks.Block()
response = self.get(block)
self.assertEqual(response.status_code, 200)
soup = self.get_soup(response.content)
main = soup.select_one("main")
self.assertIsNotNone(main)
self.assertEqual(main.text.strip(), "None")
def test_preview_value_falls_back_to_default(self):
block = blocks.IntegerBlock(default=42)
response = self.get(block)
self.assertEqual(response.status_code, 200)
soup = self.get_soup(response.content)
main = soup.select_one("main")
self.assertIsNotNone(main)
self.assertEqual(main.text.strip(), "42")
def test_preview_template(self):
class PreviewTemplateViaMeta(blocks.Block):
class Meta:
preview_template = "tests/custom_block_preview.html"
class PreviewTemplateViaMethod(blocks.Block):
def get_preview_template(self, value=None, context=None):
return "tests/custom_block_preview.html"
cases = [
("meta", PreviewTemplateViaMeta()),
("method", PreviewTemplateViaMethod()),
("kwarg", blocks.Block(preview_template="tests/custom_block_preview.html")),
]
for via, block in cases:
with self.subTest(via=via):
response = self.get(block)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "tests/custom_block_preview.html")
response = self.get(block)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "tests/custom_block_preview.html")
soup = self.get_soup(response.content)
custom_wrapper = soup.select_one("main .my-preview-wrapper")
self.assertIsNotNone(custom_wrapper)
custom_css = soup.select_one("link[rel=stylesheet]")
self.assertIsNotNone(custom_css)
self.assertEqual(custom_css["href"], "/static/css/custom.css")
custom_js = soup.select_one("script[src]")
self.assertIsNotNone(custom_js)
self.assertEqual(custom_js["src"], "/static/js/custom.js")
def test_preview_value(self):
class PreviewValueViaMeta(blocks.Block):
class Meta:
preview_value = "Hello, world!"
class PreviewValueViaMethod(blocks.Block):
def get_preview_value(self):
return "Hello, world!"
cases = [
("meta", PreviewValueViaMeta()),
("method", PreviewValueViaMethod()),
("kwarg", blocks.Block(preview_value="Hello, world!")),
]
for via, block in cases:
with self.subTest(via=via):
response = self.get(block)
self.assertEqual(response.status_code, 200)
soup = self.get_soup(response.content)
main = soup.select_one("main")
self.assertIsNotNone(main)
self.assertEqual(main.text.strip(), "Hello, world!")
def test_custom_preview_context(self):
preview_value = "With a custom context"
label = "Fancy block"
class CustomPreviewContextBlock(blocks.Block):
def get_preview_context(block, value, parent_context=None):
self.assertEqual(value, preview_value)
self.assertIsNotNone(parent_context)
self.assertIsInstance(parent_context.get("request"), HttpRequest)
self.assertIs(parent_context.get("block_def"), block)
self.assertIs(
parent_context.get("block_class"),
CustomPreviewContextBlock,
)
self.assertIsInstance(
parent_context.get("bound_block"),
blocks.BoundBlock,
)
self.assertEqual(
parent_context.get("page_title"),
"Preview for Fancy block (CustomPreviewContextBlock)",
)
return {
**parent_context,
"extra": "Added by get_preview_context",
"page_title": "Custom title",
}
block = CustomPreviewContextBlock(label=label, preview_value=preview_value)
response = self.get(block)
self.assertEqual(response.status_code, 200)
soup = self.get_soup(response.content)
main = soup.select_one("main")
self.assertIsNotNone(main)
self.assertEqual(main.text.strip(), preview_value)
# Ensure custom context can add and override values
self.assertEqual(response.context["extra"], "Added by get_preview_context")
self.assertEqual(response.context["page_title"], "Custom title")
# Ensure that the default context is still present
self.assertIs(response.context["block_def"], block)
def test_static_image_preview(self):
class StaticImagePreviewBlock(blocks.Block):
def get_preview_context(self, value, parent_context=None):
return {
"image_path": "block_previews/preview.jpg",
"image_description": "A preview of the block",
}
class Meta:
preview_template = "tests/static_block_preview.html"
block = StaticImagePreviewBlock()
response = self.get(block)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "tests/static_block_preview.html")
soup = self.get_soup(response.content)
img = soup.select_one("html body main img")
self.assertIsNotNone(img)
self.assertEqual(img["src"], "/static/block_previews/preview.jpg")
self.assertEqual(img["alt"], "A preview of the block")

Wyświetl plik

@ -19,6 +19,7 @@ from wagtail.admin.urls import reports as wagtailadmin_reports_urls
from wagtail.admin.urls import workflows as wagtailadmin_workflows_urls
from wagtail.admin.views import account, chooser, dismissibles, home, tags
from wagtail.admin.views.bulk_action import index as bulk_actions
from wagtail.admin.views.generic.preview import StreamFieldBlockPreview
from wagtail.admin.views.pages import listing
from wagtail.utils.urlpatterns import decorate_urlpatterns
@ -118,6 +119,11 @@ urlpatterns = [
namespace="wagtailadmin_editing_sessions",
),
),
path(
"block-preview/",
StreamFieldBlockPreview.as_view(),
name="wagtailadmin_block_preview",
),
]

Wyświetl plik

@ -7,9 +7,12 @@ from django.http.request import QueryDict
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views.generic import View
from django.utils.functional import cached_property
from django.utils.translation import gettext
from django.views.generic import TemplateView, View
from wagtail.admin.panels import get_edit_handler
from wagtail.blocks.base import Block
from wagtail.models import PreviewableMixin, RevisionMixin
from wagtail.utils.decorators import xframe_options_sameorigin_override
@ -163,3 +166,55 @@ class PreviewRevision(View):
raise PermissionDenied
return self.revision_object.make_preview_request(request, preview_mode)
@method_decorator(xframe_options_sameorigin_override, name="get")
class StreamFieldBlockPreview(TemplateView):
template_name = "wagtailadmin/generic/streamfield_block_preview.html"
http_method_names = ("get",)
@cached_property
def block_id(self):
return self.request.GET.get("id")
@cached_property
def block_def(self) -> Block:
if not (block := Block.definition_registry.get(self.block_id)):
raise Http404
return block
@cached_property
def block_value(self):
return self.block_def.get_preview_value()
@cached_property
def page_title(self):
return gettext("Preview for %(block_label)s (%(block_type)s)") % {
"block_label": self.block_def.label,
"block_type": self.block_def.__class__.__name__,
}
@cached_property
def base_context(self):
# Do NOT use the name `block` in the context, as it will conflict with
# the current block inside a template {% block %} tag.
return {
"request": self.request,
"block_def": self.block_def,
"block_class": self.block_def.__class__,
"bound_block": self.block_def.bind(self.block_value),
"page_title": self.page_title,
}
def get_template_names(self):
templates = [self.template_name]
if preview_template := self.block_def.get_preview_template(
self.block_value, self.base_context
):
templates.insert(0, preview_template)
return templates
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(self.base_context)
return self.block_def.get_preview_context(self.block_value, context)

Wyświetl plik

@ -159,7 +159,7 @@ class Block(metaclass=BaseBlock):
model definition time (e.g. something like StructValue which incorporates a
pointer back to the block definition object).
"""
return self.normalize(self.meta.default)
return self.normalize(getattr(self.meta, "default", None))
def clean(self, value):
"""
@ -271,6 +271,28 @@ class Block(metaclass=BaseBlock):
return mark_safe(render_to_string(template, new_context))
def get_preview_context(self, value, parent_context=None):
# We do not fall back to `get_context` here, because the preview context
# will be used for a complete view, not just the block. Instead, the
# default preview context uses `{% include_block %}`, which will use
# `get_context`.
return parent_context or {}
def get_preview_template(self, value, context=None):
# We do not fall back to `get_template` here, because the template will
# be used for a complete view, not just the block. In most cases, the
# block's template cannot stand alone for the preview, as it would be
# missing the necessary static assets.
#
# Instead, the default preview template uses `{% include_block %}`,
# which will use `get_template` if a template is defined.
return getattr(self.meta, "preview_template", None)
def get_preview_value(self):
if hasattr(self.meta, "preview_value"):
return self.normalize(self.meta.preview_value)
return self.get_default()
def get_api_representation(self, value, context=None):
"""
Can be used to customise the API response and defaults to the value returned by get_prep_value.

Wyświetl plik

@ -0,0 +1,16 @@
{% extends "wagtailadmin/generic/streamfield_block_preview.html" %}
{% load static %}
{% block content %}
<div class="my-preview-wrapper">
{{ block.super }}
</div>
{% endblock %}
{% block css %}
<link rel="stylesheet" href="{% static 'css/custom.css' %}">
{% endblock %}
{% block js %}
<script type="module" src="{% static 'js/custom.js' %}"></script>
{% endblock %}

Wyświetl plik

@ -0,0 +1,5 @@
{% extends "wagtailadmin/generic/streamfield_block_preview.html" %}
{% load static %}
{% block content %}
<img src="{% static image_path %}" alt="{{ image_description }}" />
{% endblock %}