Spec-Zone.ru › Wagtail

Как использовать 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="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 базы данных, и является временной мерой для помощи в миграции StreamFields, созданных в более ранних версиях 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="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="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="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="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="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="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="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="title")
    paragraph = blocks.RichTextBlock()
    image = ImageChooserBlock()

    class Meta:
        min_num = 2
        max_num = 5

Параметр block_counts может использоваться для установки минимального или максимального количества определенных типов блоков. Он принимает словарь, сопоставляющий имена блоков со словарем, содержащим либо минимальное, либо максимальное значение, либо оба. Например, для разрешения от 1 до 3 блоков «заголовок»:

body = StreamField([
    ('heading', blocks.CharBlock(form_classname="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="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 ведет себя как список, и блоки можно вставлять, перезаписывать и удалять перед сохранением экземпляра обратно в базу данных. Новый элемент может быть записан в список в виде кортежа (тип_блока, значение) — при повторном чтении он будет возвращен как объект 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 rich text block to the stream
from wagtail.rich_text import RichText
my_page.body.append(('paragraph', RichText("<p>And they all lived happily ever after.</p>")))

# Save the updated data back to the database
my_page.save()

Получение блоков по имени

Новое в версии 4.0: Были добавлены методы blocks_by_name и first_block_by_name.

Значения StreamField предоставляют метод blocks_by_name для извлечения всех блоков заданного имени:

my_page.body.blocks_by_name('heading')  # returns a list of 'heading' blocks

Вызов blocks_by_name без аргументов возвращает объект типа dict, сопоставляющий имена блоков с списком блоков с этим именем. Это особенно полезно в коде шаблонов, где передача аргументов невозможна:

<h2>Table of contents</h2>
<ol>
    {% for heading_block in page.body.blocks_by_name.heading %}
        <li>{{ heading_block.value }}</li>
    {% endfor %}
</ol>

Метод first_block_by_name возвращает первый блок заданного имени в потоке или None если такой блок не найден:

hero_image = my_page.body.first_block_by_name('image')

first_block_by_name также можно вызвать без аргументов, чтобы получить сопоставление типа dict:

<div class="hero-image">{{ page.body.first_block_by_name.image }}</div>

Миграция RichTextFields в StreamField

Если вы измените существующее RichTextField на StreamField, миграция базы данных завершится без ошибок, так как оба поля используют текстовую колонку в базе данных. Однако StreamField использует JSON-представление для своих данных, поэтому существующий текст требует дополнительного шага преобразования, чтобы снова стать доступным. Для этого StreamField должен включать RichTextBlock в качестве одного из доступных типов блоков. Создайте миграцию обычным способом, используя ./manage.py makemigrations, затем отредактируйте ее следующим образом (в этом примере поле «body» модели demo.BlogPage преобразуется в StreamField с RichTextBlock под именем rich_text):

Примечание

Эта миграция не может быть использована, если аргумент StreamField имеет значение 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/stable/topics/streamfield.html

Spec-Zone.ru

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