はじめに
Wagtailの管理サイトでは,既存のページにADD CHILD PAGEすることで新しいページインスタンスを作成する.例えば,前回は,デフォルトで用意されてるHomePage
クラスのインスタンス("Welcome to your new Wagtail site!"ページ)の子要素として,TopPage
クラスのインスタンスを1つ生成したことを思い出そう.
この結果,(デフォルトのHomePage
インスタンス以外の)すべてのページインスタンスはそれぞれ,ただ1つの親要素をもつことになる.また,各ページインスタンスは複数の子要素をもつことができる.このように,Wagtailの中のページインスタンスはすべてツリー構造で規定される親子関係で互いに結ばれていると言える.
各ページのURLもこのツリー構造に従って自動的に付与される(ので,urls.pyでのルーティングの指定は不要になる).例えば,デフォルトのHomePage
インスタンスのアドレスが,初期設定の
のままであるとすると,その子要素として作成されたあるTopPage
インスタンスのアドレスは,
となる(helloはこのページのSlugに指定されている文字列).さらに,その子要素として生成された別のページのSlugの文字列がworldだったとすると,そのページのアドレスは,
となるわけである.
今回は,まずPlainPage
クラスを追加した後,管理サイトでページインスタンスを増やし,このツリー構造を実感してみるとともに,それを利用したちょっとしたテクニックを試してみよう.
ページクラスの追加
最初に,前回のTopPage
に加えて,PlainPage
の定義を追加する.具体的には,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
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, PageChooserPanel
from wagtail.images.edit_handlers import ImageChooserPanel
class TopPage(Page):
...
class PlainPage(Page):
cover_image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
intro = models.CharField(max_length=255)
main_body = RichTextField(blank=True)
content_panels = Page.content_panels + [
ImageChooserPanel('cover_image'),
FieldPanel('intro'),
FieldPanel('main_body', classname="full"),
]
parent_page_types = ['cms.TopPage']
subpage_types = []
def get_top_page(self):
pages = self.get_ancestors().type(TopPage)
return pages[0]
cover_image
,intro
,main_body
はTopPage
にもあったフィールドなので,content_panels
までは特に説明の必要はないだろう.parent_page_types
とsubpage_types
は親子関係でこのメージクラスと結びつけることのできるページクラスを限定するための記述である.具体的には,PlainPage
の親要素はTopPage
でなければならないことと,子要素をもつことはできないことを指定している.
get_top_page()
は,自分の親要素であるTopPage
インスタンスを返す自作メソッドである.この自作メソッドの中でget_ancestors()
というメソッドを利用しているが,これはWagtailが用意してくれているページインスタンス間の親子関係を利用して他のページを取得するメソッドの1つである.他にも類似のメソッドが用意されているので,ここで確認してみてほしい.
これでPlainPage
クラスが定義できたので,管理サイトにログインして,前回作成したTopPage
インスタンスの子要素としてPlainPage
インスタンスをいくつか生成しておこう(それらを後で利用する).
ナビゲーションアイテムの保持
新しいページクラスを定義し,そのインスタンスをいくつか作成したので,次にそれらをブラウザに表示させてみたくなるが,その前に,TopPage
クラスの方にも少し拡張を加えておこう.前回ダミーのまま放置しておいたnav_barブロックとfooterブロックをそれぞれ実装するための準備である.具体的には,下記のようにコードを拡張した.
class TopPage(Page):
...
footer_text = models.CharField(blank=True, max_length=255)
content_panels = Page.content_panels + [
...,
FieldPanel('footer_text'),
InlinePanel('nav_items', label="Nav items"),
]
def get_top_page(self):
return self
class PlainPage(Page):
...
class NavItems(Orderable):
top_page = ParentalKey(TopPage, related_name='nav_items')
label = models.CharField(max_length=255)
page = models.ForeignKey(
Page,
on_delete=models.CASCADE,
related_name='+'
)
panels = [
FieldPanel('label'),
PageChooserPanel('page'),
]
TopPage
クラスに追加したfooter_text
はフッターに表示するテキストを格納するためのものである.content_panels
を見ると,このfooter_text
のためのパネルに加えて,nav_items
のためのInlinePanel
が追加されている.これは何だろうか.
これを理解するためには,下のNavItems
クラスの定義を参照する必要がある.まず,NavItems
クラスはOrderable
クラスを継承したサブクラスとして定義されていることがわかる.Orderable
クラスは,ページの部品をページとは独立に定義し,多対一でページに紐付けできるようにするためにWagtailが用意してくれているクラスである.ここでは,これをナビゲーションバーに設置するアイテム(リンク先ページpage
とそのラベルlabel
)を管理するために用いているわけである.
各ナビゲーションアイテムのリンク先ページpage
は,models.ForeignKey
でPage
クラスに紐付けすればよい.そしてその編集にはPageChooserPanel
を用いる.なお,Orderable
クラスでは編集パネルの指定はcontent_panels
ではなくpanels
で指定することになるので注意しておこう.
このNavItems
クラスはTopPage
クラスに部品として紐付けするわけであるが,これにはtop_page
フィールドに記述されているように,django-modelclusterのParentalKey
を用いる.そうすると,TopPage
クラスの編集画面で,contant_panels
に追加されたInlinePanel
を用いて,NavItems
クラスのインスタンスが編集できるようになるという仕組みである.
TopPage
クラスにもget_top_page()
メソッドが追加されていることにも注意しておこう.これはTopPage
クラスの場合は自分自身を返すメソッドになる.なぜこのようなメソッドをわざわざ追加するのかは下で明らかになる(ので,少し我慢して読み進めてほしい).
この段階で,もう一度,管理サイトにログインして,前回作成したTopPage
インスタンスの編集画面を開き,footer_text
の文字列とともに,NavItems
クラスのインスタンスをいくつか追加しておこう.
ナビゲーションバーとフッターの作成
これで準備が整ったので,ページの表示に進む.最初に,新しく作成したPlainPage
を表示させてみよう.このクラスのためのテンプレートtemplates/cms/plain_page.htmlを下のように定義した.
{% extends 'cms/base.html' %}
{% load static wagtailcore_tags wagtailimages_tags %}
{% block nav_bar %}{% endblock %}
{% block header %}
{% if page.cover_image %}
{% image page.cover_image fill-1000x400 as my_image %}
{% else %}
{% image page.get_top_page.specific.cover_image fill-1000x400 as my_image %}
{% endif %}
<div class="jumbotron jumbotron-fluid" style="background-image: url('{{ my_image.url }}');">
<div class="container">
<h1 class="display-4">{{ page.title }}</h1>
<p class="lead">{{ page.intro }}</p>
</div>
</div>
{% endblock %}
{% block main %}
<div class="container">
<div class="row">
<div class="col-md-8">
{% block main_body %}
<div class="rich-text my-5">
{{ page.main_body|richtext }}
</div>
{% endblock %}
</div>
<div class="col-md-4">
{% block side_bar %}
<div class="card my-4">
<h4 class="card-header">
{{ page.get_top_page.specific.side_title}}
</h4>
<div class="card-body">
{% image page.get_top_page.specific.side_image fill-200x200 class="img-fluid rounded-circle d-block mx-auto" alt="" %}
</div>
<div class="card-body">
<div class="rich-text">
{{ page.get_top_page.specific.side_body|richtext }}
</div>
</div>
</div>
{% endblock %}
</div>
</div>
</div>
{% endblock %}
{% block footer %}
<nav class="navbar navbar-dark bg-dark fixed-bottom">
<span class="navbar-text mx-auto py-0">
{{ page.get_top_page.specific.footer_text }}
</span>
</nav>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script type="text/javascript" src="{% static 'js/navbar.min.js' %}"></script>
<script>
const nav_props = {
color: "dark",
variant: "dark",
brandlabel: "{{ page.get_top_page.specific.title }}",
brandhref: "{% pageurl page.get_top_page %}",
navs: [
{% for nav_item in page.get_top_page.specific.nav_items.all %}
{
label: "{{ nav_item.label }}",
href: "{% pageurl nav_item.page %}"
},
{% endfor %}
],
};
ReactDOM.render(
React.createElement(MyNavbar, nav_props),
document.getElementById('nav_bar')
);
</script>
{% endblock %}
headerブロックから見ていこう.基本的には前回のtemplates/cms/top_page.htmlの記述と同じであるが,背景画像my_image
として,もしこのPlainPage
インスタンス自体にcover_image
が登録されていなければ,get_top_page()
メソッドで取得した親要素のTopPage
インスタンスのcover_image
を利用するように工夫されていることがわかる.
また,その際に,page.get_top_page.cover_image
ではなく,page.get_top_page.specific.cover_image
となっているのが気になったかもしれない.コード全体を眺めると,他にも.specific
が追加されている箇所が散見される.この.specific
とは何だろうか.
実は,get_top_page()
メソッド(の中のget_ancestors()
メソッドなど)で他のページインスタンスを取得した場合,デフォルトでは,Page
クラス(今の場合だとTopPage
ではなく,そのスーパークラス)のインスタンスが参照されるようになっているらしい.したがって,そのままでは,サブクラスで追加された属性にはアクセスできないことになるのだが,.specific
をつけることでそれが可能になるというトリックになっている.
次に,mainブロックの中のside_barブロックや,footerブロックのコードを見てみると,それぞれ対応するTopPage
インスタンスから必要な情報を取得してきてレンダリングしていることがわかる.
最後に,nav_barブロックに目を移そう.このブロックの中身が空になっているのは,この<div id="nav_bar">
のレンダリングをReactとReact Bootstrapに任せるようにしたからである.具体的なスクリプトはextra_jsのブロックに記載されている.上で用意したnav_items
が利用されていることや,Wagtailのpageurlタグでリンク先ページのURLを取得できることなども確認できる.
なお,このブロックの頭で読み込んでいるjs/navbar.min.jsは,JSXを用いて作成した下記のReactコードをコンパイル・圧縮したものである.
var Navbar = ReactBootstrap.Navbar;
var Nav = ReactBootstrap.Nav;
function MyNavbar (props) {
return (
<Navbar bg={props.color} variant={props.variant} fixed="top" expand="sm">
<Navbar.Brand href={props.brandhref}>{props.brandlabel}</Navbar.Brand>
<Navbar.Toggle/>
<Navbar.Collapse>
<Nav className="ml-auto">
{props.navs.map((nav) => (
<Nav.Link href={nav.href}>{nav.label}</Nav.Link>
))}
</Nav>
</Navbar.Collapse>
</Navbar>
);
};
これでPlainPage
クラスのページも無事に表示できるようになった.
最後に,上でTopPage
クラスを拡張したので,そちらの方のテンプレートtemplates/cms/top_page.htmlもそれに応じて修正したいところである.実は,そのためにこのファイルにわざわざ多くのコードを書き足す必要はない.むしろ,下のように大幅に単純化するだけでOKだ.
{% extends 'cms/plain_page.html' %}
すなわち,TopPage
クラスのページにもPlainPage
クラスのためのテンプレートをそのまま利用することができる.そう,TopPage
クラスに一見無駄なように見えるget_top_page()
メソッドを追加したのは,このためだったというわけだ.
おわりに
今回は,Wagtailの管理下にあるページがツリー構造にまとめられていることを理解した上で,それを利用するテクニックの一端に触れてみた.また,Orderable
クラスを利用してページの部品を定義することも試した.
次回は,ListPage
クラスを追加するとともに,それに関連してまたちょっとしたテクニックにも触れてみたい.