The last time we spoke of Django, we installed Django CMS: perhaps the most popular out-of-the-box CMS available for Django. Now that we've explored the most popular CMS let's move on to the best CMS (probably).

I realize I'm speaking of something subjective as though it were fact. As much as I'd like to agree that this is wrong, doing so would make for a boring read. I'll stick to my guns here.

Installing Wagtail

We'll start with the obligatory server updates and Python3 setup. This way, the copy-and-paste crew will have no problem following along.

Dependencies & Environment

Note that this example assumes you're running a Ubuntu/Debian server. If you're using a Mac, I'm confident you can figure out the equivalent commands by now (hint: brew update and brew upgrade):

apt update && apt upgrade -y
Update available Ubuntu/Debian libraries

This is the part where we should cover the installation of Python3, pip3, Python3-dev, and so forth. Luckily, we covered this subject in a previous post with extreme detail. If you're not familiar with Python installation, I highly recommend managing your system's Python versions in this post:

Managing Multiple Versions of Python on Ubuntu 20.04
Install and manage multiple versions of Python on Ubuntu 20.04 or newer.

Setup Environment & Dependencies

As with any Python project, we pick a directory and set up shop by creating a Virtual Environment. You should recognize the steps below as

  1. Navigating to a system directory.
  2. Creating a virtual environment (named .venv in this example).
  3. Activating the above virtual environment.
cd /your/desired/project/directory
python3 -m venv .venv
. .venv/bin/activate
Create a virtual environment in your project directory

Great. With the environment activated, we can start getting to work. Before installing Wagtail, we need to install two dependencies: libjpeg and zlib. These are libraries critical to image compression and the sort:

python3 -m pip install libjpeg zlib
Install libjpeg and zlib Python libraries

Now we can finally install Wagtail:

pip3 install wagtail
Install the Python library Wagtail CMS

Good job. For our next trick, we'll create our actual Wagtail project. This installation contains Django at its core: install Wagtail with install Django and create the necessary project structure.

wagtail start mysite
cd mysite

wagtail start mysite is a command that will create a "project" called mysite in the current directory. Again, name your project as you see fit. With that directory created, change directories, so you're inside said project, and install Wagtail dependencies from the requirements.txt file:

python3 -m pip install -r requirements.txt
Install Python dependencies

As with a typical Django project, we need to run our migrations to get things set up.

python3 manage.py migrate
Execute database migrations

This command will result in the following output:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, home, sessions, taggit, wagtailadmin, wagtailcore, wagtaildocs, wagtailembeds, wagtailforms, wagtailimages, wagtailredirects, wagtailsearch, wagtailusers
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  ...
Output of database migrations

Next, we need to create a superuser:

python3 manage.py createsuperuser
Create a superuser to manage your CMS

Running this will prompt a few details as follows:

Username (leave blank to use 'root'): myuser
Email address: myuser@example.com
Password:
Password (again):
Superuser created successfully.
Creating a superuser via command-line prompt

You're a hero. Now, you're probably anxious to see what all the fuss is about. Let's get your development server up and running so you can start playing around in the browser:

python manage.py runserver
Spin up Wagtail's CMS locally

That command should give you the following output:

Performing system checks...

System check identified no issues (0 silenced).
April 20, 2019 - 03:48:58
Django version 2.1.8, using settings 'mysite.settings.dev'
Starting development server at https://127.0.0.1:8000/
Quit the server with CONTROL-C.
Successful message when running Wagtail

Great work. Now if you visit your server's default address on port 8000, you should be able to hit the barebones Wagtail project you just created. To reach the admin, hit https://localhost:8000/admin/.

Config Files

As mentioned, installing Wagtail will immediately set up a Django project. If that's so, where is our famous settings.py file? It's split into three files: base.py, dev.py, and production.py. All these are stored here:

cd mysite/settings
Navigate to the directory containing all project configs

base.py

base.py is the closest to Django's settings.py file. This contains all settings common to our dev and prod environments.

"""
Django settings for mysite project.

Generated by 'django-admin startproject' using Django 2.1.8.

For more information on this file, see
https://docs.djangoproject.com/en/2.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.1/ref/settings/
"""
import os


PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = os.path.dirname(PROJECT_DIR)

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/

# Application definition
INSTALLED_APPS = [
    'home',
    'search',
    'wagtail.contrib.forms',
    'wagtail.contrib.redirects',
    'wagtail.embeds',
    'wagtail.sites',
    'wagtail.users',
    'wagtail.snippets',
    'wagtail.documents',
    'wagtail.images',
    'wagtail.search',
    'wagtail.admin',
    'wagtail.core',
    'modelcluster',
    'taggit',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'wagtail.core.middleware.SiteMiddleware',
    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
]

ROOT_URLCONF = 'mysite.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(PROJECT_DIR, 'templates'),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'mysite.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]


# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/

STATICFILES_FINDERS = [
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]

STATICFILES_DIRS = [
    os.path.join(PROJECT_DIR, 'static'),
]

# ManifestStaticFilesStorage is recommended in production, to prevent outdated
# Javascript / CSS assets being served from cache (e.g. after a Wagtail upgrade).
# See https://docs.djangoproject.com/en/2.1/ref/contrib/staticfiles/#manifeststaticfilesstorage
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'

STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'


# General Project Settings
WAGTAIL_SITE_NAME = "mysite"
BASE_URL = 'https://example.com'
settings.py

dev.py

As you might imagine, settings stored in either dev.py or production.py are specific to dev or production, respectively. dev.py comes with some presets, such as setting DEBUG to True, as well as a pre-populated SECRET_KEY:

from .base import *


# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'i!9r654&2us(2g!2ep#n*vufp-)z#mn+^qid_w8o^thnsj)nbm'

# SECURITY WARNING: define the correct hosts in production!
ALLOWED_HOSTS = ['*']

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'


try:
    from .local import *
except ImportError:
    pass
dev.py

production.py

Unlike dev.py, production.py comes with no such predetermined values. The understanding is that you must set these values yourself: after all, if anybody were to grab hold of the secret kept in production.py, your career as a developer is over:

from .base import *

DEBUG = False

try:
    from .local import *
except ImportError:
    pass
production.py