Wagtail Tutorial Series:
To get the latest learning resource for Wagtail 4, please check Build Blog With Wagtail CMS (4.0.0)
- Create Wagtail Project
- Dockerizing Wagtail App
- Add Blog Models to Wagtail
- How to write Wagtail page template
- Add Bootstrap Theme to Wagtail
- How to use StreamField in Wagtail
- Wagtail Routable Page
- Add pagination component to Wagtail
- Customize Wagtail Page URL
- Add Full Text Search to Wagtail
- Add Markdown Support to Wagtail
- Add LaTeX Support & Code Highlight In Wagtail
- How to Build Form Page in Wagtail
- How to Create and Manage Menus in Wagtail
- Wagtail SEO Guide
- Source code: https://github.com/AccordBox/wagtail-tailwind-blog
Wagtail Tips:
- Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
- Wagtail Tip #2: How to Export & Restore Wagtail Site
Write style in Wagtail:
- How to use SCSS/SASS in your Django project (Python Way)
- How to use SCSS/SASS in your Django project (NPM Way)
Other Wagtail Topics:
Objectives
By the end of this chapter, you should be able to:
- Create Django app.
- Add blog models and understand how it works.
- Learn how to run code and check data in the Django shell.
Page structure
Let's look at the page structures before we start, which can help us better understand the next sections.
There would be two page types in our project, BlogPage
and PostPage
BlogPage
would be the index page of the PostPage
So the page structures would seem like this.
BlogPage
PostPage1
PostPage2
PostPage3
PostPage4
Create Blog App
Let's create a Django app blog
$ docker-compose run --rm web python manage.py startapp blog
Here we run python manage.py startapp blog
in a temp docker container, --rm
option means the container would be deleted when exited.
Now we can see Django app blog
created at the root directory.
.
├── Dockerfile
├── blog
├── compose
├── docker-compose.yml
├── home
├── manage.py
├── requirements.txt
├── search
└── wagtail_bootstrap_blog
Add blog
to the INSTALLED_APPS
of wagtail_bootstrap_blog/settings/base.py
INSTALLED_APPS = [
'home',
'search',
'blog',
# code omitted for brevity
]
Next, let's start adding blog models, there are mainly two types of models we need to add here.
- Page models (
BlogPage
,PostPage
) - Other models (
Category
,Tag
)
Page Models
blog/models.py
from django.db import models
from wagtail.core.models import Page
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.admin.edit_handlers import FieldPanel
class BlogPage(Page):
description = models.CharField(max_length=255, blank=True,)
content_panels = Page.content_panels + [FieldPanel("description", classname="full")]
class PostPage(Page):
header_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
content_panels = Page.content_panels + [
ImageChooserPanel("header_image"),
]
Notes:
- When we create page models, please make sure all page classes inherit from the Wagtail
Page
class. - Here we add a
description
field to theBlogPage
and aheader_image
field to thePostPage
. - We should also add edit handlers to the
content_panels
so we can edit the fields in Wagtail admin.
Category and Tag
To make the blog supports Category
and Tag
features, let's add some models.
blog/models.py
from django.db import models
from wagtail.snippets.models import register_snippet
from taggit.models import Tag as TaggitTag
@register_snippet
class BlogCategory(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True, max_length=80)
panels = [
FieldPanel("name"),
FieldPanel("slug"),
]
def __str__(self):
return self.name
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
@register_snippet
class Tag(TaggitTag):
class Meta:
proxy = True
Notes:
- Here we created two models, both of them inherit from the standard Django models.Model
register_snippet
decorator will register the models as Wagtail snippets, that can make us add/edit/delete the model instances insnippets
of Wagtail admin.- Since Wagtail already has
tag
support built ondjango-taggit
, so here we create a proxy-model to declare it aswagtail snippet
Intermediary model
Now page models and snippet models are already defined. But we still need to create Intermediary models
so the connections between them can be stored in the db.
from modelcluster.fields import ParentalKey
from taggit.models import TaggedItemBase
class PostPageBlogCategory(models.Model):
page = ParentalKey(
"blog.PostPage", on_delete=models.CASCADE, related_name="categories"
)
blog_category = models.ForeignKey(
"blog.BlogCategory", on_delete=models.CASCADE, related_name="post_pages"
)
panels = [
SnippetChooserPanel("blog_category"),
]
class Meta:
unique_together = ("page", "blog_category")
class PostPageTag(TaggedItemBase):
content_object = ParentalKey("PostPage", related_name="post_tags")
Notes:
PostPageBlogCategory
is to store the connection betweenPostPage
andCategory
- Please remember to use
ParentalKey
instead ofmodels.ForeignKey
, I will talk about it in a bit. unique_together = ("page", "blog_category")
would adddb constraints
to avoid duplicate records. You can check Django unique_together to learn more.- Some online resources teach people to use
ParentalManyToManyField
, I do not recommend useParentalManyToManyField
in Wagtail app even it seems more easy to understand. You can check this Wagtail tip for more details.
Next, let's update the PostPage
model so we can add/edit/remove Category
and Tag
for the page in Wagtail admin.
from modelcluster.tags import ClusterTaggableManager
class PostPage(Page):
header_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
tags = ClusterTaggableManager(through="blog.PostPageTag", blank=True)
content_panels = Page.content_panels + [
ImageChooserPanel("header_image"),
InlinePanel("categories", label="category"),
FieldPanel("tags"),
]
Notes:
- For tag support, We add
ClusterTaggableManager
and usethrough
to specify the intermediary model we just created. - For category support, add
InlinePanel("categories", label="category")
to thecontent_panels
. Thecategories
relationship is already defined inPostPageBlogCategory.page.related_name
- The
PostPageBlogCategory.panels
defines the behavior inInlinePanel
, which means we can set multipleblog_category
when we create or edit page.
Source Code
Below is the full code of the blog/models.py for reference
from django.db import models
from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager
from taggit.models import Tag as TaggitTag
from taggit.models import TaggedItemBase
from wagtail.admin.edit_handlers import (
FieldPanel,
FieldRowPanel,
InlinePanel,
MultiFieldPanel,
PageChooserPanel,
StreamFieldPanel,
)
from wagtail.core.models import Page
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.snippets.edit_handlers import SnippetChooserPanel
from wagtail.snippets.models import register_snippet
class BlogPage(Page):
description = models.CharField(max_length=255, blank=True,)
content_panels = Page.content_panels + [FieldPanel("description", classname="full")]
class PostPage(Page):
header_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
tags = ClusterTaggableManager(through="blog.PostPageTag", blank=True)
content_panels = Page.content_panels + [
ImageChooserPanel("header_image"),
InlinePanel("categories", label="category"),
FieldPanel("tags"),
]
class PostPageBlogCategory(models.Model):
page = ParentalKey(
"blog.PostPage", on_delete=models.CASCADE, related_name="categories"
)
blog_category = models.ForeignKey(
"blog.BlogCategory", on_delete=models.CASCADE, related_name="post_pages"
)
panels = [
SnippetChooserPanel("blog_category"),
]
class Meta:
unique_together = ("page", "blog_category")
@register_snippet
class BlogCategory(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True, max_length=80)
panels = [
FieldPanel("name"),
FieldPanel("slug"),
]
def __str__(self):
return self.name
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
class PostPageTag(TaggedItemBase):
content_object = ParentalKey("PostPage", related_name="post_tags")
@register_snippet
class Tag(TaggitTag):
class Meta:
proxy = True
Migrate DB
After we finish the models part, let's migrate our db so db tables would be created or migrated.
$ docker-compose run --rm web python manage.py makemigrations
$ docker-compose run --rm web python manage.py migrate
Setup The Site
# create superuser and password
$ docker-compose run --rm web python manage.py createsuperuser
$ docker-compose up -d
# tail the log
$ docker-compose logs -f
Notes:
- Login on http://127.0.0.1:8000/admin/
- Go to http://127.0.0.1:8000/admin/pages/ to create
BlogPage
beside theHomePage
. - Follow
settings/site
in the sidebar to change theroot page
of thelocalhost
site to theBlogPage
we just created. - Go to http://127.0.0.1:8000/admin/pages/ delete the
HomePage
- Now if we visit http://127.0.0.1:8000/ we will see
TemplateDoesNotExist
exception. This is correct and we will fix it later, do not worry.
Add PostPage
- Follow
Pages/BlogPage
in the sidebar (not the edit icon) - Now the URl would seem like
http://127.0.0.1:8000/admin/pages/4/
- Click the
Add child page
button to start addingPostPage
as children of theBlogPage
- You can create Category and Tag when you create the
PostPage
- Remember to publish the page after you edit the page.
Simple Test
Even we do not write code for the templates yet, we can still use Python code to quickly test code or data in the Django shell.
# please run code in new Django shell if you change something
$ docker-compose run --rm web python manage.py shell
>>> from blog.models import PostPage
>>> page = PostPage.objects.first()
>>> page.title
'PostPage1'
>>> page.tags.all()
[<Tag: Django>]
>>> page.categories.all()
<QuerySet [<PostPageBlogCategory: PostPageBlogCategory object (1)>]>
>>> page.categories.first().blog_category
<BlogCategory: Programming>
>>> exit()
ParentalKey
Many people have not much experience on Django when they learn Wagtail. So here I'd like to talk about a little more about the ParentalKey
and the difference between with ForeignKey
Let's assume you are building a CMS framework which support preview
feature, and now you have a live
post page which has category category 1
So in the table, the data would seem like this.
PostPage: postpage 1 (pk=1)
Category: category 1 (pk=1)
PostPageCategory (pk=1, blog_category=1, page=1)
Some editor wants to change the page category to category 2
, and he even wants to preview
it before publishing it. So what is your solution?
- You need to create something like
PostPageCategory (blog_category=2, page=1)
inmemory
and not write it toPostPageCategory
table. (Because if you do, it will affect thelive
page) - You need to write code to convert the page data, and the
PostPageCategory
to some serialize format (JSON for example), and save it to somerevision
table as the latest revision. - On the preview page, fetch the data from the
revision
table and deserialize to a normal page object, and then render it to HTML.
Unfortunately, Django's ForeignKey
can not work in this case, because it needs PostPageCategory (blog_category=2, page=1)
to save to db first, so it has pk
That is why django-modelcluster is created and ParentalKey
is introduced.
Now We can solve the above problem in this way.
- Make the
PostPage
inherit frommodelcluster.models.ClusterableModel
. Actually, Wagtail Page class already did this - And define the
PostPageCategory.page
asParentalKey
field. - So the Wagtail page (
ClusterableModel
) can hold thePostPageCategory
in memory even it is not created in db yet. (hasnull
pk) - We can then serialize the page to JSON format (also contains
PostPageCategory
info) and save torevision
table. - Now editor can preview the page before publishing it.
If you want to dive deeper, try to use code below to check on your local:
>>> from wagtail.core.models import PageRevision
# page__pk is the primary key of the page
>>> revision = PageRevision.objects.filter(page__pk=page__pk).first()
>>> revision.content_json
So below are tips:
- If you define some
ForeignKey
relationship withPage
inPage
class, for examplePostPage.header_image
, useForeignKey
. (This has no the above problem) - If you define some
ForeignKey
relationship withPage
inother
class, for example,PostPageBlogCategory.page
, useParentalKey
.
Wagtail Tutorial Series:
To get the latest learning resource for Wagtail 4, please check Build Blog With Wagtail CMS (4.0.0)
- Create Wagtail Project
- Dockerizing Wagtail App
- Add Blog Models to Wagtail
- How to write Wagtail page template
- Add Bootstrap Theme to Wagtail
- How to use StreamField in Wagtail
- Wagtail Routable Page
- Add pagination component to Wagtail
- Customize Wagtail Page URL
- Add Full Text Search to Wagtail
- Add Markdown Support to Wagtail
- Add LaTeX Support & Code Highlight In Wagtail
- How to Build Form Page in Wagtail
- How to Create and Manage Menus in Wagtail
- Wagtail SEO Guide
- Source code: https://github.com/AccordBox/wagtail-tailwind-blog
Wagtail Tips:
- Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
- Wagtail Tip #2: How to Export & Restore Wagtail Site
Write style in Wagtail:
- How to use SCSS/SASS in your Django project (Python Way)
- How to use SCSS/SASS in your Django project (NPM Way)
Other Wagtail Topics: