Как создать пользовательские блоки 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>
Новое в версии 2.13: Атрибут data-contentpath теперь обязателен для контейнерного элемента вокруг вывода render_form.
Дополнительный 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, как в следующих классах:
FieldBlockDefinitionListBlockDefinitionStaticBlockDefinitionStreamBlockDefinitionStructBlockDefinition
Сначала мы определим адаптер telepath для AddressBlock, чтобы он использовал наш собственный JavaScript-класс вместо стандартного StructBlockDefinition. Это можно сделать в том же модуле, что и определение AddressBlock:
from wagtail.core.blocks.struct_block import StructBlockAdapter
from wagtail.core.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.core.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.core.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.core.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/v2.16.3/advanced_topics/customisation/streamfield_blocks.html