Spec-Zone.ru › Wagtail 3

Как создать пользовательские блоки StreamField

Пользовательские интерфейсы редактирования для StructBlock

Чтобы настроить внешний вид StructBlock в редакторе страницы, можно указать атрибут form_classname (как ключевой аргумент конструктора StructBlock, или в подклассе Meta). Это позволит переопределить значение по умолчанию struct-block:

class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    class Meta:
        icon = 'user'
        form_classname = 'person-block struct-block'

Затем можно предоставить пользовательский CSS для этого блока, используя указанный класс, с помощью хука insert_editor_css.

Примечание

В Wagtail есть встроенная стилизация редактора для класса struct-block и других связанных элементов. Если вы укажете значение для form_classname, оно перекроет уже применённые классы к StructBlock, поэтому необходимо указать также struct-block.

Для более сложных настроек, требующих изменений в разметке HTML, можно переопределить атрибут form_template в Meta и указать собственный путь к шаблону. В этом шаблоне доступны следующие переменные:

Для добавления дополнительных переменных можно переопределить метод get_form_context блока:

class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    def get_form_context(self, value, prefix='', errors=None):
        context = super().get_form_context(value, prefix=prefix, errors=errors)
        context['suggested_first_names'] = ['John', 'Paul', 'George', 'Ringo']
        return context

    class Meta:
        icon = 'user'
        form_template = 'myapp/block_forms/person.html'

Шаблон формы для StructBlock должен содержать вывод render_form для каждого дочернего блока в словаре children, внутри контейнера с атрибутом data-contentpath равным имени блока. Этот атрибут используется системой комментирования для прикрепления комментариев к соответствующим полям. Шаблон формы StructBlock также отвечает за отображение меток для каждого поля, но (и вся остальная разметка HTML) может быть настраиваема по вашему усмотрению. Приведённый ниже шаблон дублирует стандартное отображение формы StructBlock:

{% load wagtailadmin_tags  %}

<div class="{{ classname }}">
    {% if help_text %}
        <span>
            <div class="help">
                {% icon name="help" class_name="default" %}
                {{ help_text }}
            </div>
        </span>
    {% endif %}

    {% for child in children.values %}
        <div class="field {% if child.block.required %}required{% endif %}" data-contentpath="{{ child.block.name }}">
            {% if child.block.label %}
                <label class="field__label" {% if child.id_for_label %}for="{{ child.id_for_label }}"{% endif %}>{{ child.block.label }}</label>
            {% endif %}
            {{ child.render_form }}
        </div>
    {% endfor %}
</div>

Дополнительный JavaScript для форм StructBlock

Часто требуется добавить пользовательское поведение JavaScript к форме StructBlock. Например, для блока:

class AddressBlock(StructBlock):
    street = CharBlock()
    town = CharBlock()
    state = CharBlock(required=False)
    country = ChoiceBlock(choices=[
        ('us', 'United States'),
        ('ca', 'Canada'),
        ('mx', 'Mexico'),
    ])

можно отключить поле «состояние» при выборе страны, отличной от США. Поскольку новые блоки могут добавляться динамически, необходимо интегрироваться с собственным фронтенд-логикой StreamField, чтобы гарантировать выполнение пользовательского кода JavaScript при инициализации нового блока.

StreamField использует библиотеку telepath для сопоставления классов блоков Python, таких как StructBlock, с соответствующей реализацией JavaScript. К этим реализациям JavaScript можно получить доступ через пространство имён window.wagtailStreamField.blocks, как к следующим классам:

  • FieldBlockDefinition
  • ListBlockDefinition
  • StaticBlockDefinition
  • StreamBlockDefinition
  • StructBlockDefinition

Сначала определим адаптер telepath для AddressBlock, чтобы он использовал собственный класс JavaScript вместо стандартного StructBlockDefinition. Это можно сделать в том же модуле, что и определение AddressBlock:

from wagtail.blocks.struct_block import StructBlockAdapter
from wagtail.telepath import register
from django import forms
from django.utils.functional import cached_property

class AddressBlockAdapter(StructBlockAdapter):
    js_constructor = 'myapp.blocks.AddressBlock'

    @cached_property
    def media(self):
        structblock_media = super().media
        return forms.Media(
            js=structblock_media._js + ['js/address-block.js'],
            css=structblock_media._css
        )

register(AddressBlockAdapter(), AddressBlock)

Здесь 'myapp.blocks.AddressBlock' — идентификатор нашего класса JavaScript, который будет зарегистрирован в коде телепатии на стороне клиента, а 'js/address-block.js' — файл, который его определяет (как путь внутри любого расположения статических файлов, распознаваемого Django).

Эта реализация наследуется от StructBlockDefinition и добавляет пользовательский код в метод render:

class AddressBlockDefinition extends window.wagtailStreamField.blocks.StructBlockDefinition {
    render(placeholder, prefix, initialState, initialError) {
        const block = super.render(placeholder, prefix, initialState, initialError);

        const stateField = document.getElementById(prefix + '-state');
        const countryField = document.getElementById(prefix + '-country');
        const updateStateInput = () => {
            if (countryField.value == 'us') {
                stateField.removeAttribute('disabled');
            } else {
                stateField.setAttribute('disabled', true);
            }
        }
        updateStateInput();
        countryField.addEventListener('change', updateStateInput);

        return block;
    }
}
window.telepath.register('myapp.blocks.AddressBlock', AddressBlockDefinition);

Дополнительные методы и свойства для StructBlock значений

При отображении содержимого StreamField в шаблоне значения StructBlock представлены как объекты, похожие на dict, где ключи соответствуют именам дочерних блоков. В частности, эти значения являются экземплярами класса wagtail.blocks.StructValue.

Иногда требуется сделать доступными дополнительные методы или свойства для этого объекта. Например, для StructBlock, представляющего внутреннюю или внешнюю ссылку:

class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

можно создать свойство url, которое возвращает URL страницы или внешний URL в зависимости от заполненного поля. Распространённая ошибка — определить это свойство в классе блока:

class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

    @property
    def url(self):  # INCORRECT - will not work
        return self.external_url or self.page.url

Это не работает, потому что значение, отображаемое в шаблоне, не является экземпляром LinkBlock. Экземпляры StructBlock служат только для определения поведения блока и не хранят данные блока в своём внутреннем состоянии — в этом отношении они похожи на объекты виджетов форм Django (они предоставляют методы для отображения заданного значения в виде поля формы, но не хранят само значение).

Вместо этого необходимо определить подкласс StructValue с реализацией пользовательского свойства или метода. Внутри этого метода данные блока можно получить как self['page'] или self.get('page'), так как StructValue — объект типа словарь.

from wagtail.blocks import StructValue


class LinkStructValue(StructValue):
    def url(self):
        external_url = self.get('external_url')
        page = self.get('page')
        return external_url or page.url

После определения нужно установить параметр value_class блока, чтобы он использовал этот класс вместо простого StructValue:

class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

    class Meta:
        value_class = LinkStructValue

Теперь методы расширенного класса значения будут доступны в вашем шаблоне:

{% for block in page.body %}
    {% if block.block_type == 'link' %}
        <a href="{{ link.value.url }}">{{ link.value.text }}</a>
    {% endif %}
{% endfor %}

Пользовательские типы блоков

Если требуется реализовать пользовательский интерфейс или обработать тип данных, не предоставляемый встроенными типами блоков Wagtail (и не может быть построен как структура из существующих полей), можно определить собственные типы блоков. Для получения дополнительной информации см. исходный код встроенных классов блоков Wagtail.

Для типов блоков, которые просто оборачивают существующее поле Django формы, Wagtail предоставляет абстрактный класс wagtail.blocks.FieldBlock в качестве помощника. Подклассы должны установить свойство field, которое возвращает объект поля формы:

class IPAddressBlock(FieldBlock):
    def __init__(self, required=True, help_text=None, **kwargs):
        self.field = forms.GenericIPAddressField(required=required, help_text=help_text)
        super().__init__(**kwargs)

Поскольку интерфейс редактирования StreamField нуждается в динамическом создании блоков, некоторым сложным типам виджетов потребуется дополнительный код JavaScript, определяющий, как их отображать и заполнять на стороне клиента. Если поле использует тип виджета, который не наследуется от одного из классов, наследующихся от django.forms.widgets.Input, django.forms.Select или django.forms.RadioSelect, или имеет настроенное поведение на стороне клиента до такой степени, что его данные невозможно прочитать или записать, просто обратившись к свойству элемента формы value, необходимо предоставить объект обработчика JavaScript, реализующий методы, описанные в API виджетов форм на стороне клиента.

Обработка определений блоков в миграциях

Как и любое поле модели в Django, любые изменения в определении модели, влияющие на StreamField, приведут к файлу миграции, содержащему «замороженную» копию этого определения поля. Поскольку определение StreamField сложнее, чем типичное поле модели, возрастает вероятность импорта определений вашего проекта в миграцию, что создаст проблемы в дальнейшем, если эти определения перемещаются или удаляются.

Для минимизации этой проблемы, StructBlock, StreamBlock и ChoiceBlock реализуют дополнительную логику, гарантирующую, что любые подклассы этих блоков деконструируются в простые экземпляры StructBlock, StreamBlock и ChoiceBlock — таким образом, миграции избегают ссылок на ваши пользовательские классы.

Это возможно, поскольку эти типы блоков обеспечивают стандартную схему наследования и знают, как реконструировать определение блока для любого подкласса, следующего этой схеме.

Если вы создаёте подкласс любого другого класса блока, например FieldBlock, вам необходимо либо сохранить этот класс в проекте на протяжении всего его жизненного цикла, либо реализовать метод пользовательского метода deconstruct, выражающий ваш блок исключительно через классы, чьё существование гарантировано. Аналогично, если вы настраиваете подкласс StructBlock, StreamBlock или ChoiceBlock настолько, что его больше нельзя выразить как экземпляр базового типа блока (например, если вы добавляете дополнительные аргументы в конструктор), необходимо предоставить собственный метод deconstruct.

© 2014-present Torchbox Ltd and individual contributors.
All rights are reserved.
Licensed under the BSD License.
https://docs.wagtail.org/en/v3.0.3/advanced_topics/customisation/streamfield_blocks.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API