kopia lustrzana https://github.com/wagtail/wagtail
Add mechanism for previewing StreamField blocks
rodzic
7c0712cc38
commit
3bad585b74
|
@ -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>
|
|
@ -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")
|
|
@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 %}
|
|
@ -0,0 +1,5 @@
|
|||
{% extends "wagtailadmin/generic/streamfield_block_preview.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<img src="{% static image_path %}" alt="{{ image_description }}" />
|
||||
{% endblock %}
|
Ładowanie…
Reference in New Issue