Wagtail Photography 2

This post talks about creating a Wagtail project and working on a simple HomePage.

So in the previous post post I talked about the client’s requirements. Now let’s create the project and work on the HomePage. I tend to have Python virtual environments for each one of my projects (well actually I have separate containers for them) let’s first create a virtual environment and activate it. All my virtual environments are in one big folder let’s put it next to the project folder. Also please note that my python command refers to python3 (do check if that’s the case for you as well, you might have to use python3 instead).

(Also as I am writing this and testing it, I realise some of the code might be difficult to see with the dark theme so I’m gonna quickly try a few different styles. So when you read this and if the code is impossible to read with the dark theme, please use the light theme until I fix it.)

mkdir wagtail
cd wagtail
python -m venv wagtail_env
source wagtail_env/bin/activate

Ok we have created and activated our virtual environment. Let’s upgrade pip, install wagtail and create a wagtail project.

pip install --upgrade pip
pip install wagtail
wagtail start portfolio

Now a folder called portfolio should have been created. Go inside the folder, and let’s run the following steps.

pip install -r requirements.txt
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver

When we created the portfolio project using Wagtail, it added a few extra files in there as well: a requirements.txt file that lists the modules that Wagtail needs by default (Django and wagtail) and a Dockerfile (in case you want to deploy it as a container). After you install the requirements.txt, you have a choice. Update the project settings in portfolio/settings/base.py in order to choose the database backend that you want: SQLite3, MariaDB, PostgreSQL, etc. Do take a moment and think of the database that you would need based on your project requirements. Some developers tend to use SQLite during development and then perhaps PostgreSQL during production but I think it’s better to use the same database type during both development and production. So let me pause and think of the client’s requirements. It’s a single user website (confirmed and confirmed and confirmed multiple times, they will only ever log in in one place at a time, won’t have too many photos on it, etc) so concurrent writing will not be an issue. What about the images? Won’t the database file get very big? No, the images are stored as static files and references to them created in the database. Will our db.sqlite file ever get really big? nope. But Vlad, what about the contact form? Surely that writes to the database. But it is very unlikely they will get many many many many many contact form submissions at once. When was the last time you used one? So I went with SQLite. Ok now that we have had that quick (ish) aside, feel free to run the migrate, createsuperuser and runserver commands. Now our server is running live on our computer. You can then go to http://127.0.0.1:8000 and check it out. That’s a pretty egg.

Ok it’s time to edit our HomePage. Remember what we wanted on our HomePage?

  1. Header with menu links (we will add the header for now, but no menu links)
  2. Footer with social links and footer image (we will add a footer to template, but empty for now)
  3. big picture in the middle so we need to be able to choose an image

I will start with the last part. Let’s look at the home/modes.py file and what the HomePage class is currently defined as.


class HomePage(Page):
    pass

Empty page, nice. Let’s add an author to it, a body text (will explain why shortly), and an image. This is the entire code for the page. It is necessary to add it all at once because we will jump back and forth between two related classes.

from django.db import models
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


class HomePage(Page):
    author = models.CharField(blank=True, max_length=255)
    body = RichTextField(blank=True)

    # define a main image
    def main_image(self):
        single_image = self.homepage_image.first()
        if single_image:
            return single_image.image
        else:
            return None

    # Editor panels
    content_panels = Page.content_panels + [
        FieldPanel('author'),
        FieldPanel('body', classname="full"),
        InlinePanel('homepage_image', label="Gallery images"),
    ]


class HomePageImage(Orderable):
    page = ParentalKey(HomePage, on_delete=models.CASCADE,
                       related_name='homepage_image')
    image = models.ForeignKey(
        'wagtailimages.Image', on_delete=models.CASCADE,
        related_name='+'
    )
    caption = models.CharField(blank=True, max_length=250)

    panels = [
        ImageChooserPanel('image'),
        FieldPanel('caption'),
    ]

Let’s discuss our imports first and why/where we use them:

  • models from django.db is used to get some of the database field classes that we can use for our content. Most of them can come from Django, but Wagtail also defines a few of its own including RichTextField (imported lower) and StreamField (not used on this page). Please make sure you don’t use the same names for your fields that you use for your classes, you will make Django very confused and it will cry and throw errors at you. A brief introduction to page models is available on the Wagtail documentation site
  • ParentalKey from modelcluster.fields is used for creating a ForeignKey-like relatinship between an extra model to a Wagtail page -> in this case between the HomePageImage and the HomePage. There is a related question of why we are doing this rather than just using models.ForeignKey directly and we will explain this later on.
  • Page and Orderable from wagtail.core.models are some of the core models that Wagtail has. Page is pretty straightforward, means it’s a Page. What about Orderable? It’s a model that allows something to be ordered.
  • RichTextField from wagtail.core.fields is one of those fields that Django doesn’t have (i think there’s a plugin that can be installed) but Wagtail has it by default. It includes a WYSIWYG editor.
  • FieldPanel and InlinePanel from wagtail.admin.edit_handlers are ways to edit the content when editing a page in the admin section. Fields can be edited in FieldPanels. What about InlinePanel? Where/why do we use that? An InlinePanel can be used to create a collection of related objects from a separate model. In our case, our HomePageImage will be a related model so the InlinePanel that we’ll use will be to add an interface to HomePageImage.
  • ImageChooserPanel from wagtail.images.edit_handlers is to allow us to choose an image from either one of our already-uploaded collections or upload a new image

Ok we’re done with our imports. Let’s look at the HomePage class. What do we have? We have an author field which is a CharField and a body which is a RichTextField. Why do we have a body if the client simply wants an image on the main page? The reason we have it is in case they wanted to display a notice on the main page (so far it’s only been used once when the site was being updated). Please note that for the two fields we just defined, we also have corresponding FieldPanel(s) added to our content_panels. So to add to our content_panels, we used the existing Page.content_panels and added to those a list of new panels: 2 FieldPanel(s) and 1 InlinePanel.

Why did I skip over my main_image function? It’s because I wanted to first talk about the HomePageImage class. Remember the client wanted a photo on the main page. You might ask: why not just use models.ForeignKey and reference wagtailimages.Image like in the following code:

    main_image = models.ForeignKey(
        "wagtailimages.Image", on_delete=models.SET_NULL,
        related_name="+",
        null=True,
        blank=False,
    )

The reason is that the client might at some point want a carousel of images, so using an Orderable will be easier to use. Also, they might upload a few different ones and add them the main page. Then, they can choose different photos at different times. So I used a HomePageImage class that inherits from Orderable meaning the photos will be able to be ordered. Why is that important? We will see shortly.

So in our HomePageImage class, we have a page attribute which is a ParentalKey referring to the Page where this is added. Note that the related_name is homepage_image. This is how we will refer to the HomePageImage from HomePage (the parent class). Then we add an image to it, which we refer to by a ForeignKey to the default image model in Wagtail: wagtailimages.Image. Then we add a caption to it as a CharField (in case the client wants to add some caption text to it at some point). Finally, we add the panels necessary for each image to be used. We use two: ImageChooserPanel and FieldPanel. The first one is used to choose the image, the second one is used to edit the caption CharField. It’s important to note that here we have panels not content_panels. That confused me quite a bit at the beginning when starting with Wagtail. So what is the difference between panels and content_panels in Wagtail? The way I see it, we use the content_panels when when editing the contents of a Page. We use panels when we have it in a model that is not a Page. (Please correct and shout at me if I’m wrong.)

Ok let’s get back to our HomePage class. We have this function defined on it.

def main_image(self):
    single_image = self.homepage_image.first()
    if single_image:
        return single_image.image
    else:
        return None

What does it do? It looks at the homepage_image of our Page (remember that homepage_image is how we access HomePageImage from the parent class of Page). Then it uses the first image from there. If there is one, it returns the first image. If not, it returns None. This way I can refer to the first_image of our homepage in our template.

And that’s the code for our HomePage. Should be relatively straightforward. What about the template? Let’s have a look. At the moment, in our /home/templates/home directory we have home_page.html and welcome_page.html. In our project’s /portfolio/templates we also have 404.html, 500.html and base.html. What follows now is personal preference. Some Django people prefer having separate templates for each of their apps in order for them to be more reusable, some people prefer to have them all in one project directory. I prefer to have everything in one place but am open to both. First, if you create a home directory under portfolio/templates and move the home_page.html and welcome_page.html in there, and then move the /home/static/css/welcome_page.css into /portfolio/static/css/ everything should still look the same. Django (on which Wagtail is based) looks in both the app template directories and project directories. Now I will delete the welcome_page.html (and the welcome_page.css) and change the home_page.html to the following only:

{% extends "base.html" %}
{% load static %}

{% block body_class %}template-homepage{% endblock %}

{% block extra_css %}

{% endblock extra_css %}

{% block content %}

{% endblock content %}

Now if you refresh your page, you should see an empty page. Great! I’m gonna use a Bootstrap example as a starting block but for now, I will delete most of the HTML in there and the styles and I’m simply gonna add a bit of css that adds a background image to our HTML body tag. I went into the admin interface, edited the HomePage and added a photos of mountains I got from Pexels I edited the template as follows:

{% extends "base.html" %}

{% load wagtailcore_tags wagtailimages_tags %}
{% block body_class %}template-homepage{% endblock %}

{% block extra_css %}
{% if page.main_image %}
{% image page.main_image original as background_photo %}
<style>
  body { 
  background: url("{{background_photo.url}}") no-repeat center center fixed; 
  -webkit-background-size: cover;
  -moz-background-size: cover;
  -o-background-size: cover;
  background-size: cover;
}
</style>
{% endif %}
{% endblock %}

{% block content %}
<div class="container text-center">
 <main role="main" class="inner cover">
 </main>
</div>

{% endblock %}

The important part here is this:

{% load wagtailcore_tags wagtailimages_tags %}

{% block extra_css %}
{% if page.main_image %}
{% image page.main_image original as background_photo %}
<style>
  body { 
  background: url("{{background_photo.url}}") no-repeat center center fixed; 
  -webkit-background-size: cover;
  -moz-background-size: cover;
  -o-background-size: cover;
  background-size: cover;
}
</style>
{% endif %}
{% endblock %}

We load wagtailcore_tags and wagtailimages_tags. In the {% extra_css %} block, we add our custom css. But before, we check if page.main_image exists. If it does, we say the image given by “page.main_image original” will be referred to as background_photo. Then in our css, we include the location of the photo with {{background_photo.url}}. If you refresh the page, you will see your image as background. We will worry about different sizes for different screen sizes in the next post. For now, that’s it for today. We will also talk more about the base.html file and what it has and why we extend from it in the next post. Until then, stay home!!!

Oh, but remember our to do list?

  1. Header with menu links (we will add the header for now, but no menu links)
  2. Footer with social links and footer image (we will add a footer to template, but empty for now)
  3. big picture in the middle so we need to be able to choose an image

We’ve done one of them!