Ваш первый сайт Wagtail
Примечание
Этот учебник посвящен настройке нового проекта Wagtail. Если вы хотите добавить Wagtail к существующему проекту Django, см. Интеграцию Wagtail в проект Django.
Установка и запуск Wagtail
Установка зависимостей
Wagtail поддерживает Python 3.7, 3.8, 3.9 и 3.10.
Чтобы проверить, есть ли у вас подходящая версия Python 3:
$ python3 --version
Если это не вернёт номер версии или вернёт версию ниже 3.7, вам нужно будет установить Python 3.
Важно
Перед установкой Wagtail необходимо установить библиотеки libjpeg и zlib, которые обеспечивают поддержку работы с изображениями JPEG, PNG и GIF (через библиотеку Python Pillow). Способ установки зависит от платформы — см. инструкции по установке, специфичные для платформы библиотеки Pillow.
Создание и активация виртуальной среды
Рекомендуется использовать виртуальную среду, которая изолирует установленные зависимости от других проектов. В этом руководстве используется venv, которая входит в состав Python 3.
В Windows (cmd.exe):
> python3 -m venv mysite\env > mysite\env\Scripts\activate.bat
В GNU/Linux или macOS (bash):
$ python3 -m venv mysite/env $ source mysite/env/bin/activate
Для других оболочек см. venv документацию.
Примечание
Если вы используете систему контроля версий (например, git), mysite будет каталогом для вашего проекта. Каталог env внутри него следует исключить из системы контроля версий.
Установка Wagtail
Используйте pip, который входит в состав Python, для установки Wagtail и его зависимостей:
$ pip install wagtail
Генерация сайта
Wagtail предоставляет команду start, аналогичную команде django-admin startproject. Выполнение команды wagtail start mysite в вашем проекте сгенерирует новый каталог mysite с несколькими дополнительными элементами, специфичными для Wagtail, включая необходимые настройки проекта, приложение «home» с пустой моделью HomePage и базовыми шаблонами, а также пример приложения «search».
Поскольку каталог mysite уже был создан командой venv, выполните команду wagtail start с дополнительным аргументом, чтобы указать целевой каталог:
$ wagtail start mysite mysite
Примечание
Как правило, в Wagtail каждый тип страницы или тип контента представлен отдельным приложением. Однако разные приложения могут быть осведомлены о существовании друг друга и получать доступ к данным друг друга. Все приложения должны быть зарегистрированы в разделе INSTALLED_APPS файла settings. Посмотрите на этот файл, чтобы увидеть, как команда start перечисляет их там.
Установка зависимостей проекта
$ cd mysite $ pip install -r requirements.txt
Это гарантирует, что у вас установлены соответствующие версии Wagtail, Django и других зависимостей для только что созданного проекта.
Создание базы данных
Если вы не обновили настройки проекта, это будет файл базы данных SQLite в каталоге проекта.
$ python manage.py migrate
Эта команда гарантирует, что таблицы в вашей базе данных соответствуют моделям в вашем проекте. Каждый раз, когда вы изменяете модель (например, добавляете поле в модель), вам необходимо выполнить эту команду, чтобы обновить базу данных.
Создание администраторского пользователя
$ python manage.py createsuperuser
После входа в административный интерфейс суперпользователь обладает полными правами и может просматривать/создавать/управлять базой данных.
Запуск сервера
$ python manage.py runserver
Если всё прошло успешно, по адресу http://127.0.0.1:8000 отобразится приветственная страница:
Теперь вы можете получить доступ к административной области по адресу http://127.0.0.1:8000/admin

Расширение модели HomePage
По умолчанию приложение «home» определяет пустую модель HomePage в файле models.py, а также миграцию, которая создаёт главную страницу и настраивает Wagtail для её использования.
Измените файл home/models.py следующим образом, чтобы добавить поле body в модель:
from django.db import models
from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel
class HomePage(Page):
body = RichTextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('body', classname="full"),
]
Поле body определено как RichTextField, специальное поле Wagtail. Когда blank=True, это означает, что это поле не является обязательным и может быть пустым. Вы можете использовать любые из полей ядра Django. content_panels определяют возможности и макет интерфейса редактирования. Когда вы добавляете поля в content_panels, они становятся доступны для редактирования в интерфейсе Wagtail. Дополнительная информация о создании моделей страниц.
Запустите команду python manage.py makemigrations (это создаст файл миграций), а затем python manage.py migrate (это выполнит миграции и обновит базу данных с изменениями в вашей модели). Вы должны запускать эти команды каждый раз, когда вносите изменения в определение модели.
Теперь вы можете отредактировать главную страницу в административной области Wagtail (перейдите в Страницы, Главная страница, затем Редактировать), чтобы увидеть новое поле body. Введите текст в поле body и опубликуйте страницу, выбрав Опубликовать в нижней части редактора страницы, а не Сохранить черновик.
Теперь шаблон страницы нужно обновить, чтобы отразить изменения, внесённые в модель. Wagtail использует обычные шаблоны Django для отображения каждого типа страницы. По умолчанию он будет искать шаблон с именем, образованным из имени приложения и модели, разделяя заглавные буквы символом подчеркивания (например, HomePage в приложении «home» превращается в home/home_page.html). Этот файл шаблона может находиться в любом расположении, распознаваемом правилами шаблонов Django; обычно он размещается в каталоге templates внутри приложения.
Измените файл home/templates/home/home_page.html следующим образом:
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block body_class %}template-homepage{% endblock %}
{% block content %}
{{ page.body|richtext }}
{% endblock %}
base.html ссылается на родительский шаблон и всегда должен быть первой меткой шаблона, используемой в шаблоне. Расширение от этого шаблона позволяет избежать переписывания кода и позволяет страницам в вашем приложении использовать похожий фрейм (используя теги блоков в дочернем шаблоне, вы можете переопределять определённое содержимое в родительском шаблоне).
wagtailcore_tags также необходимо загрузить в начало шаблона, чтобы предоставить дополнительные теги, помимо тех, которые предоставляет Django.
Теги шаблонов Wagtail
В дополнение к тегам и фильтрам шаблонов Django и фильтрам, Wagtail предоставляет ряд собственных тегов тегов и фильтров шаблонов, которые можно загрузить, включив {% load wagtailcore_tags %} в начало файла шаблона.
В этом руководстве мы используем фильтр richtext для экранирования и вывода содержимого RichTextField:
{% load wagtailcore_tags %}
{{ page.body|richtext }}
Результат:
<p>
<b>Welcome</b> to our new site!
</p>
Примечание: Вам необходимо включать {% load wagtailcore_tags %} в каждый шаблон, использующий теги Wagtail. Django выдаст ошибку TemplateSyntaxError, если теги не загружены.
Базовый блог
Теперь мы готовы создать блог. Для этого выполните команду python manage.py startapp blog для создания нового приложения в вашем сайте Wagtail.
Добавьте новое приложение blog в файл INSTALLED_APPS в разделе mysite/settings/base.py.
Индекс блога и статьи
Начнём с простой страницы индекса для нашего блога. В файле blog/models.py:
from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel
class BlogIndexPage(Page):
intro = RichTextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('intro', classname="full")
]
Запустите команды python manage.py makemigrations и python manage.py migrate.
Поскольку модель называется BlogIndexPage, имя шаблона по умолчанию (если мы не переопределим его) будет blog/templates/blog/blog_index_page.html. Создайте этот файл со следующим содержимым:
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block body_class %}template-blogindexpage{% endblock %}
{% block content %}
<h1>{{ page.title }}</h1>
<div class="intro">{{ page.intro|richtext }}</div>
{% for post in page.get_children %}
<h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
{{ post.specific.intro }}
{{ post.specific.body|richtext }}
{% endfor %}
{% endblock %}
Большая часть этого должна быть вам знакома, но мы объясним get_children чуть позже. Обратите внимание на тег pageurl, который похож на тег Django url, но принимает в качестве аргумента объект Wagtail Page.
В административной области Wagtail создайте BlogIndexPage как дочерний элемент главной страницы, убедитесь, что у неё установлен псевдоним «blog» на вкладке «Продвинутые настройки», и опубликуйте её. Теперь вы можете получить доступ к URL-адресу /blog на вашем сайте (обратите внимание, как псевдоним с вкладки «Продвинутые настройки» определяет URL-адрес страницы).
Теперь нам нужна модель и шаблон для наших статей блога. В файле blog/models.py:
from django.db import models
from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.search import index
# Keep the definition of BlogIndexPage, and add:
class BlogPage(Page):
date = models.DateField("Post date")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
search_fields = Page.search_fields + [
index.SearchField('intro'),
index.SearchField('body'),
]
content_panels = Page.content_panels + [
FieldPanel('date'),
FieldPanel('intro'),
FieldPanel('body', classname="full"),
]
В модели выше мы импортируем index, так как это делает модель поисковой. Затем вы можете перечислить поля, которые вы хотите, чтобы пользователь смог искать.
Запустите команды python manage.py makemigrations и python manage.py migrate.
Создайте шаблон в файле blog/templates/blog/blog_page.html:
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block body_class %}template-blogpage{% endblock %}
{% block content %}
<h1>{{ page.title }}</h1>
<p class="meta">{{ page.date }}</p>
<div class="intro">{{ page.intro }}</div>
{{ page.body|richtext }}
<p><a href="{{ page.get_parent.url }}">Return to blog</a></p>
{% endblock %}
Обратите внимание на использование встроенного метода Wagtail get_parent() для получения URL-адреса блога, к которому относится эта статья.
Теперь создайте несколько статей блога как дочерние элементы BlogIndexPage. При создании записей обязательно выберите тип «Страница блога».
Wagtail даёт вам полный контроль над типами контента, которые могут быть созданы под различными родительскими типами контента. По умолчанию любой тип страницы может быть дочерним элементом любого другого типа страницы.

Опубликуйте каждую статью блога, когда закончите её редактирование.
Теперь у вас должна быть заготовка работающего блога. Перейдите к URL-адресу /blog, и вы должны увидеть что-то подобное:

Заголовки должны переходить на страницы статей, а ссылка обратно на главную страницу блога должна появляться в нижней части каждой страницы статьи.
Родители и дети
Большая часть работы, которую вы будете выполнять в Wagtail, связана с концепцией иерархических структур «деревьев», состоящих из узлов и листьев (см. Теория). В этом случае, BlogIndexPage является «узлом», а отдельные экземпляры BlogPage являются «листьями».
Посмотрите ещё раз на внутренности blog_index_page.html:
{% for post in page.get_children %}
<h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
{{ post.specific.intro }}
{{ post.specific.body|richtext }}
{% endfor %}
Каждая «страница» в Wagtail может ссылаться на своего родителя или потомков из своего положения в иерархии. Но почему мы должны указывать post.specific.intro, а не post.intro? Это связано с тем, как мы определили нашу модель:
class BlogPage(Page):
Метод get_children() возвращает список экземпляров базового класса Page. Когда мы хотим обратиться к свойствам экземпляров, которые наследуют от базового класса, Wagtail предоставляет метод specific, который извлекает фактический объект BlogPage. Хотя поле «заголовок» присутствует в базовой модели Page, «введение» присутствует только в модели BlogPage, поэтому нам нужен .specific, чтобы получить к нему доступ.
Для упрощения кода шаблона можно использовать тег Django with:
{% for post in page.get_children %}
{% with post=post.specific %}
<h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
<p>{{ post.intro }}</p>
{{ post.body|richtext }}
{% endwith %}
{% endfor %}
Когда вы начнете писать более настраиваемый код Wagtail, вы найдете целый набор модификаторов QuerySet, чтобы помочь вам перемещаться по иерархии.
# Given a page object 'somepage': MyModel.objects.descendant_of(somepage) child_of(page) / not_child_of(somepage) ancestor_of(somepage) / not_ancestor_of(somepage) parent_of(somepage) / not_parent_of(somepage) sibling_of(somepage) / not_sibling_of(somepage) # ... and ... somepage.get_children() somepage.get_ancestors() somepage.get_descendants() somepage.get_siblings()
Для получения дополнительной информации см.: Справочник по Page QuerySet
Переопределение контекста
У нашего представления индекса блога есть несколько проблем:
- Блоги обычно отображают контент в обратном хронологическом порядке
- Мы хотим убедиться, что мы отображаем только опубликованный контент.
Для этого нам нужно сделать больше, чем просто получить потомков страницы индекса в шаблоне. Вместо этого мы хотим изменить QuerySet в определении модели. Wagtail делает это возможным через переопределяемый метод get_context(). Измените свою модель BlogIndexPage следующим образом:
class BlogIndexPage(Page):
intro = RichTextField(blank=True)
def get_context(self, request):
# Update context to include only published posts, ordered by reverse-chron
context = super().get_context(request)
blogpages = self.get_children().live().order_by('-first_published_at')
context['blogpages'] = blogpages
return context
Всё, что мы сделали здесь, это извлекли исходный контекст, создали настраиваемый QuerySet, добавили его в извлечённый контекст и вернули изменённый контекст обратно в представление. Вам также необходимо немного изменить шаблон blog_index_page.html. Измените:
{% for post in page.get_children %} на {% for post in blogpages %}
Теперь попробуйте убрать публикацию одного из ваших постов — он должен исчезнуть со страницы индекса блога. Остальные посты теперь должны быть отсортированы так, чтобы самые недавно опубликованные были первыми.
Изображения
Добавим возможность прикреплять галерею изображений к нашим записям блога. Хотя можно просто вставить изображения в поле rich text body, есть несколько преимуществ при настройке изображений галереи как нового отдельного типа объектов в базе данных — таким образом, вы полностью контролируете макет и стилизацию изображений в шаблоне, а не должны выкладывать их определенным образом в поле rich text. Это также позволяет использовать изображения в других местах независимо от текста блога — например, отображать миниатюру на странице индекса блога.
Добавьте новую модель BlogPageGalleryImage в models.py:
from django.db import models
# New imports added for ParentalKey, Orderable, InlinePanel, ImageChooserPanel
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
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.search import index
# ... (Keep the definition of BlogIndexPage, and update BlogPage:)
class BlogPage(Page):
date = models.DateField("Post date")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
search_fields = Page.search_fields + [
index.SearchField('intro'),
index.SearchField('body'),
]
content_panels = Page.content_panels + [
FieldPanel('date'),
FieldPanel('intro'),
FieldPanel('body', classname="full"),
InlinePanel('gallery_images', label="Gallery images"),
]
class BlogPageGalleryImage(Orderable):
page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name='gallery_images')
image = models.ForeignKey(
'wagtailimages.Image', on_delete=models.CASCADE, related_name='+'
)
caption = models.CharField(blank=True, max_length=250)
panels = [
ImageChooserPanel('image'),
FieldPanel('caption'),
]
Запустите python manage.py makemigrations и python manage.py migrate.
Здесь несколько новых понятий, поэтому давайте рассмотрим их по одному:
Наследование от Orderable добавляет поле sort_order в модель, чтобы отслеживать порядок изображений в галерее.
Связь от ParentalKey к BlogPage прикрепляет изображения галереи к определённой странице. ParentalKey работает аналогично ForeignKey, но также определяет BlogPageGalleryImage как «потомка» модели BlogPage, поэтому он обрабатывается как фундаментальная часть страницы при операциях, таких как отправка на модерацию и отслеживание истории изменений.
image — это ForeignKey к встроенной модели Wagtail Image, где хранятся сами изображения. Это сопровождается специализированным типом панели ImageChooserPanel, который предоставляет всплывающее окно для выбора существующего изображения или загрузки нового. Таким образом, мы позволяем изображению существовать в нескольких галереях — по сути, мы создали связь «многие ко многим» между страницами и изображениями.
Указание on_delete=models.CASCADE на внешнем ключе означает, что если изображение удаляется из системы, запись галереи также удаляется. (В других ситуациях может быть уместно оставить запись на месте — например, если страница «наш персонал» содержала список людей с фотографиями, и одна из этих фотографий была удалена, мы предпочли бы оставить человека на странице без фотографии. В этом случае мы установили внешний ключ на blank=True, null=True, on_delete=models.SET_NULL.)
Наконец, добавление InlinePanel к BlogPage.content_panels делает изображения галереи доступными в интерфейсе редактирования для BlogPage.
Измените шаблон страницы блога, чтобы включить изображения:
{% extends "base.html" %}
{% load wagtailcore_tags wagtailimages_tags %}
{% block body_class %}template-blogpage{% endblock %}
{% block content %}
<h1>{{ page.title }}</h1>
<p class="meta">{{ page.date }}</p>
<div class="intro">{{ page.intro }}</div>
{{ page.body|richtext }}
{% for item in page.gallery_images.all %}
<div style="float: left; margin: 10px">
{% image item.image fill-320x240 %}
<p>{{ item.caption }}</p>
</div>
{% endfor %}
<p><a href="{{ page.get_parent.url }}">Return to blog</a></p>
{% endblock %}
Здесь мы используем тег {% image %} (который существует в библиотеке wagtailimages_tags, импортированной в начале шаблона), чтобы вставить элемент <img> с параметром fill-320x240, указывающим, что изображение должно быть измененно размера и обрезано, чтобы заполнить прямоугольник 320x240. Вы можете узнать больше о работе с изображениями в шаблонах в документации.

Поскольку изображения нашей галереи — это объекты базы данных, мы можем теперь запросить и повторно использовать их независимо от тела поста в блоге. Давайте определим метод main_image, который возвращает изображение из первого элемента галереи (или None, если элементов галереи нет):
class BlogPage(Page):
date = models.DateField("Post date")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
def main_image(self):
gallery_item = self.gallery_images.first()
if gallery_item:
return gallery_item.image
else:
return None
search_fields = Page.search_fields + [
index.SearchField('intro'),
index.SearchField('body'),
]
content_panels = Page.content_panels + [
FieldPanel('date'),
FieldPanel('intro'),
FieldPanel('body', classname="full"),
InlinePanel('gallery_images', label="Gallery images"),
]
Этот метод теперь доступен из наших шаблонов. Обновите blog_index_page.html, чтобы включить главное изображение в качестве миниатюры рядом с каждым постом:
{% load wagtailcore_tags wagtailimages_tags %}
...
{% for post in blogpages %}
{% with post=post.specific %}
<h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
{% with post.main_image as main_image %}
{% if main_image %}{% image main_image fill-160x100 %}{% endif %}
{% endwith %}
<p>{{ post.intro }}</p>
{{ post.body|richtext }}
{% endwith %}
{% endfor %}
Тегирование постов
Предположим, мы хотим позволить редакторам «тегировать» свои посты, чтобы читатели, например, могли просматривать вместе весь контент, связанный с велосипедами. Для этого нам нужно использовать систему тегов, включенную в Wagtail, подключить её к модели BlogPage и панелям контента, а также отображать связанные теги в шаблоне поста в блоге. Конечно, нам также потребуется рабочее представление для тегов URL.
Сначала измените models.py ещё раз:
from django.db import models
# New imports added for ClusterTaggableManager, TaggedItemBase, MultiFieldPanel
from modelcluster.fields import ParentalKey
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase
from wagtail.core.models import Page, Orderable
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, MultiFieldPanel
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.search import index
# ... (Keep the definition of BlogIndexPage)
class BlogPageTag(TaggedItemBase):
content_object = ParentalKey(
'BlogPage',
related_name='tagged_items',
on_delete=models.CASCADE
)
class BlogPage(Page):
date = models.DateField("Post date")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
# ... (Keep the main_image method and search_fields definition)
content_panels = Page.content_panels + [
MultiFieldPanel([
FieldPanel('date'),
FieldPanel('tags'),
], heading="Blog information"),
FieldPanel('intro'),
FieldPanel('body'),
InlinePanel('gallery_images', label="Gallery images"),
]
Запустите python manage.py makemigrations и python manage.py migrate.
Обратите внимание на новые импорты modelcluster и taggit, добавление новой модели BlogPageTag и добавление поля tags в BlogPage. Мы также воспользовались возможностью использовать MultiFieldPanel в content_panels для группировки полей даты и тегов для улучшения читабельности.
Отредактируйте одну из ваших экземпляров BlogPage, и теперь вы должны иметь возможность тегировать посты:
Чтобы отобразить теги на BlogPage, добавьте это в blog_page.html:
{% if page.tags.all.count %}
<div class="tags">
<h3>Tags</h3>
{% for tag in page.tags.all %}
<a href="{% slugurl 'tags' %}?tag={{ tag }}"><button type="button">{{ tag }}</button></a>
{% endfor %}
</div>
{% endif %}
Обратите внимание, что здесь мы ссылаемся на страницы с помощью встроенного тега slugurl, а не pageurl, который мы использовали ранее. Разница в том, что slugurl принимает slug страницы (из вкладки «Назначить») в качестве аргумента. pageurl чаще используется, потому что он недвусмыслен и избегает дополнительных запросов к базе данных. Но в этом цикле объект страницы не доступен непосредственно, поэтому мы используем менее предпочтительный тег slugurl.
При посещении поста блога с тегами должны отображаться набор связанных кнопок внизу — по одной на каждый тег. Однако нажатие кнопки приведет к ошибке 404, поскольку мы ещё не определили представление «теги». Добавьте в models.py:
class BlogTagIndexPage(Page):
def get_context(self, request):
# Filter by tag
tag = request.GET.get('tag')
blogpages = BlogPage.objects.filter(tags__name=tag)
# Update template context
context = super().get_context(request)
context['blogpages'] = blogpages
return context
Обратите внимание, что эта модель на основе страницы не определяет собственных полей. Даже без полей наследование от Page делает её частью экосистемы Wagtail, так что вы можете присвоить ей заголовок и URL в админ-панели, а также можете управлять её содержимым, возвращая QuerySet из метода get_context().
Импортируйте это, а затем создайте новую BlogTagIndexPage в админ-панели. Вероятно, вы захотите создать новую страницу/представление как потомка Главной страницы, параллельно вашему Индексу блога. Присвойте ей slug «теги» во вкладке «Назначить».
Обратитесь к /tags, и Django скажет вам то, что вы, вероятно, уже знаете: вам нужно создать шаблон blog/blog_tag_index_page.html:
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block content %}
{% if request.GET.tag %}
<h4>Showing pages tagged "{{ request.GET.tag }}"</h4>
{% endif %}
{% for blogpage in blogpages %}
<p>
<strong><a href="{% pageurl blogpage %}">{{ blogpage.title }}</a></strong><br />
<small>Revised: {{ blogpage.latest_revision_created_at }}</small><br />
{% if blogpage.author %}
<p>By {{ blogpage.author.profile }}</p>
{% endif %}
</p>
{% empty %}
No pages found with that tag.
{% endfor %}
{% endblock %}
Мы вызываем встроенное поле latest_revision_created_at модели Page — удобно знать, что оно всегда доступно.
Мы пока не добавили поле «автор» в нашу модель BlogPage, и у нас нет модели профиля для авторов — мы оставим это упражнением для читателя.
Нажатие на кнопку тега внизу поста в блоге теперь должно отобразить страницу примерно так:
Категории
Добавим систему категорий в наш блог. В отличие от тегов, где автор страницы может создать тег, просто использовав его на странице, наши категории будут фиксированным списком, управляемым владельцем сайта через отдельную область интерфейса администрирования.
Сначала определим модель BlogCategory. Категория не является страницей, поэтому мы определяем её как стандартную Django-модель models.Model, а не наследуем от Page. Wagtail вводит понятие «снимков» для многократно используемых фрагментов контента, которые нужно управлять через интерфейс администрирования, но которые не существуют в иерархии страниц; модель можно зарегистрировать как снимок, добавив декоратор @register_snippet. Все типы полей, которые мы использовали до сих пор на страницах, можно использовать и на снимках — здесь мы дадим каждой категории значок изображения, а также имя. Добавьте в blog/models.py:
from wagtail.snippets.models import register_snippet
@register_snippet
class BlogCategory(models.Model):
name = models.CharField(max_length=255)
icon = models.ForeignKey(
'wagtailimages.Image', null=True, blank=True,
on_delete=models.SET_NULL, related_name='+'
)
panels = [
FieldPanel('name'),
ImageChooserPanel('icon'),
]
def __str__(self):
return self.name
class Meta:
verbose_name_plural = 'blog categories'
Примечание
Обратите внимание, что мы используем panels, а не content_panels здесь — поскольку снимки, как правило, не нуждаются в полях, таких как slug или дата публикации, интерфейс редактирования для них не разделяется на отдельные вкладки «содержимое» / «назначить» / «настройки», и поэтому нет необходимости различать «панели содержимого» и «панели назначения».
Примените это изменение и создайте несколько категорий через раздел «Снимки», который теперь появляется в меню администрирования.
Теперь мы можем добавить категории в модель BlogPage в качестве поля «многие ко многим». Тип поля, который мы используем для этого, — ParentalManyToManyField — это вариант стандартного Django-поля ManyToManyField, которое гарантирует, что выбранные объекты правильно сохраняются в записи страницы в истории изменений, так же, как ParentalKey заменяет ForeignKey для отношений «один ко многим».
# New imports added for forms and ParentalManyToManyField
from django import forms
from django.db import models
from modelcluster.fields import ParentalKey, ParentalManyToManyField
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase
# ...
class BlogPage(Page):
date = models.DateField("Post date")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
categories = ParentalManyToManyField('blog.BlogCategory', blank=True)
# ... (Keep the main_image method and search_fields definition)
content_panels = Page.content_panels + [
MultiFieldPanel([
FieldPanel('date'),
FieldPanel('tags'),
FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
], heading="Blog information"),
FieldPanel('intro'),
FieldPanel('body'),
InlinePanel('gallery_images', label="Gallery images"),
]
Здесь мы используем ключевой аргумент widget в определении FieldPanel, чтобы указать виджет на основе флажка вместо стандартного многострочного поля, так как это часто считается более удобным для пользователя.
Наконец, мы можем обновить шаблон blog_page.html, чтобы отобразить категории:
<h1>{{ page.title }}</h1>
<p class="meta">{{ page.date }}</p>
{% with categories=page.categories.all %}
{% if categories %}
<h3>Posted in:</h3>
<ul>
{% for category in categories %}
<li style="display: inline">
{% image category.icon fill-32x32 style="vertical-align: middle" %}
{{ category.name }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}

Куда дальше
- Прочитайте документацию Wagtail по темам и справочнику
- Узнайте, как реализовать 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/getting_started/tutorial.html