Add Blog Models to Wagtail

Table of Contents

Wagtail Tutorial Series:

To learn more about Wagtail CMS, please check Build Blog With Wagtail CMS (4.0.0)

  1. Create Wagtail Project
  2. Modern Frontend Techs for Wagtail
  3. Dockerizing Wagtail App
  4. Add Blog Models to Wagtail
  5. How to write Wagtail page template
  6. Create Stylish Wagtail Pages with Tailwind CSS
  7. How to use StreamField in Wagtail
  8. Wagtail Routable Page
  9. Add Pagination Component to Wagtail
  10. Customize Wagtail Page URL
  11. Add Full Text Search to Wagtail
  12. Add Markdown Support to Wagtail
  13. Add LaTeX Support & Code Highlight In Wagtail
  14. How to Build Form Page in Wagtail
  15. How to Create and Manage Menus in Wagtail
  16. Wagtail SEO Guide
  17. Online Demo http://wagtail-blog.accordbox.com/
  18. Source code: https://github.com/AccordBox/wagtail-tailwind-blog

Wagtail Tips:

  1. Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
  2. Wagtail Tip #2: How to Export & Restore Wagtail Site

Write style in Wagtail:

  1. How to use SCSS/SASS in your Django project (Python Way)
  2. How to use SCSS/SASS in your Django project (NPM Way)

Other Wagtail Topics:

  1. How to make Wagtail project have good coding style
  2. How to do A/B Testing in Wagtail CMS 
  3. How to build a landing page using Wagtail CMS 
  4. How to support multi-language in Wagtail CMS 
  5. Add Bootstrap Theme to Wagtail

More Wagtail articles and eBooks written by me

Objectives

By the end of this chapter, you should be able to:

  1. Add blog models and understand how it works.
  2. 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.

  1. Page models (BlogPage, PostPage)
  2. 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:

  1. When we create page models, please make sure all page classes inherit from the Wagtail Page class.
  2. Here we add a header_image field to the PostPage.
  3. 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:

  1. Here we created two models, both of them inherit from the standard Django models.Model
  2. register_snippet decorator will register the models as Wagtail snippets, that can make us add/edit/delete the model instances in snippets of Wagtail admin.
  3. Since Wagtail already has tag support built on django-taggit, so here we create a proxy-model to declare it as wagtail 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:

  1. PostPageBlogCategory is to store the connection between PostPage and Category
  2. Please remember to use ParentalKey instead of models.ForeignKey, I will talk about it in a bit.
  3. unique_together = ("page", "blog_category") would add db constraints to avoid duplicate records. You can check Django unique_together to learn more.
  4. Some online resources teach people to use ParentalManyToManyField, I do not recommend use ParentalManyToManyField 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:

  1. For tag support, We add ClusterTaggableManager and use through to specify the intermediary model we just created.
  2. For category support, add InlinePanel("categories", label="category") to the content_panels. The categories relationship is already defined in PostPageBlogCategory.page.related_name
  3. The PostPageBlogCategory.panels defines the behavior in InlinePanel, which means we can set multiple blog_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:

  1. Login on http://127.0.0.1:8000/cms/
  2. Go to http://127.0.0.1:8000/cms/pages/ to create BlogPage beside the Welcome to your new Wagtail site! page. You can click button in the top navbar to do this.
  3. Follow settings/site in the sidebar to change the root page of the localhost site to the BlogPage we just created.
  4. Go to http://127.0.0.1:8000/cms/pages/ delete the default welcome page.
  5. 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

  1. Follow Pages/BlogPage in the sidebar (not the edit icon)
  2. Click the top Add child page button to start adding PostPage as children of the BlogPage
  3. You can create Category and Tag when you create the PostPage
  4. 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?

  1. You need to create something like PostPageBlogCategory (blog_category=2, page=1) in memory and not write it to PostPageBlogCategory table. (Because if you do, it will affect the live version of the page)
  2. You need to write code to convert the page data, and the PostPageBlogCategory to some serialize format (JSON for example), and save it to some revision table as the latest revision.
  3. 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.

  1. Make the PostPage inherit from modelcluster.models.ClusterableModel. Actually, Wagtail Page class already did this
  2. And define the PostPageBlogCategory.page as ParentalKey field.
  3. So the Wagtail page (ClusterableModel) can detect and hold the PostPageCategory in memory even it is not created in db yet. (has null pk)
  4. We can then serialize the page to JSON format (which contains PostPageBlogCategory info) and save to revision table.
  5. 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:

  1. If you define some ForeignKey relationship with Page in Page class, for example PostPage.header_image, use ForeignKey. (This has no the above problem)
  2. If you define some ForeignKey relationship with Page in other class, for example, PostPageCategory.page, use ParentalKey.

Wagtail Tutorial Series:

To learn more about Wagtail CMS, please check Build Blog With Wagtail CMS (4.0.0)

  1. Create Wagtail Project
  2. Modern Frontend Techs for Wagtail
  3. Dockerizing Wagtail App
  4. Add Blog Models to Wagtail
  5. How to write Wagtail page template
  6. Create Stylish Wagtail Pages with Tailwind CSS
  7. How to use StreamField in Wagtail
  8. Wagtail Routable Page
  9. Add Pagination Component to Wagtail
  10. Customize Wagtail Page URL
  11. Add Full Text Search to Wagtail
  12. Add Markdown Support to Wagtail
  13. Add LaTeX Support & Code Highlight In Wagtail
  14. How to Build Form Page in Wagtail
  15. How to Create and Manage Menus in Wagtail
  16. Wagtail SEO Guide
  17. Online Demo http://wagtail-blog.accordbox.com/
  18. Source code: https://github.com/AccordBox/wagtail-tailwind-blog

Wagtail Tips:

  1. Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
  2. Wagtail Tip #2: How to Export & Restore Wagtail Site

Write style in Wagtail:

  1. How to use SCSS/SASS in your Django project (Python Way)
  2. How to use SCSS/SASS in your Django project (NPM Way)

Other Wagtail Topics:

  1. How to make Wagtail project have good coding style
  2. How to do A/B Testing in Wagtail CMS 
  3. How to build a landing page using Wagtail CMS 
  4. How to support multi-language in Wagtail CMS 
  5. Add Bootstrap Theme to Wagtail

More Wagtail articles and eBooks written by me

Launch Products Faster with Django

SaaS Hammer helps you launch products in faster way. It contains all the foundations you need so you can focus on your product.

Michael Yin

Michael is a Full Stack Developer from China who loves writing code, tutorials about Django, and modern frontend tech.

He has published some ebooks on leanpub and tech course on testdriven.io.

He is also the founder of the AccordBox which provides the web development services.

Django SaaS Template

It aims to save your time and money building your product

Learn More

Build Jamstack web app with Next.js and Wagtail CMS.

Read More
© 2018 - 2024 AccordBox