Как использовать StreamField для смешанного содержимого
StreamField предоставляет модель редактирования содержимого, подходящую для страниц, не имеющих фиксированной структуры – таких как посты блога или новости – где текст может быть перемешан с подзаголовками, изображениями, цитатами и видео. Она также подходит для более специализированных типов контента, таких как карты и диаграммы (или, для блога о программировании, фрагменты кода). В этой модели различные типы контента представлены в виде последовательности «блоков», которые могут повторяться и располагаться в любом порядке.
Для более подробной информации о StreamField и о том, почему вы должны использовать её вместо поля rich text для тела статьи, см. запись в блоге Rich text fields and faster horses.
StreamField также предоставляет богатый API для определения собственных типов блоков, начиная от простых коллекций подблоков (например, «человек», состоящий из имени, фамилии и фотографии) до полностью настраиваемых компонентов со своим собственным интерфейсом редактирования. В базе данных содержимое StreamField хранится в формате JSON, гарантируя, что весь информационный контент поля сохраняется, а не только его HTML-представление.
Использование StreamField
StreamField это поле модели, которое можно определить в вашей модели страницы, как и любое другое поле:
from django.db import models
from wagtail.models import Page
from wagtail.fields import StreamField
from wagtail import blocks
from wagtail.admin.panels import FieldPanel
from wagtail.images.blocks import ImageChooserBlock
class BlogPage(Page):
author = models.CharField(max_length=255)
date = models.DateField("Post date")
body = StreamField([
('heading', blocks.CharBlock(form_classname="full title")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
], use_json_field=True)
content_panels = Page.content_panels + [
FieldPanel('author'),
FieldPanel('date'),
FieldPanel('body'),
]
В этом примере поле body модели BlogPage определено как StreamField, где авторы могут создавать контент из трёх разных типов блоков: заголовки, абзацы и изображения, которые могут использоваться и повторяться в любом порядке. Типы блоков, доступные авторам, определены как список (name, block_type) кортежей: «имя» используется для идентификации типа блока в шаблонах и должно соответствовать стандартным соглашениям Python для имён переменных: строчные буквы и знаки нижнего подчеркивания, без пробелов.
Полный список доступных типов блоков можно найти в Справочнике по блокам StreamField.
Примечание
StreamField не является прямым заменителем других типов полей, таких как RichTextField. Если вам нужно мигрировать существующее поле в StreamField, обратитесь к Миграции RichTextFields в StreamField.
Изменено в версии 3.0: Добавлен аргумент use_json_field=True. Это указывает на то, что для данного поля следует использовать встроенную поддержку JSONField в базе данных, и является временной мерой для помощи в миграции StreamField, созданных в более ранних версиях Wagtail; в будущем релизе он станет стандартным.
Вывод шаблона
StreamField предоставляет HTML-представление для всего содержимого потока, а также для каждого отдельного блока. Чтобы включить этот HTML в свою страницу, используйте тег {% include_block %}:
{% load wagtailcore_tags %}
...
{% include_block page.body %}
В стандартном отображении каждый блок потока заключён в элемент <div class="block-my_block_name"> (где my_block_name – имя блока, указанное в определении StreamField). Если вы хотите предоставить собственный HTML-разметку, вы можете вместо этого пройтись по значению поля и вызвать {% include_block %} для каждого блока поочерёдно:
{% load wagtailcore_tags %}
...
<article>
{% for block in page.body %}
<section>{% include_block block %}</section>
{% endfor %}
</article>
Для большего контроля над отображением конкретных типов блоков, каждый объект блока предоставляет свойства block_type и value:
{% load wagtailcore_tags %}
...
<article>
{% for block in page.body %}
{% if block.block_type == 'heading' %}
<h1>{{ block.value }}</h1>
{% else %}
<section class="block-{{ block.block_type }}">
{% include_block block %}
</section>
{% endif %}
{% endfor %}
</article>
Объединение блоков
Помимо прямого использования встроенных типов блоков в StreamField, можно создавать новые типы блоков, комбинируя подблоки различными способами. Примеры включают:
- Блок «изображение с подписью», состоящий из выбора изображения и текстового поля
- Раздел «связанные ссылки», где автор может предоставить любое количество ссылок на другие страницы
- Блок слайдов, где каждый слайд может быть изображением, текстом или видео, расположенными в любом порядке
После создания нового типа блока таким образом, вы можете использовать его везде, где использовался встроенный тип блока – включая использование его в качестве компонента для другого типа блока. Например, вы можете определить блок галереи изображений, где каждый элемент является блоком «изображение с подписью».
StructBlock
StructBlock позволяет сгруппировать несколько подблоков для отображения как одного блока. Подблоки передаются StructBlock в виде списка (name, block_type) кортежей:
body = StreamField([
('person', blocks.StructBlock([
('first_name', blocks.CharBlock()),
('surname', blocks.CharBlock()),
('photo', ImageChooserBlock(required=False)),
('biography', blocks.RichTextBlock()),
])),
('heading', blocks.CharBlock(form_classname="full title")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
], use_json_field=True)
При чтении содержимого StreamField (например, при выводе в шаблоне), значение StructBlock – это объект типа dict со ключами, соответствующими именам блоков, заданным в определении:
<article>
{% for block in page.body %}
{% if block.block_type == 'person' %}
<div class="person">
{% image block.value.photo width-400 %}
<h2>{{ block.value.first_name }} {{ block.value.surname }}</h2>
{{ block.value.biography }}
</div>
{% else %}
(rendering for other block types)
{% endif %}
{% endfor %}
</article>
Наследование от StructBlock
Размещение списка подблоков StructBlock внутри определения StreamField часто бывает трудно читать и затрудняет повторное использование того же блока в нескольких местах. В качестве альтернативы, StructBlock может быть расширен, с подблоками, определёнными как атрибуты подкласса. Блок «человек» в примере выше можно переписать как:
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock()
surname = blocks.CharBlock()
photo = ImageChooserBlock(required=False)
biography = blocks.RichTextBlock()
PersonBlock затем можно использовать в определении StreamField аналогично встроенным типам блоков:
body = StreamField([
('person', PersonBlock()),
('heading', blocks.CharBlock(form_classname="full title")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
], use_json_field=True)
Иконки блоков
В меню, которое авторы используют для добавления новых блоков в StreamField, каждый тип блока имеет связанную иконку. Для StructBlock и других структурных типов блоков используется запасная иконка, так как назначение этих блоков специфично для вашего проекта. Чтобы установить пользовательскую иконку, передайте опцию icon в качестве ключевого аргумента StructBlock, или как атрибут класса Meta:
body = StreamField([
('person', blocks.StructBlock([
('first_name', blocks.CharBlock()),
('surname', blocks.CharBlock()),
('photo', ImageChooserBlock(required=False)),
('biography', blocks.RichTextBlock()),
], icon='user')),
('heading', blocks.CharBlock(form_classname="full title")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
], use_json_field=True)
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock()
surname = blocks.CharBlock()
photo = ImageChooserBlock(required=False)
biography = blocks.RichTextBlock()
class Meta:
icon = 'user'
Список допустимых идентификаторов иконок см. в Руководстве по стилю пользовательского интерфейса.
ListBlock
ListBlock определяет повторяющийся блок, позволяющий авторам вставлять любое количество экземпляров определённого типа блока. Например, блок «галерея», состоящий из нескольких изображений, можно определить следующим образом:
body = StreamField([
('gallery', blocks.ListBlock(ImageChooserBlock())),
('heading', blocks.CharBlock(form_classname="full title")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
], use_json_field=True)
При чтении содержимого StreamField (например, при выводе в шаблоне), значение ListBlock – это список подзначений:
<article>
{% for block in page.body %}
{% if block.block_type == 'gallery' %}
<ul class="gallery">
{% for img in block.value %}
<li>{% image img width-400 %}</li>
{% endfor %}
</ul>
{% else %}
(rendering for other block types)
{% endif %}
{% endfor %}
</article>
StreamBlock
StreamBlock определяет набор подблоков, которые могут быть смешаны и повторены в любом порядке, посредством механизма, аналогичного самому StreamField. Например, карусель, поддерживающая изображения и видео-слайды, может быть определена следующим образом:
body = StreamField([
('carousel', blocks.StreamBlock([
('image', ImageChooserBlock()),
('video', EmbedBlock()),
])),
('heading', blocks.CharBlock(form_classname="full title")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
], use_json_field=True)
StreamBlock также может быть расширен аналогично StructBlock, с подблоками, указанными как атрибуты класса:
class CarouselBlock(blocks.StreamBlock):
image = ImageChooserBlock()
video = EmbedBlock()
class Meta:
icon = 'image'
Подкласс StreamBlock, определённый таким образом, также может быть передан в определение StreamField вместо списка типов блоков. Это позволяет настроить общий набор типов блоков для использования на нескольких типах страниц:
class CommonContentBlock(blocks.StreamBlock):
heading = blocks.CharBlock(form_classname="full title")
paragraph = blocks.RichTextBlock()
image = ImageChooserBlock()
class BlogPage(Page):
body = StreamField(CommonContentBlock(), use_json_field=True)
При чтении содержимого StreamField, значение StreamBlock представляет собой последовательность объектов блоков со свойствами block_type и value, подобно верхнеуровневому значению самого StreamField.
<article>
{% for block in page.body %}
{% if block.block_type == 'carousel' %}
<ul class="carousel">
{% for slide in block.value %}
{% if slide.block_type == 'image' %}
<li class="image">{% image slide.value width-200 %}</li>
{% else %}
<li class="video">{% include_block slide %}</li>
{% endif %}
{% endfor %}
</ul>
{% else %}
(rendering for other block types)
{% endif %}
{% endfor %}
</article>
Ограничение количества блоков
По умолчанию StreamField может содержать неограниченное количество блоков. Опции min_num и max_num в StreamField или StreamBlock позволяют установить минимальное или максимальное количество блоков:
body = StreamField([
('heading', blocks.CharBlock(form_classname="full title")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
], min_num=2, max_num=5, use_json_field=True)
Или аналогично:
class CommonContentBlock(blocks.StreamBlock):
heading = blocks.CharBlock(form_classname="full title")
paragraph = blocks.RichTextBlock()
image = ImageChooserBlock()
class Meta:
min_num = 2
max_num = 5
Опция block_counts может быть использована для установки минимального или максимального количества конкретных типов блоков. Она принимает словарь, сопоставляющий имена блоков со словарем, содержащим либо min_num, либо max_num, или оба. Например, для разрешения от 1 до 3 блоков «заголовок»:
body = StreamField([
('heading', blocks.CharBlock(form_classname="full title")),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
], block_counts={
'heading': {'min_num': 1, 'max_num': 3},
}, use_json_field=True)
Или аналогично:
class CommonContentBlock(blocks.StreamBlock):
heading = blocks.CharBlock(form_classname="full title")
paragraph = blocks.RichTextBlock()
image = ImageChooserBlock()
class Meta:
block_counts = {
'heading': {'min_num': 1, 'max_num': 3},
}
Шаблоны для каждого блока
По умолчанию каждый блок отображается с помощью простого минимального HTML-разметки или вообще без разметки. Например, значение CharBlock отображается как обычный текст, а ListBlock выводит свои дочерние блоки в <ul> оболочке. Чтобы переопределить это собственным пользовательским HTML-отображением, вы можете передать template аргумент блоку, указав имя файла шаблона для рендеринга. Это особенно полезно для типов пользовательских блоков, полученных от StructBlock:
('person', blocks.StructBlock(
[
('first_name', blocks.CharBlock()),
('surname', blocks.CharBlock()),
('photo', ImageChooserBlock(required=False)),
('biography', blocks.RichTextBlock()),
],
template='myapp/blocks/person.html',
icon='user'
))
Или, когда определен как подкласс StructBlock:
class PersonBlock(blocks.StructBlock):
first_name = blocks.CharBlock()
surname = blocks.CharBlock()
photo = ImageChooserBlock(required=False)
biography = blocks.RichTextBlock()
class Meta:
template = 'myapp/blocks/person.html'
icon = 'user'
Внутри шаблона значение блока доступно как переменная value:
{% load wagtailimages_tags %}
<div class="person">
{% image value.photo width-400 %}
<h2>{{ value.first_name }} {{ value.surname }}</h2>
{{ value.biography }}
</div>
Поскольку first_name, surname, photo и biography определены как блоки сами по себе, это также можно записать как:
{% load wagtailcore_tags wagtailimages_tags %}
<div class="person">
{% image value.photo width-400 %}
<h2>{% include_block value.first_name %} {% include_block value.surname %}</h2>
{% include_block value.biography %}
</div>
Запись {{ my_block }} примерно эквивалентна {% include_block my_block %}, но короткий вариант более ограничен, так как он не передает переменные из вызывающего шаблона, такие как request или page; по этой причине рекомендуется использовать его только для простых значений, которые сами по себе не рендерят HTML. Например, если наш PersonBlock использовал шаблон:
{% load wagtailimages_tags %}
<div class="person">
{% image value.photo width-400 %}
<h2>{{ value.first_name }} {{ value.surname }}</h2>
{% if request.user.is_authenticated %}
<a href="#">Contact this person</a>
{% endif %}
{{ value.biography }}
</div>
то тест request.user.is_authenticated не будет работать правильно при рендеринге блока через {{ ... }} тег:
{# Incorrect: #}
{% for block in page.body %}
{% if block.block_type == 'person' %}
<div>
{{ block }}
</div>
{% endif %}
{% endfor %}
{# Correct: #}
{% for block in page.body %}
{% if block.block_type == 'person' %}
<div>
{% include_block block %}
</div>
{% endif %}
{% endfor %}
Как и Django's {% include %} тег, {% include_block %} также позволяет передавать дополнительные переменные в включенный шаблон с помощью синтаксиса {% include_block my_block with foo="bar" %}:
{# In page template: #}
{% for block in page.body %}
{% if block.block_type == 'person' %}
{% include_block block with classname="important" %}
{% endif %}
{% endfor %}
{# In PersonBlock template: #}
<div class="{{ classname }}">
...
</div>
Синтаксис {% include_block my_block with foo="bar" only %} также поддерживается, чтобы указать, что переменные из родительского шаблона, кроме foo не будут переданы в дочерний шаблон.
Помимо передачи переменных из родительского шаблона, подклассы блоков могут передавать дополнительные переменные шаблона от себя, переопределяя метод get_context:
import datetime
class EventBlock(blocks.StructBlock):
title = blocks.CharBlock()
date = blocks.DateBlock()
def get_context(self, value, parent_context=None):
context = super().get_context(value, parent_context=parent_context)
context['is_happening_today'] = (value['date'] == datetime.date.today())
return context
class Meta:
template = 'myapp/blocks/event.html'
В этом примере переменная is_happening_today будет доступна внутри шаблона блока. Аргумент parent_context доступен, когда блок рендерится через {% include_block %} тег и представляет собой словарь переменных, переданных из вызывающего шаблона.
Все типы блоков, а не только StructBlock, поддерживают свойство template. Однако для блоков, которые обрабатывают базовые типы данных Python, такие как CharBlock и IntegerBlock, существуют некоторые ограничения на то, где шаблон будет действовать. Для получения дополнительной информации см. О StreamField BoundBlocks и значениях.
Настройки
Все типы блоков реализуют общий API для рендеринга своего представления на клиенте и формы, хранения и извлечения значений в базу данных и из нее. Путем наследования различных классов блоков и переопределения этих методов возможны всевозможные настройки, от изменения макета полей формы StructBlock до реализации совершенно новых способов объединения блоков. Для получения дополнительной информации см. Как создать пользовательские блоки StreamField.
Изменение данных StreamField
Значение StreamField ведет себя как список, и блоки можно вставлять, перезаписывать и удалять перед сохранением экземпляра обратно в базу данных. Новый элемент может быть записан в список как кортеж (тип_блока, значение) — при повторном чтении он будет возвращен как объект BoundBlock.
# Replace the first block with a new block of type 'heading'
my_page.body[0] = ('heading', "My story")
# Delete the last block
del my_page.body[-1]
# Append a block to the stream
my_page.body.append(('paragraph', "<p>And they all lived happily ever after.</p>"))
# Save the updated data back to the database
my_page.save()
Миграция RichTextField в StreamField
Если вы изменяете существующее RichTextField на StreamField, миграция базы данных завершится без ошибок, так как оба поля используют текстовую колонку в базе данных. Однако StreamField использует JSON-представление для своих данных, поэтому существующий текст требует дополнительной операции преобразования, чтобы снова стать доступным. Для этого StreamField должен включать RichTextBlock в качестве одного из доступных типов блоков. Создайте миграцию обычным способом с помощью ./manage.py makemigrations, а затем измените ее следующим образом (в этом примере поле «body» модели demo.BlogPage преобразуется в StreamField с RichTextBlock с именем rich_text):
Примечание
Эта миграция не может быть использована, если аргумент use_json_field установлен в True. Для миграции сначала установите аргумент use_json_field в False, выполните миграцию данных, а затем верните его в True.
# -*- coding: utf-8 -*-
from django.db import models, migrations
from wagtail.rich_text import RichText
def convert_to_streamfield(apps, schema_editor):
BlogPage = apps.get_model("demo", "BlogPage")
for page in BlogPage.objects.all():
if page.body.raw_text and not page.body:
page.body = [('rich_text', RichText(page.body.raw_text))]
page.save()
def convert_to_richtext(apps, schema_editor):
BlogPage = apps.get_model("demo", "BlogPage")
for page in BlogPage.objects.all():
if page.body.raw_text is None:
raw_text = ''.join([
child.value.source for child in page.body
if child.block_type == 'rich_text'
])
page.body = raw_text
page.save()
class Migration(migrations.Migration):
dependencies = [
# leave the dependency line from the generated migration intact!
('demo', '0001_initial'),
]
operations = [
# leave the generated AlterField intact!
migrations.AlterField(
model_name='BlogPage',
name='body',
field=wagtail.fields.StreamField([('rich_text', wagtail.blocks.RichTextBlock())]),
),
migrations.RunPython(
convert_to_streamfield,
convert_to_richtext,
),
]
Обратите внимание, что приведенная выше миграция будет работать только на опубликованных объектах Page. Если вам также нужно мигрировать черновики страниц и версии страниц, измените миграцию, как в следующем примере:
# -*- coding: utf-8 -*-
import json
from django.core.serializers.json import DjangoJSONEncoder
from django.db import migrations, models
from wagtail.rich_text import RichText
def page_to_streamfield(page):
changed = False
if page.body.raw_text and not page.body:
page.body = [('rich_text', {'rich_text': RichText(page.body.raw_text)})]
changed = True
return page, changed
def pagerevision_to_streamfield(revision_data):
changed = False
body = revision_data.get('body')
if body:
try:
json.loads(body)
except ValueError:
revision_data['body'] = json.dumps(
[{
"value": {"rich_text": body},
"type": "rich_text"
}],
cls=DjangoJSONEncoder)
changed = True
else:
# It's already valid JSON. Leave it.
pass
return revision_data, changed
def page_to_richtext(page):
changed = False
if page.body.raw_text is None:
raw_text = ''.join([
child.value['rich_text'].source for child in page.body
if child.block_type == 'rich_text'
])
page.body = raw_text
changed = True
return page, changed
def pagerevision_to_richtext(revision_data):
changed = False
body = revision_data.get('body', 'definitely non-JSON string')
if body:
try:
body_data = json.loads(body)
except ValueError:
# It's not apparently a StreamField. Leave it.
pass
else:
raw_text = ''.join([
child['value']['rich_text'] for child in body_data
if child['type'] == 'rich_text'
])
revision_data['body'] = raw_text
changed = True
return revision_data, changed
def convert(apps, schema_editor, page_converter, pagerevision_converter):
BlogPage = apps.get_model("demo", "BlogPage")
for page in BlogPage.objects.all():
page, changed = page_converter(page)
if changed:
page.save()
for revision in page.revisions.all():
revision_data = revision.content
revision_data, changed = pagerevision_converter(revision_data)
if changed:
revision.content = revision_data
revision.save()
def convert_to_streamfield(apps, schema_editor):
return convert(apps, schema_editor, page_to_streamfield, pagerevision_to_streamfield)
def convert_to_richtext(apps, schema_editor):
return convert(apps, schema_editor, page_to_richtext, pagerevision_to_richtext)
class Migration(migrations.Migration):
dependencies = [
# leave the dependency line from the generated migration intact!
('demo', '0001_initial'),
]
operations = [
# leave the generated AlterField intact!
migrations.AlterField(
model_name='BlogPage',
name='body',
field=wagtail.fields.StreamField([('rich_text', wagtail.blocks.RichTextBlock())]),
),
migrations.RunPython(
convert_to_streamfield,
convert_to_richtext,
),
]
© 2014-present Torchbox Ltd and individual contributors.
All rights are reserved.
Licensed under the BSD License.
https://docs.wagtail.org/en/v3.0.3/topics/streamfield.html