В мире Python концепт CMS, не похож с тем что вы возможно встречали в PHP (WordPress). Разобраться в PHP с готовым CMS гораздо легче чем в Python. На данный момент существуют несколько CMS которые используют фреймворк Django, самые популярные из них это django-cms и Wagtail. В данной статье мы будем выполнять первые шаги к собственному сайту на Python используя Wagtail.
Настройка сервера (VPS)
Обычный хостинг для такого проекта будет недостаточно. Если для блога на PHP достаточно заказать обычный хостинг, то для веб проекта на Python мы рекомендуем полноценный VPS на операционной системе Linux от Fornex.com Мы долгое время пользуемся их услугами для нескольких наших проектов и можем рекомендовать их.
Заказ VPS
Ссылка на хостинг: Fornex
После регистрации и входа в ваш личный кабинет, у вас появится возможность заказать VPS и указать необходимые настройки. В моем случае это SSD CLOUD 1GB на операционной системе Ubuntu 18.04 LTS (это самая актуальная версия на момент написания статьи, советуем всегда выбирает самые свежие версии).
Если вы профи в Linux, то можете выбрать настройку «Без панели» при выборе панели управления, но если вы хотите иметь некий интерфейс настроек, то советуем выбрать панель управления «Vesta CP«.
Подключаемся по SSH и выполняем необходимые команды в консоли.
1 2 |
$ apt update $ apt upgrade |
На VPS у меня уже был установлен Python 3.6.7 но если по каким либо причинам его у вас нет, то устанавливаем его таким образом:
1 |
$ apt install python3 |
Далее, устанавливаем необходимые библиотеки:
1 2 3 |
$ apt install python3-setuptools python3-dev python3-venv $ apt install libtiff5-dev libjpeg8-dev zlib1g-dev $ apt install libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk |
Не забываем про pip:
1 |
$ apt install python3-pip |
Все манипуляции и установки через pip выполняем исключительно в виртуальном окружении, чтобы не засорять ненужными библиотеками главное окружение. В папке /home
создаем новую папку /home/web
1 2 3 |
cd /home mkdir web cd web |
Находясь в папке /home/web
мы создадим виртуальное окружение для нашего приложения:
1 2 |
python3 -m venv venv source venv/bin/activate |
Вводная строка терминала изменилась на что-то подобное:
1 |
(venv) user@kvmde58-14996:/home/web# |
Установка Wagtail и его зависимостей:
1 |
$ pip install wagtail |
Запустите сайт:
1 2 |
$ wagtail start mysite $ cd mysite |
Wagtail предоставляет команду start
, аналогичную django-admin.py startproject
. Запуск wagtail start mysite
в вашем проекте создаст новую папку mysite
со специальными дополнениями Wagtail, включая необходимые настройки проекта, приложение “home” с пустой домашней страницей, основными шаблонами и примером приложения “search”.
Установка проектных зависимостей
1 |
$ pip install -r requirements.txt |
Здесь указывается, что у вас подходящая версия Django для созданного вами проекта.
Создание базы данных
1 |
$ python manage.py migrate |
Если вы не обновили настройки проекта, то у вас будет файл базы данных SQLite в папке проекта.
Создание пользователя admin:
1 |
$ python manage.py createsuperuser |
Запуск сервера:
1 |
$ python manage.py runserver |
Если все сработало, то http://127.0.0.1:8000
покажет страницу приветствия:
Вы можете перейти к административному разделу в http://127.0.0.1:8000/admin
Расширяем модель домашней страницы
Изначально, приложение “home” определяет пустую модель домашней страницы в models.py
, наряду с миграцией, которая создает домашнюю страницу и указывает Wagtail использовать ее.
Измените home/models.py
как указано внизу, чтобы внести поле body
в модель:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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. Вы можете использовать любые поля Django. content_panels
определяет возможности и макет интерфейса редактирования.
Запустите manage.py makemigrations
, затем python manage.py migrate
, чтобы обновить базу данных с изменениями вашей модели. Вам следует вводить указанные команды каждый раз, когда вы вносите изменения в модели.
Теперь вы можете редактировать домашнюю страницу внутри раздела администратора Wagtail (переходить на Pages, Homepage и Edit), чтобы увидеть новое поле body
. Введите какой-нибудь текст в поле body, и опубликуйте страницу.
Шаблон страницы теперь ждет обновления для отображения изменений, внесенных в модель. Wagtail использует обычные шаблоны Django для рендера каждого типа страницы. По умолчанию, он будет искать название шаблона, состоящего из названий приложения и модели, с разделенными нижним подчеркиванием названиями (например, HomePage внутри приложения “home” называется home/home_page.html
). Этот файл шаблона может существовать в любой распознаваемой правилами шаблонов Django локации. Условно, он помещает под папкой с шаблонами внутри приложения.
Измените home/templates/home/home_page.html
:
1 2 3 4 5 6 7 8 9 |
{% extends "base.html" %} {% load wagtailcore_tags %} {% block body_class %}template-homepage{% endblock %} {% block content %} {{ page.body|richtext }} {% endblock %} |
Теги шаблонов Wagtail
Wagtail предоставляет ряд тегов шаблонов и фильтров, которые можно загрузить внесением {% load wagtailcore_tags %}
в начале вашего файла шаблона.
В данном руководстве мы используем фильтр richtext
для экранирования и вывода содержимого RichTextField
:
1 2 |
{% load wagtailcore_tags %} {{ page.body|richtext }} |
Генерирует:
1 2 3 4 5 |
<div class="rich-text"> <p> <b>Welcome</b> to our new site! </p> </div> |
Обратите внимание: вам нужно будет вставлять {% load wagtailcore_tags %}
в каждый шаблон, который использует теги Wagtail. Django будет выдавать ошибку TemplateSyntaxError, если теги не будут загружены.
Пример: Простой блог
С этого момента мы можем приступить к созданию блога. Чтобы сделать это, запустите python manage.py startapp blog
, чтобы создать новое приложение в вашем сайте Wagtail.
Внесите новое приложение blog
в INSTALLED_APPS
в mysite/settings/base.py
.
Главная страница блога и посты
Давайте начнем с простой страницы индекса для нашего блога. В blog/models.py
:
1 2 3 4 5 6 7 8 9 10 11 |
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
. Создайте этот файл со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{% 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
, который аналогичен тегу url
в Django, но принимает объект страницы Wagtail в качестве аргумента.
В админке Wagtail создайте BlogIndexPage
в качестве дочернего элемента Homepage, убедитесь, что у него есть слаг “blog” во вкладке Promote и опубликуйте его. Теперь у вас должен появиться доступ к url /blog
на вашем сайте (обратите внимание на то, как слаг из панели Promote определяет URL страницы).
Теперь нам нужна модель и шаблон для наших постов в блоге. В blog/models.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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 # Сохраняем определение BlogIndexPage и вносим: 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"), ] |
Запускаем в терминале python manage.py makemigrations
и python manage.py migrate
.
Создаем шаблон в blog/templates/blog/blog_page.html
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{% 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 %} |
Обратите внимание на то, что мы использовали метод get_parent()
для получения URL блога, частью которого является этот пост.
Теперь создаем несколько постов в блоге в качестве дочерних элементов BlogIndexPage
. Убедитесь в том, что выбрали “Blog Page” при создании ваших постов.
Wagtail дает вам полный контроль над тем, какой тип содержимого может быть создан под различными родительскими типами контента. По умолчанию, любая страница может быть дочерней любого типа страницы.
Теперь у вас в распоряжении базовый рабочий блог. Перейдите по URL /blog
и увидите что-нибудь вроде следующего:
Заголовки должны переводить к страницам постов, а ссылка на главную страницу должна появиться в футере каждой страницы поста.
Дочерние и родительские элементы
Большая часть работы, выполняемая в Wagtail вертится вокруг концепции иерархии структуры “дерева”, состоящего из ветвей и листьев. В данном случае, BlogIndexPage
— ветвь, а экземпляры страниц блога являются листьями.
Рассмотрим blog_index_page.html
изнутри:
1 2 3 4 5 |
{% 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
? Это напрямую связано с тем, как мы определили нашу модель:
1 |
class BlogPage(Page): |
Метод get_children()
выдает нам список экземпляров основного класса Page
. Когда нам нужно сослаться на свойства экземпляров, которые наследуются от базового класса, Wagtail предоставляет особый метод, который возвращает фактическую запись BlogPage
. В то время как поле “title” присутствует в базовой модели Page
, “intro” присутствует только в модели BlogPage
, так что нам нужен .specific
для получения доступа.
Чтобы сжать код шаблона, мы можем использовать тег Django под названием with
:
1 2 3 4 5 6 7 |
{% 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
, которые помогут вам ориентироваться по иерархии.
1 2 3 4 5 6 7 8 9 10 11 12 |
# Задан объект страницы '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) # ... и далее... somepage.get_children() somepage.get_ancestors() somepage.get_descendants() somepage.get_siblings() |
Переопределение контекста
Есть небольшие проблемы с главной страницей нашего блога:
- Контент в блогах (как правило) показывается в обратном хронологическом порядке;
- Нам нужно быть уверенными в том, что мы показываем только опубликованный контент (без черновиков).
Чтобы достичь этих целей, нам нужно сделать нечто большее, чем просто взять дочерние элементы страниц индекса в шаблоне.
Вместо этого, нам нужно будет обновить QuerySet
в определении модели. Wagtail позволяет сделать это просто, при помощи переопределяемого метода get_context()
. Изменение модели BlogIndexPage
проходит следующим образом:
1 2 3 4 5 6 7 8 9 |
class BlogIndexPage(Page): intro = RichTextField(blank=True) def get_context(self, request): # Обновляем контекст для внесения только опубликованных постов в обратном хронологическом порядке 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 %}
Теперь попробуйте отменить публикацию одного из ваших постов — он должен исчезнуть с главной страницы блога. Оставшиеся посты должны отсортированы начиная с самого нового.
Изображения
Давайте добавим возможность внесения галереи изображений в наши посты. Хотя мы можем просто вставлять картинки в тело текста, все же есть несколько преимуществ в настройке галереи изображений как нового выделенного типа объекта внутри базы данных.
Таким образом, у вас будет полный контроль над макетами и стилями изображений в шаблоне, вместо того, чтобы выкладывать их определенным образом в поле текста. Это также позволяет использовать изображения в различных местах, вне зависимости от текста блога. Например, в оглавлении на главной странице блога.
Добавим новую модель BlogPageGalleryImage
в models.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
from django.db import models # Новые импорты добавлены для 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 # ... (Оставляем модель BlogIndexPage без изменений, и обновляем 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'), ] |
Запускаем manage.py makemigrations
и python manage.py migrate
.
Здесь есть несколько новых концепций, давайте рассмотрим их все сразу:
Наследование от Orderable
добавляет поле sort_order
в модель, чтобы отслеживать порядок изображений в галерее.
ParentalKey
от BlogPage
отвечает за прикрепление изображений галереи в определенную страницу. ParentalKey
работает аналогично ForeignKey
, но также рассматривается как фундаментальная часть страницы в таких операциях, как отправка на модерацию и отслеживание истории изменений.
image
является ForeignKey
во встроенной модели Wagtail под названием Image
, в которой хранятся изображения. Это включает выделенный тип панели ImageChooserPanel
, который предоставляет всплывающий интерфейс для выбора существующего изображения или выбора нового. Таким образом, мы позволяем изображению существовать в нескольких галереях — фактически, мы создали мульти-отношение между страницами и изображениями.
Указание on_delete=models.CASCADE
для внешнего ключа означает, что если изображение было удалено из системы, оно также будет удалено из галереи. (В других ситуациях, было бы разумным оставлять изображение в галерее, например если речь идет о странице “наши сотрудники”, где размещены снимки сотрудников, и одна из фотографий удалена по той или иной причине — было бы неплохо оставить это изображение в базе. В данном случае, мы меняем внешний ключ на blank=True, null=True, on_delete=models.SET_NULL
.)
Наконец, внесение InlinePanel
в BlogPage.content_panels
делает изображения галереи доступными для интерфейса редактирования в BlogPage
.
Настройте шаблон страницы блога, чтобы включить изображения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{% 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
, импортированной вверху шаблона) для внесения элемента с параметром fill-320x240
чтобы отметить, что изображение должно попадать под прямоугольник размером 320х240.
Так как изображения нашей галереи являются объектами базы данных сами по себе, мы можем запрашивать и повторно использовать их, вне зависимости от тела поста в блоге. Давайте определим метод main_image
, который возвращает изображение из первого элемента галереи (или None
, если в галерее нет элементов):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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
для внесения главного изображения в анонс рядом с каждым изображением:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{% 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
еще раз:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
from django.db import models # Добавление новых импортов для 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 # ... (Сохраняем определение 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) # ... (Сохраняем определение методов main_image и search_fields) 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
и внесение поля тегов в BlogPage
. Мы также воспользовались возможность использовать MultiFieldPanel
в content_panels
для группировки данных и полей тегов вместе для читаемости.
Поменяйте один из экземпляров ваших BlogPage
, и вы сможете отмечать посты:
Для отображение тегов в BlogPage
, добавьте следующее в blog_page.html
:
1 2 3 4 5 6 7 8 |
{% 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
использует слаг Page
(из панели Promote) в качестве аргумента. В то же время, pageurl
чаще используется, так как он прямолинеен и избегает дополнительных поисков в базе данных. Но в случае с данным циклом, объект Page
не является доступным, так что нам понадобится менее предпочитаемый тег slugurl
.
Переход к тегам в постах блога теперь должно показывать набор связанных кнопок внизу — по одной на каждый тег. Однако, нажатие на кнопку выведет ошибку 404, так как мы еще не определили вид тегов. Нужно добавить следующее в models.py
:
1 2 3 4 5 6 7 8 9 10 11 12 |
class BlogTagIndexPage(Page): def get_context(self, request): # Фильтр по тегам tag = request.GET.get('tag') blogpages = BlogPage.objects.filter(tags__name=tag) # Обновление контекста шаблона context = super().get_context(request) context['blogpages'] = blogpages return context |
Обратите внимание на то, что эта базируемая на Page
модель по умолчанию не определяет поля. Даже без полей, создание подкласса Page
делает его частью экосистемы Wagtail, так что вы можете дать ему заголовок и URL в админке, а также управлять его содержимым, возвращая QuerySet
из метода get_context()
.
Проведите миграцию, затем создайте BlogTagIndexPage
в админке. Вам возможно понадобится создать новую страницу или вид в качестве дочернего элемента домашней страницы, параллельно с главной страницей вашего блога (/blog
). Назначьте ему слаг “tags
” в панели Promote.
Перейдите к /tags
и Django скажет вам то, что вы скорее всего уже знаете: вам нужно создать шаблон blog/blog_tag_index_page.html
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{% extends "base.html" %} {% load wagtailcore_tags %} {% block content %} {% if request.GET.tag|length %} <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
— приятно знать, что оно всегда в доступе.
Мы еще не добавили поле “author” в нашу модель BlogPage
, как и модель профиля для авторов — оставим это как практическое задание для читателя.
Нажатие на кнопку тега внизу поста теперь должно открывать страницу следующим образом:
Категории
Давайте добавим систему категорий в наш блог. В отличие от тегов, где автор страницы может внести тег, просто используя его на странице, наши категории будут фиксированным списком, управляемым владельцем сайта из отдельной области интерфейса админки.
Сначала, мы определим модель BlogCategory
. Категория не является страницей сама по себе, так что мы определим ее как стандартную models.Model
в Django, вместо наследования из Page
. Wagtail предоставляет концепт “сниппетов” для используемых повторно частей контента, которыми нужно управлять из админки, но не существует как часть дерева сайта.
Модель может быть зарегистрирована путем внесения декоратора @register_snippet
. Все типы полей, которые мы использовали на данный момент на странице могут быть использованы в сниппетах — здесь мы дадим иконку каждой категории, а также название. Внесем в blog/models.py
следующее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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
в данном коде, так как сниппеты в целом не нуждаются в полях так, как слаги или дата публикации, интерфейс редактирования для них не делится на отдельные панели “контент” / “настройки” / “продвижение” в качестве стандартных, и здесь нет необходимости проводить различие между “информационными панелями” и “рекламными панелями”.
Выполните миграцию данного изменения, и создайте несколько категорий через область сниппетов, которая теперь должна появиться в меню админки.
Теперь мы можем добавлять категории в модель BlogPage
в качестве мульти-поля. Тип поля, который мы используем для этого — ParentalManyToManyField
. Это вариант стандартного поля ManyToManyField
в Django, которое проверяет, правильно ли хранятся выбранные объекты в записях истории изменений. Во многом это тот же способ, которым ParentalKey
заменяет ForeignKey
для отношений “один ко многим”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# Новые импорты вносятся для форм и 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) # ... (Сохраняем определение методов main_image и search_fields) 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
для отображения категорий:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<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 %} |
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»