1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Wagtailのすすめ(5) StreamFieldに独自ブロックを追加しよう

Last updated at Posted at 2021-01-04

はじめに

Wagtailが用意してくれているフィールドの中にStreamFieldと呼ばれるものがある,これを用いると,複数の異なるブロックを好きな順序で組み合わせてコンテンツを構成していくことができる.例えば,Google Colaboratoryでコードブロックとテキストブロックを組み合わせていくようなイメージだ.

今回は,PostPageクラスを追加し,その中でこのStreamFieldを使ってみることにしよう.

StreamFieldをもつページクラスの追加

ということで,最初にStreamFieldをもつ新しいページクラスPostPageをcms/models.pyの中で下記のように定義する.

from django.db import models

from modelcluster.fields import ParentalKey

from wagtail.core.models import Page, Orderable
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.blocks import RawHTMLBlock, RichTextBlock

from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, PageChooserPanel, StreamFieldPanel
from wagtail.images.edit_handlers import ImageChooserPanel

class TopPage(Page):
    ...

class PlainPage(Page):
    ...

class ListPage(Page):
    ...
    subpage_types = ['cms.ListPage', 'cms.PostPage']
    ...

class PostPage(Page):
    cover_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )
    intro = models.CharField(max_length=250)
    main_body = StreamField([
        ('rich_text', RichTextBlock(icon='doc-full', label='Rich Text')),
        ('html', RawHTMLBlock(icon='code', label='Raw Html')),
        ('code', CodeBlock(icon='code', label='Pretty Code')),
    ])

    content_panels = Page.content_panels + [
        ImageChooserPanel('cover_image'),
        FieldPanel('intro'),
        StreamFieldPanel('main_body'),
        InlinePanel('related_pages', label="Related Pages"),
    ]

    parent_page_types = ['cms.ListPage']
    subpage_types = []

    def get_top_page(self):
        ...

    def get_breads(self):
        ...

    def get_context(self, request):
        ...

class NavItems(Orderable):
    ...

class RelatedPages(Orderable):
    ...

これまでに定義してきたTopPagePlainPageListPageなどとよく似ていることがわかる.なお,テンプレートと同じように,例えばPlainPageをベースにして,他のページクラスをそれを継承したサブクラスとして定義する方法も考えられるが,ここではそれには立ち入らない.

これまでのページとの主な相違点として,main_bodyRichTextFieldではなくStreamFieldで定義されていることを確認しよう.そして,content_panelsを見ると,StreamFieldの編集パネルはStreamFieldPanelと指定しておけばよさそうなこともわかる.これらのSteamFieldに関係する箇所以外は特に説明の必要はないと思う.

StreamFieldでコンテンツを構成していく際に利用可能なブロックは,引数の中のリストに列挙されている.それらのうち,RichTextBlockRawHTMLBlockは,Wagtailがデフォルトで用意してくれているものであり,それらの引数iconlabelは管理サイトの編集画面に表示されるアイコンやラベルを指定している.RichTextBlockは,リッチテキスト形式で文書を投入するためのブロック,RawHTMLBlockは,htmlのコードを直接そのまま投入するためのブロックである.

一方,CodeBlockは(RawHTMLBlockを継承して)独自に定義したブロックである.これについては,次の節で少し詳しく説明しよう.

独自ブロックの追加

StreamFieldに独自ブロックを追加する例として,Google Code Prettifyを用いてシンタックスハイライトされたプログラムコードを表示するためのブロックCodeBlockを追加してみよう.自作のブロックを含めるためには,そのブロックのクラスと,それを表示するためのテンプレートを用意する必要がある.

まず,CodeBlockクラスを定義しよう.cms/blocks.pyというファイルを作成し,その中に下記のコードを書き込む.

from wagtail.core.blocks import RawHTMLBlock

class CodeBlock(RawHTMLBlock):
    class Meta:
        template = 'cms/blocks/code.html'

これを見ればわかるように,CodeBlockは実質的にはデフォルトのRawHTMLBlockと同じであるが,Metaに指定してあるように,レンダリングの際に利用するテンプレートが変更されている.

次にこのテンプレートを用意する.templates/cms/の下にblocks/というディレクトリを追加し,その中にcode.htmlというファイルを作成して,下記のコードを書き込む.

<pre class="prettyprint">
<code>{{ value }}</code>
</pre>

このように,<pre>タグにclass="prettyprint"を指定しておくことでGoogle Code Prettifyが適用されるはずである.ただし,Google Code PrettifyのためのスクリプトをCDNから読み込んでおく必要がある.これは,templates/cms/base.htmlに下記のように追加した.

...
{% block extra_js %}
    ...
    <script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js"></script>
{% endblock %}

この段階で,管理サイトにログインして,いずれかのListPageクラスのページの下にPostPageのページをいくつか追加しておこう.

ポストページのテンプレート

以上で準備が整ったので,PostPageクラスのページの表示に進む.PostPageはブログポストをイメージしたページなので,ジャンボトロンの中に投稿日時,もしくは更新日時を表示させるようにしてみよう.そこで,まずtemplates/cms/plain_page.htmlのheaderブロック内のテキスト部分を下記のようにheader_textブロックで囲んでおく.

...
{% block header %}
    ...
        <div class="container">
            {% block header_text %}
                <h1 class="display-4">{{ page.title }}</h1>
                <p class="lead">{{ page.intro }}</p>
            {% endblock %}
        </div>
    </div>
{% endblock %}
...

次に,このtemplates/cms/plain_page.htmlを継承して,templates/cms/post_page.htmlを次のように作成した.

{% extends 'cms/plain_page.html' %}
{% load wagtailcore_tags %}

{% block header_text %}
    {{ block.super }}
    {% if page.last_published_at > page.first_published_at %}
        <span>Last modified on {{ page.last_published_at|date }}</span>
    {% else %}
        <span>Posted on {{ page.first_published_at|date }}</span>
    {% endif %}
{% endblock %}


{% block main_body %}
    <div class="rich-text my-5">
        {% for block in page.main_body %}
            {% include_block block %}
        {% endfor %}
    </div>

    {% for item_page in page_list %}
        <hr>
        <div class="my-4">
            <a href="{% pageurl item_page %}">
                <h4>{{ item_page.title }}</h4>
                {{ item_page.specific.intro }}
            </a>
            {% if item_page.last_published_at <= item_page.first_published_at %}
                <p>Posted on {{ item_page.first_published_at|date }}</p>
            {% else %}
                <p>Last modified on {{ item_page.last_published_at|date }}</p>
            {% endif %}
        </div>
    {% endfor %}
    <hr>
{% endblock %}

header_textブロックの中を見ると,何度か更新されているページは最終更新日,そうでないページは投稿日がそれぞれ表示されるようになっていることが見て取れる.

また,main_bodyブロックの中の最初の<div>を見ると,テンプレートの中でStreamFieldを処理する方法がわかる.具体的には,for文でブロックを順に取り出して,include_blockすればよい.

実際にブラウザに表示させてみて,プログラムコードに無事シンタックスハイライトが適用されていることを確認しておこう.

RichTextフィールドでのコードハイライト

上ではStreamFieldに独自ブロックを追加することでコードハイライトを行ってみたが,別の方法として,RichTextFieldにコードハイライトの機能を盛り込むこともできる.ここでは,おまけとして,その方法も簡単に紹介しておこう.

Wagtailで用いられているリッチテキストエディタDraftailには,ここに説明されているように,独自の編集機能を追加できるようになっている.

ここでは,コードハイライトのための編集機能を追加してみる.具体的には,cms/wagtail_hooks.pyというファイルを作成し,下記の内容を書き込めばよい.この結果,リッチテキストエディタのツールバーにCodeというボタンが追加される.そして,ある範囲を選択してこのボタンを押すと,その範囲のテキストにこの編集機能が適用される.すなわち,そのテキストは,tagに指定に従って,<pre class="prettyprint">というタグで囲まれることになる.

import wagtail.admin.rich_text.editors.draftail.features as draftail_features
from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler
from wagtail.core import hooks

@hooks.register('register_rich_text_features')
def register_code_feature(features):
    feature_name = 'code'
    type_ = 'code'
    tag = 'pre class="prettyprint"'

    control = {
        'type': type_,
        'label': 'Code',
        'description': 'code',
        'element': 'code',
    }

    features.register_editor_plugin(
        'draftail', feature_name, draftail_features.BlockFeature(control)
    )

    features.register_converter_rule('contentstate', feature_name, {
        'from_database_format': {tag: BlockElementHandler(type_)},
        'to_database_format': {'block_map': {type_: tag}},
    })

    features.default_features.append(feature_name)

おわりに

今回は,StreamFieldの便利さを紹介するとともに,RichTextFieldに簡単な編集機能を追加する方法にも触れてみた.

次回は,公式サイトのチュートリアルYour first Wagtail siteでも扱われている,タグとカテゴリの機能を追加してみたいと思う.

リンク

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?