Introduction to Wagtail | Coders of Colour

Developing and Building our Site

Check out the video based on this section here.

Edit the homepage model

Out of the box, the “home” app defines a blank HomePage model in models.py, along with a migration that creates a homepage and configures Wagtail to use it.

Edit home/models.py as follows, to add a body field to the model:

home/models.py
1from wagtail.core.models import Page
2from wagtail.core.fields import RichTextField
3from wagtail.admin.edit_handlers import FieldPanel
4
5
6class HomePage(Page):
7 body = RichTextField(blank=True)

Generate and run the migrations needed

The changes we just made will need to be applied to the database. Here we are adding a body field, so the database needs to be able to store the information we add to the body. We do this using the manage.py file with the two following commands:

python manage.py makemigrations
python manage.py migrate

The above commands should do the following:

  • make a new migration file at mysite/home/migrations/003_homepage_body.py
  • Run the new migration file to populate or change the schema for the database
output
(mysite-venv) kevin@mulder:~/shared/mysite/mysite
python manage.py makemigrations
Migrations for 'home': home/migrations/0003_homepage_body.py
...

What is the manage.py file and migrate?

The manage.py is a python file we use to execute administration tasks. When we run python manage.py migrate we are executing the django migrate method. This will create automatically create python files that define or adjust database schema. It saves us manually creating tables when we need them.

Migrations are Django’s way of propagating changes you make to your models (adding a field, deleting a model, etc.) into your database schema. They’re designed to be mostly automatic, but you’ll need to know when to make migrations, when to run them, and the common problems you might run into. Read more about migration on the Django docs

At the moment we have a 'body' field on the homepage, but if you create a page in the admin interface you will not see any way to edit it. In order to make the body field editable we can add a FieldPanel to the list of fields that should be shown when editing the page.

home/models.py
1class HomePage(Page):
2 body = RichTextField(blank=True)
3 content_panels = Page.content_panels + [
4 FieldPanel('body', classname="full"),
5 ]

Check your terminal to see if the site is still running, if it isn't, run

python manage.py runserver

It's a good idea to have two terminal windows open whilst developing

Go to the local site in your browser at http://localhost:8000/admin.

From the admin menu select pages > home.

This will land you http://localhost:8000/admin/pages/3/ which is your homepage. If there where child pages they would be listed here. Under the home title click 'edit' to edit the page. (note you can also do this straight from the admin menu by clicking page > then the small pencil icon.)

You should be on http://localhost:8000/admin/pages/3/edit/

Add some body text and in the bottom bar click on the arrow next to "Save Draft" and click "Publish".

edit homepage

Update the template

Your homepage has now been stored in the database. At present 0.0.0.0:8000 still shows the dafault welcome page, rather than our new homepage, however. In order to display our content, we need a template to show django how it should be presented.

Take a moment read through the django documentation on templates for an overview of how this works.

Edit home/templates/home/home_page.html. Remove all of the current content (the welcome page) and replace it with the following:

home/templates/home/home_page.html
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block content %}
<h1>{{ self.title }}</h1>
{{ page.body|richtext }}
{% endblock %}

Your homepage title and field should now show on the root page of your local site at http://localhost:8000/.

Create a blog app

Check out the video based on this section here.

An app refers to a submodule in our project. It's self-sufficient and not intertwined with the other apps in the project such that, in theory, you could pick it up and move it down into another project without any modification. An app typically has its own models.py. You might think of it as a standalone python module. A simple project might only have one app, whereas more complex projects or websites will have multiple apps that define different models. E.G a news app defining a NewsPost model, a video app defining a VideoPage model etc.

If you find one app is solving two problems, split them into two apps. If you find two apps are so intertwined you could never reuse one without the other, combine them into a single app.

Because a blog has its own structure, with things like 'posts' and 'comments', it makes sense to separate those sorts of things into a blog specific app. This is done via the manage.py file. It should be created at the site root:

python manage.py startapp blog

so your file structure should now be:

.
├── blog # The blog app we just created
├── db.sqlite3
├── Dockerfile
├── env
├── home
├── manage.py
├── mysite
├── requirements.txt
└── search

To view your file structure like above, type dir if you are using Windows, and ls on Mac and Linux.

The startapp command generated an app with some boilerplate files for us (use dir blog or ls blog to view the files):

blog
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py

Now we need to enable the blog by adding it to our site settings. add blog to INSTALLED_APPS in mysite/settings/base.py

mysite/settings/base.py
1# Application definition
2INSTALLED_APPS = [
3 'home',
4 'search',
5 'blog',
6...

Define the model for the blog index page

Edit blog/models.py:

blog/models.py
1from wagtail.core.models import Page
2from wagtail.core.fields import RichTextField
3from wagtail.admin.edit_handlers import FieldPanel
4
5
6class BlogIndexPage(Page):
7 intro = RichTextField(blank=True)
8 content_panels = Page.content_panels + [
9 FieldPanel('intro', classname="full")
10 ]

Similar to what we did earlier, run python manage.py makemigrations and python manage.py migrate. This will add define the schema in our database for the blog.

After running this, go the the cms admin and add a child page to our homepage:

add child to homepage

Create a template for the blog index

Make a file at blog/templates/blog/blog_index_page.html with this content:

blog/templates/blog/blog_index_page.html
1{% extends "base.html" %}
2{% load wagtailcore_tags %}
3{% block content %}
4 <h1>{{ page.title }}</h1>
5 <div class="intro">{{ page.intro|richtext }}</div>
6 {% for post in page.get_children %}
7 <h2>
8 <a href="{% pageurl post %}">
9 {{ post.title }}
10 </a>
11 </h2>
12 {{ post.specific.date }}
13 {% endfor %}
14{% endblock %}

(Restart the local server if your template isn’t found by ctrl + c then python manage.py runserver)

Add a model for blog posts

blog/models.py
1from django.db import models
2from wagtail.core.models import Page
3from wagtail.core.fields import RichTextField
4from wagtail.admin.edit_handlers import FieldPanel
5
6
7class BlogIndexPage(Page):
8 intro = RichTextField(blank=True)
9
10 content_panels = Page.content_panels + [
11 FieldPanel('intro', classname="full")
12 ]
13
14
15class BlogPage(Page):
16 date = models.DateField("Post date")
17 intro = models.CharField(max_length=250)
18 body = RichTextField(blank=True)
19
20 content_panels = Page.content_panels + [
21 FieldPanel('date'),
22 FieldPanel('intro'),
23 FieldPanel('body', classname="full"),
24 ]

Same again, run python manage.py makemigrations and python manage.py migrate to create the database changes

Create a template for blog posts

Make a file at blog/templates/blog/blog_page.html with this content:

blog/templates/blog/blog_page.html
1{% extends "base.html" %}
2{% load wagtailcore_tags %}
3
4{% block content %}
5 <h1>{{ page.title }}</h1>
6 <p class="meta">{{ page.date }}</p>
7 <div class="intro">{{ page.intro }}</div>
8 {{ page.body|richtext }}
9 <p><a href="{% pageurl page.get_parent %}">Return to blog</a></p>
10{% endblock %}

Now we can add a blog page as a child of our blog index:

add child to homepage

Let's see what we have by going to our admin and checking the front end of the pages we created:

blog index

Improve the blog listing

The template that we added for the blog index shows each of the 'child' blog post pages already, but we can improve on this.

Posts should be in reverse chronological order, and we should only list published content. We will do this by adding a new variable to the 'context' that is passed to the template, and passing in a filtered and sorted list of blog posts, instead of just asking wagtail for all the child pages of that blog index.

Edit models.py, adding this get_context method to the BlogIndexPage class:

blog/models.py
1class BlogIndexPage(Page):
2 intro = RichTextField(blank=True)
3
4 content_panels = Page.content_panels + [
5 FieldPanel('intro', classname="full")
6 ]
7
8 def get_context(self, request):
9 # Update context to include only published posts,
10 # in reverse chronological order
11 context = super(BlogIndexPage, self).get_context(request)
12 live_blogpages = self.get_children().live()
13 context['blogpages'] = live_blogpages.order_by('-first_published_at')
14 return context

Now we can update the blog index template to loop over blogpages instead of page.get_children.

blog/templates/blog/blog_index_page.html
1{% extends "base.html" %}
2{% load wagtailcore_tags %}
3{% block content %}
4 <h1>{{ page.title }}</h1>
5 <div class="intro">{{ page.intro|richtext }}</div>
6 {% for post in blogpages %}
7 <h2>
8 <a href="{% pageurl post %}">
9 {{ post.title }}
10 </a>
11 </h2>
12 {{ post.specific.date }}
13 {% endfor %}
14{% endblock %}

Add an image to your blog post model

Check out the video based on this section here.

blog/models.py
1# in the imports
2from wagtail.images.edit_handlers import ImageChooserPanel
3
4# in your BlogPage class
5image = models.ForeignKey(
6 'wagtailimages.Image',
7 null=True,
8 blank=True,
9 on_delete=models.SET_NULL
10)
11
12# in your content_panels for BlogPage
13ImageChooserPanel('image'),

Again, because we added a field to hold data... run python manage.py makemigrations and python manage.py migrate

Update the blog post template to output images

1{% extends "base.html" %}
2{% load wagtailcore_tags wagtailimages_tags %}
3{% block content %}
4 <h1>{{ page.title }}</h1>
5 <p class="meta">{{ page.date }}</p>
6 {% image page.image fill-320x320 %}
7 <div class="intro">{{ page.intro }}</div>
8 {{ page.body|richtext }}
9 <p><a href="{% pageurl page.get_parent %}">Return to blog</a></p>
10{% endblock %}

fill is just one of Wagtail's image resizing methods. Details of the others are listed in the docs. Try them out!

Basic styling

Although the basic content is being shown, it doesn't look great.

To get some styling in place that will make this blog a bit more presentable, we can add some CSS. To get us started with the minimum of effort, are going to use an existing CSS framework called tachyons, rather than writing all the CSS ourselves. Add the following to mysite/templates/base.html, before {# Global stylesheets #}:

mysite/templates/base.html
<link rel="stylesheet" href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css">
<!-- Tachyons CSS minified -->
<link rel="stylesheet" href="https://unpkg.com/tachyons@4.10.0/css/tachyons.min.css"/>

Then wrap the {% block content %}{% endblock %} block in a container div and add the tachyons classes to style the widths of the container:

mysite/templates/base.html
<div class="container ph3 ph5-ns">
{% block content %}{% endblock %}
</div>

Add some css styling to your custom css file at mysite/static/css/mysite.css

I'm just going to make the body colour grey.

body {
background: #ccc;
}

Committing our code to git

Check out the video based on this section here.

Create a .gitignore file in the project root:

touch .gitignore

Add the following to the .gitignore file, this will ensure we aren't tracking certain files in git. For example, python cache files, databases and our virtual environment folder.

*.swp
*.pyc
.DS_Store
/.coverage
/dist/
/build/
/MANIFEST
/wagtail.egg-info/
/docs/_build/
/.tox/
/node_modules/
npm-debug.log*
*.idea/
/*.egg/
/.cache/
/.pytest_cache/
db.sqlite3
/media/
/mysite/media/
/static/
### JetBrains
.idea/
*.iml
*.ipr
*.iws
coverage/
client/node_modules
### vscode
.vscode
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

This will stop us committing files to git that we don't really need.

Set up the project first in github

Head to github.com and create a repo for your code. I've called mine 'codersofcolor'

After creating your repo you will see some information:

repo init

We'll use this to add to our local repo so we can push code to github.

Now in the project root, we need to initialize the git repository, add our first initial commit, and push the code to github.

git init
git add . # the dot means all uncommitted files
git commit -m "Initial commit"
git remote add origin https://github.com/kevinhowbrook/codersofcolor.git # replace your origin here
git push -u origin master

If all went well... when you go back to the github page and refresh, you should see all your code.

repo init

Final task

Read through https://www.makeareadme.com/ and locally add/edit your README.md file in the project root and push it up to github. Take some time to consider what a good README should cover... you'll thank yourself later.

Edit this page on GitHub