Spec-Zone.ru › Wagtail

Как создать пользовательские блоки 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 для указания собственного шаблона. В этом шаблоне доступны следующие переменные:

children
Список OrderedDict BoundBlock для всех дочерних блоков, составляющих этот StructBlock.

help_text
Текст справки для этого блока, если он указан.

classname Имя класса, переданное как form_classname (по умолчанию struct-block).

block_definition Экземпляр StructBlock, который определяет этот блок.

prefix Префикс, используемый для полей формы этого экземпляра блока, гарантированно уникальный для всей формы.

Для добавления дополнительных переменных можно переопределить метод 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="w-field" data-field data-contentpath="{{ child.block.name }}">
            {% if child.block.label %}
                <label class="w-field__label" {% if child.id_for_label %}for="{{ child.id_for_label }}"{% endif %}>{{ child.block.label }}{% if child.block.required %}<span class="w-required-mark">*</span>{% endif %}</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'),
    ])

мы можем захотеть отключить поле «state», когда выбранная страна отличается от Соединенных Штатов. Поскольку новые блоки могут добавляться динамически, нам необходимо интегрироваться с собственным фронтенд-логикой 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-класса, который будет зарегистрирован с клиентским кодом telepath, а '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.Textarea, django.forms.Select или django.forms.RadioSelect, или имеет настроенное клиентское поведение в той степени, в которой его данные нельзя просто прочитать или записать, обратившись к свойству value элемента формы, вам нужно будет предоставить объект обработчика JavaScript, реализующий методы, описанные в API клиентской стороны виджетов формы.

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

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

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

Если вы создадите подкласс любого другого класса блока, такого как FieldBlock, вам нужно будет либо сохранить определение этого класса на всё время существования проекта, либо реализовать собственный метод деконструирования, который выражает ваш блок исключительно через классы, которые гарантированно останутся на месте. Аналогично, если вы настроите подкласс 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/stable/advanced_topics/customisation/streamfield_blocks.html

Spec-Zone.ru

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