Spec-Zone.ru › Wagtail 2

Как использовать 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.core.models import Page
from wagtail.core.fields import StreamField
from wagtail.core import blocks
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
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()),
    ])

    content_panels = Page.content_panels + [
        FieldPanel('author'),
        FieldPanel('date'),
        StreamFieldPanel('body'),
    ]

В этом примере поле body модели BlogPage определено как StreamField, где авторы могут создавать контент из трех различных типов блоков: заголовки, абзацы и изображения, которые могут использоваться и повторяться в любом порядке. Типы блоков, доступные авторам, определены как список кортежей (name, block_type): «name» используется для идентификации типа блока в шаблонах и должен следовать стандартным соглашениям Python для имен переменных: строчные буквы и символы подчеркивания, без пробелов.

Полный список доступных типов блоков вы найдете в Справочнике по блокам StreamField.

Примечание

StreamField не является прямым заменителем других типов полей, таких как RichTextField. Если вам нужно перенести существующее поле в StreamField, обратитесь к Переносу RichTextFields в StreamField.

Отображение шаблонов StreamField

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()),
 ])

При чтении содержимого 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()),
])

Иконки блоков

В меню, которое авторы контента используют для добавления новых блоков в 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()),
 ])
 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()),
 ])

При чтении содержимого 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()),
 ])

StreamBlock также можно наследоваться аналогичным образом StructBlock, указав дочерние блоки как атрибуты класса:

class PersonBlock(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())

При чтении содержимого 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)

Или, эквивалентно:

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, или оба.

body = StreamField([
    ('heading', blocks.CharBlock(form_classname="full title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], block_counts={
    'heading': {'min_num': 1, 'max_num': 3},
})

Или, эквивалентно:

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 %}

Как и тег {% include %} Django, {% 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 ведет себя как список, и блоки могут быть вставлены, перезаписаны и удалены перед сохранением экземпляра обратно в базу данных. Новый элемент может быть записан в список в виде кортежа (block_type, value) — при повторном чтении он будет возвращен как объект 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 в качестве одного из доступных типов блоков. (Не забудьте также изменить FieldPanel на StreamFieldPanel при обновлении модели.) Создайте миграцию обычным способом, используя ./manage.py makemigrations, а затем отредактируйте ее следующим образом (в этом примере поле «body» модели demo.BlogPage преобразуется в StreamField с RichTextBlock под названием rich_text):

# -*- coding: utf-8 -*-
from django.db import models, migrations
from wagtail.core.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.core.fields.StreamField([('rich_text', wagtail.core.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.core.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 = json.loads(revision.content_json)
            revision_data, changed = pagerevision_converter(revision_data)
            if changed:
                revision.content_json = json.dumps(revision_data, cls=DjangoJSONEncoder)
                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.core.fields.StreamField([('rich_text', wagtail.core.blocks.RichTextBlock())]),
        ),

        migrations.RunPython(
            convert_to_streamfield,
            convert_to_richtext,
        ),
    ]
  • Предыдущая Фрагменты кода
  • Следующая Разрешения

Содержание страницы

  • Как использовать StreamField для смешанного содержимого
    • Использование StreamField
    • Отображение шаблонов
    • Комбинирование блоков
      • StructBlock
      • Наследование от StructBlock
      • Иконки блоков
      • ListBlock
      • StreamBlock
      • Ограничение количества блоков
    • Шаблоны для каждого блока
    • Настройка
    • Изменение данных StreamField
    • Миграция RichTextField в StreamField

© 2014-present Torchbox Ltd and individual contributors.
All rights are reserved.
Licensed under the BSD License.
https://docs.wagtail.org/en/v2.16.3/topics/streamfield.html

Spec-Zone.ru

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