Before installing Wagtail, it is necessary to install the **libjpeg** and **zlib** libraries, which provide support for working with JPEG, PNG and GIF images (via the Python **Pillow** library).
The way to do this varies by platform—see Pillow's
We recommend using a virtual environment, which isolates installed dependencies from other projects.
This tutorial uses [`venv`](https://docs.python.org/3/tutorial/venv.html), which is packaged with Python 3.
**On Windows** (cmd.exe):
```doscon
> python3 -m venv mysite\env
> mysite\env\Scripts\activate.bat
```
**On GNU/Linux or MacOS** (bash):
```console
$ python3 -m venv mysite/env
$ source mysite/env/bin/activate
```
**For other shells** see the [`venv` documentation](https://docs.python.org/3/library/venv.html).
```eval_rst
.. note::
If you're using version control (e.g. git), ``mysite`` will be the directory for your project.
The ``env`` directory inside of it should be excluded from any version control.
```
### Install Wagtail
Use pip, which is packaged with Python, to install Wagtail and its dependencies:
```console
$ pip install wagtail
```
### Generate your site
Wagtail provides a `start` command similar to `django-admin startproject`.
Running `wagtail start mysite` in your project will generate a new `mysite` folder with a few Wagtail-specific extras, including
the required project settings,
a "home" app with a blank `HomePage` model and basic templates,
and a sample "search" app.
Because the folder `mysite` was already created by `venv`, run `wagtail start` with an additional argument to specify the destination directory:
```console
$ wagtail start mysite mysite
```
```eval_rst
.. note::
Generally, in Wagtail, each page type, or content type, is represented by a single app. However, different apps can be aware of each other and access each other's data. All of the apps need to be registered within the ``INSTALLED_APPS`` section of the ``settings`` file. Look at this file to see how the ``start`` command has listed them in there.
```
### Install project dependencies
```console
$ cd mysite
$ pip install -r requirements.txt
```
This ensures that you have the relevant versions of
Wagtail,
Django,
and any other dependencies for the project you have just created.
### Create the database
If you haven't updated the project settings, this will be a SQLite database file in the project directory.
```console
$ python manage.py migrate
```
This command ensures that the tables in your database are matched to the models in your project. Every time you alter your model (eg. you may add a field to a model) you will need to run this command in order to update the database.
### Create an admin user
```console
$ python manage.py createsuperuser
```
When logged into the admin site, a superuser has full permissions and is able to view/create/manage the database.
### Start the server
```console
$ python manage.py runserver
```
If everything worked, <http://127.0.0.1:8000> will show you a welcome page:
![](../_static/images/tutorial/tutorial_1.png)
You can now access the administrative area at <http://127.0.0.1:8000/admin>
![](../_static/images/tutorial/tutorial_2.png)
## Extend 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:
```python
from django.db import models
from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel
class HomePage(Page):
body = RichTextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('body', classname="full"),
]
```
`body` is defined as `RichTextField`, a special Wagtail field. When `blank=True`,
it means that this field is not required and can be empty. You
can use any of the [Django core fields](https://docs.djangoproject.com/en/stable/ref/models/fields). `content_panels` define the
capabilities and the layout of the editing interface. When you add fields to `content_panels`, it enables them to be edited on the Wagtail interface. [More on creating Page models](../topics/pages).
Run `python manage.py makemigrations` (this will create the migrations file), then
`python manage.py migrate` (this executes the migrations and updates the database with your model
changes). You must run the above commands each time you make changes to
You can now edit the homepage within the Wagtail admin area (go to Pages, Homepage, then Edit) to see the new body field. Enter some text into the body field, and publish
the page by selecting *Publish* at the bottom of the page editor, rather than *Save Draft*.
The page template now needs to be updated to reflect the changes made
to the model. Wagtail uses normal Django templates to render each page
type. By default, it will look for a template filename formed from the app and model name,
separating capital letters with underscores (e.g. HomePage within the 'home' app becomes
`home/home_page.html`). This template file can exist in any location recognised by
[Django's template rules](https://docs.djangoproject.com/en/stable/intro/tutorial03/#write-views-that-actually-do-something); conventionally it is placed under a `templates` folder within the app.
Edit `home/templates/home/home_page.html` to contain the following:
`base.html` refers to a parent template and must always be the first template tag used in a template. Extending from this template saves you from rewriting code and allows pages across your app to share a similar frame (by using block tags in the child template, you are able to override specific content within the parent template).
`wagtailcore_tags` must also be loaded at the top of the template and provide additional tags to those provided by Django.
![](../_static/images/tutorial/tutorial_3.png)
### Wagtail template tags
In addition to Django's [template tags and filters](https://docs.djangoproject.com/en/3.1/ref/templates/builtins/),
All we've done here is retrieve the original context, create a custom QuerySet,
add it to the retrieved context, and return the modified context back to the view.
You'll also need to modify your `blog_index_page.html` template slightly.
Change:
`{% for post in page.get_children %}` to `{% for post in blogpages %}`
Now try unpublishing one of your posts - it should disappear from the blog index
page. The remaining posts should now be sorted with the most recently published
posts first.
### Images
Let's add the ability to attach an image gallery to our blog posts. While it's possible to simply insert images into the `body` rich text field, there are several advantages to setting up our gallery images as a new dedicated object type within the database - this way, you have full control of the layout and styling of the images on the template, rather than having to lay them out in a particular way within the rich text field. It also makes it possible for the images to be used elsewhere, independently of the blog text - for example, displaying a thumbnail on the blog index page.
Add a new `BlogPageGalleryImage` model to `models.py`:
```python
from django.db import models
# New imports added for ParentalKey, Orderable, InlinePanel, ImageChooserPanel
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
from wagtail.search import index
# ... (Keep the definition of BlogIndexPage, and update BlogPage:)
Run `python manage.py makemigrations` and `python manage.py migrate`.
There are a few new concepts here, so let's take them one at a time:
Inheriting from `Orderable` adds a `sort_order` field to the model, to keep track of the ordering of images in the gallery.
The `ParentalKey` to `BlogPage` is what attaches the gallery images to a specific page. A `ParentalKey` works similarly to a `ForeignKey`, but also defines `BlogPageGalleryImage` as a "child" of the `BlogPage` model, so that it's treated as a fundamental part of the page in operations like submitting for moderation, and tracking revision history.
`image` is a `ForeignKey` to Wagtail's built-in `Image` model, where the images themselves are stored. This comes with a dedicated panel type, `ImageChooserPanel`, which provides a pop-up interface for choosing an existing image or uploading a new one. This way, we allow an image to exist in multiple galleries - effectively, we've created a many-to-many relationship between pages and images.
Specifying `on_delete=models.CASCADE` on the foreign key means that if the image is deleted from the system, the gallery entry is deleted as well. (In other situations, it might be appropriate to leave the entry in place - for example, if an "our staff" page included a list of people with headshots, and one of those photos was deleted, we'd rather leave the person in place on the page without a photo. In this case, we'd set the foreign key to `blank=True, null=True, on_delete=models.SET_NULL`.)
Finally, adding the `InlinePanel` to `BlogPage.content_panels` makes the gallery images available on the editing interface for `BlogPage`.
Adjust your blog page template to include the images:
<p><ahref="{{ page.get_parent.url }}">Return to blog</a></p>
{% endblock %}
```
Here we use the `{% image %}` tag (which exists in the `wagtailimages_tags` library, imported at the top of the template) to insert an `<img>` element, with a `fill-320x240` parameter to indicate that the image should be resized and cropped to fill a 320x240 rectangle. You can read more about using images in templates in the [docs](../topics/images).
![](../_static/images/tutorial/tutorial_6.jpg)
Since our gallery images are database objects in their own right, we can now query and re-use them independently of the blog post body. Let's define a `main_image` method, which returns the image from the first gallery item (or `None` if no gallery items exist):
We're calling the built-in `latest_revision_created_at` field on the `Page`
model - handy to know this is always available.
We haven't yet added an "author" field to our `BlogPage` model, nor do we have
a Profile model for authors - we'll leave those as an exercise for the reader.
Clicking the tag button at the bottom of a BlogPost should now render a page
something like this:
![](../_static/images/tutorial/tutorial_9.png)
```eval_rst
.. _tutorial_categories:
```
### Categories
Let's add a category system to our blog. Unlike tags, where a page author can bring a tag into existence simply by using it on a page, our categories will be a fixed list, managed by the site owner through a separate area of the admin interface.
First, we define a `BlogCategory` model. A category is not a page in its own right, and so we define it as a standard Django `models.Model` rather than inheriting from `Page`. Wagtail introduces the concept of "snippets" for reusable pieces of content that need to be managed through the admin interface, but do not exist as part of the page tree themselves; a model can be registered as a snippet by adding the `@register_snippet` decorator. All the field types we've used so far on pages can be used on snippets too - here we'll give each category an icon image as well as a name. Add to `blog/models.py`:
```python
from wagtail.snippets.models import register_snippet
@register_snippet
class BlogCategory(models.Model):
name = models.CharField(max_length=255)
icon = models.ForeignKey(
'wagtailimages.Image', null=True, blank=True,
on_delete=models.SET_NULL, related_name='+'
)
panels = [
FieldPanel('name'),
ImageChooserPanel('icon'),
]
def __str__(self):
return self.name
class Meta:
verbose_name_plural = 'blog categories'
```
```eval_rst
.. note::
Note that we are using ``panels`` rather than ``content_panels`` here - since snippets generally have no need for fields such as slug or publish date, the editing interface for them is not split into separate 'content' / 'promote' / 'settings' tabs as standard, and so there is no need to distinguish between 'content panels' and 'promote panels'.
```
Migrate this change in, and create a few categories through the Snippets area which now appears in the admin menu.
We can now add categories to the `BlogPage` model, as a many-to-many field. The field type we use for this is `ParentalManyToManyField` - this is a variant of the standard Django `ManyToManyField` which ensures that the chosen objects are correctly stored against the page record in the revision history, in much the same way that `ParentalKey` replaces `ForeignKey` for one-to-many relations.
```python
# New imports added for forms and ParentalManyToManyField
from django import forms
from django.db import models
from modelcluster.fields import ParentalKey, ParentalManyToManyField
from modelcluster.contrib.taggit import ClusterTaggableManager
Here we're making use of the `widget` keyword argument on the `FieldPanel` definition to specify a checkbox-based widget instead of the default multiple select box, as this is often considered more user-friendly.
Finally, we can update the `blog_page.html` template to display the categories: