Wagtail Tutorial Series:
To learn more about Wagtail CMS, please check Build Blog With Wagtail CMS (4.0.0)
- Create Wagtail Project
- Modern Frontend Techs for Wagtail
- Dockerizing Wagtail App
- Add Blog Models to Wagtail
- How to write Wagtail page template
- Create Stylish Wagtail Pages with Tailwind CSS
- 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
- Online Demo http://wagtail-blog.accordbox.com/
- 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:
- 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
$ mkdir -p ./wagtail_app/blog
$ docker-compose run --rm web python manage.py startapp blog wagtail_app/blog
We will have structure like this:
wagtail_app
├── __init__.py
├── asgi.py
├── blog
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── settings.py
├── templates
├── urls.py
└── wsgi.py
Update wagtail_app/blog/apps.py to change the name to wagtail_app.blog
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'wagtail_app.blog'
Add wagtail_app.blog
to the INSTALLED_APPS
in wagtail_app/settings.py
INSTALLED_APPS = [
...
'wagtail_app.blog', # new
]
$ docker-compose run --rm web python manage.py check
Page Models
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
)
wagtail_app/blog/models.py
from django.db import models
from wagtail.core.models import Page
from wagtail.admin.edit_handlers import FieldPanel
class BlogPage(Page):
pass
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 + [
FieldPanel("header_image"),
]
Notes:
- When we create page models, please make sure all page classes inherit from the Wagtail
Page
class. - Here we add a
header_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.
wagtail_app/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 = [
FieldPanel("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 wagtail.admin.edit_handlers import FieldPanel, InlinePanel
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 + [
FieldPanel("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 wagtail_app/blog/models.py for reference
from django.db import models
from wagtail.core.models import Page
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel
from wagtail.snippets.models import register_snippet
from taggit.models import Tag as TaggitTag
from modelcluster.fields import ParentalKey
from taggit.models import TaggedItemBase
from modelcluster.tags import ClusterTaggableManager
class BlogPage(Page):
pass
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 + [
FieldPanel("header_image"),
InlinePanel("categories", label="category"),
FieldPanel("tags"),
]
@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
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 = [
FieldPanel("blog_category"),
]
class Meta:
unique_together = ("page", "blog_category")
class PostPageTag(TaggedItemBase):
content_object = ParentalKey("PostPage", related_name="post_tags")
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/cms/
- Go to http://127.0.0.1:8000/cms/pages/ to create
BlogPage
beside theWelcome to your new Wagtail site!
page. You can click button in the top navbar to do this. - 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/cms/pages/ delete the default welcome page.
- 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) - Click the top
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
# please run code in new Django shell if you change something
$ docker-compose run --rm web python manage.py shell
Now you are in Django shell, and you can run some Python code to quickly check the data and code. This is very useful during the development.
>>> from wagtail.core.models import Page
# number 4 is the post page primary key we just created
# you can get it from the url when on the edit page
>>> page = Page.objects.get(pk=4).specific
>>> page.title
'PostPage1'
>>> page.tags.all()
[<Tag: Django>]
>>> page.categories.all()
<QuerySet [<PostPageBlogCategory: PostPageBlogCategory object (1)>]>
>>> page.categories.first().blog_category
<BlogCategory: Programming>
ParentalKey
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 db table, the data would seem like this.
PostPage: postpage 1 (pk=1)
BlogCategory: category 1 (pk=1)
PostPageBlogCategory (pk=1, blog_category=1, page=1)
Now 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
PostPageBlogCategory (blog_category=2, page=1)
inmemory
and not write it toPostPageBlogCategory
table. (Because if you do, it will affect thelive
version of the page) - You need to write code to convert the page data, and the
PostPageBlogCategory
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 PostPageBlogCategory (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
PostPageBlogCategory.page
asParentalKey
field. - So the Wagtail page (
ClusterableModel
) can detect and hold thePostPageCategory
in memory even it is not created in db yet. (hasnull
pk) - We can then serialize the page to JSON format (which contains
PostPageBlogCategory
info) and save torevision
table. - Now editor can preview the page without touching the live version.
If you want to dive deeper, try to use code below to check on your local:
>>> from wagtail.core.models import Revision
# page__pk is the primary key of the page
>>> revision = Revision.objects.filter(content_type__model='postpage').filter(object_id=page__pk).first()
>>> revision.content
{'pk': 4, 'live': True, 'path': '000100020001', 'slug': 'postpage1', 'depth': 3, 'owner': 1, 'title': 'PostPage1', 'locale': 1, 'locked': False, 'expired': False, 'alias_of': None, 'numchild': 0, 'url_path': '/blogpage/postpage1/', 'expire_at': None, 'locked_at': None, 'locked_by': None, 'post_tags': [{'pk': 1, 'tag': 1, 'content_object': 4}], 'seo_title': '', 'categories': [{'pk': 1, 'page': 4, 'blog_category': 1}], 'go_live_at': None, 'draft_title': 'PostPage1', 'content_type': 47, 'header_image': 3, 'live_revision': None, 'show_in_menus': False, 'latest_revision': None, 'translation_key': '84e6b2b2-5c26-4f20-a3fe-c5985081ac3c', 'last_published_at': None, 'first_published_at': None, 'search_description': '', 'wagtail_admin_comments': [], 'has_unpublished_changes': False, 'latest_revision_created_at': None}
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,PostPageCategory.page
, useParentalKey
.
Wagtail Tutorial Series:
To learn more about Wagtail CMS, please check Build Blog With Wagtail CMS (4.0.0)
- Create Wagtail Project
- Modern Frontend Techs for Wagtail
- Dockerizing Wagtail App
- Add Blog Models to Wagtail
- How to write Wagtail page template
- Create Stylish Wagtail Pages with Tailwind CSS
- 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
- Online Demo http://wagtail-blog.accordbox.com/
- 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: