

Welcome to Wagtail’s documentation
==================================

 Wagtail is an open source CMS written in [Python](https://www.python.org/) and built on the [Django web framework](https://www.djangoproject.com/).

 Below are some useful links to help you get started with Wagtail.

 If you’d like to get a quick feel for Wagtail, try spinning up a [temporary developer environment](https://gitpod.io/#https://github.com/wagtail/wagtail-gitpod) in your browser (running on Gitpod - here’s [how it works](https://wagtail.org/blog/gitpod/)).

 * **First steps**


	+ [Getting started](getting_started/index)
	+ [Your first Wagtail site](getting_started/tutorial)
	+ [Demo site](getting_started/demo_site)
* **Using Wagtail**


	+ [Page models](topics/pages)
	+ [Writing templates](topics/writing_templates)
	+ [How to use images in templates](topics/images)
	+ [Search](topics/search/index)
	+ [Third-party tutorials](advanced_topics/third_party_tutorials)
* **For editors**


	+ [Editors guide (separate site)](https://guide.wagtail.org/)

  Index
-----

 * [Getting started](getting_started/index)
	+ [Your first Wagtail site](getting_started/tutorial)
	+ [Demo site](getting_started/demo_site)
	+ [Integrating Wagtail into a Django project](getting_started/integrating_into_django)
	+ [The Zen of Wagtail](getting_started/the_zen_of_wagtail)
* [Usage guide](topics/index)
	+ [Page models](topics/pages)
	+ [Writing templates](topics/writing_templates)
	+ [How to use images in templates](topics/images)
	+ [Search](topics/search/index)
	+ [Snippets](topics/snippets)
	+ [How to use StreamField for mixed content](topics/streamfield)
	+ [Permissions](topics/permissions)
* [Advanced topics](advanced_topics/index)
	+ [Images](advanced_topics/images/index)
	+ [Documents](advanced_topics/documents/index)
	+ [Embedded content](advanced_topics/embeds)
	+ [How to add Wagtail into an existing Django project](advanced_topics/add_to_django_project)
	+ [Deploying Wagtail](advanced_topics/deploying)
	+ [Performance](advanced_topics/performance)
	+ [Internationalisation](advanced_topics/i18n)
	+ [Private pages](advanced_topics/privacy)
	+ [Customising Wagtail](advanced_topics/customisation/index)
	+ [Third-party tutorials](advanced_topics/third_party_tutorials)
	+ [Testing your Wagtail site](advanced_topics/testing)
	+ [Wagtail API](advanced_topics/api/index)
	+ [How to build a site with AMP support](advanced_topics/amp)
	+ [Accessibility considerations](advanced_topics/accessibility_considerations)
	+ [About StreamField BoundBlocks and values](advanced_topics/boundblocks_and_values)
	+ [Multi-site, multi-instance and multi-tenancy](advanced_topics/multi_site_multi_instance_multi_tenancy)
	+ [How to use a redirect with Form builder to prevent double submission](advanced_topics/formbuilder_routablepage_redirect)
* [Extending Wagtail](extending/index)
	+ [Creating admin views](extending/admin_views)
	+ [Generic views](extending/generic_views)
	+ [Template components](extending/template_components)
	+ [Using forms in admin views](extending/forms)
	+ [Adding reports](extending/adding_reports)
	+ [Adding new Task types](extending/custom_tasks)
	+ [Audit log](extending/audit_log)
	+ [Customising the user account settings form](extending/custom_account_settings)
	+ [Customising group edit/create views](extending/customising_group_views)
	+ [Rich text internals](extending/rich_text_internals)
	+ [Extending the Draftail Editor](extending/extending_draftail)
	+ [Adding custom bulk actions](extending/custom_bulk_actions)
* [Reference](reference/index)
	+ [Pages](reference/pages/index)
	+ [StreamField reference](reference/streamfield/index)
	+ [Contrib modules](reference/contrib/index)
	+ [Management commands](reference/management_commands)
	+ [Hooks](reference/hooks)
	+ [Signals](reference/signals)
	+ [Settings](reference/settings)
	+ [The project template](reference/project_template)
	+ [Jinja2 template support](reference/jinja2)
	+ [Panel API](reference/panel_api)
	+ [Viewsets](reference/viewsets)
* [Support](support)
* [Using Wagtail: an Editor’s guide](editor_manual/index)
* [Contributing to Wagtail](contributing/index)
	+ [Issue tracking](contributing/issue_tracking)
	+ [Development](contributing/developing)
	+ [Committing code](contributing/committing)
	+ [UI Styleguide](contributing/styleguide)
	+ [General coding guidelines](contributing/general_guidelines)
	+ [Python coding guidelines](contributing/python_guidelines)
	+ [UI Guidelines](contributing/ui_guidelines)
	+ [Documentation guidelines](contributing/documentation_guidelines)
	+ [Writing documentation](contributing/documentation_modes)
	+ [Reporting security issues](contributing/security)
	+ [Wagtail’s release process](contributing/release_process)

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/index.html>

Writing templates
=================

 Wagtail uses Django’s templating language. For developers new to Django, start with Django’s own template documentation: [Templates](https://docs.djangoproject.com/en/stable/topics/templates/ "(in Django v4.1)")

 Python programmers new to Django/Wagtail may prefer more technical documentation: [The Django template language: for Python programmers](https://docs.djangoproject.com/en/stable/ref/templates/api/ "(in Django v4.1)")

 You should be familiar with Django templating basics before continuing with this documentation.

  Templates
---------

 Every type of page or “content type” in Wagtail is defined as a “model” in a file called `models.py`. If your site has a blog, you might have a `BlogPage` model and another called `BlogPageListing`. The names of the models are up to the Django developer.

 For each page model in `models.py`, Wagtail assumes an HTML template file exists of (almost) the same name. The Front End developer may need to create these templates themselves by referring to `models.py` to infer template names from the models defined therein.

 To find a suitable template, Wagtail converts CamelCase names to snake\_case. So for a `BlogPage`, a template `blog_page.html` will be expected. The name of the template file can be overridden per model if necessary.

 Template files are assumed to exist here:

 
```
name_of_project/
    name_of_app/
        templates/
            name_of_app/
                blog_page.html
        models.py

```
 For more information, see the Django documentation for the [application directories template loader](https://docs.djangoproject.com/en/stable/ref/templates/api/ "(in Django v4.1)").

  ### Page content

 The data/content entered into each page is accessed/output through Django’s `{{ double-brace }}` notation. Each field from the model must be accessed by prefixing `page.`. For example the page title `{{ page.title }}` or another field `{{ page.author }}`.

 A custom variable name can be configured on the page model [`wagtail.models.Page.context_object_name`](../reference/pages/model_reference#wagtail.models.Page.context_object_name "wagtail.models.Page.context_object_name"). If a custom name is defined, `page` is still available for use in shared templates.

 Additionally `request.` is available and contains Django’s request object.

    Static assets
-------------

 Static files (such as CSS, JS and images) are typically stored here:

 
```
name_of_project/
    name_of_app/
        static/
            name_of_app/
                css/
                js/
                images/
        models.py

```
 (The names “css”, “js” etc aren’t important, only their position within the tree.)

 Any file within the static folder should be inserted into your HTML using the `{% static %}` tag. More about it: [Static files (tag)](#static-tag).

  ### User images

 Images uploaded to a Wagtail site by its users (as opposed to a developer’s static files, mentioned above) go into the image library and from there are added to pages via the page editor interface.

 Unlike other CMSs, adding images to a page does not involve choosing a “version” of the image to use. Wagtail has no predefined image “formats” or “sizes”. Instead the template developer defines image manipulation to occur *on the fly* when the image is requested, via a special syntax within the template.

 Images from the library must be requested using this syntax, but a developer’s static images can be added via conventional means like `img` tags. Only images from the library can be manipulated on the fly.

 Read more about the image manipulation syntax here: [How to use images in templates](images#image-tag).

    Template tags & filters
-----------------------

 In addition to Django’s standard tags and filters, Wagtail provides some of its own, which can be `load`-ed [just like any other](https://docs.djangoproject.com/en/stable/howto/custom-template-tags/ "(in Django v4.1)").

   Images (tag)
------------

 The `image` tag inserts an XHTML-compatible `img` element into the page, setting its `src`, `width`, `height` and `alt`. See also [More control over the img tag](images#image-tag-alt).

 The syntax for the `image` tag is thus:

 
```
{% image [image] [resize-rule] %}

```
 For example:

 
```
{% load wagtailimages_tags %}
...

{% image page.photo width-400 %}

<!-- or a square thumbnail: -->
{% image page.photo fill-80x80 %}

```
 See [How to use images in templates](images#image-tag) for full documentation.

   Rich text (filter)
------------------

 This filter takes a chunk of HTML content and renders it as safe HTML in the page. Importantly, it also expands internal shorthand references to embedded images, and links made in the Wagtail editor, into fully-baked HTML ready for display.

 Only fields using `RichTextField` need this applied in the template.

 
```
{% load wagtailcore_tags %}
...
{{ page.body|richtext }}

```
  ### Responsive Embeds

 As Wagtail does not impose any styling of its own on templates, images and embedded media will be displayed at a fixed width as determined by the HTML. Images can be made to resize to fit their container using a CSS rule such as the following:

 
```
.body img {
    max-width: 100%;
    height: auto;
}

```
 where `body` is a container element in your template surrounding the images.

 Making embedded media resizable is also possible, but typically requires custom style rules matching the media’s aspect ratio. To assist in this, Wagtail provides built-in support for responsive embeds, which can be enabled by setting `WAGTAILEMBEDS_RESPONSIVE_HTML = True` in your project settings. This adds a CSS class of `responsive-object` and an inline `padding-bottom` style to the embed, to be used in conjunction with the following CSS:

 
```
.responsive-object {
    position: relative;
}

.responsive-object iframe,
.responsive-object object,
.responsive-object embed {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

```
    Internal links (tag)
--------------------

  ### `pageurl`

 Takes a Page object and returns a relative URL (`/foo/bar/`) if within the same Site as the current page, or absolute (`http://example.com/foo/bar/`) if not.

 
```
{% load wagtailcore_tags %}
...
<a href="{% pageurl page.get_parent %}">Back to index</a>

```
 A `fallback` keyword argument can be provided - this can be a URL string, a named URL route that can be resolved with no parameters, or an object with a `get_absolute_url` method, and will be used as a substitute URL when the passed page is `None`.

 
```
{% load wagtailcore_tags %}

{% for publication in page.related_publications.all %}
    <li>
        <a href="{% pageurl publication.detail_page fallback='coming_soon' %}">
            {{ publication.title }}
        </a>
    </li>
{% endfor %}

```
   ### `slugurl`

 Takes any `slug` as defined in a page’s “Promote” tab and returns the URL for the matching Page. If multiple pages exist with the same slug, the page chosen is undetermined.

 Like `pageurl`, this will try to provide a relative link if possible, but will default to an absolute link if the Page is on a different Site. This is most useful when creating shared page furniture, for example top level navigation or site-wide links.

 
```
{% load wagtailcore_tags %}
...
<a href="{% slugurl 'news' %}">News index</a>

```
    Static files (tag)
------------------

 Used to load anything from your static files directory. Use of this tag avoids rewriting all static paths if hosting arrangements change, as they might between development and live environments.

 
```
{% load static %}
...
<img src="{% static "name_of_app/myimage.jpg" %}" alt="My image"/>

```
 Notice that the full path is not required - the path given here is relative to the app’s `static` directory. To avoid clashes with static files from other apps (including Wagtail itself), it’s recommended to place static files in a subdirectory of `static` with the same name as the app.

   Multi-site support
------------------

  ### `wagtail_site`

 Returns the Site object corresponding to the current request.

 
```
{% load wagtailcore_tags %}

{% wagtail_site as current_site %}

```
    Wagtail User Bar
----------------

 This tag provides a contextual flyout menu for logged-in users. The menu gives editors the ability to edit the current page or add a child page, besides the options to show the page in the Wagtail page explorer or jump to the Wagtail admin dashboard. Moderators are also given the ability to accept or reject a page being previewed as part of content moderation.

 This tag may be used on standard Django views, without page object. The user bar will contain one item pointing to the admin.

 We recommend putting the tag near the top of the `<body>` element so keyboard users can reach it. You should consider putting the tag after any `[skip links](https://webaim.org/techniques/skipnav/) but before the navigation and main content of your page.

 
```
{% load wagtailuserbar %}
...
<body>
    <a id="#content">Skip to content</a>
    {% wagtailuserbar %} {# This is a good place for the userbar #}
    <nav>
    ...
    </nav>
    <main id="content">
    ...
    </main>
</body>

```
 By default the User Bar appears in the bottom right of the browser window, inset from the edge. If this conflicts with your design it can be moved by passing a parameter to the template tag. These examples show you how to position the userbar in each corner of the screen:

 
```
...
{% wagtailuserbar 'top-left' %}
{% wagtailuserbar 'top-right' %}
{% wagtailuserbar 'bottom-left' %}
{% wagtailuserbar 'bottom-right' %}
...

```
 The userbar can be positioned where it works best with your design. Alternatively, you can position it with a CSS rule in your own CSS files, for example:

 
```
.wagtail-userbar {
    top: 200px !important;
    left: 10px !important;
}

```
   Varying output between preview and live
---------------------------------------

 Sometimes you may wish to vary the template output depending on whether the page is being previewed or viewed live. For example, if you have visitor tracking code such as Google Analytics in place on your site, it’s a good idea to leave this out when previewing, so that editor activity doesn’t appear in your analytics reports. Wagtail provides a `request.is_preview` variable to distinguish between preview and live:

 
```
{% if not request.is_preview %}
    <script>
        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
        ...
    </script>
{% endif %}

```
 If the page is being previewed, `request.preview_mode` can be used to determine the specific preview mode being used, if the page supports [multiple preview modes](../reference/pages/model_reference#wagtail.models.Page.preview_modes "wagtail.models.Page.preview_modes").

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/topics/writing_templates.html>

Indexing
========

 To make a model searchable, you’ll need to add it into the search index. All pages, images and documents are indexed for you, so you can start searching them right away.

 If you have created some extra fields in a subclass of Page or Image, you may want to add these new fields to the search index too so that a user’s search query will match on their content. See [Indexing extra fields](#wagtailsearch-indexing-fields) for info on how to do this.

 If you have a custom model that you would like to make searchable, see [Indexing custom models](#wagtailsearch-indexing-models).

  Updating the index
------------------

 If the search index is kept separate from the database (when using Elasticsearch for example), you need to keep them both in sync. There are two ways to do this: using the search signal handlers, or calling the `update_index` command periodically. For best speed and reliability, it’s best to use both if possible.

  ### Signal handlers

 `wagtailsearch` provides some signal handlers which bind to the save/delete signals of all indexed models. This would automatically add and delete them from all backends you have registered in `WAGTAILSEARCH_BACKENDS`. These signal handlers are automatically registered when the `wagtail.search` app is loaded.

 In some cases, you may not want your content to be automatically reindexed and instead rely on the `update_index` command for indexing. If you need to disable these signal handlers, use one of the following methods:

  #### Disabling auto update signal handlers for a model

 You can disable the signal handlers for an individual model by adding `search_auto_update = False` as an attribute on the model class.

   #### Disabling auto update signal handlers for a search backend/whole site

 You can disable the signal handlers for a whole search backend by setting the `AUTO_UPDATE` setting on the backend to `False`.

 If all search backends have `AUTO_UPDATE` set to `False`, the signal handlers will be completely disabled for the whole site.

 For documentation on the `AUTO_UPDATE` setting, see [AUTO\_UPDATE](backends#wagtailsearch-backends-auto-update).

    ### The `update_index` command

 Wagtail also provides a command for rebuilding the index from scratch.

 `./manage.py update_index`

 It is recommended to run this command once a week and at the following times:

 * whenever any pages have been created through a script (after an import, for example)
* whenever any changes have been made to models or search configuration

 The search may not return any results while this command is running, so avoid running it at peak times.

  Note

 The `update_index` command is also aliased as `wagtail_update_index`, for use when another installed package (such as [Haystack](https://haystacksearch.org/)) provides a conflicting `update_index` command. In this case, the other package’s entry in `INSTALLED_APPS` should appear above `wagtail.search` so that its `update_index` command takes precedence over Wagtail’s.

     Indexing extra fields
---------------------

 Fields must be explicitly added to the `search_fields` property of your `Page`-derived model, in order for you to be able to search/filter on them. This is done by overriding `search_fields` to append a list of extra `SearchField`/`FilterField` objects to it.

  ### Example

 This creates an `EventPage` model with two fields: `description` and `date`. `description` is indexed as a `SearchField` and `date` is indexed as a `FilterField`.

 
```
from wagtail.search import index
from django.utils import timezone

class EventPage(Page):
    description = models.TextField()
    date = models.DateField()

    search_fields = Page.search_fields + [ # Inherit search_fields from Page
        index.SearchField('description'),
        index.FilterField('date'),
    ]


# Get future events which contain the string "Christmas" in the title or description
>>> EventPage.objects.filter(date__gt=timezone.now()).search("Christmas")

```
   ### `index.SearchField`

 These are used for performing full-text searches on your models, usually for text fields.

  #### Options

 * **partial\_match** (`boolean`) - Setting this to true allows results to be matched on parts of words. For example, this is set on the title field by default, so a page titled `Hello World!` will be found if the user only types `Hel` into the search box.
* **boost** (`int/float`) - This allows you to set fields as being more important than others. Setting this to a high number on a field will cause pages with matches in that field to be ranked higher. By default, this is set to 2 on the Page title field and 1 on all other fields.

  Note

 The PostgresSQL full text search only supports [four weight levels (A, B, C, D)](https://www.postgresql.org/docs/current/textsearch-features.html). When the database search backend `wagtail.search.backends.database` is used on a PostgreSQL database, it will take all boost values in the project into consideration and group them into the four available weights.

 This means that in this configuration there are effectively only four boost levels used for ranking the search results, even if more boost values have been used.

 You can find out roughly which boost thresholds map to which weight in PostgresSQL by starting an new Django shell with `./manage.py shell` and inspecting `wagtail.search.backends.database.postgres.weights.BOOST_WEIGHTS`. You should see something like `[(10.0, 'A'), (7.166666666666666, 'B'), (4.333333333333333, 'C'), (1.5, 'D')]`. Boost values above each threshold will be treated with the respective weight.
* **es\_extra** (`dict`) - This field is to allow the developer to set or override any setting on the field in the Elasticsearch mapping. Use this if you want to make use of any Elasticsearch features that are not yet supported in Wagtail.

    ### `index.AutocompleteField`

 These are used for autocomplete queries which match partial words. For example, a page titled `Hello World!` will be found if the user only types `Hel` into the search box.

 This takes the exact same options as `index.SearchField` (with the exception of `partial_match`, which has no effect).

  Note

 Only index fields that are displayed in the search results with `index.AutocompleteField`. This allows users to see any words that were partial-matched on.

    ### `index.FilterField`

 These are added to the search index but are not used for full-text searches. Instead, they allow you to run filters on your search results.

   ### `index.RelatedFields`

 This allows you to index fields from related objects. It works on all types of related fields, including their reverse accessors.

 For example, if we have a book that has a `ForeignKey` to its author, we can nest the author’s `name` and `date_of_birth` fields inside the book:

 
```
from wagtail.search import index

class Book(models.Model, index.Indexed):
    ...

    search_fields = [
        index.SearchField('title'),
        index.FilterField('published_date'),

        index.RelatedFields('author', [
            index.SearchField('name'),
            index.FilterField('date_of_birth'),
        ]),
    ]

```
 This will allow you to search for books by their author’s name.

 It works the other way around as well. You can index an author’s books, allowing an author to be searched for by the titles of books they’ve published:

 
```
from wagtail.search import index

class Author(models.Model, index.Indexed):
    ...

    search_fields = [
        index.SearchField('name'),
        index.FilterField('date_of_birth'),

        index.RelatedFields('books', [
            index.SearchField('title'),
            index.FilterField('published_date'),
        ]),
    ]

```
  #### Filtering on `index.RelatedFields`

 It’s not possible to filter on any `index.FilterFields` within `index.RelatedFields` using the `QuerySet` API. However, the fields are indexed, so it should be possible to use them by querying Elasticsearch manually.

 Filtering on `index.RelatedFields` with the `QuerySet` API is planned for a future release of Wagtail.

    ### Indexing callables and other attributes

 Search/filter fields do not need to be Django model fields. They can also be any method or attribute on your model class.

 One use for this is indexing the `get_*_display` methods Django creates automatically for fields with choices.

 
```
from wagtail.search import index

class EventPage(Page):
    IS_PRIVATE_CHOICES = (
        (False, "Public"),
        (True, "Private"),
    )

    is_private = models.BooleanField(choices=IS_PRIVATE_CHOICES)

    search_fields = Page.search_fields + [
        # Index the human-readable string for searching.
        index.SearchField('get_is_private_display'),

        # Index the boolean value for filtering.
        index.FilterField('is_private'),
    ]

```
 Callables also provide a way to index fields from related models. In the example from [Inline Panels and Model Clusters](../../reference/pages/panels#inline-panels), to index each BookPage by the titles of its related\_links:

 
```
class BookPage(Page):
    # ...
    def get_related_link_titles(self):
        # Get list of titles and concatenate them
        return '\n'.join(self.related_links.all().values_list('name', flat=True))

    search_fields = Page.search_fields + [
        # ...
        index.SearchField('get_related_link_titles'),
    ]

```
    Indexing custom models
----------------------

 Any Django model can be indexed and searched.

 To do this, inherit from `index.Indexed` and add some `search_fields` to the model.

 
```
from wagtail.search import index

class Book(index.Indexed, models.Model):
    title = models.CharField(max_length=255)
    genre = models.CharField(max_length=255, choices=GENRE_CHOICES)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    published_date = models.DateTimeField()

    search_fields = [
        index.SearchField('title', partial_match=True, boost=10),
        index.SearchField('get_genre_display'),

        index.FilterField('genre'),
        index.FilterField('author'),
        index.FilterField('published_date'),
    ]

# As this model doesn't have a search method in its QuerySet, we have to call search directly on the backend
>>> from wagtail.search.backends import get_search_backend
>>> s = get_search_backend()

# Run a search for a book by Roald Dahl
>>> roald_dahl = Author.objects.get(name="Roald Dahl")
>>> s.search("chocolate factory", Book.objects.filter(author=roald_dahl))
[<Book: Charlie and the chocolate factory>]

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/topics/search/indexing.html>

Your first Wagtail site
=======================

  Note

 This tutorial covers setting up a brand new Wagtail project. If you’d like to add Wagtail to an existing Django project instead, see [Integrating Wagtail into a Django project](integrating_into_django).

   Install and run Wagtail
-----------------------

  ### Install dependencies

 Wagtail supports Python 3.7, 3.8, 3.9, 3.10, and 3.11.

 To check whether you have an appropriate version of Python 3:

 
```
python --version

```
 **On Windows** (cmd.exe, with the Python Launcher for Windows):

 
```
py --version

```
 If this does not return a version number or returns a version lower than 3.7, you will need to [install Python 3](https://www.python.org/downloads/).

  Note

 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 [platform-specific installation instructions](https://pillow.readthedocs.io/en/stable/installation.html#external-libraries).

    ### Create and activate a virtual environment

 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):

 
```
py -m venv mysite\env
mysite\env\Scripts\activate.bat
# or:
mysite\env\Scripts\activate

```
 **On GNU/Linux or MacOS** (bash):

 
```
python -m venv mysite/env
source mysite/env/bin/activate

```
 **For other shells** see the [`venv` documentation](https://docs.python.org/3/library/venv.html).

  Note

 If you’re using version control (such as 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:

 
```
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:

 
```
wagtail start mysite mysite

```
  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.py` file. Look at this file to see how the `start` command has listed them in there.

    ### Install project dependencies

 
```
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. The `requirements.txt` file contains all the dependencies needed in order to run the project.

   ### Create the database

 If you haven’t updated the project settings, this will be a SQLite database file in the project directory.

 
```
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 (for example 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

 
```
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

 
```
python manage.py runserver

```
 If everything worked, <http://127.0.0.1:8000> will show you a welcome page:

 ![Browser screenshot of "Welcome to your new Wagtail site!" page, with teal egg above the title, and links to different resources. The page is shown inside a browswer tab, with browser URL bar at the top]

  Note

 Throughout this tutorial we will use `http://127.0.0.1:8000` but depending on your setup, this could be `http://localhost:8000/` or a different IP address or port. Please read the console output of `manage.py runserver` to determine the correct url for your local site.

  You can now access the administrative area at <http://127.0.0.1:8000/admin>

 ![Screenshot of Wagtail’s dashboard, with "Welcome to the mysite Wagtail CMS" heading, 1 page, 0 images, 0 documents. Underneath is a "Your most recent edits" section, with the Home page listed]

    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:

 
```
from django.db import models

from wagtail.models import Page
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel


class HomePage(Page):
    body = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('body'),
    ]

```
 `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 the model definition.

 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 (for example 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:

 
```
{% extends "base.html" %}

{% load wagtailcore_tags %}

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

{% block content %}
    {{ page.body|richtext }}
{% endblock %}

```
 `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.

 ![Screenshot of an almost empty page – white background, "Welcome to our new site!" in the top left, and Wagtail logo in circled cyan in the bottom right]

  ### Wagtail template tags

 In addition to Django’s [template tags and filters](https://docs.djangoproject.com/en/3.1/ref/templates/builtins/), Wagtail provides a number of its own [template tags & filters](../topics/writing_templates#template-tags-and-filters) which can be loaded by including `{% load wagtailcore_tags %}` at the top of your template file.

 In this tutorial, we use the *richtext* filter to escape and print the contents of a `RichTextField`:

 
```
{% load wagtailcore_tags %}
{{ page.body|richtext }}

```
 Produces:

 
```
<p><b>Welcome</b> to our new site!</p>

```
 **Note:** You’ll need to include `{% load wagtailcore_tags %}` in each template that uses Wagtail’s tags. Django will throw a `TemplateSyntaxError` if the tags aren’t loaded.

    A basic blog
------------

 We are now ready to create a blog. To do so, run `python manage.py startapp blog` to create a new app in your Wagtail site.

 Add the new `blog` app to `INSTALLED_APPS` in `mysite/settings/base.py`.

  ### Blog Index and Posts

 Lets start with a simple index page for our blog. In `blog/models.py`:

 
```
from wagtail.models import Page
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel


class BlogIndexPage(Page):
    intro = RichTextField(blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('intro')
    ]

```
 Run `python manage.py makemigrations` and `python manage.py migrate`.

 Since the model is called `BlogIndexPage`, the default template name (unless we override it) will be `blog_index_page.html`. Django will look for a template whose name matches the name of your Page model within the templates directory in your blog app folder. This default behaviour can be overridden if needed.  
 To create a template for the `BlogIndexPage` model, create a file at the location `blog/templates/blog/blog_index_page.html`.

  Note

 You may need to create the folders `templates/blog` within your `blog` app folder

  In your `blog_index_page.html` file enter the following content:

 
```
{% extends "base.html" %}

{% load wagtailcore_tags %}

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

{% block content %}
    <h1>{{ page.title }}</h1>

    <div class="intro">{{ page.intro|richtext }}</div>

    {% for post in page.get_children %}
        <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
        {{ post.specific.intro }}
        {{ post.specific.body|richtext }}
    {% endfor %}

{% endblock %}

```
 Most of this should be familiar, but we’ll explain `get_children` a bit later. Note the `pageurl` tag, which is similar to Django’s `url` tag but takes a Wagtail Page object as an argument.

 In the Wagtail admin, go to Pages, then Home. Add a child page to the Home page by clicking on the “Actions” icon and selecting the option “Add child page”. Choose “Blog index page” from the list of the page types. Use “Our Blog” as your page title, make sure it has the slug “blog” on the Promote tab, and publish it. You should now be able to access the url `http://127.0.0.1:8000/blog` on your site (note how the slug from the Promote tab defines the page URL).

 Now we need a model and template for our blog posts. In `blog/models.py`:

 
```
from django.db import models

from wagtail.models import Page
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel
from wagtail.search import index


# Keep the definition of BlogIndexPage, and add:


class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        FieldPanel('body'),
    ]

```
 In the model above, we import `index` as this makes the model searchable. You can then list fields that you want to be searchable for the user.

 Run `python manage.py makemigrations` and `python manage.py migrate`.

 Create a new template file at the location `blog/templates/blog/blog_page.html`. Now add the following content to your newly created `blog_page.html` file:

 
```
{% extends "base.html" %}

{% load wagtailcore_tags %}

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

{% block content %}
    <h1>{{ page.title }}</h1>
    <p class="meta">{{ page.date }}</p>

    <div class="intro">{{ page.intro }}</div>

    {{ page.body|richtext }}

    <p><a href="{{ page.get_parent.url }}">Return to blog</a></p>

{% endblock %}

```
 Note the use of Wagtail’s built-in `get_parent()` method to obtain the URL of the blog this post is a part of.

 Now create a few blog posts as children of `BlogIndexPage`. Be sure to select type “Blog Page” when creating your posts.

 ![Page listing for Home page with the "Add Child Page" button highlighted in red]

 !["Create a page in our blog" page type selector, with Blog page button highlighted in red]

 Wagtail gives you full control over what kinds of content can be created under various parent content types. By default, any page type can be a child of any other page type.

 ![Page editor for "First blog post" page, with Post date, Intro, Body fields]

 Publish each blog post when you are done editing.

 You should now have the very beginnings of a working blog. Access the `/blog` URL and you should see something like this:

 ![Basic "Our blog" page with three blogs listed, with their title, content]

 Titles should link to post pages, and a link back to the blog’s homepage should appear in the footer of each post page.

   ### Parents and Children

 Much of the work you’ll be doing in Wagtail revolves around the concept of hierarchical “tree” structures consisting of nodes and leaves (see [Theory](../reference/pages/theory)). In this case, the `BlogIndexPage` is a “node” and individual `BlogPage` instances are the “leaves”.

 Take another look at the guts of `blog_index_page.html`:

 
```
{% for post in page.get_children %}
    <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
    {{ post.specific.intro }}
    {{ post.specific.body|richtext }}
{% endfor %}

```
 Every “page” in Wagtail can call out to its parent or children from its own position in the hierarchy. But why do we have to specify `post.specific.intro` rather than `post.intro`? This has to do with the way we defined our model:

 `class BlogPage(Page):`

 The `get_children()` method gets us a list of instances of the `Page` base class. When we want to reference properties of the instances that inherit from the base class, Wagtail provides the `specific` method that retrieves the actual `BlogPage` record. While the “title” field is present on the base `Page` model, “intro” is only present on the `BlogPage` model, so we need `.specific` to access it.

 To tighten up template code like this, we could use Django’s `with` tag:

 
```
{% for post in page.get_children %}
    {% with post=post.specific %}
        <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
        <p>{{ post.intro }}</p>
        {{ post.body|richtext }}
    {% endwith %}
{% endfor %}

```
 When you start writing more customised Wagtail code, you’ll find a whole set of QuerySet modifiers to help you navigate the hierarchy.

 
```
# Given a page object 'somepage':
MyModel.objects.descendant_of(somepage)
child_of(page) / not_child_of(somepage)
ancestor_of(somepage) / not_ancestor_of(somepage)
parent_of(somepage) / not_parent_of(somepage)
sibling_of(somepage) / not_sibling_of(somepage)
# ... and ...
somepage.get_children()
somepage.get_ancestors()
somepage.get_descendants()
somepage.get_siblings()

```
 For more information, see: [Page QuerySet reference](../reference/pages/queryset_reference)

   ### Overriding Context

 There are a couple of problems with our blog index view:

 1. Blogs generally display content in *reverse* chronological order
2. We want to make sure we’re only displaying *published* content.

 To accomplish these things, we need to do more than just grab the index page’s children in the template. Instead, we’ll want to modify the QuerySet in the model definition. Wagtail makes this possible via the overridable `get_context()` method. Modify your `BlogIndexPage` model like this:

 
```
class BlogIndexPage(Page):
    intro = RichTextField(blank=True)

    def get_context(self, request):
        # Update context to include only published posts, ordered by reverse-chron
        context = super().get_context(request)
        blogpages = self.get_children().live().order_by('-first_published_at')
        context['blogpages'] = blogpages
        return context

```
 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`:

 
```
from django.db import models

# New imports added for ParentalKey, Orderable, InlinePanel

from modelcluster.fields import ParentalKey

from wagtail.models import Page, Orderable
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel, InlinePanel
from wagtail.search import index


# ... (Keep the definition of BlogIndexPage, and update BlogPage:)


class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        FieldPanel('body'),
        InlinePanel('gallery_images', label="Gallery images"),
    ]


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

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

```
 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 appears in the page editor as 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:

 
```
{% extends "base.html" %}

{% load wagtailcore_tags wagtailimages_tags %}

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

{% block content %}
    <h1>{{ page.title }}</h1>
    <p class="meta">{{ page.date }}</p>

    <div class="intro">{{ page.intro }}</div>

    {{ page.body|richtext }}

    {% for item in page.gallery_images.all %}
        <div style="float: left; margin: 10px">
            {% image item.image fill-320x240 %}
            <p>{{ item.caption }}</p>
        </div>
    {% endfor %}

    <p><a href="{{ 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).

 !["Second Post" page, with title, date, intro, body, and a gallery of three images]

 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):

 
```
class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)

    def main_image(self):
        gallery_item = self.gallery_images.first()
        if gallery_item:
            return gallery_item.image
        else:
            return None

    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
    ]

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('intro'),
        FieldPanel('body'),
        InlinePanel('gallery_images', label="Gallery images"),
    ]

```
 This method is now available from our templates. Update `blog_index_page.html` to include the main image as a thumbnail alongside each post:

 
```
{% load wagtailcore_tags wagtailimages_tags %}

...

{% for post in blogpages %}
    {% with post=post.specific %}
        <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>

        {% with post.main_image as main_image %}
            {% if main_image %}{% image main_image fill-160x100 %}{% endif %}
        {% endwith %}

        <p>{{ post.intro }}</p>
        {{ post.body|richtext }}
    {% endwith %}
{% endfor %}

```
   ### Tagging Posts

 Let’s say we want to let editors “tag” their posts, so that readers can, for example, view all bicycle-related content together. For this, we’ll need to invoke the tagging system bundled with Wagtail, attach it to the `BlogPage` model and content panels, and render linked tags on the blog post template. Of course, we’ll need a working tag-specific URL view as well.

 First, alter `models.py` once more:

 
```
from django.db import models

# New imports added for ClusterTaggableManager, TaggedItemBase, MultiFieldPanel

from modelcluster.fields import ParentalKey
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase

from wagtail.models import Page, Orderable
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
from wagtail.search import index


# ... (Keep the definition of BlogIndexPage)


class BlogPageTag(TaggedItemBase):
    content_object = ParentalKey(
        'BlogPage',
        related_name='tagged_items',
        on_delete=models.CASCADE
    )


class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)

    # ... (Keep the main_image method and search_fields definition)

    content_panels = Page.content_panels + [
        MultiFieldPanel([
            FieldPanel('date'),
            FieldPanel('tags'),
        ], heading="Blog information"),
        FieldPanel('intro'),
        FieldPanel('body'),
        InlinePanel('gallery_images', label="Gallery images"),
    ]

```
 Run `python manage.py makemigrations` and `python manage.py migrate`.

 Note the new `modelcluster` and `taggit` imports, the addition of a new `BlogPageTag` model, and the addition of a `tags` field on `BlogPage`. We’ve also taken the opportunity to use a `MultiFieldPanel` in `content_panels` to group the date and tags fields together for readability.

 Edit one of your `BlogPage` instances, and you should now be able to tag posts:

 ![Screenshot of the "Second Post" page in the editor form, showing the Content tab]

 To render tags on a `BlogPage`, add this to `blog_page.html`:

 
```
{% if page.tags.all.count %}
    <div class="tags">
        <h3>Tags</h3>
        {% for tag in page.tags.all %}
            <a href="{% slugurl 'tags' %}?tag={{ tag }}"><button type="button">{{ tag }}</button></a>
        {% endfor %}
    </div>
{% endif %}

```
 Notice that we’re linking to pages here with the builtin `slugurl` tag rather than `pageurl`, which we used earlier. The difference is that `slugurl` takes a Page slug (from the Promote tab) as an argument. `pageurl` is more commonly used because it is unambiguous and avoids extra database lookups. But in the case of this loop, the Page object isn’t readily available, so we fall back on the less-preferred `slugurl` tag.

 Visiting a blog post with tags should now show a set of linked buttons at the bottom - one for each tag. However, clicking a button will get you a 404, since we haven’t yet defined a “tags” view. Add to `models.py`:

 
```
class BlogTagIndexPage(Page):

    def get_context(self, request):

        # Filter by tag
        tag = request.GET.get('tag')
        blogpages = BlogPage.objects.filter(tags__name=tag)

        # Update template context
        context = super().get_context(request)
        context['blogpages'] = blogpages
        return context

```
 Note that this Page-based model defines no fields of its own. Even without fields, subclassing `Page` makes it a part of the Wagtail ecosystem, so that you can give it a title and URL in the admin, and so that you can manipulate its contents by returning a QuerySet from its `get_context()` method.

 Migrate this in, then create a new `BlogTagIndexPage` in the admin. You’ll probably want to create the new page/view as a child of Homepage, parallel to your Blog index. Give it the slug “tags” on the Promote tab.

 Access `/tags` and Django will tell you what you probably already knew: you need to create a template `blog/blog_tag_index_page.html`:

 
```
{% extends "base.html" %}
{% load wagtailcore_tags %}

{% block content %}

    {% if request.GET.tag %}
        <h4>Showing pages tagged "{{ request.GET.tag }}"</h4>
    {% endif %}

    {% for blogpage in blogpages %}

          <p>
              <strong><a href="{% pageurl blogpage %}">{{ blogpage.title }}</a></strong><br />
              <small>Revised: {{ blogpage.latest_revision_created_at }}</small><br />
              {% if blogpage.author %}
                <p>By {{ blogpage.author.profile }}</p>
              {% endif %}
          </p>

    {% empty %}
        No pages found with that tag.
    {% endfor %}

{% endblock %}

```
 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:

 ![A page titled "Showing pages tagged bread", with two page links underneath]

   ### 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`:

 
```
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'),
        FieldPanel('icon'),
    ]

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = 'blog categories'

```
  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 by running `python manage.py makemigrations` and `python manage.py migrate`. 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.To add categories to the `BlogPage`, modify `models.py` in your blog app folder:

 
```
# 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
from taggit.models import TaggedItemBase

# ...

class BlogPage(Page):
    date = models.DateField("Post date")
    intro = models.CharField(max_length=250)
    body = RichTextField(blank=True)
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
    categories = ParentalManyToManyField('blog.BlogCategory', blank=True)

    # ... (Keep the main_image method and search_fields definition)

    content_panels = Page.content_panels + [
        MultiFieldPanel([
            FieldPanel('date'),
            FieldPanel('tags'),
            FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
        ], heading="Blog information"),
        FieldPanel('intro'),
        FieldPanel('body'),
        InlinePanel('gallery_images', label="Gallery images"),
    ]

```
 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:

 
```
<h1>{{ page.title }}</h1>
<p class="meta">{{ page.date }}</p>

{% with categories=page.categories.all %}
    {% if categories %}
        <h3>Posted in:</h3>
        <ul>
            {% for category in categories %}
                <li style="display: inline">
                    {% image category.icon fill-32x32 style="vertical-align: middle" %}
                    {{ category.name }}
                </li>
            {% endfor %}
        </ul>
    {% endif %}
{% endwith %}

```
 !["Second Post" page, with title, date, categories, intro, body, and a gallery of three images]

    Where next
----------

 * Read the Wagtail [topics](../topics/index) and [reference](../reference/index) documentation
* Learn how to implement [StreamField](../topics/streamfield) for freeform page content
* Browse through the [advanced topics](../advanced_topics/index) section and read [third-party tutorials](../advanced_topics/third_party_tutorials)

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/getting_started/tutorial.html>

Demo site
=========

 To create a new site on Wagtail, we recommend the `wagtail start` command in [Getting started](index). We also have a demo site, The Wagtail Bakery, which contains example page types and models. We recommend you use the demo site for testing during development of Wagtail itself.

 The source code and installation instructions can be found at <https://github.com/wagtail/bakerydemo>



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/getting_started/demo_site.html>

Usage guide
===========

 * [Page models](pages)
* [Writing templates](writing_templates)
* [How to use images in templates](images)
* [Search](search/index)
	+ [Indexing](search/indexing)
	+ [Searching](search/searching)
	+ [Backends](search/backends)
* [Snippets](snippets)
* [How to use StreamField for mixed content](streamfield)
* [Permissions](permissions)



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/topics/index.html>

Page models
===========

 Each page type (a.k.a. content type) in Wagtail is represented by a Django model. All page models must inherit from the [`wagtail.models.Page`](../reference/pages/model_reference#wagtail.models.Page "wagtail.models.Page") class.

 As all page types are Django models, you can use any field type that Django provides. See [Model field reference](https://docs.djangoproject.com/en/stable/ref/models/fields/) for a complete list of field types you can use. Wagtail also provides `wagtail.fields.RichTextField` which provides a WYSIWYG editor for editing rich-text content.

  Note

 If you’re not yet familiar with Django models, have a quick look at the following links to get you started:

 * [Creating models](https://docs.djangoproject.com/en/stable/intro/tutorial02/#creating-models "(in Django v4.1)")
* [Model syntax](https://docs.djangoproject.com/en/stable/topics/db/models/ "(in Django v4.1)")

   An example Wagtail page model
-----------------------------

 This example represents a typical blog post:

 
```
from django.db import models

from modelcluster.fields import ParentalKey

from wagtail.models import Page, Orderable
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel, MultiFieldPanel, InlinePanel
from wagtail.search import index


class BlogPage(Page):

    # Database fields

    body = RichTextField()
    date = models.DateField("Post date")
    feed_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )


    # Search index configuration

    search_fields = Page.search_fields + [
        index.SearchField('body'),
        index.FilterField('date'),
    ]


    # Editor panels configuration

    content_panels = Page.content_panels + [
        FieldPanel('date'),
        FieldPanel('body'),
        InlinePanel('related_links', heading="Related links", label="Related link"),
    ]

    promote_panels = [
        MultiFieldPanel(Page.promote_panels, "Common page configuration"),
        FieldPanel('feed_image'),
    ]


    # Parent page / subpage type rules

    parent_page_types = ['blog.BlogIndex']
    subpage_types = []


class BlogPageRelatedLink(Orderable):
    page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name='related_links')
    name = models.CharField(max_length=255)
    url = models.URLField()

    panels = [
        FieldPanel('name'),
        FieldPanel('url'),
    ]

```
  Note

 Ensure that none of your field names are the same as your class names. This will cause errors due to the way Django handles relations ([read more](https://github.com/wagtail/wagtail/issues/503)). In our examples we have avoided this by appending “Page” to each model name.

    Writing page models
-------------------

 Here we’ll describe each section of the above example to help you create your own page models.

  ### Database fields

 Each Wagtail page type is a Django model, represented in the database as a separate table.

 Each page type can have its own set of fields. For example, a news article may have body text and a published date, whereas an event page may need separate fields for venue and start/finish times.

 In Wagtail, you can use any Django field class. Most field classes provided by third party apps should work as well.

 Wagtail also provides a couple of field classes of its own:

 * `RichTextField` - For rich text content
* `StreamField` - A block-based content field (see: [Freeform page content using StreamField](streamfield))

 For tagging, Wagtail fully supports [django-taggit](https://django-taggit.readthedocs.org/en/latest/) so we recommend using that.

   ### Search

 The `search_fields` attribute defines which fields are added to the search index and how they are indexed.

 This should be a list of `SearchField` and `FilterField` objects. `SearchField` adds a field for full-text search. `FilterField` adds a field for filtering the results. A field can be indexed with both `SearchField` and `FilterField` at the same time (but only one instance of each).

 In the above example, we’ve indexed `body` for full-text search and `date` for filtering.

 The arguments that these field types accept are documented in [indexing extra fields](search/indexing#wagtailsearch-indexing-fields).

   ### Editor panels

 There are a few attributes for defining how the page’s fields will be arranged in the page editor interface:

 * `content_panels` - For content, such as main body text
* `promote_panels` - For metadata, such as tags, thumbnail image and SEO title
* `settings_panels` - For settings, such as publish date

 Each of these attributes is set to a list of `Panel` objects, which defines which fields appear on which tabs and how they are structured on each tab.

 Here’s a summary of the `Panel` classes that Wagtail provides out of the box. See [Panel types](../reference/pages/panels) for full descriptions.

 **Basic**

 These allow editing of model fields. The `FieldPanel` class will choose the correct widget based on the type of the field, such as a rich text editor for `RichTextField`, or an image chooser for a `ForeignKey` to an image model. `FieldPanel` also provides a page chooser interface for `ForeignKey`s to page models, but for more fine-grained control over which page types can be chosen, `PageChooserPanel` provides additional configuration options.

 * [`FieldPanel`](../reference/pages/panels#wagtail.admin.panels.FieldPanel "wagtail.admin.panels.FieldPanel")
* [`PageChooserPanel`](../reference/pages/panels#wagtail.admin.panels.PageChooserPanel "wagtail.admin.panels.PageChooserPanel")

  Changed in version 3.0: Previously, certain field types required special-purpose panels: `StreamFieldPanel`, `ImageChooserPanel`, `DocumentChooserPanel` and `SnippetChooserPanel`. These are now all handled by `FieldPanel`.

  **Structural**

 These are used for structuring fields in the interface.

 * [`MultiFieldPanel`](../reference/pages/panels#wagtail.admin.panels.MultiFieldPanel "wagtail.admin.panels.MultiFieldPanel")
* [`InlinePanel`](../reference/pages/panels#wagtail.admin.panels.InlinePanel "wagtail.admin.panels.InlinePanel")
* [`FieldRowPanel`](../reference/pages/panels#wagtail.admin.panels.FieldRowPanel "wagtail.admin.panels.FieldRowPanel")

  #### Customising the page editor interface

 The page editor can be customised further. See [Customising the editing interface](../advanced_topics/customisation/page_editing_interface).

    ### Parent page / subpage type rules

 These two attributes allow you to control where page types may be used in your site. It allows you to define rules like “blog entries may only be created under a blog index”.

 Both take a list of model classes or model names. Model names are of the format `app_label.ModelName`. If the `app_label` is omitted, the same app is assumed.

 * `parent_page_types` limits which page types this type can be created under
* `subpage_types` limits which page types can be created under this type

 By default, any page type can be created under any page type and it is not necessary to set these attributes if that’s the desired behaviour.

 Setting `parent_page_types` to an empty list is a good way of preventing a particular page type from being created in the editor interface.

   ### Page descriptions

 With every Wagtail Page you are able to add a helpful description text, similar to a `help_text` model attribute. By adding `page_description` to your Page model you’ll be adding a short description that can be seen when you create a new page, edit an existing page or when you’re prompted to select a child page type.

 
```
class LandingPage(Page):

    page_description = "Use this page for converting users"

```
   ### Page URLs

 The most common method of retrieving page URLs is by using the `{% pageurl %}` template tag. Since it’s called from a template, `pageurl` automatically includes the optimizations mentioned below. For more information, see [pageurl](writing_templates#pageurl-tag).

 Page models also include several low-level methods for overriding or accessing page URLs.

  #### Customising URL patterns for a page model

 The `Page.get_url_parts(request)` method will not typically be called directly, but may be overridden to define custom URL routing for a given page model. It should return a tuple of `(site_id, root_url, page_path)`, which are used by `get_url` and `get_full_url` (see below) to construct the given type of page URL.

 When overriding `get_url_parts()`, you should accept `*args, **kwargs`:

 
```
def get_url_parts(self, *args, **kwargs):

```
 and pass those through at the point where you are calling `get_url_parts` on `super` (if applicable), for example:

 
```
super().get_url_parts(*args, **kwargs)

```
 While you could pass only the `request` keyword argument, passing all arguments as-is ensures compatibility with any future changes to these method signatures.

 For more information, please see [`wagtail.models.Page.get_url_parts()`](../reference/pages/model_reference#wagtail.models.Page.get_url_parts "wagtail.models.Page.get_url_parts").

   #### Obtaining URLs for page instances

 The `Page.get_url(request)` method can be called whenever a page URL is needed. It defaults to returning local URLs (not including the protocol or domain) if it determines that the page is on the current site (via the hostname in `request`); otherwise, a full URL including the protocol and domain is returned. Whenever possible, the optional `request` argument should be included to enable per-request caching of site-level URL information and facilitate the generation of local URLs.

 A common use case for `get_url(request)` is in any custom template tag your project may include for generating navigation menus. When writing such a custom template tag, ensure that it includes `takes_context=True` and use `context.get('request')` to safely pass the request or `None` if no request exists in the context.

 For more information, please see [`wagtail.models.Page.get_url()`](../reference/pages/model_reference#wagtail.models.Page.get_url "wagtail.models.Page.get_url").

 In the event a full URL (including the protocol and domain) is needed, `Page.get_full_url(request)` can be used instead. Whenever possible, the optional `request` argument should be included to enable per-request caching of site-level URL information.

 For more information, please see [`wagtail.models.Page.get_full_url()`](../reference/pages/model_reference#wagtail.models.Page.get_full_url "wagtail.models.Page.get_full_url").

     Template rendering
------------------

 Each page model can be given an HTML template which is rendered when a user browses to a page on the site frontend. This is the simplest and most common way to get Wagtail content to end users (but not the only way).

  ### Adding a template for a page model

 Wagtail automatically chooses a name for the template based on the app label and model class name.

 Format: `<app_label>/<model_name (snake cased)>.html`

 For example, the template for the above blog page will be: `blog/blog_page.html`

 You just need to create a template in a location where it can be accessed with this name.

   ### Template context

 Wagtail renders templates with the `page` variable bound to the page instance being rendered. Use this to access the content of the page. For example, to get the title of the current page, use `{{ page.title }}`. All variables provided by [context processors](https://docs.djangoproject.com/en/stable/ref/templates/api/#subclassing-context-requestcontext) are also available.

  #### Customising template context

 All pages have a `get_context` method that is called whenever the template is rendered and returns a dictionary of variables to bind into the template.

 To add more variables to the template context, you can override this method:

 
```
class BlogIndexPage(Page):
    ...

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)

        # Add extra variables and return the updated context
        context['blog_entries'] = BlogPage.objects.child_of(self).live()
        return context

```
 The variables can then be used in the template:

 
```
{{ page.title }}

{% for entry in blog_entries %}
    {{ entry.title }}
{% endfor %}

```
    ### Changing the template

 Set the `template` attribute on the class to use a different template file:

 
```
class BlogPage(Page):
    ...

    template = 'other_template.html'

```
  #### Dynamically choosing the template

 The template can be changed on a per-instance basis by defining a `get_template` method on the page class. This method is called every time the page is rendered:

 
```
class BlogPage(Page):
    ...

    use_other_template = models.BooleanField()

    def get_template(self, request, *args, **kwargs):
        if self.use_other_template:
            return 'blog/other_blog_page.html'

        return 'blog/blog_page.html'

```
 In this example, pages that have the `use_other_template` boolean field set will use the `blog/other_blog_page.html` template. All other pages will use the default `blog/blog_page.html`.

   #### Ajax Templates

 If you want to add AJAX functionality to a page, such as a paginated listing that updates in-place on the page rather than triggering a full page reload, you can set the `ajax_template` attribute to specify an alternative template to be used when the page is requested via an AJAX call (as indicated by the `X-Requested-With: XMLHttpRequest` HTTP header):

 
```
class BlogPage(Page):
    ...

    ajax_template = 'other_template_fragment.html'
    template = 'other_template.html'

```
    ### More control over page rendering

 All page classes have a `serve()` method that internally calls the `get_context` and `get_template` methods and renders the template. This method is similar to a Django view function, taking a Django `Request` object and returning a Django `Response` object.

 This method can also be overridden for complete control over page rendering.

 For example, here’s a way to make a page respond with a JSON representation of itself:

 
```
from django.http import JsonResponse


class BlogPage(Page):
    ...

    def serve(self, request):
        return JsonResponse({
            'title': self.title,
            'body': self.body,
            'date': self.date,

            # Resizes the image to 300px width and gets a URL to it
            'feed_image': self.feed_image.get_rendition('width-300').url,
        })

```
    Inline models
-------------

 Wagtail can nest the content of other models within the page. This is useful for creating repeated fields, such as related links or items to display in a carousel. Inline model content is also versioned with the rest of the page content.

 Each inline model requires the following:

 * It must inherit from [`wagtail.models.Orderable`](../reference/pages/model_reference#wagtail.models.Orderable "wagtail.models.Orderable")
* It must have a `ParentalKey` to the parent model

  Note

 The model inlining feature is provided by [django-modelcluster](https://github.com/wagtail/django-modelcluster) and the `ParentalKey` field type must be imported from there:

 
```
from modelcluster.fields import ParentalKey

```
 `ParentalKey` is a subclass of Django’s `ForeignKey`, and takes the same arguments.

  For example, the following inline model can be used to add related links (a list of name, url pairs) to the `BlogPage` model:

 
```
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.models import Orderable


class BlogPageRelatedLink(Orderable):
    page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name='related_links')
    name = models.CharField(max_length=255)
    url = models.URLField()

    panels = [
        FieldPanel('name'),
        FieldPanel('url'),
    ]

```
 To add this to the admin interface, use the [`InlinePanel`](../reference/pages/panels#wagtail.admin.panels.InlinePanel "wagtail.admin.panels.InlinePanel") edit panel class:

 
```
content_panels = [
    ...

    InlinePanel('related_links', label="Related links"),
]

```
 The first argument must match the value of the `related_name` attribute of the `ParentalKey`.

   Working with pages
------------------

 Wagtail uses Django’s [multi-table inheritance](https://docs.djangoproject.com/en/3.1/topics/db/models/#multi-table-inheritance) feature to allow multiple page models to be used in the same tree.

 Each page is added to both Wagtail’s built-in [`Page`](../reference/pages/model_reference#wagtail.models.Page "wagtail.models.Page") model as well as its user-defined model (such as the `BlogPage` model created earlier).

 Pages can exist in Python code in two forms, an instance of `Page` or an instance of the page model.

 When working with multiple page types together, you will typically use instances of Wagtail’s `Page` model, which don’t give you access to any fields specific to their type.

 
```
# Get all pages in the database
>>> from wagtail.models import Page
>>> Page.objects.all()
[<Page: Homepage>, <Page: About us>, <Page: Blog>, <Page: A Blog post>, <Page: Another Blog post>]

```
 When working with a single page type, you can work with instances of the user-defined model. These give access to all the fields available in `Page`, along with any user-defined fields for that type.

 
```
# Get all blog entries in the database
>>> BlogPage.objects.all()
[<BlogPage: A Blog post>, <BlogPage: Another Blog post>]

```
 You can convert a `Page` object to its more specific user-defined equivalent using the `.specific` property. This may cause an additional database lookup.

 
```
>>> page = Page.objects.get(title="A Blog post")
>>> page
<Page: A Blog post>

# Note: the blog post is an instance of Page so we cannot access body, date or feed_image

>>> page.specific
<BlogPage: A Blog post>

```
   Tips
----

  ### Friendly model names

 You can make your model names more friendly to users of Wagtail by using Django’s internal `Meta` class with a `verbose_name`, for example:

 
```
class HomePage(Page):
    ...

    class Meta:
        verbose_name = "homepage"

```
 When users are given a choice of pages to create, the list of page types is generated by splitting your model names on each of their capital letters. Thus a `HomePage` model would be named “Home Page” which is a little clumsy. Defining `verbose_name` as in the example above would change this to read “Homepage”, which is slightly more conventional.

   ### Page QuerySet ordering

 `Page`-derived models *cannot* be given a default ordering by using the standard Django approach of adding an `ordering` attribute to the internal `Meta` class.

 
```
class NewsItemPage(Page):
    publication_date = models.DateField()
    ...

    class Meta:
        ordering = ('-publication_date', )  # will not work

```
 This is because `Page` enforces ordering QuerySets by path. Instead, you must apply the ordering explicitly when constructing a QuerySet:

 
```
news_items = NewsItemPage.objects.live().order_by('-publication_date')

```
   ### Custom Page managers

 You can add a custom `Manager` to your `Page` class. Any custom Managers should inherit from `wagtail.models.PageManager`:

 
```
from django.db import models
from wagtail.models import Page, PageManager

class EventPageManager(PageManager):
    """ Custom manager for Event pages """

class EventPage(Page):
    start_date = models.DateField()

    objects = EventPageManager()

```
 Alternately, if you only need to add extra `QuerySet` methods, you can inherit from `wagtail.models.PageQuerySet` to build a custom `Manager`:

 
```
from django.db import models
from django.utils import timezone
from wagtail.models import Page, PageManager, PageQuerySet

class EventPageQuerySet(PageQuerySet):
    def future(self):
        today = timezone.localtime(timezone.now()).date()
        return self.filter(start_date__gte=today)

EventPageManager = PageManager.from_queryset(EventPageQuerySet)

class EventPage(Page):
    start_date = models.DateField()

    objects = EventPageManager()

```
  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/topics/pages.html>

How to use images in templates
==============================

 The `image` tag inserts an XHTML-compatible `img` element into the page, setting its `src`, `width`, `height` and `alt`. See also [More control over the img tag](#image-tag-alt).

 The syntax for the tag is thus:

 
```
{% image [image] [resize-rule] %}

```
 **Both the image and resize rule must be passed to the template tag.**

 For example:

 
```
{% load wagtailimages_tags %}
...

<!-- Display the image scaled to a width of 400 pixels: -->
{% image page.photo width-400 %}

<!-- Display it again, but this time as a square thumbnail: -->
{% image page.photo fill-80x80 %}

```
 In the above syntax example `[image]` is the Django object referring to the image. If your page model defined a field called “photo” then `[image]` would probably be `page.photo`. The `[resize-rule]` defines how the image is to be resized when inserted into the page. Various resizing methods are supported, to cater to different use cases (for example lead images that span the whole width of the page, or thumbnails to be cropped to a fixed size).

 Note that a space separates `[image]` and `[resize-rule]`, but the resize rule must not contain spaces. The width is always specified before the height. Resized images will maintain their original aspect ratio unless the `fill` rule is used, which may result in some pixels being cropped.

  Available resizing methods
--------------------------

 The available resizing methods are as follows:

  ### `max`

 (takes two dimensions)

 
```
{% image page.photo max-1000x500 %}

```
 Fit **within** the given dimensions.

 The longest edge will be reduced to the matching dimension specified. For example, a portrait image of width 1000 and height 2000, treated with the `max-1000x500` rule (a landscape layout) would result in the image being shrunk so the *height* was 500 pixels and the width was 250.

 ![Example of max filter on an image](https://docs.wagtail.org/en/stable/_images/image_filter_max.png)

 Example: The image will keep its proportions but fit within the max (green line) dimensions provided.

   ### `min`

 (takes two dimensions)

 
```
{% image page.photo min-500x200 %}

```
 **Cover** the given dimensions.

 This may result in an image slightly **larger** than the dimensions you specify. A square image of width 2000 and height 2000, treated with the `min-500x200` rule would have its height and width changed to 500, that is matching the *width* of the resize-rule, but greater than the height.

 ![Example of min filter on an image](https://docs.wagtail.org/en/stable/_images/image_filter_min.png)

 Example: The image will keep its proportions while filling at least the min (green line) dimensions provided.

   ### `width`

 (takes one dimension)

 
```
{% image page.photo width-640 %}

```
 Reduces the width of the image to the dimension specified.

   ### `height`

 (takes one dimension)

 
```
{% image page.photo height-480 %}

```
 Reduces the height of the image to the dimension specified.

   ### `scale`

 (takes percentage)

 
```
{% image page.photo scale-50 %}

```
 Resize the image to the percentage specified.

   ### `fill`

 (takes two dimensions and an optional `-c` parameter)

 
```
{% image page.photo fill-200x200 %}

```
 Resize and **crop** to fill the **exact** dimensions specified.

 This can be particularly useful for websites requiring square thumbnails of arbitrary images. For example, a landscape image of width 2000 and height 1000 treated with the `fill-200x200` rule would have its height reduced to 200, then its width (ordinarily 400) cropped to 200.

 This resize-rule will crop to the image’s focal point if it has been set. If not, it will crop to the centre of the image.

 ![Example of fill filter on an image](https://docs.wagtail.org/en/stable/_images/image_filter_fill.png)

 Example: The image is scaled and also cropped (red line) to fit as much of the image as possible within the provided dimensions.

 **On images that won’t upscale**

 It’s possible to request an image with `fill` dimensions that the image can’t support without upscaling. For example an image of width 400 and height 200 requested with `fill-400x400`. In this situation the *ratio of the requested fill* will be matched, but the dimension will not. So that example 400x200 image (a 2:1 ratio) could become 200x200 (a 1:1 ratio, matching the resize-rule).

 **Cropping closer to the focal point**

 By default, Wagtail will only crop enough to change the aspect ratio of the image to match the ratio in the resize-rule.

 In some cases (for example thumbnails), it may be preferable to crop closer to the focal point, so that the subject of the image is more prominent.

 You can do this by appending `-c<percentage>` at the end of the resize-rule. For example, if you would like the image to be cropped as closely as possible to its focal point, add `-c100`:

 
```
{% image page.photo fill-200x200-c100 %}

```
 This will crop the image as much as it can, without cropping into the focal point.

 If you find that `-c100` is too close, you can try `-c75` or `-c50`. Any whole number from 0 to 100 is accepted.

 ![Example of fill filter on an image with a focal point set](https://docs.wagtail.org/en/stable/_images/image_filter_fill_focal.png)

 Example: The focal point is set off centre so the image is scaled and also cropped like fill, however the center point of the crop is positioned closer the focal point.

 ![Example of fill and closeness filter on an image with a focal point set](https://docs.wagtail.org/en/stable/_images/image_filter_fill_focal_close.png)

 Example: With `-c75` set, the final crop will be closer to the focal point.

   ### `original`

 (takes no dimensions)

 
```
{% image page.photo original %}

```
 Renders the image at its original size.

  Note

 Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will “max out” at their native dimensions.

     More control over the `img` tag
-------------------------------

 Wagtail provides two shortcuts to give greater control over the `img` element:

  ### 1. Adding attributes to the {% image %} tag

 Extra attributes can be specified with the syntax `attribute="value"`:

 
```
{% image page.photo width-400 class="foo" id="bar" %}

```
 You can set a more relevant `alt` attribute this way, overriding the one automatically generated from the title of the image. The `src`, `width`, and `height` attributes can also be overridden, if necessary.

 You can also add default attributes to all images (a default class or data attribute for example) - see [Adding default attributes to all images](#adding-default-attributes-to-images).

   ### 2. Generating the image “as foo” to access individual properties

 Wagtail can assign the image data to another variable using Django’s `as` syntax:

 
```
{% image page.photo width-400 as tmp_photo %}

<img src="{{ tmp_photo.url }}" width="{{ tmp_photo.width }}"
    height="{{ tmp_photo.height }}" alt="{{ tmp_photo.alt }}" class="my-custom-class" />

```
  Note

 The image property used for the `src` attribute is `image.url`, not `image.src`.

  This syntax exposes the underlying image Rendition (`tmp_photo`) to the developer. A “Rendition” contains the information specific to the way you’ve requested to format the image using the resize-rule, dimensions and source URL. The following properties are available:

   ### `url`

 URL to the resized version of the image. This may be a local URL (such as `/static/images/example.jpg`) or a full URL (such as `https://assets.example.com/images/example.jpg`), depending on how static files are configured.

   ### `width`

 Image width after resizing.

   ### `height`

 Image height after resizing.

   ### `alt`

 Alternative text for the image, typically taken from the image title.

   ### `attrs`

 A shorthand for outputting the attributes `src`, `width`, `height` and `alt` in one go:

 
```
<img {{ tmp_photo.attrs }} class="my-custom-class" />

```
   ### `full_url`

 Same as `url`, but always returns a full absolute URL. This requires `WAGTAILADMIN_BASE_URL` to be set in the project settings.

 This is useful for images that will be re-used outside of the current site, such as social share images:

 
```
<meta name="twitter:image" content="{{ tmp_photo.full_url }}">

```
 If your site defines a custom image model using `AbstractImage`, any additional fields you add to an image (such as a copyright holder) are **not** included in the rendition.

 Therefore, if you’d added the field `author` to your AbstractImage in the above example, you’d access it using `{{ page.photo.author }}` rather than `{{ tmp_photo.author }}`.

 (Due to the links in the database between renditions and their parent image, you *could* access it as `{{ tmp_photo.image.author }}`, but that has reduced readability.)

    Adding default attributes to all images
---------------------------------------

 We can configure the `wagtail.images` application to specify additional attributes to add to images. This is done by setting up a custom `AppConfig` class within your project folder (i.e. the package containing the top-level settings and urls modules).

 To do this, create or update your existing `apps.py` file with the following:

 
```
from wagtail.images.apps import WagtailImagesAppConfig


class CustomImagesAppConfig(WagtailImagesAppConfig):
    default_attrs = {"decoding": "async", "loading": "lazy"}

```
 Then, replace `wagtail.images` in `settings.INSTALLED_APPS` with the path to `CustomUsersAppConfig`:

 
```
INSTALLED_APPS = [
    ...,
    "myapplication.apps.CustomImagesAppConfig",
    # "wagtail.images",
    ...,
]

```
 Now, images created with `{% image %}` will additionally have `decoding="async" loading="lazy"` attributes. This also goes for images added to Rich Text and `ImageBlock` blocks.

   Alternative HTML tags
---------------------

 The `as` keyword allows alternative HTML image tags (such as `<picture>` or `<amp-img>`) to be used. For example, to use the `<picture>` tag:

 
```
<picture>
    {% image page.photo width-800 as wide_photo %}
    <source srcset="{{ wide_photo.url }}" media="(min-width: 800px)">
    {% image page.photo width-400 %}
</picture>

```
 And to use the `<amp-img>` tag (based on the [Mountains example](https://amp.dev/documentation/components/amp-img/#example:-specifying-a-fallback-image) from the AMP docs):

 
```
{% image image width-550 format-webp as webp_image %}
{% image image width-550 format-jpeg as jpeg_image %}

<amp-img alt="{{ image.alt }}"
    width="{{ webp_image.width }}"
    height="{{ webp_image.height }}"
    src="{{ webp_image.url }}">
    <amp-img alt="{{ image.alt }}"
        fallback
        width="{{ jpeg_image.width }}"
        height="{{ jpeg_image.height }}"
        src="{{ jpeg_image.url }}"></amp-img>
</amp-img>

```
   Images embedded in rich text
----------------------------

 The information above relates to images defined via image-specific fields in your model. However, images can also be embedded arbitrarily in Rich Text fields by the page editor (see [Rich Text (HTML)](../advanced_topics/customisation/page_editing_interface#rich-text)).

 Images embedded in Rich Text fields can’t be controlled by the template developer as easily. There are no image objects to work with, so the `{% image %}` template tag can’t be used. Instead, editors can choose from one of a number of image “Formats” at the point of inserting images into their text.

 Wagtail comes with three pre-defined image formats, but more can be defined in Python by the developer. These formats are:

  ### `Full width`

 Creates an image rendition using `width-800`, giving the ![]() tag the CSS class `full-width`.

   ### `Left-aligned`

 Creates an image rendition using `width-500`, giving the ![]() tag the CSS class `left`.

   ### `Right-aligned`

 Creates an image rendition using `width-500`, giving the ![]() tag the CSS class `right`.

  Note

 The CSS classes added to images do **not** come with any accompanying stylesheets, or inline styles. For example the `left` class will do nothing, by default. The developer is expected to add these classes to their front end CSS files, to define exactly what they want `left`, `right` or `full-width` to mean.

  For more information about image formats, including creating your own, see [Image Formats in the Rich Text Editor](../advanced_topics/customisation/page_editing_interface#rich-text-image-formats).

    Output image format
-------------------

 Wagtail may automatically change the format of some images when they are resized:

 * PNG and JPEG images don’t change format
* GIF images without animation are converted to PNGs
* BMP images are converted to PNGs
* WebP images are converted to PNGs

 It is also possible to override the output format on a per-tag basis by using the `format` filter after the resize rule.

 For example, to make the tag always convert the image to a JPEG, use `format-jpeg`:

 
```
{% image page.photo width-400 format-jpeg %}

```
 You may also use `format-png` or `format-gif`.

  ### Lossless WebP

 You can encode the image into lossless WebP format by using the `format-webp-lossless` filter:

 
```
{% image page.photo width-400 format-webp-lossless %}

```
    Background colour
-----------------

 The PNG and GIF image formats both support transparency, but if you want to convert images to JPEG format, the transparency will need to be replaced with a solid background colour.

 By default, Wagtail will set the background to white. But if a white background doesn’t fit your design, you can specify a colour using the `bgcolor` filter.

 This filter takes a single argument, which is a CSS 3 or 6 digit hex code representing the colour you would like to use:

 
```
{# Sets the image background to black #}
{% image page.photo width-400 bgcolor-000 format-jpeg %}

```
   Image quality
-------------

 Wagtail’s JPEG and WebP image quality settings default to 85 (which is quite high). This can be changed either globally or on a per-tag basis.

  ### Changing globally

 Use the `WAGTAILIMAGES_JPEG_QUALITY` and `WAGTAILIMAGES_WEBP_QUALITY` settings to change the global defaults of JPEG and WebP quality:

 
```
# settings.py

# Make low-quality but small images
WAGTAILIMAGES_JPEG_QUALITY = 40
WAGTAILIMAGES_WEBP_QUALITY = 45

```
 Note that this won’t affect any previously generated images so you may want to delete all renditions so they can regenerate with the new setting. This can be done from the Django shell:

 
```
# Replace this with your custom rendition model if you use one
>>> from wagtail.images.models import Rendition
>>> Rendition.objects.all().delete()

```
 You can also directly use the image management command from the console for regenerating the renditions:

 
```
./manage.py wagtail_update_image_renditions --purge

```
 You can read more about this command from [wagtail\_update\_image\_renditions](../reference/management_commands#wagtail-update-image-renditions)

   ### Changing per-tag

 It’s also possible to have different JPEG and WebP qualities on individual tags by using `jpegquality` and `webpquality` filters. This will always override the default setting:

 
```
{% image page.photo_jpeg width-400 jpegquality-40 %}
{% image page.photo_webp width-400 webpquality-50 %}

```
 Note that this will have no effect on PNG or GIF files. If you want all images to be low quality, you can use this filter with `format-jpeg` or `format-webp` (which forces all images to output in JPEG or WebP format):

 
```
{% image page.photo width-400 format-jpeg jpegquality-40 %}
{% image page.photo width-400 format-webp webpquality-50 %}

```
   ### Generating image renditions in Python

 All of the image transformations mentioned above can also be used directly in Python code. See [Generating renditions in Python](../advanced_topics/images/renditions#image-renditions).

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/topics/images.html>

Backends
========

 Wagtailsearch has support for multiple backends, giving you the choice between using the database for search or an external service such as Elasticsearch.

 You can configure which backend to use with the `WAGTAILSEARCH_BACKENDS` setting:

 
```
WAGTAILSEARCH_BACKENDS = {
    'default': {
        'BACKEND': 'wagtail.search.backends.database',
    }
}

```
  `AUTO_UPDATE`
-------------

 By default, Wagtail will automatically keep all indexes up to date. This could impact performance when editing content, especially if your index is hosted on an external service.

 The `AUTO_UPDATE` setting allows you to disable this on a per-index basis:

 
```
WAGTAILSEARCH_BACKENDS = {
    'default': {
        'BACKEND': ...,
        'AUTO_UPDATE': False,
    }
}

```
 If you have disabled auto update, you must run the [update\_index](../../reference/management_commands#update-index) command on a regular basis to keep the index in sync with the database.

   `ATOMIC_REBUILD`
----------------

  Warning

 This option may not work on Elasticsearch version 5.4.x, due to [a bug in the handling of aliases](https://github.com/elastic/elasticsearch/issues/24644) - please upgrade to 5.5 or later.

  By default (when using the Elasticsearch backend), when the `update_index` command is run, Wagtail deletes the index and rebuilds it from scratch. This causes the search engine to not return results until the rebuild is complete and is also risky as you can’t rollback if an error occurs.

 Setting the `ATOMIC_REBUILD` setting to `True` makes Wagtail rebuild into a separate index while keep the old index active until the new one is fully built. When the rebuild is finished, the indexes are swapped atomically and the old index is deleted.

   `BACKEND`
---------

 Here’s a list of backends that Wagtail supports out of the box.

  ### Database Backend (default)

 `wagtail.search.backends.database`

 The database search backend searches content in the database using the full text search features of the database backend in use (such as PostgreSQL FTS, SQLite FTS5). This backend is intended to be used for development and also should be good enough to use in production on sites that don’t require any Elasticsearch specific features.

   ### Elasticsearch Backend

 Elasticsearch versions 5, 6 and 7 are supported. Use the appropriate backend for your version:

 * `wagtail.search.backends.elasticsearch5` (Elasticsearch 5.x)
* `wagtail.search.backends.elasticsearch6` (Elasticsearch 6.x)
* `wagtail.search.backends.elasticsearch7` (Elasticsearch 7.x)

 Prerequisites are the [Elasticsearch](https://www.elastic.co/downloads/elasticsearch) service itself and, via pip, the [elasticsearch-py](https://elasticsearch-py.readthedocs.io/) package. The major version of the package must match the installed version of Elasticsearch:

 
```
pip install "elasticsearch>=5.0.0,<6.0.0"  # for Elasticsearch 5.x

```
 
```
pip install "elasticsearch>=6.4.0,<7.0.0"  # for Elasticsearch 6.x

```
 
```
pip install "elasticsearch>=7.0.0,<8.0.0"  # for Elasticsearch 7.x

```
  Warning

 Version 6.3.1 of the Elasticsearch client library is incompatible with Wagtail. Use 6.4.0 or above.

  The backend is configured in settings:

 
```
WAGTAILSEARCH_BACKENDS = {
    'default': {
        'BACKEND': 'wagtail.search.backends.elasticsearch5',
        'URLS': ['http://localhost:9200'],
        'INDEX': 'wagtail',
        'TIMEOUT': 5,
        'OPTIONS': {},
        'INDEX_SETTINGS': {},
    }
}

```
 Other than `BACKEND`, the keys are optional and default to the values shown. Any defined key in `OPTIONS` is passed directly to the Elasticsearch constructor as case-sensitive keyword argument (for example `'max_retries': 1`).

 A username and password may be optionally be supplied to the `URL` field to provide authentication credentials for the Elasticsearch service:

 
```
WAGTAILSEARCH_BACKENDS = {
    'default': {
        ...
        'URLS': ['http://username:password@localhost:9200'],
        ...
    }
}

```
 `INDEX_SETTINGS` is a dictionary used to override the default settings to create the index. The default settings are defined inside the `ElasticsearchSearchBackend` class in the module `wagtail/wagtail/search/backends/elasticsearch7.py`. Any new key is added, any existing key, if not a dictionary, is replaced with the new value. Here’s a sample on how to configure the number of shards and setting the Italian LanguageAnalyzer as the default analyzer:

 
```
WAGTAILSEARCH_BACKENDS = {
    'default': {
        ...,
        'INDEX_SETTINGS': {
            'settings': {
                'index': {
                    'number_of_shards': 1,
                },
                'analysis': {
                    'analyzer': {
                        'default': {
                            'type': 'italian'
                        }
                    }
                }
            }
        }
    }

```
 If you prefer not to run an Elasticsearch server in development or production, there are many hosted services available, including [Bonsai](https://bonsai.io/), who offer a free account suitable for testing and development. To use Bonsai:

 * Sign up for an account at `Bonsai`
* Use your Bonsai dashboard to create a Cluster.
* Configure `URLS` in the Elasticsearch entry in `WAGTAILSEARCH_BACKENDS` using the Cluster URL from your Bonsai dashboard
* Run `./manage.py update_index`

   ### Amazon AWS Elasticsearch

 The Elasticsearch backend is compatible with [Amazon Elasticsearch Service](https://aws.amazon.com/opensearch-service/), but requires additional configuration to handle IAM based authentication. This can be done with the [requests-aws4auth](https://pypi.org/project/requests-aws4auth/) package along with the following configuration:

 
```
from elasticsearch import RequestsHttpConnection
from requests_aws4auth import AWS4Auth

WAGTAILSEARCH_BACKENDS = {
    'default': {
        'BACKEND': 'wagtail.search.backends.elasticsearch5',
        'INDEX': 'wagtail',
        'TIMEOUT': 5,
        'HOSTS': [{
            'host': 'YOURCLUSTER.REGION.es.amazonaws.com',
            'port': 443,
            'use_ssl': True,
            'verify_certs': True,
            'http_auth': AWS4Auth('ACCESS_KEY', 'SECRET_KEY', 'REGION', 'es'),
        }],
        'OPTIONS': {
            'connection_class': RequestsHttpConnection,
        },
    }
}

```
    Rolling Your Own
----------------

 Wagtail search backends implement the interface defined in `wagtail/wagtail/wagtailsearch/backends/base.py`. At a minimum, the backend’s `search()` method must return a collection of objects or `model.objects.none()`. For a fully-featured search backend, examine the Elasticsearch backend code in `elasticsearch.py`.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/topics/search/backends.html>

Images
======

 * [Generating renditions in Python](renditions)
	+ [Prefetching image renditions](renditions#prefetching-image-renditions)
	+ [Model methods involved in rendition generation](renditions#model-methods-involved-in-rendition-generation)
* [Animated GIF support](animated_gifs)
* [Image file formats](image_file_formats)
	+ [Using the picture element](image_file_formats#using-the-picture-element)
* [Custom image models](custom_image_model)
	+ [Migrating from the builtin image model](custom_image_model#migrating-from-the-builtin-image-model)
	+ [Referring to the image model](custom_image_model#module-wagtail.images)
* [Changing rich text representation](changing_rich_text_representation)
* [Feature Detection](feature_detection)
	+ [Installation](feature_detection#installation)
	+ [Cropping](feature_detection#cropping)
	+ [Switching on feature detection in Wagtail](feature_detection#switching-on-feature-detection-in-wagtail)
	+ [Manually running feature detection](feature_detection#manually-running-feature-detection)
* [Dynamic image serve view](image_serve_view)
	+ [Setup](image_serve_view#setup)
	+ [Usage](image_serve_view#usage)
	+ [Advanced configuration](image_serve_view#advanced-configuration)
	+ [Integration with django-sendfile](image_serve_view#integration-with-django-sendfile)
* [Focal points](focal_points)
	+ [Setting the `background-position` inline style based on the focal point](focal_points#setting-the-background-position-inline-style-based-on-the-focal-point)
	+ [Accessing the focal point in templates](focal_points#accessing-the-focal-point-in-templates)
* [Title generation on upload](title_generation_on_upload)
	+ [DOM Event](title_generation_on_upload#dom-event)
	+ [Code Examples](title_generation_on_upload#code-examples)



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/images/index.html>

Animated GIF support
====================

 Pillow, Wagtail’s default image library, doesn’t support animated GIFs.

 To get animated GIF support, you will have to [install Wand](https://docs.wand-py.org/en/0.6.7/guide/install.html). Wand is a binding to ImageMagick so make sure that has been installed as well.

 When installed, Wagtail will automatically use Wand for resizing GIF files but continue to resize other images with Pillow.



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/images/animated_gifs.html>

Getting started
===============

  Note

 These instructions assume familiarity with virtual environments and the [Django web framework](https://www.djangoproject.com/). For more detailed instructions, see [Your first Wagtail site](tutorial). To add Wagtail to an existing Django project, see [Integrating Wagtail into a Django project](integrating_into_django).

   Dependencies needed for installation
------------------------------------

 * [Python 3](https://www.python.org/downloads/)
* **libjpeg** and **zlib**, libraries required for Django’s **Pillow** library. See Pillow’s [platform-specific installation instructions](https://pillow.readthedocs.io/en/stable/installation.html#external-libraries).

   Quick install
-------------

 Run the following commands in a virtual environment of your choice:

 
```
pip install wagtail

```
 (Installing wagtail outside a virtual environment may require `sudo`. sudo is a program to run other programs with the security privileges of another user, by default the superuser)

 Once installed, Wagtail provides a command similar to Django’s `django-admin startproject` to generate a new site/project:

 
```
wagtail start mysite

```
 This will create a new folder `mysite`, based on a template containing everything you need to get started. More information on that template is available in [the project template reference](../reference/project_template).

 Inside your `mysite` folder, run the setup steps necessary for any Django project:

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

```
 Your site is now accessible at `http://localhost:8000`, with the admin backend available at `http://localhost:8000/admin/`.

 This will set you up with a new stand-alone Wagtail project. If you’d like to add Wagtail to an existing Django project instead, see [Integrating Wagtail into a Django project](integrating_into_django).

 There are a few optional packages which are not installed by default but are recommended to improve performance or add features to Wagtail, including:

 * [Elasticsearch](../advanced_topics/performance).
* [Feature Detection](../advanced_topics/images/feature_detection#image-feature-detection).

 * [Your first Wagtail site](tutorial)
* [Demo site](demo_site)
* [Integrating Wagtail into a Django project](integrating_into_django)
* [The Zen of Wagtail](the_zen_of_wagtail)

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/getting_started/index.html>

Integrating Wagtail into a Django project
=========================================

 Wagtail provides the `wagtail start` command and project template to get you started with a new Wagtail project as quickly as possible, but it’s easy to integrate Wagtail into an existing Django project too.

 Wagtail is currently compatible with Django 3.2, 4.0 and 4.1. First, install the `wagtail` package from PyPI:

 
```
pip install wagtail

```
 or add the package to your existing requirements file. This will also install the **Pillow** library as a dependency, which requires libjpeg and zlib - see Pillow’s [platform-specific installation instructions](https://pillow.readthedocs.io/en/stable/installation.html#external-libraries).

  Settings
--------

 In your settings.py file, add the following apps to `INSTALLED_APPS`:

 
```
'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail',

'modelcluster',
'taggit',

```
 Add the following entry to `MIDDLEWARE`:

 
```
'wagtail.contrib.redirects.middleware.RedirectMiddleware',

```
 Add a `STATIC_ROOT` setting, if your project does not have one already:

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

```
 Add `MEDIA_ROOT` and `MEDIA_URL` settings, if your project does not have these already:

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

```
 Add a `WAGTAIL_SITE_NAME` - this will be displayed on the main dashboard of the Wagtail admin backend:

 
```
WAGTAIL_SITE_NAME = 'My Example Site'

```
 Various other settings are available to configure Wagtail’s behaviour - see [Settings](../reference/settings).

   URL configuration
-----------------

 Now make the following additions to your `urls.py` file:

 
```
from django.urls import path, include

from wagtail.admin import urls as wagtailadmin_urls
from wagtail import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls

urlpatterns = [
    ...
    path('cms/', include(wagtailadmin_urls)),
    path('documents/', include(wagtaildocs_urls)),
    path('pages/', include(wagtail_urls)),
    ...
]

```
 The URL paths here can be altered as necessary to fit your project’s URL scheme.

 `wagtailadmin_urls` provides the admin interface for Wagtail. This is separate from the Django admin interface (`django.contrib.admin`); Wagtail-only projects typically host the Wagtail admin at `/admin/`, but if this would clash with your project’s existing admin backend then an alternative path can be used, such as `/cms/` here.

 `wagtaildocs_urls` is the location from where document files will be served. This can be omitted if you do not intend to use Wagtail’s document management features.

 `wagtail_urls` is the base location from where the pages of your Wagtail site will be served. In the above example, Wagtail will handle URLs under `/pages/`, leaving the root URL and other paths to be handled as normal by your Django project. If you want Wagtail to handle the entire URL space including the root URL, this can be replaced with:

 
```
path('', include(wagtail_urls)),

```
 In this case, this should be placed at the end of the `urlpatterns` list, so that it does not override more specific URL patterns.

 Finally, your project needs to be set up to serve user-uploaded files from `MEDIA_ROOT`. Your Django project may already have this in place, but if not, add the following snippet to `urls.py`:

 
```
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # ... the rest of your URLconf goes here ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

```
 Note that this only works in development mode (`DEBUG = True`); in production, you will need to configure your web server to serve files from `MEDIA_ROOT`. For further details, see the Django documentation: [Serving files uploaded by a user during development](https://docs.djangoproject.com/en/stable/howto/static-files/#serving-files-uploaded-by-a-user-during-development) and [Deploying static files](https://docs.djangoproject.com/en/stable/howto/static-files/deployment/).

 With this configuration in place, you are ready to run `python manage.py migrate` to create the database tables used by Wagtail.

   User accounts
-------------

 Superuser accounts receive automatic access to the Wagtail admin interface; use `python manage.py createsuperuser` if you don’t already have one. Custom user models are supported, with some restrictions; Wagtail uses an extension of Django’s permissions framework, so your user model must at minimum inherit from `AbstractBaseUser` and `PermissionsMixin`.

   Start developing
----------------

 You’re now ready to add a new app to your Django project (via `python manage.py startapp` - remember to add it to `INSTALLED_APPS` in your settings.py file) and set up page models, as described in [Your first Wagtail site](tutorial).

 Note that there’s one small difference when not using the Wagtail project template: Wagtail creates an initial homepage of the basic type `Page`, which does not include any content fields beyond the title. You’ll probably want to replace this with your own `HomePage` class - when you do so, ensure that you set up a site record (under Settings / Sites in the Wagtail admin) to point to the new homepage.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/getting_started/integrating_into_django.html>

The Zen of Wagtail
==================

 Wagtail has been born out of many years of experience building websites, learning approaches that work and ones that don’t, and striking a balance between power and simplicity, structure and flexibility. We hope you’ll find that Wagtail is in that sweet spot. However, as a piece of software, Wagtail can only take that mission so far - it’s now up to you to create a site that’s beautiful and a joy to work with. So, while it’s tempting to rush ahead and start building, it’s worth taking a moment to understand the design principles that Wagtail is built on.

 In the spirit of [“The Zen of Python”](https://www.python.org/dev/peps/pep-0020/), The Zen of Wagtail is a set of guiding principles, both for building websites in Wagtail, and for the ongoing development of Wagtail itself.

  Wagtail is not an instant website in a box.
-------------------------------------------

 You can’t make a beautiful website by plugging off-the-shelf modules together - expect to write code.

   Always wear the right hat.
--------------------------

 The key to using Wagtail effectively is to recognise that there are multiple roles involved in creating a website: the content author, site administrator, developer and designer. These may well be different people, but they don’t have to be - if you’re using Wagtail to build your personal blog, you’ll probably find yourself hopping between those different roles. Either way, it’s important to be aware of which of those hats you’re wearing at any moment, and to use the right tools for that job. A content author or site administrator will do the bulk of their work through the Wagtail admin interface; a developer or designer will spend most of their time writing Python, HTML or CSS code. This is a good thing: Wagtail isn’t designed to replace the job of programming. Maybe one day someone will come up with a drag-and-drop UI for building websites that’s as powerful as writing code, but Wagtail is not that tool, and does not try to be.

 A common mistake is to push too much power and responsibility into the hands of the content author and site administrator - indeed, if those people are your clients, they’ll probably be loudly clamouring for exactly that. The success of your site depends on your ability to say no. The real power of content management comes not from handing control over to CMS users, but from setting clear boundaries between the different roles. Amongst other things, this means not having editors doing design and layout within the content editing interface, and not having site administrators building complex interaction workflows that would be better achieved in code.

   A CMS should get information out of an editor’s head and into a database, as efficiently and directly as possible.
------------------------------------------------------------------------------------------------------------------

 Whether your site is about cars, cats, cakes or conveyancing, your content authors will be arriving at the Wagtail admin interface with some domain-specific information they want to put up on the website. Your aim as a site builder is to extract and store this information in its raw form - not one particular author’s idea of how that information should look.

 Keeping design concerns out of page content has numerous advantages. It ensures that the design remains consistent across the whole site, not subject to the whims of editors from one day to the next. It allows you to make full use of the informational content of the pages - for example, if your pages are about events, then having a dedicated “Event” page type with data fields for the event date and location will let you present the events in a calendar view or filtered listing, which wouldn’t be possible if those were just implemented as different styles of heading on a generic page. Finally, if you redesign the site at some point in the future, or move it to a different platform entirely, you can be confident that the site content will work in its new setting, and not be reliant on being formatted a particular way.

 Suppose a content author comes to you with a request: “We need this text to be in bright pink Comic Sans”. Your question to them should be “Why? What’s special about this particular bit of text?” If the reply is “I just like the look of it”, then you’ll have to gently persuade them that it’s not up to them to make design choices. (Sorry.) But if the answer is “it’s for our Children’s section”, then that gives you a way to divide the editorial and design concerns: give your editors the ability to designate certain pages as being “the Children’s section” (through tagging, different page models, or the site hierarchy) and let designers decide how to apply styles based on that.

   The best user interface for a programmer is usually a programming language.
---------------------------------------------------------------------------

 A common sight in content management systems is a point-and-click interface to let you define the data model that makes up a page:

 ![Image of a CMS interface]

 It looks nice in the sales pitch, but in reality, no CMS end-user can realistically make that kind of fundamental change - on a live site, no less - unless they have a programmer’s insight into how the site is built, and what impact the change will have. As such, it will always be the programmer’s job to negotiate that point-and-click interface - all you’ve done is taken them away from the comfortable world of writing code, where they have a whole ecosystem of tools, from text editors to version control systems, to help them develop, test and deploy their code changes.

 Wagtail recognises that most programming tasks are best done by writing code, and does not try to turn them into box-filling exercises when there’s no good reason to. Likewise, when building functionality for your site, you should keep in mind that some features are destined to be maintained by the programmer rather than a content editor, and consider whether making them configurable through the Wagtail admin is going to be more of a hindrance than a convenience. For example, Wagtail provides a form builder to allow content authors to create general-purpose data collection forms. You might be tempted to use this as the basis for more complex forms that integrate with (for example) a CRM system or payment processor - however, in this case there’s no way to edit the form fields without rewriting the backend logic, so making them editable through Wagtail has limited value. More likely, you’d be better off building these using Django’s form framework, where the form fields are defined entirely in code.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/getting_started/the_zen_of_wagtail.html>

Search
======

 Wagtail provides a comprehensive and extensible search interface. In addition, it provides ways to promote search results through “Editor’s Picks”. Wagtail also collects simple statistics on queries made through the search interface.

 * [Indexing](indexing)
	+ [Updating the index](indexing#updating-the-index)
	+ [Indexing extra fields](indexing#indexing-extra-fields)
	+ [Indexing custom models](indexing#indexing-custom-models)
* [Searching](searching)
	+ [Searching QuerySets](searching#searching-querysets)
	+ [Changing search behaviour](searching#changing-search-behaviour)
	+ [An example page search view](searching#an-example-page-search-view)
	+ [Promoted search results](searching#promoted-search-results)
* [Backends](backends)
	+ [`AUTO_UPDATE`](backends#auto-update)
	+ [`ATOMIC_REBUILD`](backends#atomic-rebuild)
	+ [`BACKEND`](backends#backend)
	+ [Rolling Your Own](backends#rolling-your-own)

  Indexing
--------

 To make objects searchable, they must first be added to the search index. This involves configuring the models and fields that you would like to index (which is done for you for Pages, Images and Documents), and then actually inserting them into the index.

 See [Updating the index](indexing#wagtailsearch-indexing-update) for information on how to keep the objects in your search index in sync with the objects in your database.

 If you have created some extra fields in a subclass of `Page` or `Image`, you may want to add these new fields to the search index, so a user’s search query can match the Page or Image’s extra content. See [Indexing extra fields](indexing#wagtailsearch-indexing-fields).

 If you have a custom model which doesn’t derive from `Page` or `Image` that you would like to make searchable, see [Indexing custom models](indexing#wagtailsearch-indexing-models).

   Searching
---------

 Wagtail provides an API for performing search queries on your models. You can also perform search queries on Django QuerySets.

 See [Searching](searching#wagtailsearch-searching).

   Backends
--------

 Wagtail provides two backends for storing the search index and performing search queries: one using the database’s full-text search capabilities, and another using Elasticsearch. It’s also possible to roll your own search backend.

 See [Backends](backends#wagtailsearch-backends).

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/topics/search/index.html>

Permissions
===========

 Wagtail adapts and extends [the Django permission system](https://docs.djangoproject.com/en/stable/topics/auth/default/#topic-authorization) to cater for the needs of website content creation, such as moderation workflows, and multiple teams working on different areas of a site (or multiple sites within the same Wagtail installation). Permissions can be configured through the ‘Groups’ area of the Wagtail admin interface, under ‘Settings’.

  Page permissions
----------------

 Permissions can be attached at any point in the page tree, and propagate down the tree. For example, if a site had the page tree:

 
```
MegaCorp/
    About us
    Offices/
        UK
        France
        Germany

```
 then a group with ‘edit’ permissions on the ‘Offices’ page would automatically receive the ability to edit the ‘UK’, ‘France’ and ‘Germany’ pages. Permissions can be set globally for the entire tree by assigning them on the ‘root’ page - since all pages must exist underneath the root node, and the root cannot be deleted, this permission will cover all pages that exist now and in future.

 Whenever a user creates a page through the Wagtail admin, that user is designated as the owner of that page. Any user with ‘add’ permission has the ability to edit pages they own, as well as adding new ones. This is in recognition of the fact that creating pages is typically an iterative process involving creating a number of draft versions - giving a user the ability to create a draft but not letting them subsequently edit it would not be very useful. Ability to edit a page also implies the ability to delete it; unlike Django’s standard permission model, there is no distinct ‘delete’ permission.

 The full set of available permission types is as follows:

 * **Add** - grants the ability to create new subpages underneath this page (provided the page model permits this - see [Parent page / subpage type rules](pages#page-type-business-rules)), and to edit and delete pages owned by the current user. Published pages cannot be deleted unless the user also has ‘publish’ permission.
* **Edit** - grants the ability to edit and delete this page, and any pages underneath it, regardless of ownership. A user with only ‘edit’ permission may not create new pages, only edit existing ones. Published pages cannot be deleted unless the user also has ‘publish’ permission.
* **Publish** - grants the ability to publish and unpublish this page and/or its children. A user without publish permission cannot directly make changes that are visible to visitors of the website; instead, they must submit their changes for moderation. Publish permission is independent of edit permission; a user with only publish permission will not be able to make any edits of their own.
* **Bulk delete** - allows a user to delete pages that have descendants, in a single operation. Without this permission, a user has to delete the descendant pages individually before deleting the parent. This is a safeguard against accidental deletion. This permission must be used in conjunction with ‘add’ / ‘edit’ permission, as it does not provide any deletion rights of its own; it only provides a ‘shortcut’ for the permissions the user has already. For example, a user with just ‘add’ and ‘bulk delete’ permissions will only be able to bulk-delete if all the affected pages are owned by that user, and are unpublished.
* **Lock** - grants the ability to lock or unlock this page (and any pages underneath it) for editing, preventing users from making any further edits to it.

 Drafts can be viewed only if the user has either Edit or Publish permission.

   Image / document permissions
----------------------------

 The permission rules for images and documents work on a similar basis to pages. Images and documents are considered to be ‘owned’ by the user who uploaded them; a user with ‘add’ permission also has the ability to edit items they own; and deletion is considered equivalent to editing rather than having a specific permission type.

 Access to specific sets of images and documents can be controlled by setting up *collections*. By default all images and documents belong to the ‘root’ collection, but users with appropriate permissions can create new collections the Settings -> Collections area of the admin interface. Permissions set on ‘root’ apply to all collections, so a user with ‘edit’ permission for images in the root collection can edit all images; permissions set on other collections only apply to that collection and any of its sub-collections.

 The ‘choose’ permission for images and documents determines which collections are visible within the chooser interface used to select images and document links for insertion into pages (and other models, such as snippets). Typically, all users are granted choose permission for all collections, allowing them to use any uploaded image or document on pages they create, but this permission can be limited to allow creating collections that are only visible to specific groups.

   Collection management permissions
---------------------------------

 Permission for managing collections themselves can be attached at any point in the collection tree. The available collection management permissions are as follows:

 * **Add** - grants the ability to create new collections underneath this collection.
* **Edit** - grants the ability to edit the name of the collection, change its location in the collection tree, and to change the privacy settings for documents within this collection.
* **Delete** - grants the ability to delete collections that were added below this collection. *Note:* a collection must have no subcollections under it and the collection itself must be empty before it can be deleted.

  Note

 Users are not allowed to move or delete the collection that is used to assign them permission to manage collections.

    Displaying custom permissions in the admin
------------------------------------------

 Most permissions will automatically show up in the wagtail admin Group edit form, however, you can also add them using the `register_permissions` hook (see [register\_permissions](../reference/hooks#register-permissions)).

   
`FieldPanel` permissions
-------------------------

 Permissions can be used to restrict access to fields within the editor interface. See `permission` on [FieldPanel](../reference/pages/panels#field-panel).

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/topics/permissions.html>

Advanced topics
===============

 * [Images](images/index)
	+ [Generating renditions in Python](images/renditions)
	+ [Animated GIF support](images/animated_gifs)
	+ [Image file formats](images/image_file_formats)
	+ [Custom image models](images/custom_image_model)
	+ [Changing rich text representation](images/changing_rich_text_representation)
	+ [Feature Detection](images/feature_detection)
	+ [Dynamic image serve view](images/image_serve_view)
	+ [Focal points](images/focal_points)
	+ [Title generation on upload](images/title_generation_on_upload)
* [Documents](documents/index)
	+ [Custom document model](documents/custom_document_model)
	+ [Title generation on upload](documents/title_generation_on_upload)
* [Embedded content](embeds)
	+ [Embedding content on your site](embeds#embedding-content-on-your-site)
	+ [Configuring embed “finders”](embeds#configuring-embed-finders)
	+ [The `Embed` model](embeds#the-embed-model)
* [How to add Wagtail into an existing Django project](add_to_django_project)
	+ [Middleware (`settings.py`)](add_to_django_project#middleware-settings-py)
	+ [Apps (`settings.py`)](add_to_django_project#apps-settings-py)
	+ [URL Patterns](add_to_django_project#url-patterns)
	+ [Ready to Use Example Configuration Files](add_to_django_project#ready-to-use-example-configuration-files)
* [Deploying Wagtail](deploying)
	+ [On your server](deploying#on-your-server)
	+ [On Divio Cloud](deploying#on-divio-cloud)
	+ [On PythonAnywhere](deploying#on-pythonanywhere)
	+ [On Google Cloud](deploying#on-google-cloud)
	+ [On alwaysdata](deploying#on-alwaysdata)
	+ [On other PAASs and IAASs](deploying#on-other-paass-and-iaass)
	+ [Deployment tips](deploying#deployment-tips)
* [Performance](performance)
	+ [Editor interface](performance#editor-interface)
	+ [Public users](performance#public-users)
* [Internationalisation](i18n)
	+ [Multi-language content](i18n#multi-language-content)
	+ [Alternative internationalisation plugins](i18n#alternative-internationalisation-plugins)
	+ [Wagtail admin translations](i18n#wagtail-admin-translations)
	+ [Change Wagtail admin language on a per-user basis](i18n#change-wagtail-admin-language-on-a-per-user-basis)
	+ [Changing the primary language of your Wagtail installation](i18n#changing-the-primary-language-of-your-wagtail-installation)
* [Private pages](privacy)
	+ [Setting up a login page](privacy#setting-up-a-login-page)
	+ [Setting up a global “password required” page](privacy#setting-up-a-global-password-required-page)
	+ [Setting a “password required” page for a specific page type](privacy#setting-a-password-required-page-for-a-specific-page-type)
* [Customising Wagtail](customisation/index)
	+ [Customising the editing interface](customisation/page_editing_interface)
	+ [Customising admin templates](customisation/admin_templates)
	+ [Custom user models](customisation/custom_user_models)
	+ [How to build custom StreamField blocks](customisation/streamfield_blocks)
* [Third-party tutorials](third_party_tutorials)
	+ [Tip](third_party_tutorials#tip)
* [Testing your Wagtail site](testing)
	+ [WagtailPageTestCase](testing#wagtailpagetestcase)
	+ [Form data helpers](testing#module-wagtail.test.utils.form_data)
	+ [Fixtures](testing#fixtures)
* [Wagtail API](api/index)
	+ [Wagtail API v2 Configuration Guide](api/v2/configuration)
	+ [Wagtail API v2 Usage Guide](api/v2/usage)
* [How to build a site with AMP support](amp)
	+ [Overview](amp#overview)
	+ [Creating the second page tree](amp#creating-the-second-page-tree)
	+ [Making pages aware of “AMP mode”](amp#making-pages-aware-of-amp-mode)
	+ [Write a template context processor so that AMP state can be checked in templates](amp#write-a-template-context-processor-so-that-amp-state-can-be-checked-in-templates)
	+ [Using a different page template when AMP mode is active](amp#using-a-different-page-template-when-amp-mode-is-active)
	+ [Overriding the `{% image %}` tag to output `<amp-img>` tags](amp#overriding-the-image-tag-to-output-amp-img-tags)
* [Accessibility considerations](accessibility_considerations)
	+ [Content modeling](accessibility_considerations#content-modeling)
	+ [Accessibility in templates](accessibility_considerations#accessibility-in-templates)
	+ [Authoring accessible content](accessibility_considerations#authoring-accessible-content)
	+ [Accessibility resources](accessibility_considerations#accessibility-resources)
* [About StreamField BoundBlocks and values](boundblocks_and_values)
* [Multi-site, multi-instance and multi-tenancy](multi_site_multi_instance_multi_tenancy)
	+ [Multi-site](multi_site_multi_instance_multi_tenancy#multi-site)
	+ [Multi-instance](multi_site_multi_instance_multi_tenancy#multi-instance)
	+ [Multi-tenancy](multi_site_multi_instance_multi_tenancy#multi-tenancy)
* [How to use a redirect with Form builder to prevent double submission](formbuilder_routablepage_redirect)



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/index.html>

Generating renditions in Python
===============================

 Rendered versions of original images generated by the Wagtail `{% image %}` template tag are called “renditions”, and are stored as new image files in the site’s `[media]/images` directory on the first invocation.

 Image renditions can also be generated dynamically from Python via the native `get_rendition()` method, for example:

 
```
newimage = myimage.get_rendition('fill-300x150|jpegquality-60')

```
 If `myimage` had a filename of `foo.jpg`, a new rendition of the image file called `foo.fill-300x150.jpegquality-60.jpg` would be generated and saved into the site’s `[media]/images` directory. Argument options are identical to the `{% image %}` template tag’s filter spec, and should be separated with `|`.

 The generated `Rendition` object will have properties specific to that version of the image, such as `url`, `width` and `height`, so something like this could be used in an API generator, for example:

 
```
url = myimage.get_rendition('fill-300x186|jpegquality-60').url

```
 Properties belonging to the original image from which the generated Rendition was created, such as `title`, can be accessed through the Rendition’s `image` property:

 
```
    >>> newimage.image.title
    'Blue Sky'
    >>> newimage.image.is_landscape()
    True

```
 See also: [How to use images in templates](../../topics/images#image-tag)

  Prefetching image renditions
----------------------------

 When using a queryset to render a list of images or objects with images, you can prefetch the renditions needed with a single additional query. For long lists of items, or where multiple renditions are used for each item, this can provide a significant boost to performance.

  New in version 4.0: The `prefetch_renditions` method is only applicable in Wagtail versions 4.0 and above.

   ### Image QuerySets

 When working with an Image QuerySet, you can make use of Wagtail’s built-in `prefetch_renditions` queryset method to prefetch the renditions needed.

 For example, say you were rendering a list of all the images uploaded by a certain user:

 
```
def get_images_uploaded_by_user(user):
    return ImageModel.objects.filter(uploaded_by_user=user)

```
 The above can be modified slightly to prefetch the renditions of the images returned:

 
```
def get_images_uploaded_by_user(user)::
    return ImageModel.objects.filter(uploaded_by_user=user).prefetch_renditions()

```
 The above will prefetch all renditions even if we may not need them.

 If images in your project tend to have very large numbers of renditions, and you know in advance the ones you need, you might want to consider specifying a set of filters to the `prefetch_renditions` method and only select the renditions you need for rendering. For example:

 
```
def get_images_uploaded_by_user(user):
    # Only specify the renditions required for rendering
    return ImageModel.objects.filter(uploaded_by_user=user).prefetch_renditions(
        "fill-700x586", "min-600x400", "max-940x680"
    )

```
   ### Non Image Querysets

 If you’re working with a non Image Model, you can make use of Django’s built-in `prefetch_related()` queryset method to prefetch renditions.

 For example, say you were rendering a list of events (with thumbnail images for each). Your code might look something like this:

 
```
def get_events():
    return EventPage.objects.live().select_related("listing_image")

```
 The above can be modified slightly to prefetch the renditions for listing images:

 
```
def get_events():
    return EventPage.objects.live().select_related("listing_image").prefetch_related("listing_image__renditions")

```
 If you know in advance the renditions you’ll need, you can filter the renditions queryset to use:

 
```
from django.db.models import Prefetch
from wagtail.images import get_image_model


def get_events():
    Image = get_image_model()
    filters = ["fill-300x186", "fill-600x400", "fill-940x680"]

    # `Prefetch` is used to fetch only the required renditions
    prefetch_images_and_renditions = Prefetch(
        "listing_image",
        queryset=Image.objects.prefetch_renditions(*filters)
    )
    return EventPage.objects.live().prefetch_related(prefetch_images_and_renditions)

```
    Model methods involved in rendition generation
----------------------------------------------

  New in version 3.0: The following method references are only applicable to Wagtail versions 3.0 and above.

  The following `AbstractImage` model methods are involved in finding and generating a renditions. If using a custom image model, you can customise the behaviour of either of these methods by overriding them on your model:

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/images/renditions.html>

Image file formats
==================

  Using the picture element
-------------------------

 The [picture element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture) can be used with the `format-<type>` image operation to specify different image formats and let the browser choose the one it prefers. For example:

 
```
{% load wagtailimages_tags %}

<picture>
    {% image myimage width-1000 format-webp as image_webp %}
    <source srcset="{{ image_webp.url }}" type="image/webp">

    {% image myimage width-1000 format-png as image_png %}
    <source srcset="{{ image_png.url }}" type="image/png">

    {% image myimage width-1000 format-png %}
</picture>

```
  ### Customising output formats

 By default all `bmp` and `webp` images are converted to the `png` format when no image output format is given.

 The default conversion mapping can be changed by setting the `WAGTAILIMAGES_FORMAT_CONVERSIONS` to a dictionary which maps the input type to an output type.

 For example:

 
```
    WAGTAILIMAGES_FORMAT_CONVERSIONS = {
        'bmp': 'jpeg',
        'webp': 'webp',
    }

```
 will convert `bmp` images to `jpeg` and disable the default `webp` to `png` conversion.

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/images/image_file_formats.html>

Custom image models
===================

 The `Image` model can be customised, allowing additional fields to be added to images.

 To do this, you need to add two models to your project:

 * The image model itself that inherits from `wagtail.images.models.AbstractImage`. This is where you would add your additional fields
* The renditions model that inherits from `wagtail.images.models.AbstractRendition`. This is used to store renditions for the new model.

 Here’s an example:

 
```
# models.py
from django.db import models

from wagtail.images.models import Image, AbstractImage, AbstractRendition


class CustomImage(AbstractImage):
    # Add any extra fields to image here

    # To add a caption field:
    # caption = models.CharField(max_length=255, blank=True)

    admin_form_fields = Image.admin_form_fields + (
        # Then add the field names here to make them appear in the form:
        # 'caption',
    )


class CustomRendition(AbstractRendition):
    image = models.ForeignKey(CustomImage, on_delete=models.CASCADE, related_name='renditions')

    class Meta:
        unique_together = (
            ('image', 'filter_spec', 'focal_point_key'),
        )

```
 Then set the `WAGTAILIMAGES_IMAGE_MODEL` setting to point to it:

 
```
WAGTAILIMAGES_IMAGE_MODEL = 'images.CustomImage'

```
  Migrating from the builtin image model
--------------------------------------

 When changing an existing site to use a custom image model, no images will be copied to the new model automatically. Copying old images to the new model would need to be done manually with a [data migration](https://docs.djangoproject.com/en/stable/topics/migrations/#data-migrations "(in Django v4.1)").

 Any templates that reference the builtin image model will still continue to work as before but would need to be updated in order to see any new images.

   Referring to the image model
----------------------------

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/images/custom_image_model.html>

Searching
=========

  Searching QuerySets
-------------------

 Wagtail search is built on Django’s [QuerySet API](https://docs.djangoproject.com/en/stable/ref/models/querysets/ "(in Django v4.1)"). You should be able to search any Django QuerySet provided the model and the fields being filtered on have been added to the search index.

  ### Searching Pages

 Wagtail provides a shortcut for searching pages: the `.search()` `QuerySet` method. You can call this on any `PageQuerySet`. For example:

 
```
# Search future EventPages
>>> from wagtail.models import EventPage
>>> EventPage.objects.filter(date__gt=timezone.now()).search("Hello world!")

```
 All other methods of `PageQuerySet` can be used with `search()`. For example:

 
```
# Search all live EventPages that are under the events index
>>> EventPage.objects.live().descendant_of(events_index).search("Event")
[<EventPage: Event 1>, <EventPage: Event 2>]

```
  Note

 The `search()` method will convert your `QuerySet` into an instance of one of Wagtail’s `SearchResults` classes (depending on backend). This means that you must perform filtering before calling `search()`.

  Before the `autocomplete()` method was introduced, the search method also did partial matching. This behaviour is will be deprecated and you should either switch to the new `autocomplete()` method or pass `partial_match=False` into the search method to opt-in to the new behaviour. The partial matching in `search()` will be completely removed in a future release.

   ### Autocomplete searches

 Wagtail provides a separate method which performs partial matching on specific autocomplete fields. This is useful for suggesting pages to the user in real-time as they type their query.

 
```
>>> EventPage.objects.live().autocomplete("Eve")
[<EventPage: Event 1>, <EventPage: Event 2>]

```
  Note

 This method should only be used for real-time autocomplete and actual search requests should always use the `search()` method.

    ### Searching Images, Documents and custom models

 Wagtail’s document and image models provide a `search` method on their QuerySets, just as pages do:

 
```
>>> from wagtail.images.models import Image

>>> Image.objects.filter(uploaded_by_user=user).search("Hello")
[<Image: Hello>, <Image: Hello world!>]

```
 [Custom models](indexing#wagtailsearch-indexing-models) can be searched by using the `search` method on the search backend directly:

 
```
>>> from myapp.models import Book
>>> from wagtail.search.backends import get_search_backend

# Search books
>>> s = get_search_backend()
>>> s.search("Great", Book)
[<Book: Great Expectations>, <Book: The Great Gatsby>]

```
 You can also pass a QuerySet into the `search` method which allows you to add filters to your search results:

 
```
>>> from myapp.models import Book
>>> from wagtail.search.backends import get_search_backend

# Search books
>>> s = get_search_backend()
>>> s.search("Great", Book.objects.filter(published_date__year__lt=1900))
[<Book: Great Expectations>]

```
   ### Specifying the fields to search

 By default, Wagtail will search all fields that have been indexed using `index.SearchField`.

 This can be limited to a certain set of fields by using the `fields` keyword argument:

 
```
# Search just the title field
>>> EventPage.objects.search("Event", fields=["title"])
[<EventPage: Event 1>, <EventPage: Event 2>]

```
   ### Faceted search

 Wagtail supports faceted search which is a kind of filtering based on a taxonomy field (such as category or page type).

 The `.facet(field_name)` method returns an `OrderedDict`. The keys are the IDs of the related objects that have been referenced by the specified field, and the values are the number of references found for each ID. The results are ordered by number of references descending.

 For example, to find the most common page types in the search results:

 
```
>>> Page.objects.search("Test").facet("content_type_id")

# Note: The keys correspond to the ID of a ContentType object; the values are the
# number of pages returned for that type
OrderedDict([
    ('2', 4),  # 4 pages have content_type_id == 2
    ('1', 2),  # 2 pages have content_type_id == 1
])

```
    Changing search behaviour
-------------------------

  ### Search operator

 The search operator specifies how search should behave when the user has typed in multiple search terms. There are two possible values:

 * “or” - The results must match at least one term (default for Elasticsearch)
* “and” - The results must match all terms (default for database search)

 Both operators have benefits and drawbacks. The “or” operator will return many more results but will likely contain a lot of results that aren’t relevant. The “and” operator only returns results that contain all search terms, but require the user to be more precise with their query.

 We recommend using the “or” operator when ordering by relevance and the “and” operator when ordering by anything else (note: the database backend doesn’t currently support ordering by relevance).

 Here’s an example of using the `operator` keyword argument:

 
```
# The database contains a "Thing" model with the following items:
# - Hello world
# - Hello
# - World


# Search with the "or" operator
>>> s = get_search_backend()
>>> s.search("Hello world", Things, operator="or")

# All records returned as they all contain either "hello" or "world"
[<Thing: Hello World>, <Thing: Hello>, <Thing: World>]


# Search with the "and" operator
>>> s = get_search_backend()
>>> s.search("Hello world", Things, operator="and")

# Only "hello world" returned as that's the only item that contains both terms
[<Thing: Hello world>]

```
 For page, image and document models, the `operator` keyword argument is also supported on the QuerySet’s `search` method:

 
```
>>> Page.objects.search("Hello world", operator="or")

# All pages containing either "hello" or "world" are returned
[<Page: Hello World>, <Page: Hello>, <Page: World>]

```
   ### Phrase searching

 Phrase searching is used for finding whole sentence or phrase rather than individual terms. The terms must appear together and in the same order.

 For example:

 
```
>>> from wagtail.search.query import Phrase

>>> Page.objects.search(Phrase("Hello world"))
[<Page: Hello World>]

>>> Page.objects.search(Phrase("World hello"))
[<Page: World Hello day>]

```
 If you are looking to implement phrase queries using the double-quote syntax, see [Query string parsing](#wagtailsearch-query-string-parsing).

   ### Fuzzy matching

  New in version 4.0.

  Fuzzy matching will return documents which contain terms similar to the search term, as measured by a [Levenshtein edit distance](https://en.wikipedia.org/wiki/Levenshtein_distance).

 A maximum of one edit (transposition, insertion, or removal of a character) is permitted for three to five letter terms, two edits for longer terms, and shorter terms must match exactly.

 For example:

 
```
>>> from wagtail.search.query import Fuzzy

>>> Page.objects.search(Fuzzy("Hallo"))
[<Page: Hello World>]

```
 Fuzzy matching is supported by the Elasticsearch search backend only.

   ### Complex search queries

 Through the use of search query classes, Wagtail also supports building search queries as Python objects which can be wrapped by and combined with other search queries. The following classes are available:

 `PlainText(query_string, operator=None, boost=1.0)`

 This class wraps a string of separate terms. This is the same as searching without query classes.

 It takes a query string, operator and boost.

 For example:

 
```
>>> from wagtail.search.query import PlainText
>>> Page.objects.search(PlainText("Hello world"))

# Multiple plain text queries can be combined. This example will match both "hello world" and "Hello earth"
>>> Page.objects.search(PlainText("Hello") & (PlainText("world") | PlainText("earth")))

```
 `Phrase(query_string)`

 This class wraps a string containing a phrase. See previous section for how this works.

 For example:

 
```
# This example will match both the phrases "hello world" and "Hello earth"
>>> Page.objects.search(Phrase("Hello world") | Phrase("Hello earth"))

```
 `Boost(query, boost)`

 This class boosts the score of another query.

 For example:

 
```
>>> from wagtail.search.query import PlainText, Boost

# This example will match both the phrases "hello world" and "Hello earth" but matches for "hello world" will be ranked higher
>>> Page.objects.search(Boost(Phrase("Hello world"), 10.0) | Phrase("Hello earth"))

```
 Note that this isn’t supported by the PostgreSQL or database search backends.

   ### Query string parsing

 The previous sections show how to construct a phrase search query manually, but a lot of search engines (Wagtail admin included, try it!) support writing phrase queries by wrapping the phrase with double-quotes. In addition to phrases, you might also want to allow users to add filters into the query using the colon syntax (`hello world published:yes`).

 These two features can be implemented using the `parse_query_string` utility function. This function takes a query string that a user typed and returns a query object and dictionary of filters:

 For example:

 
```
>>> from wagtail.search.utils import parse_query_string
>>> filters, query = parse_query_string('my query string "this is a phrase" this-is-a:filter', operator='and')

>>> filters
{
    'this-is-a': 'filter',
}

>>> query
And([
    PlainText("my query string", operator='and'),
    Phrase("this is a phrase"),
])

```
 Here’s an example of how this function can be used in a search view:

 
```
from wagtail.search.utils import parse_query_string

def search(request):
    query_string = request.GET['query']

    # Parse query
    filters, query = parse_query_string(query_string, operator='and')

    # Published filter
    # An example filter that accepts either `published:yes` or `published:no` and filters the pages accordingly
    published_filter = filters.get('published')
    published_filter = published_filter and published_filter.lower()
    if published_filter in ['yes', 'true']:
        pages = pages.filter(live=True)
    elif published_filter in ['no', 'false']:
        pages = pages.filter(live=False)

    # Search
    pages = pages.search(query)

    return render(request, 'search_results.html', {'pages': pages})

```
   ### Custom ordering

 By default, search results are ordered by relevance, if the backend supports it. To preserve the QuerySet’s existing ordering, the `order_by_relevance` keyword argument needs to be set to `False` on the `search()` method.

 For example:

 
```
# Get a list of events ordered by date
>>> EventPage.objects.order_by('date').search("Event", order_by_relevance=False)

# Events ordered by date
[<EventPage: Easter>, <EventPage: Halloween>, <EventPage: Christmas>]

```
   ### Annotating results with score

 For each matched result, Elasticsearch calculates a “score”, which is a number that represents how relevant the result is based on the user’s query. The results are usually ordered based on the score.

 There are some cases where having access to the score is useful (such as programmatically combining two queries for different models). You can add the score to each result by calling the `.annotate_score(field)` method on the `SearchQuerySet`.

 For example:

 
```
>>> events = EventPage.objects.search("Event").annotate_score("_score")
>>> for event in events:
...    print(event.title, event._score)
...
("Easter", 2.5),
("Halloween", 1.7),
("Christmas", 1.5),

```
 Note that the score itself is arbitrary and it is only useful for comparison of results for the same query.

    An example page search view
---------------------------

 Here’s an example Django view that could be used to add a “search” page to your site:

 
```
# views.py

from django.shortcuts import render

from wagtail.models import Page
from wagtail.search.models import Query


def search(request):
    # Search
    search_query = request.GET.get('query', None)
    if search_query:
        search_results = Page.objects.live().search(search_query)

        # Log the query so Wagtail can suggest promoted results
        Query.get(search_query).add_hit()
    else:
        search_results = Page.objects.none()

    # Render template
    return render(request, 'search_results.html', {
        'search_query': search_query,
        'search_results': search_results,
    })

```
 And here’s a template to go with it:

 
```
{% extends "base.html" %}
{% load wagtailcore_tags %}

{% block title %}Search{% endblock %}

{% block content %}
    <form action="{% url 'search' %}" method="get">
        <input type="text" name="query" value="{{ search_query }}">
        <input type="submit" value="Search">
    </form>

    {% if search_results %}
        <ul>
            {% for result in search_results %}
                <li>
                    <h4><a href="{% pageurl result %}">{{ result }}</a></h4>
                    {% if result.search_description %}
                        {{ result.search_description|safe }}
                    {% endif %}
                </li>
            {% endfor %}
        </ul>
    {% elif search_query %}
        No results found
    {% else %}
        Please type something into the search box
    {% endif %}
{% endblock %}

```
   Promoted search results
-----------------------

 “Promoted search results” allow editors to explicitly link relevant content to search terms, so results pages can contain curated content in addition to results from the search engine.

 This functionality is provided by the [`search_promotions`](../../reference/contrib/searchpromotions#module-wagtail.contrib.search_promotions "wagtail.contrib.search_promotions") contrib module.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/topics/search/searching.html>

Snippets
========

 Snippets are pieces of content which do not necessitate a full webpage to render. They could be used for making secondary content, such as headers, footers, and sidebars, editable in the Wagtail admin. Snippets are Django models which do not inherit the [`Page`](../reference/pages/model_reference#wagtail.models.Page "wagtail.models.Page") class and are thus not organised into the Wagtail tree. However, they can still be made editable by assigning panels and identifying the model as a snippet with the `register_snippet` class decorator.

 Snippets lack many of the features of pages, such as being orderable in the Wagtail admin or having a defined URL. Decide carefully if the content type you would want to build into a snippet might be more suited to a page.

  Snippet models
--------------

 Here’s an example snippet model:

 
```
from django.db import models

from wagtail.admin.panels import FieldPanel
from wagtail.snippets.models import register_snippet

# ...

@register_snippet
class Advert(models.Model):
    url = models.URLField(null=True, blank=True)
    text = models.CharField(max_length=255)

    panels = [
        FieldPanel('url'),
        FieldPanel('text'),
    ]

    def __str__(self):
        return self.text

```
 The `Advert` model uses the basic Django model class and defines two properties: text and URL. The editing interface is very close to that provided for `Page`-derived models, with fields assigned in the `panels` property. Snippets do not use multiple tabs of fields, nor do they provide the “save as draft” or “submit for moderation” features.

 `@register_snippet` tells Wagtail to treat the model as a snippet. The `panels` list defines the fields to show on the snippet editing page. It’s also important to provide a string representation of the class through `def __str__(self):` so that the snippet objects make sense when listed in the Wagtail admin.

   Including snippets in template tags
-----------------------------------

 The simplest way to make your snippets available to templates is with a template tag. This is mostly done with vanilla Django, so perhaps reviewing Django’s documentation for [custom template tags](https://docs.djangoproject.com/en/stable/howto/custom-template-tags/ "(in Django v4.1)") will be more helpful. We’ll go over the basics, though, and point out any considerations to make for Wagtail.

 First, add a new python file to a `templatetags` folder within your app - for example, `myproject/demo/templatetags/demo_tags.py`. We’ll need to load some Django modules and our app’s models, and ready the `register` decorator:

 
```
from django import template
from demo.models import Advert

register = template.Library()

# ...

# Advert snippets
@register.inclusion_tag('demo/tags/adverts.html', takes_context=True)
def adverts(context):
    return {
        'adverts': Advert.objects.all(),
        'request': context['request'],
    }

```
 `@register.inclusion_tag()` takes two variables: a template and a boolean on whether that template should be passed a request context. It’s a good idea to include request contexts in your custom template tags, since some Wagtail-specific template tags like `pageurl` need the context to work properly. The template tag function could take arguments and filter the adverts to return a specific instance of the model, but for brevity we’ll just use `Advert.objects.all()`.

 Here’s what’s in the template used by this template tag:

 
```
{% for advert in adverts %}
    <p>
        <a href="{{ advert.url }}">
            {{ advert.text }}
        </a>
    </p>
{% endfor %}

```
 Then, in your own page templates, you can include your snippet template tag with:

 
```
{% load wagtailcore_tags demo_tags %}

...

{% block content %}

    ...

    {% adverts %}

{% endblock %}

```
   Binding pages to snippets
-------------------------

 In the above example, the list of adverts is a fixed list that is displayed via the custom template tag independent of any other content on the page. This might be what you want for a common panel in a sidebar, but, in another scenario, you might wish to display just one specific instance of a snippet on a particular page. This can be accomplished by defining a foreign key to the snippet model within your page model and adding a [`FieldPanel`](../reference/pages/panels#wagtail.admin.panels.FieldPanel "wagtail.admin.panels.FieldPanel") to the page’s `content_panels` list. For example, if you wanted to display a specific advert on a `BookPage` instance:

 
```
  # ...
  class BookPage(Page):
      advert = models.ForeignKey(
          'demo.Advert',
          null=True,
          blank=True,
          on_delete=models.SET_NULL,
          related_name='+'
      )

      content_panels = Page.content_panels + [
          FieldPanel('advert'),
          # ...
      ]

```
 The snippet could then be accessed within your template as `page.advert`.

 To attach multiple adverts to a page, the `FieldPanel` can be placed on an inline child object of `BookPage` rather than on `BookPage` itself. Here, this child model is named `BookPageAdvertPlacement` (so called because there is one such object for each time that an advert is placed on a BookPage):

 
```
from django.db import models

from wagtail.models import Page, Orderable

from modelcluster.fields import ParentalKey

# ...

class BookPageAdvertPlacement(Orderable, models.Model):
    page = ParentalKey('demo.BookPage', on_delete=models.CASCADE, related_name='advert_placements')
    advert = models.ForeignKey('demo.Advert', on_delete=models.CASCADE, related_name='+')

    class Meta(Orderable.Meta):
        verbose_name = "advert placement"
        verbose_name_plural = "advert placements"

    panels = [
        FieldPanel('advert'),
    ]

    def __str__(self):
        return self.page.title + " -> " + self.advert.text


class BookPage(Page):
    # ...

    content_panels = Page.content_panels + [
        InlinePanel('advert_placements', label="Adverts"),
        # ...
    ]

```
 These child objects are now accessible through the page’s `advert_placements` property, and from there we can access the linked `Advert` snippet as `advert`. In the template for `BookPage`, we could include the following:

 
```
{% for advert_placement in page.advert_placements.all %}
    <p>
        <a href="{{ advert_placement.advert.url }}">
            {{ advert_placement.advert.text }}
        </a>
    </p>
{% endfor %}

```
   Making snippets previewable
---------------------------

  New in version 4.0: The `PreviewableMixin` class was introduced.

  If a snippet model inherits from [`PreviewableMixin`](../reference/pages/model_reference#wagtail.models.PreviewableMixin "wagtail.models.PreviewableMixin"), Wagtail will automatically add a live preview panel in the editor. In addition to inheriting the mixin, the model must also override [`get_preview_template()`](../reference/pages/model_reference#wagtail.models.PreviewableMixin.get_preview_template "wagtail.models.PreviewableMixin.get_preview_template") or [`serve_preview()`](../reference/pages/model_reference#wagtail.models.PreviewableMixin.serve_preview "wagtail.models.PreviewableMixin.serve_preview"). For example, the `Advert` snippet could be made previewable as follows:

 
```
# ...

from wagtail.models import PreviewableMixin

# ...

@register_snippet
class Advert(PreviewableMixin, models.Model):
    url = models.URLField(null=True, blank=True)
    text = models.CharField(max_length=255)

    panels = [
        FieldPanel('url'),
        FieldPanel('text'),
    ]

    def get_preview_template(self, request, mode_name):
        return "demo/previews/advert.html"

```
 With the following `demo/previews/advert.html` template:

 
```
<!DOCTYPE html>
<html>
    <head>
        <title>{{ object.text }}</title>
    </head>
    <body>
        <a href="{{ object.url }}">{{ object.text }}</a>
    </body>
</html>

```
 The variables available in the default context are `request` (a fake [`HttpRequest`](https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpRequest "(in Django v4.1)") object) and `object` (the snippet instance). To customise the context, you can override the [`get_preview_context()`](../reference/pages/model_reference#wagtail.models.PreviewableMixin.get_preview_context "wagtail.models.PreviewableMixin.get_preview_context") method.

 By default, the `serve_preview` method returns a [`TemplateResponse`](https://docs.djangoproject.com/en/stable/ref/template-response/#django.template.response.TemplateResponse "(in Django v4.1)") that is rendered using the request object, the template returned by `get_preview_template`, and the context object returned by `get_preview_context`. You can override the `serve_preview` method to customise the rendering and/or routing logic.

 Similar to pages, you can define multiple preview modes by overriding the [`preview_modes`](../reference/pages/model_reference#wagtail.models.PreviewableMixin.preview_modes "wagtail.models.PreviewableMixin.preview_modes") property. For example, the following `Advert` snippet has two preview modes:

 
```
# ...

from wagtail.models import PreviewableMixin

# ...

@register_snippet
class Advert(PreviewableMixin, models.Model):
    url = models.URLField(null=True, blank=True)
    text = models.CharField(max_length=255)

    panels = [
        FieldPanel('url'),
        FieldPanel('text'),
    ]

    @property
    def preview_modes(self):
        return PreviewableMixin.DEFAULT_PREVIEW_MODES + [("alt", "Alternate")]

    def get_preview_template(self, request, mode_name):
        templates = {
            "": "demo/previews/advert.html",  # Default preview mode
            "alt": "demo/previews/advert_alt.html",  # Alternate preview mode
        }
        return templates.get(mode_name, templates[""])

    def get_preview_context(self, request, mode_name):
        context = super().get_preview_context(request, mode_name)
        if mode_name == "alt":
            context["extra_context"] = "Alternate preview mode"
        return context

```
   Making snippets searchable
--------------------------

 If a snippet model inherits from `wagtail.search.index.Indexed`, as described in [Indexing custom models](search/indexing#wagtailsearch-indexing-models), Wagtail will automatically add a search box to the chooser interface for that snippet type. For example, the `Advert` snippet could be made searchable as follows:

 
```
# ...

from wagtail.search import index

# ...

@register_snippet
class Advert(index.Indexed, models.Model):
    url = models.URLField(null=True, blank=True)
    text = models.CharField(max_length=255)

    panels = [
        FieldPanel('url'),
        FieldPanel('text'),
    ]

    search_fields = [
        index.SearchField('text', partial_match=True),
    ]

```
   Saving revisions of snippets
----------------------------

  New in version 4.0: The `RevisionMixin` class was introduced.

  If a snippet model inherits from [`RevisionMixin`](../reference/pages/model_reference#wagtail.models.RevisionMixin "wagtail.models.RevisionMixin"), Wagtail will automatically save revisions when you save any changes in the snippets admin. In addition to inheriting the mixin, it is recommended to define a [`GenericRelation`](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#django.contrib.contenttypes.fields.GenericRelation "(in Django v4.1)") to the [`Revision`](../reference/pages/model_reference#wagtail.models.Revision "wagtail.models.Revision") model and override the [`revisions`](../reference/pages/model_reference#wagtail.models.RevisionMixin.revisions "wagtail.models.RevisionMixin.revisions") property to return the `GenericRelation`. For example, the `Advert` snippet could be made revisable as follows:

 
```
# ...

from django.contrib.contenttypes.fields import GenericRelation
from wagtail.models import RevisionMixin

# ...

@register_snippet
class Advert(RevisionMixin, models.Model):
    url = models.URLField(null=True, blank=True)
    text = models.CharField(max_length=255)
    _revisions = GenericRelation("wagtailcore.Revision", related_query_name="advert")

    panels = [
        FieldPanel('url'),
        FieldPanel('text'),
    ]

    @property
    def revisions(self):
        return self._revisions

```
 The `RevisionMixin` includes a `latest_revision` field that needs to be added to your database table. Make sure to run the `makemigrations` and `migrate` management commands after making the above changes to apply the changes to your database.

 With the `RevisionMixin` applied, any changes made from the snippets admin will create an instance of the `Revision` model that contains the state of the snippet instance. The revision instance is attached to the [audit log](../extending/audit_log#audit-log) entry of the edit action, allowing you to revert to a previous revision or compare the changes between revisions from the snippet history page.

 You can also save revisions programmatically by calling the [`save_revision()`](../reference/pages/model_reference#wagtail.models.RevisionMixin.save_revision "wagtail.models.RevisionMixin.save_revision") method. After applying the mixin, it is recommended to call this method (or save the snippet in the admin) at least once for each instance of the snippet that already exists (if any), so that the `latest_revision` field is populated in the database table.

   Saving draft changes of snippets
--------------------------------

  New in version 4.0: The `DraftStateMixin` class was introduced.

   New in version 4.1: Support for scheduled publishing via `PublishingPanel` was introduced.

  If a snippet model inherits from [`DraftStateMixin`](../reference/pages/model_reference#wagtail.models.DraftStateMixin "wagtail.models.DraftStateMixin"), Wagtail will automatically add a live/draft status column to the listing view, change the “Save” action menu to “Save draft”, and add a new “Publish” action menu in the editor. Any changes you save in the snippets admin will be saved as revisions and will not be reflected to the “live” snippet instance until you publish the changes.

 Wagtail will also allow you to set publishing schedules for instances of the model if there is a `PublishingPanel` in the model’s panels definition.

 For example, the `Advert` snippet could save draft changes and publishing schedules by defining it as follows:

 
```
# ...

from django.contrib.contenttypes.fields import GenericRelation
from wagtail.admin.panels import PublishingPanel
from wagtail.models import DraftStateMixin, RevisionMixin

# ...

@register_snippet
class Advert(DraftStateMixin, RevisionMixin, models.Model):
    url = models.URLField(null=True, blank=True)
    text = models.CharField(max_length=255)
    _revisions = GenericRelation("wagtailcore.Revision", related_query_name="advert")

    panels = [
        FieldPanel('url'),
        FieldPanel('text'),
        PublishingPanel(),
    ]

    @property
    def revisions(self):
        return self._revisions

```
 The `DraftStateMixin` includes additional fields that need to be added to your database table. Make sure to run the `makemigrations` and `migrate` management commands after making the above changes to apply the changes to your database.

 You can publish revisions programmatically by calling [`instance.publish(revision)`](../reference/pages/model_reference#wagtail.models.DraftStateMixin.publish "wagtail.models.DraftStateMixin.publish") or by calling [`revision.publish()`](../reference/pages/model_reference#wagtail.models.Revision.publish "wagtail.models.Revision.publish"). After applying the mixin, it is recommended to publish at least one revision for each instance of the snippet that already exists (if any), so that the `latest_revision` and `live_revision` fields are populated in the database table.

 If you use the scheduled publishing feature, make sure that you run the [`publish_scheduled`](../reference/management_commands#publish-scheduled) management command periodically. For more details, see [Scheduled Publishing](../reference/pages/theory#scheduled-publishing).

  Warning

 Wagtail does not yet have a mechanism to prevent editors from including unpublished (“draft”) snippets in pages. When including a `DraftStateMixin`-enabled snippet in pages, make sure that you add necessary checks to handle how a draft snippet should be rendered (e.g. by checking its `live` field). We are planning to improve this in the future.

    Tagging snippets
----------------

 Adding tags to snippets is very similar to adding tags to pages. The only difference is that `taggit.manager.TaggableManager` should be used in the place of `ClusterTaggableManager`.

 
```
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
from taggit.models import TaggedItemBase
from taggit.managers import TaggableManager

class AdvertTag(TaggedItemBase):
    content_object = ParentalKey('demo.Advert', on_delete=models.CASCADE, related_name='tagged_items')

@register_snippet
class Advert(ClusterableModel):
    # ...
    tags = TaggableManager(through=AdvertTag, blank=True)

    panels = [
        # ...
        FieldPanel('tags'),
    ]

```
 The [documentation on tagging pages](../reference/pages/model_recipes#tagging) has more information on how to use tags in views.

   Customising snippets admin views
--------------------------------

 You can customise the admin views for snippets by specifying a custom subclass of [`SnippetViewSet`](../reference/viewsets#wagtail.snippets.views.snippets.SnippetViewSet "wagtail.snippets.views.snippets.SnippetViewSet") to `register_snippet`.

 This can be done by removing the `@register_snippet` decorator on your model class and calling `register_snippet` (as a function, not a decorator) in your `wagtail_hooks.py` file instead as follows:

 
```
register_snippet(MyModel, viewset=MyModelViewSet)

```
 For example, with the following `Member` model and a `MemberFilterSet` class:

 
```
# models.py
from django.db import models
from wagtail.admin.filters import WagtailFilterSet


class Member(models.Model):
    class ShirtSize(models.TextChoices):
        SMALL = "S", "Small"
        MEDIUM = "M", "Medium"
        LARGE = "L", "Large"
        EXTRA_LARGE = "XL", "Extra Large"

    name = models.CharField(max_length=255)
    shirt_size = models.CharField(max_length=5, choices=ShirtSize.choices, default=ShirtSize.MEDIUM)

    def get_shirt_size_display(self):
        return self.ShirtSize(self.shirt_size).label

    get_shirt_size_display.admin_order_field = "shirt_size"
    get_shirt_size_display.short_description = "Size description"


class MemberFilterSet(WagtailFilterSet):
    class Meta:
        model = Member
        fields = ["shirt_size"]

```
 You can define a [`list_display`](../reference/viewsets#wagtail.snippets.views.snippets.SnippetViewSet.list_display "wagtail.snippets.views.snippets.SnippetViewSet.list_display") attribute to specify the columns shown on the listing view. You can also add the ability to filter the listing view by defining a [`filterset_class`](../reference/viewsets#wagtail.snippets.views.snippets.SnippetViewSet.filterset_class "wagtail.snippets.views.snippets.SnippetViewSet.filterset_class") attribute on a subclass of `SnippetViewSet`. For example:

 
```
# views.py
from wagtail.admin.ui.tables import UpdatedAtColumn
from wagtail.snippets.views.snippets import SnippetViewSet

from myapp.models import MemberFilterSet


class MemberViewSet(SnippetViewSet):
    list_display = ["name", "shirt_size", "get_shirt_size_display", UpdatedAtColumn()]
    filterset_class = MemberFilterSet

```
 Then, pass the viewset to the `register_snippet` call.

 
```
# wagtail_hooks.py
from wagtail.snippets.models import register_snippet

from myapp.models import Member
from myapp.views import MemberViewSet


register_snippet(Member, viewset=MemberViewSet)

```
 The `viewset` parameter of `register_snippet` also accepts a dotted module path to the subclass, e.g. `"myapp.views.MemberViewSet"`.

 Various additional attributes are available to customise the viewset - see [`SnippetViewSet`](../reference/viewsets#wagtail.snippets.views.snippets.SnippetViewSet "wagtail.snippets.views.snippets.SnippetViewSet").

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/topics/snippets.html>

How to use StreamField for mixed content
========================================

 StreamField provides a content editing model suitable for pages that do not follow a fixed structure – such as blog posts or news stories – where the text may be interspersed with subheadings, images, pull quotes and video. It’s also suitable for more specialised content types, such as maps and charts (or, for a programming blog, code snippets). In this model, these different content types are represented as a sequence of ‘blocks’, which can be repeated and arranged in any order.

 For further background on StreamField, and why you would use it instead of a rich text field for the article body, see the blog post [Rich text fields and faster horses](https://torchbox.com/blog/rich-text-fields-and-faster-horses/).

 StreamField also offers a rich API to define your own block types, ranging from simple collections of sub-blocks (such as a ‘person’ block consisting of first name, surname and photograph) to completely custom components with their own editing interface. Within the database, the StreamField content is stored as JSON, ensuring that the full informational content of the field is preserved, rather than just an HTML representation of it.

  Using StreamField
-----------------

 `StreamField` is a model field that can be defined within your page model like any other field:

 
```
from django.db import models

from wagtail.models import Page
from wagtail.fields import StreamField
from wagtail import blocks
from wagtail.admin.panels import FieldPanel
from wagtail.images.blocks import ImageChooserBlock

class BlogPage(Page):
    author = models.CharField(max_length=255)
    date = models.DateField("Post date")
    body = StreamField([
        ('heading', blocks.CharBlock(form_classname="title")),
        ('paragraph', blocks.RichTextBlock()),
        ('image', ImageChooserBlock()),
    ], use_json_field=True)

    content_panels = Page.content_panels + [
        FieldPanel('author'),
        FieldPanel('date'),
        FieldPanel('body'),
    ]

```
 In this example, the body field of `BlogPage` is defined as a `StreamField` where authors can compose content from three different block types: headings, paragraphs, and images, which can be used and repeated in any order. The block types available to authors are defined as a list of `(name, block_type)` tuples: ‘name’ is used to identify the block type within templates, and should follow the standard Python conventions for variable names: lower-case and underscores, no spaces.

 You can find the complete list of available block types in the [StreamField block reference](../reference/streamfield/blocks#streamfield-block-reference).

  Note

 StreamField is not a direct replacement for other field types such as RichTextField. If you need to migrate an existing field to StreamField, refer to [Migrating RichTextFields to StreamField](#streamfield-migrating-richtext).

   Changed in version 3.0: The `use_json_field=True` argument was added. This indicates that the database’s native JSONField support should be used for this field, and is a temporary measure to assist in migrating StreamFields created on earlier Wagtail versions; it will become the default in a future release.

    Template rendering
------------------

 StreamField provides an HTML representation for the stream content as a whole, as well as for each individual block. To include this HTML into your page, use the `{% include_block %}` tag:

 
```
{% load wagtailcore_tags %}

    ...

{% include_block page.body %}

```
 In the default rendering, each block of the stream is wrapped in a `<div class="block-my_block_name">` element (where `my_block_name` is the block name given in the StreamField definition). If you wish to provide your own HTML markup, you can instead iterate over the field’s value, and invoke `{% include_block %}` on each block in turn:

 
```
{% load wagtailcore_tags %}

    ...

<article>
    {% for block in page.body %}
        <section>{% include_block block %}</section>
    {% endfor %}
</article>

```
 For more control over the rendering of specific block types, each block object provides `block_type` and `value` properties:

 
```
{% load wagtailcore_tags %}

    ...

<article>
    {% for block in page.body %}
        {% if block.block_type == 'heading' %}
            <h1>{{ block.value }}</h1>
        {% else %}
            <section class="block-{{ block.block_type }}">
                {% include_block block %}
            </section>
        {% endif %}
    {% endfor %}
</article>

```
   Combining blocks
----------------

 In addition to using the built-in block types directly within StreamField, it’s possible to construct new block types by combining sub-blocks in various ways. Examples of this could include:

 * An “image with caption” block consisting of an image chooser and a text field
* A “related links” section, where an author can provide any number of links to other pages
* A slideshow block, where each slide may be an image, text or video, arranged in any order

 Once a new block type has been built up in this way, you can use it anywhere where a built-in block type would be used - including using it as a component for yet another block type. For example, you could define an image gallery block where each item is an “image with caption” block.

  ### StructBlock

 `StructBlock` allows you to group several ‘child’ blocks together to be presented as a single block. The child blocks are passed to `StructBlock` as a list of `(name, block_type)` tuples:

 
```
body = StreamField([
    ('person', blocks.StructBlock([
        ('first_name', blocks.CharBlock()),
        ('surname', blocks.CharBlock()),
        ('photo', ImageChooserBlock(required=False)),
        ('biography', blocks.RichTextBlock()),
    ])),
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], use_json_field=True)

```
 When reading back the content of a StreamField (such as when rendering a template), the value of a StructBlock is a dict-like object with keys corresponding to the block names given in the definition:

 
```
<article>
    {% for block in page.body %}
        {% if block.block_type == 'person' %}
            <div class="person">
                {% image block.value.photo width-400 %}
                <h2>{{ block.value.first_name }} {{ block.value.surname }}</h2>
                {{ block.value.biography }}
            </div>
        {% else %}
            (rendering for other block types)
        {% endif %}
    {% endfor %}
</article>

```
   ### Subclassing `StructBlock`

 Placing a StructBlock’s list of child blocks inside a `StreamField` definition can often be hard to read, and makes it difficult for the same block to be reused in multiple places. As an alternative, `StructBlock` can be subclassed, with the child blocks defined as attributes on the subclass. The ‘person’ block in the above example could be rewritten as:

 
```
class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

```
 `PersonBlock` can then be used in a `StreamField` definition in the same way as the built-in block types:

 
```
body = StreamField([
    ('person', PersonBlock()),
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], use_json_field=True)

```
   ### Block icons

 In the menu that content authors use to add new blocks to a StreamField, each block type has an associated icon. For StructBlock and other structural block types, a placeholder icon is used, since the purpose of these blocks is specific to your project. To set a custom icon, pass the option `icon` as either a keyword argument to `StructBlock`, or an attribute on a `Meta` class:

 
```
body = StreamField([
    ('person', blocks.StructBlock([
        ('first_name', blocks.CharBlock()),
        ('surname', blocks.CharBlock()),
        ('photo', ImageChooserBlock(required=False)),
        ('biography', blocks.RichTextBlock()),
    ], icon='user')),
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], use_json_field=True)

```
 
```
class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    class Meta:
        icon = 'user'

```
 For a list of the recognised icon identifiers, see the [UI Styleguide](../contributing/styleguide#styleguide).

   ### ListBlock

 `ListBlock` defines a repeating block, allowing content authors to insert as many instances of a particular block type as they like. For example, a ‘gallery’ block consisting of multiple images can be defined as follows:

 
```
body = StreamField([
    ('gallery', blocks.ListBlock(ImageChooserBlock())),
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], use_json_field=True)

```
 When reading back the content of a StreamField (such as when rendering a template), the value of a ListBlock is a list of child values:

 
```
<article>
    {% for block in page.body %}
        {% if block.block_type == 'gallery' %}
            <ul class="gallery">
                {% for img in block.value %}
                    <li>{% image img width-400 %}</li>
                {% endfor %}
            </ul>
        {% else %}
            (rendering for other block types)
        {% endif %}
    {% endfor %}
</article>

```
   ### StreamBlock

 `StreamBlock` defines a set of child block types that can be mixed and repeated in any sequence, via the same mechanism as StreamField itself. For example, a carousel that supports both image and video slides could be defined as follows:

 
```
body = StreamField([
    ('carousel', blocks.StreamBlock([
        ('image', ImageChooserBlock()),
        ('video', EmbedBlock()),
    ])),
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], use_json_field=True)

```
 `StreamBlock` can also be subclassed in the same way as `StructBlock`, with the child blocks being specified as attributes on the class:

 
```
class CarouselBlock(blocks.StreamBlock):
    image = ImageChooserBlock()
    video = EmbedBlock()

    class Meta:
        icon = 'image'

```
 A StreamBlock subclass defined in this way can also be passed to a `StreamField` definition, instead of passing a list of block types. This allows setting up a common set of block types to be used on multiple page types:

 
```
class CommonContentBlock(blocks.StreamBlock):
    heading = blocks.CharBlock(form_classname="title")
    paragraph = blocks.RichTextBlock()
    image = ImageChooserBlock()


class BlogPage(Page):
    body = StreamField(CommonContentBlock(), use_json_field=True)

```
 When reading back the content of a StreamField, the value of a StreamBlock is a sequence of block objects with `block_type` and `value` properties, just like the top-level value of the StreamField itself.

 
```
<article>
    {% for block in page.body %}
        {% if block.block_type == 'carousel' %}
            <ul class="carousel">
                {% for slide in block.value %}
                    {% if slide.block_type == 'image' %}
                        <li class="image">{% image slide.value width-200 %}</li>
                    {% else %}
                        <li class="video">{% include_block slide %}</li>
                    {% endif %}
                {% endfor %}
            </ul>
        {% else %}
            (rendering for other block types)
        {% endif %}
    {% endfor %}
</article>

```
   ### Limiting block counts

 By default, a StreamField can contain an unlimited number of blocks. The `min_num` and `max_num` options on `StreamField` or `StreamBlock` allow you to set a minimum or maximum number of blocks:

 
```
body = StreamField([
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], min_num=2, max_num=5, use_json_field=True)

```
 Or equivalently:

 
```
class CommonContentBlock(blocks.StreamBlock):
    heading = blocks.CharBlock(form_classname="title")
    paragraph = blocks.RichTextBlock()
    image = ImageChooserBlock()

    class Meta:
        min_num = 2
        max_num = 5

```
 The `block_counts` option can be used to set a minimum or maximum count for specific block types. This accepts a dict, mapping block names to a dict containing either or both of `min_num` and `max_num`. For example, to permit between 1 and 3 ‘heading’ blocks:

 
```
body = StreamField([
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], block_counts={
    'heading': {'min_num': 1, 'max_num': 3},
}, use_json_field=True)

```
 Or equivalently:

 
```
class CommonContentBlock(blocks.StreamBlock):
    heading = blocks.CharBlock(form_classname="title")
    paragraph = blocks.RichTextBlock()
    image = ImageChooserBlock()

    class Meta:
        block_counts = {
            'heading': {'min_num': 1, 'max_num': 3},
        }

```
    Per-block templates
-------------------

 By default, each block is rendered using simple, minimal HTML markup, or no markup at all. For example, a CharBlock value is rendered as plain text, while a ListBlock outputs its child blocks in a `<ul>` wrapper. To override this with your own custom HTML rendering, you can pass a `template` argument to the block, giving the filename of a template file to be rendered. This is particularly useful for custom block types derived from StructBlock:

 
```
('person', blocks.StructBlock(
    [
        ('first_name', blocks.CharBlock()),
        ('surname', blocks.CharBlock()),
        ('photo', ImageChooserBlock(required=False)),
        ('biography', blocks.RichTextBlock()),
    ],
    template='myapp/blocks/person.html',
    icon='user'
))

```
 Or, when defined as a subclass of StructBlock:

 
```
class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    class Meta:
        template = 'myapp/blocks/person.html'
        icon = 'user'

```
 Within the template, the block value is accessible as the variable `value`:

 
```
{% load wagtailimages_tags %}

<div class="person">
    {% image value.photo width-400 %}
    <h2>{{ value.first_name }} {{ value.surname }}</h2>
    {{ value.biography }}
</div>

```
 Since `first_name`, `surname`, `photo` and `biography` are defined as blocks in their own right, this could also be written as:

 
```
{% load wagtailcore_tags wagtailimages_tags %}

<div class="person">
    {% image value.photo width-400 %}
    <h2>{% include_block value.first_name %} {% include_block value.surname %}</h2>
    {% include_block value.biography %}
</div>

```
 Writing `{{ my_block }}` is roughly equivalent to `{% include_block my_block %}`, but the short form is more restrictive, as it does not pass variables from the calling template such as `request` or `page`; for this reason, it is recommended that you only use it for simple values that do not render HTML of their own. For example, if our PersonBlock used the template:

 
```
{% load wagtailimages_tags %}

<div class="person">
    {% image value.photo width-400 %}
    <h2>{{ value.first_name }} {{ value.surname }}</h2>

    {% if request.user.is_authenticated %}
        <a href="#">Contact this person</a>
    {% endif %}

    {{ value.biography }}
</div>

```
 then the `request.user.is_authenticated` test would not work correctly when rendering the block through a `{{ ... }}` tag:

 
```
{# Incorrect: #}

{% for block in page.body %}
    {% if block.block_type == 'person' %}
        <div>
            {{ block }}
        </div>
    {% endif %}
{% endfor %}

{# Correct: #}

{% for block in page.body %}
    {% if block.block_type == 'person' %}
        <div>
            {% include_block block %}
        </div>
    {% endif %}
{% endfor %}

```
 Like Django’s `{% include %}` tag, `{% include_block %}` also allows passing additional variables to the included template, through the syntax `{% include_block my_block with foo="bar" %}`:

 
```
{# In page template: #}

{% for block in page.body %}
    {% if block.block_type == 'person' %}
        {% include_block block with classname="important" %}
    {% endif %}
{% endfor %}

{# In PersonBlock template: #}

<div class="{{ classname }}">
    ...
</div>

```
 The syntax `{% include_block my_block with foo="bar" only %}` is also supported, to specify that no variables from the parent template other than `foo` will be passed to the child template.

 As well as passing variables from the parent template, block subclasses can pass additional template variables of their own by overriding the `get_context` method:

 
```
import datetime

class EventBlock(blocks.StructBlock):
    title = blocks.CharBlock()
    date = blocks.DateBlock()

    def get_context(self, value, parent_context=None):
        context = super().get_context(value, parent_context=parent_context)
        context['is_happening_today'] = (value['date'] == datetime.date.today())
        return context

    class Meta:
        template = 'myapp/blocks/event.html'

```
 In this example, the variable `is_happening_today` will be made available within the block template. The `parent_context` keyword argument is available when the block is rendered through an `{% include_block %}` tag, and is a dict of variables passed from the calling template.

 All block types, not just `StructBlock`, support the `template` property. However, for blocks that handle basic Python data types, such as `CharBlock` and `IntegerBlock`, there are some limitations on where the template will take effect. For further details, see [About StreamField BoundBlocks and values](../advanced_topics/boundblocks_and_values#boundblocks-and-values).

   Customisations
--------------

 All block types implement a common API for rendering their front-end and form representations, and storing and retrieving values to and from the database. By subclassing the various block classes and overriding these methods, all kinds of customisations are possible, from modifying the layout of StructBlock form fields to implementing completely new ways of combining blocks. For further details, see [How to build custom StreamField blocks](../advanced_topics/customisation/streamfield_blocks#custom-streamfield-blocks).

   Modifying StreamField data
--------------------------

 A StreamField’s value behaves as a list, and blocks can be inserted, overwritten and deleted before saving the instance back to the database. A new item can be written to the list as a tuple of *(block\_type, value)* - when read back, it will be returned as a `BoundBlock` object.

 
```
# Replace the first block with a new block of type 'heading'
my_page.body[0] = ('heading', "My story")

# Delete the last block
del my_page.body[-1]

# Append a rich text block to the stream
from wagtail.rich_text import RichText
my_page.body.append(('paragraph', RichText("<p>And they all lived happily ever after.</p>")))

# Save the updated data back to the database
my_page.save()

```
   Retrieving blocks by name
-------------------------

  New in version 4.0: The `blocks_by_name` and `first_block_by_name` methods were added.

  StreamField values provide a `blocks_by_name` method for retrieving all blocks of a given name:

 
```
my_page.body.blocks_by_name('heading')  # returns a list of 'heading' blocks

```
 Calling `blocks_by_name` with no arguments returns a `dict`-like object, mapping block names to the list of blocks of that name. This is particularly useful in template code, where passing arguments isn’t possible:

 
```
<h2>Table of contents</h2>
<ol>
    {% for heading_block in page.body.blocks_by_name.heading %}
        <li>{{ heading_block.value }}</li>
    {% endfor %}
</ol>

```
 The `first_block_by_name` method returns the first block of the given name in the stream, or `None` if no matching block is found:

 
```
hero_image = my_page.body.first_block_by_name('image')

```
 `first_block_by_name` can also be called without arguments to return a `dict`-like mapping:

 
```
<div class="hero-image">{{ page.body.first_block_by_name.image }}</div>

```
   Migrating RichTextFields to StreamField
---------------------------------------

 If you change an existing RichTextField to a StreamField, the database migration will complete with no errors, since both fields use a text column within the database. However, StreamField uses a JSON representation for its data, so the existing text requires an extra conversion step in order to become accessible again. For this to work, the StreamField needs to include a RichTextBlock as one of the available block types. Create the migration as normal using `./manage.py makemigrations`, then edit it as follows (in this example, the ‘body’ field of the `demo.BlogPage` model is being converted to a StreamField with a RichTextBlock named `rich_text`):

  Note

 This migration cannot be used if the StreamField has the `use_json_field` argument set to `True`. To migrate, set the `use_json_field` argument to `False` first, migrate the data, then set it back to `True`.

  
```
# -*- coding: utf-8 -*-
from django.db import models, migrations
from wagtail.rich_text import RichText


def convert_to_streamfield(apps, schema_editor):
    BlogPage = apps.get_model("demo", "BlogPage")
    for page in BlogPage.objects.all():
        if page.body.raw_text and not page.body:
            page.body = [('rich_text', RichText(page.body.raw_text))]
            page.save()


def convert_to_richtext(apps, schema_editor):
    BlogPage = apps.get_model("demo", "BlogPage")
    for page in BlogPage.objects.all():
        if page.body.raw_text is None:
            raw_text = ''.join([
                child.value.source for child in page.body
                if child.block_type == 'rich_text'
            ])
            page.body = raw_text
            page.save()


class Migration(migrations.Migration):

    dependencies = [
        # leave the dependency line from the generated migration intact!
        ('demo', '0001_initial'),
    ]

    operations = [
        # leave the generated AlterField intact!
        migrations.AlterField(
            model_name='BlogPage',
            name='body',
            field=wagtail.fields.StreamField([('rich_text', wagtail.blocks.RichTextBlock())]),
        ),

        migrations.RunPython(
            convert_to_streamfield,
            convert_to_richtext,
        ),
    ]

```
 Note that the above migration will work on published Page objects only. If you also need to migrate draft pages and page revisions, then edit the migration as in the following example instead:

 
```
# -*- coding: utf-8 -*-
import json

from django.core.serializers.json import DjangoJSONEncoder
from django.db import migrations, models

from wagtail.rich_text import RichText


def page_to_streamfield(page):
    changed = False
    if page.body.raw_text and not page.body:
        page.body = [('rich_text', {'rich_text': RichText(page.body.raw_text)})]
        changed = True
    return page, changed


def pagerevision_to_streamfield(revision_data):
    changed = False
    body = revision_data.get('body')
    if body:
        try:
            json.loads(body)
        except ValueError:
            revision_data['body'] = json.dumps(
                [{
                    "value": {"rich_text": body},
                    "type": "rich_text"
                }],
                cls=DjangoJSONEncoder)
            changed = True
        else:
            # It's already valid JSON. Leave it.
            pass
    return revision_data, changed


def page_to_richtext(page):
    changed = False
    if page.body.raw_text is None:
        raw_text = ''.join([
            child.value['rich_text'].source for child in page.body
            if child.block_type == 'rich_text'
        ])
        page.body = raw_text
        changed = True
    return page, changed


def pagerevision_to_richtext(revision_data):
    changed = False
    body = revision_data.get('body', 'definitely non-JSON string')
    if body:
        try:
            body_data = json.loads(body)
        except ValueError:
            # It's not apparently a StreamField. Leave it.
            pass
        else:
            raw_text = ''.join([
                child['value']['rich_text'] for child in body_data
                if child['type'] == 'rich_text'
            ])
            revision_data['body'] = raw_text
            changed = True
    return revision_data, changed


def convert(apps, schema_editor, page_converter, pagerevision_converter):
    BlogPage = apps.get_model("demo", "BlogPage")
    for page in BlogPage.objects.all():

        page, changed = page_converter(page)
        if changed:
            page.save()

        for revision in page.revisions.all():
            revision_data = revision.content
            revision_data, changed = pagerevision_converter(revision_data)
            if changed:
                revision.content = revision_data
                revision.save()


def convert_to_streamfield(apps, schema_editor):
    return convert(apps, schema_editor, page_to_streamfield, pagerevision_to_streamfield)


def convert_to_richtext(apps, schema_editor):
    return convert(apps, schema_editor, page_to_richtext, pagerevision_to_richtext)


class Migration(migrations.Migration):

    dependencies = [
        # leave the dependency line from the generated migration intact!
        ('demo', '0001_initial'),
    ]

    operations = [
        # leave the generated AlterField intact!
        migrations.AlterField(
            model_name='BlogPage',
            name='body',
            field=wagtail.fields.StreamField([('rich_text', wagtail.blocks.RichTextBlock())]),
        ),

        migrations.RunPython(
            convert_to_streamfield,
            convert_to_richtext,
        ),
    ]

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/topics/streamfield.html>

Changing rich text representation
=================================

 The HTML representation of an image in rich text can be customised - for example, to display captions or custom fields.

 To do this requires subclassing `Format` (see [Image Formats in the Rich Text Editor](../customisation/page_editing_interface#rich-text-image-formats)), and overriding its `image_to_html` method.

 You may then register formats of your subclass using `register_image_format` as usual.

 
```
# image_formats.py
from wagtail.images.formats import Format, register_image_format


class SubclassedImageFormat(Format):

    def image_to_html(self, image, alt_text, extra_attributes=None):

        custom_html = # the custom HTML representation of your image here
                        # in Format, the image's rendition.img_tag(extra_attributes) is used to generate the HTML
                        # representation

        return custom_html


register_image_format(
    SubclassedImageFormat('subclassed_format', 'Subclassed Format', classnames, filter_spec)
)

```
 As an example, let’s say you want the alt text to be displayed as a caption for the image as well:

 
```
# image_formats.py
from django.utils.html import format_html
from wagtail.images.formats import Format, register_image_format


class CaptionedImageFormat(Format):

    def image_to_html(self, image, alt_text, extra_attributes=None):

        default_html = super().image_to_html(image, alt_text, extra_attributes)

        return format_html("{}<figcaption>{}</figcaption>", default_html, alt_text)


register_image_format(
    CaptionedImageFormat('captioned_fullwidth', 'Full width captioned', 'bodytext-image', 'width-750')
)

```
  Note

 Any custom HTML image features will not be displayed in the Draftail editor, only on the published page.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/images/changing_rich_text_representation.html>

Feature Detection
=================

 Wagtail has the ability to automatically detect faces and features inside your images and crop the images to those features.

 Feature detection uses third-party tools to detect faces/features in an image when the image is uploaded. The detected features are stored internally as a focal point in the `focal_point_{x, y, width, height}` fields on the `Image` model. These fields are used by the `fill` image filter when an image is rendered in a template to crop the image.

  Installation
------------

 Two third-party tools are known to work with Wagtail: One based on [OpenCV](https://opencv.org/) for general feature detection and one based on [Rustface](https://github.com/torchbox/rustface-py/) for face detection.

  ### OpenCV on Debian/Ubuntu

 Feature detection requires [OpenCV](https://opencv.org/) which can be a bit tricky to install as it’s not currently pip-installable.

 There is more than one way to install these components, but in each case you will need to test that both OpenCV itself *and* the Python interface have been correctly installed.

  #### Install `opencv-python`

 [opencv-python](https://pypi.org/project/opencv-python/) is available on PyPI. It includes a Python interface to OpenCV, as well as the statically-built OpenCV binaries themselves.

 To install:

 
```
pip install opencv-python

```
 Depending on what else is installed on your system, this may be all that is required. On lighter-weight Linux systems, you may need to identify and install missing system libraries (for example, a slim version of Debian Stretch requires `libsm6 libxrender1 libxext6` to be installed with `apt`).

   #### Install a system-level package

 A system-level package can take care of all of the required components. Check what is available for your operating system. For example, [python-opencv](https://packages.debian.org/stretch/python-opencv) is available for Debian; it installs OpenCV itself, and sets up Python bindings.

 However, it may make incorrect assumptions about how you’re using Python (for example, which version you’re using) - test as described below.

   #### Testing the installation

 Test the installation:

 
```
python3
>>> import cv2

```
 An error such as:

 
```
ImportError: libSM.so.6: cannot open shared object file: No such file or directory

```
 indicates that a required system library (in this case `libsm6`) has not been installed.

 On the other hand,

 
```
ModuleNotFoundError: No module named 'cv2'

```
 means that the Python components have not been set up correctly in your Python environment.

 If you don’t get an import error, installation has probably been successful.

    ### Rustface

 [Rustface](https://github.com/torchbox/rustface-py/) is Python library with prebuilt wheel files provided for Linux and macOS. Although implemented in Rust it is pip-installable:

 
```
pip install wheel
pip install rustface

```
  #### Registering with Willow

 Rustface provides a plug-in that needs to be registered with [Willow](https://github.com/wagtail/Willow).

 This should be done somewhere that gets run on application startup:

 
```
from willow.registry import registry
import rustface.willow

registry.register_plugin(rustface.willow)

```
 For example, in an app’s [AppConfig.ready](https://docs.djangoproject.com/en/2.2/ref/applications/#django.apps.AppConfig.ready).

     Cropping
--------

 The face detection algorithm produces a focal area that is tightly cropped to the face rather than the whole head.

 For images with a single face this can be okay in some cases (thumbnails for example), it might be overly tight for “headshots”. Image renditions can encompass more of the head by reducing the crop percentage (`-c<percentage>`), at the end of the resize-rule, down to as low as 0%:

 
```
{% image page.photo fill-200x200-c0 %}

```
   Switching on feature detection in Wagtail
-----------------------------------------

 Once installed, you need to set the `WAGTAILIMAGES_FEATURE_DETECTION_ENABLED` setting to `True` to automatically detect faces/features whenever a new image is uploaded in to Wagtail or when an image without a focal point is saved (this is done via a pre-save signal handler):

 
```
# settings.py

WAGTAILIMAGES_FEATURE_DETECTION_ENABLED = True

```
   Manually running feature detection
----------------------------------

 If you already have images in your Wagtail site and would like to run feature detection on them, or you want to apply feature detection selectively when the `WAGTAILIMAGES_FEATURE_DETECTION_ENABLED` is set to `False` you can run it manually using the `get_suggested_focal_point()` method on the `Image` model.

 For example, you can manually run feature detection on all images by running the following code in the python shell:

 
```
from wagtail.images import get_image_model

Image = get_image_model()

for image in Image.objects.all():
    if not image.has_focal_point():
        image.set_focal_point(image.get_suggested_focal_point())
        image.save()

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/images/feature_detection.html>

Dynamic image serve view
========================

 In most cases, developers wanting to generate image renditions in Python should use the `get_rendition()` method. See [Generating renditions in Python](renditions#image-renditions).

 If you need to be able to generate image versions for an *external* system such as a blog or mobile app, Wagtail provides a view for dynamically generating renditions of images by calling a unique URL.

 The view takes an image id, filter spec and security signature in the URL. If these parameters are valid, it serves an image file matching that criteria.

 Like the `{% image %}` tag, the rendition is generated on the first call and subsequent calls are served from a cache.

  Setup
-----

 Add an entry for the view into your URLs configuration:

 
```
from wagtail.images.views.serve import ServeView

urlpatterns = [
    ...

    re_path(r'^images/([^/]*)/(\d*)/([^/]*)/[^/]*$', ServeView.as_view(), name='wagtailimages_serve'),

    ...

    # Ensure that the wagtailimages_serve line appears above the default Wagtail page serving route
    re_path(r'', include(wagtail_urls)),
]

```
   Usage
-----

  ### Image URL generator UI

 When the dynamic serve view is enabled, an image URL generator in the admin interface becomes available automatically. This can be accessed through the edit page of any image by clicking the “URL generator” button on the right hand side.

 This interface allows editors to generate URLs to cropped versions of the image.

   ### Generating dynamic image URLs in Python

 Dynamic image URLs can also be generated using Python code and served to a client over an API or used directly in the template.

 One advantage of using dynamic image URLs in the template is that they do not block the initial response while rendering like the `{% image %}` tag does.

 The `generate_image_url` function in `wagtail.images.views.serve` is a convenience method to generate a dynamic image URL.

 Here’s an example of this being used in a view:

 
```
def display_image(request, image_id):
    image = get_object_or_404(Image, id=image_id)

    return render(request, 'display_image.html', {
        'image_url': generate_image_url(image, 'fill-100x100')
    })

```
 Image operations can be chained by joining them with a `|` character:

 
```
return render(request, 'display_image.html', {
    'image_url': generate_image_url(image, 'fill-100x100|jpegquality-40')
})

```
 In your templates:

 
```
{% load wagtailimages_tags %}
...

<!-- Get the url for the image scaled to a width of 400 pixels: -->
{% image_url page.photo "width-400" %}

<!-- Again, but this time as a square thumbnail: -->
{% image_url page.photo "fill-100x100|jpegquality-40" %}

<!-- This time using our custom image serve view: -->
{% image_url page.photo "width-400" "mycustomview_serve" %}

```
 You can pass an optional view name that will be used to serve the image through. The default is `wagtailimages_serve`

    Advanced configuration
----------------------

  ### Making the view redirect instead of serve

 By default, the view will serve the image file directly. This behaviour can be changed to a 301 redirect instead which may be useful if you host your images externally.

 To enable this, pass `action='redirect'` into the `ServeView.as_view()` method in your urls configuration:

 
```
from wagtail.images.views.serve import ServeView

urlpatterns = [
    ...

    re_path(r'^images/([^/]*)/(\d*)/([^/]*)/[^/]*$', ServeView.as_view(action='redirect'), name='wagtailimages_serve'),
]

```
    Integration with django-sendfile
--------------------------------

 [django-sendfile](https://github.com/johnsensible/django-sendfile) offloads the job of transferring the image data to the web server instead of serving it directly from the Django application. This could greatly reduce server load in situations where your site has many images being downloaded but you’re unable to use a [Caching proxy](../performance#caching-proxy) or a CDN.

 You firstly need to install and configure django-sendfile and configure your web server to use it. If you haven’t done this already, please refer to the [installation docs](https://github.com/johnsensible/django-sendfile#django-sendfile).

 To serve images with django-sendfile, you can use the `SendFileView` class. This view can be used out of the box:

 
```
from wagtail.images.views.serve import SendFileView

urlpatterns = [
    ...

    re_path(r'^images/([^/]*)/(\d*)/([^/]*)/[^/]*$', SendFileView.as_view(), name='wagtailimages_serve'),
]

```
 You can customise it to override the backend defined in the `SENDFILE_BACKEND` setting:

 
```
from wagtail.images.views.serve import SendFileView
from project.sendfile_backends import MyCustomBackend

class MySendFileView(SendFileView):
    backend = MyCustomBackend

```
 You can also customise it to serve private files. For example, if the only need is to be authenticated (Django >= 1.9):

 
```
from django.contrib.auth.mixins import LoginRequiredMixin
from wagtail.images.views.serve import SendFileView

class PrivateSendFileView(LoginRequiredMixin, SendFileView):
    raise_exception = True

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/images/image_serve_view.html>

Focal points
============

 Focal points are used to indicate to Wagtail the area of an image that contains the subject. This is used by the `fill` filter to focus the cropping on the subject, and avoid cropping into it.

 Focal points can be defined manually by a Wagtail user, or automatically by using face or feature detection.

  Setting the `background-position` inline style based on the focal point
-----------------------------------------------------------------------

 When using a Wagtail image as the background of an element, you can use the `.background_position_style` attribute on the rendition to position the rendition based on the focal point in the image:

 
```
{% image page.image width-1024 as image %}

<div style="background-image: url('{{ image.url }}'); {{ image.background_position_style }}">
</div>

```
   Accessing the focal point in templates
--------------------------------------

 You can access the focal point in the template by accessing the `.focal_point` attribute of a rendition:

 
```
{% load wagtailimages %}

{% image myimage width-800 as myrendition %}

<img
    src="{{ myrendition.url }}"
    alt="{{ myimage.title }}"
    {% if myrendition.focal_point %}
        data-focus-x="{{ myrendition.focal_point.centroid.x }}"
        data-focus-y="{{ myrendition.focal_point.centroid.y }}"
        data-focus-width="{{ myrendition.focal_point.width }}"
        data-focus-height="{{ myrendition.focal_point.height }}"
    {% endif %}
/>

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/images/focal_points.html>

Title generation on upload
==========================

 When uploading an image, Wagtail takes the filename, removes the file extension, and populates the title field. This section is about how to customise this filename to title conversion.

 The filename to title conversion is used on the single file widget, multiple upload widget, and within chooser modals.

 You can also customise this [same behaviour for documents](../documents/title_generation_on_upload).

 You can customise the resolved value of this title using a JavaScript [event listener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) which will listen to the `'wagtail:images-upload'` event.

 The simplest way to add JavaScript to the editor is via the [`insert_global_admin_js` hook](../../reference/hooks#insert-global-admin-js), however any JavaScript that adds the event listener will work.

  DOM Event
---------

 The event name to listen for is `'wagtail:images-upload'`. It will be dispatched on the image upload `form`. The event’s `detail` attribute will contain:

 * `data` - An object which includes the `title` to be used. It is the filename with the extension removed.
* `maxTitleLength` - An integer (or `null`) which is the maximum length of the `Image` model title field.
* `filename` - The original filename without the extension removed.

 To modify the generated `Image` title, access and update `event.detail.data.title`, no return value is needed.

 For single image uploads, the custom event will only run if the title does not already have a value so that we do not overwrite whatever the user has typed.

 You can prevent the default behaviour by calling `event.preventDefault()`. For the single upload page or modals, this will not pre-fill any value into the title. For multiple upload, this will avoid any title submission and use the filename title only (with file extension) as a title is required to save the image.

 The event will ‘bubble’ up so that you can simply add a global `document` listener to capture all of these events, or you can scope your listener or handler logic as needed to ensure you only adjust titles in some specific scenarios.

 See MDN for more information about [custom JavasScript events](https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events).

   Code Examples
-------------

  ### Removing any url unsafe characters from the title

 
```
# wagtail_hooks.py
from django.utils.safestring import mark_safe

from wagtail import hooks


@hooks.register("insert_global_admin_js")
def get_global_admin_js():
    return mark_safe(
    """
    <script>
    window.addEventListener('DOMContentLoaded', function () {
        document.addEventListener('wagtail:images-upload', function(event) {
            var newTitle = (event.detail.data.title || '').replace(/[^a-zA-Z0-9\s-]/g, "");
            event.detail.data.title = newTitle;
        });
    });
    </script>
    """
    )

```
   ### Changing generated titles on the page editor only to remove dashes/underscores

 Using the [`insert_editor_js` hook](../../reference/hooks#insert-editor-js) instead so that this script will not run on the `Image` upload page, only on page editors.

 
```
# wagtail_hooks.py
from django.utils.safestring import mark_safe

from wagtail import hooks


@hooks.register("insert_editor_js")
def get_global_admin_js():
    return mark_safe(
    """
    <script>
    window.addEventListener('DOMContentLoaded', function () {
        document.addEventListener('wagtail:images-upload', function(event) {
            // replace dashes/underscores with a space
            var newTitle = (event.detail.data.title || '').replace(/(\s|_|-)/g, " ");
            event.detail.data.title = newTitle;
        });
    });
    </script>
    """
    )

```
   ### Stopping pre-filling of title based on filename

 
```
# wagtail_hooks.py
from django.utils.safestring import mark_safe

from wagtail import hooks


@hooks.register("insert_global_admin_js")
def get_global_admin_js():
    return mark_safe(
    """
    <script>
    window.addEventListener('DOMContentLoaded', function () {
        document.addEventListener('wagtail:images-upload', function(event) {
            // will stop title pre-fill on single file uploads
            // will set the multiple upload title to the filename (with extension)
            event.preventDefault();
        });
    });
    </script>
    """
    )

```
  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/images/title_generation_on_upload.html>

Documents
=========

 * [Custom document model](custom_document_model)
	+ [Referring to the document model](custom_document_model#module-wagtail.documents)
* [Title generation on upload](title_generation_on_upload)
	+ [DOM Event](title_generation_on_upload#dom-event)
	+ [Code Examples](title_generation_on_upload#code-examples)



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/documents/index.html>

Custom document model
=====================

 An alternate `Document` model can be used to add custom behaviour and additional fields.

 You need to complete the following steps in your project to do this:

 * Create a new document model that inherits from `wagtail.documents.models.AbstractDocument`. This is where you would add additional fields.
* Point `WAGTAILDOCS_DOCUMENT_MODEL` to the new model.

 Here’s an example:

 
```
# models.py
from django.db import models

from wagtail.documents.models import Document, AbstractDocument

class CustomDocument(AbstractDocument):
    # Custom field example:
    source = models.CharField(
        max_length=255,
        blank=True,
        null=True
    )

    admin_form_fields = Document.admin_form_fields + (
        # Add all custom fields names to make them appear in the form:
        'source',
    )

```
 Then in your settings module:

 
```
# Ensure that you replace app_label with the app you placed your custom
# model in.
WAGTAILDOCS_DOCUMENT_MODEL = 'app_label.CustomDocument'

```
  Note

 Migrating from the builtin document model

 When changing an existing site to use a custom document model, no documents will be copied to the new model automatically. Copying old documents to the new model would need to be done manually with a [data migration](https://docs.djangoproject.com/en/stable/topics/migrations/#data-migrations "(in Django v4.1)").

 Any templates that reference the builtin document model will still continue to work as before.

   Referring to the document model
-------------------------------

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/documents/custom_document_model.html>

Title generation on upload
==========================

 When uploading a file (document), Wagtail takes the filename, removes the file extension, and populates the title field. This section is about how to customise this filename to title conversion.

 The filename to title conversion is used on the single file widget, multiple upload widget, and within chooser modals.

 You can also customise this [same behaviour for images](../images/title_generation_on_upload).

 You can customise the resolved value of this title using a JavaScript [event listener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) which will listen to the `'wagtail:documents-upload'` event.

 The simplest way to add JavaScript to the editor is via the [`insert_global_admin_js` hook](../../reference/hooks#insert-global-admin-js), however any JavaScript that adds the event listener will work.

  DOM Event
---------

 The event name to listen for is `'wagtail:documents-upload'`. It will be dispatched on the document upload `form`. The event’s `detail` attribute will contain:

 * `data` - An object which includes the `title` to be used. It is the filename with the extension removed.
* `maxTitleLength` - An integer (or `null`) which is the maximum length of the `Document` model title field.
* `filename` - The original filename without the extension removed.

 To modify the generated `Document` title, access and update `event.detail.data.title`, no return value is needed.

 For single document uploads, the custom event will only run if the title does not already have a value so that we do not overwrite whatever the user has typed.

 You can prevent the default behaviour by calling `event.preventDefault()`. For the single upload page or modals, this will not pre-fill any value into the title. For multiple uploads, this will avoid any title submission and use the filename title only (with file extension) as a title is required to save the document.

 The event will ‘bubble’ up so that you can simply add a global `document` listener to capture all of these events, or you can scope your listener or handler logic as needed to ensure you only adjust titles in some specific scenarios.

 See MDN for more information about [custom JavasScript events](https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events).

   Code Examples
-------------

  ### Adding the file extension to the start of the title

 
```
# wagtail_hooks.py
from django.utils.safestring import mark_safe

from wagtail import hooks


@hooks.register("insert_global_admin_js")
def get_global_admin_js():
    return mark_safe(
    """
    <script>
    window.addEventListener('DOMContentLoaded', function () {
        document.addEventListener('wagtail:documents-upload', function(event) {
            var extension = (event.detail.filename.match(/\.([^.]*?)(?=\?|#|$)/) || [''])[1];
            var newTitle = '(' + extension.toUpperCase() + ') ' + (event.detail.data.title || '');
            event.detail.data.title = newTitle;
        });
    });
    </script>
    """
    )

```
   ### Changing generated titles on the page editor only to remove dashes/underscores

 Using the [`insert_editor_js` hook](../../reference/hooks#insert-editor-js) instead so that this script will not run on the `Document` upload page, only on page editors.

 
```
# wagtail_hooks.py
from django.utils.safestring import mark_safe

from wagtail import hooks


@hooks.register("insert_editor_js")
def get_global_admin_js():
    return mark_safe(
    """
    <script>
    window.addEventListener('DOMContentLoaded', function () {
        document.addEventListener('wagtail:documents-upload', function(event) {
            // replace dashes/underscores with a space
            var newTitle = (event.detail.data.title || '').replace(/(\s|_|-)/g, " ");
            event.detail.data.title = newTitle;
        });
    });
    </script>
    """
    )

```
   ### Stopping pre-filling of title based on filename

 
```
# wagtail_hooks.py
from django.utils.safestring import mark_safe

from wagtail import hooks


@hooks.register("insert_global_admin_js")
def get_global_admin_js():
    return mark_safe(
    """
    <script>
    window.addEventListener('DOMContentLoaded', function () {
        document.addEventListener('wagtail:documents-upload', function(event) {
            // will stop title pre-fill on single file uploads
            // will set the multiple upload title to the filename (with extension)
            event.preventDefault();
        });
    });
    </script>
    """
    )

```
  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/documents/title_generation_on_upload.html>

How to add Wagtail into an existing Django project
==================================================

 To install Wagtail completely from scratch, create a new Django project and an app within that project. For instructions on these tasks, see [Writing your first Django app](https://docs.djangoproject.com/en/stable/intro/tutorial01/ "(in Django v4.1)"). Your project directory will look like the following:

 
```
myproject/
    myproject/
        __init__.py
        settings.py
        urls.py
        wsgi.py
    myapp/
        __init__.py
        models.py
        tests.py
        admin.py
        views.py
    manage.py

```
 From your app directory, you can safely remove `admin.py` and `views.py`, since Wagtail will provide this functionality for your models. Configuring Django to load Wagtail involves adding modules and variables to `settings.py` and URL configuration to `urls.py`. For a more complete view of what’s defined in these files, see [Django Settings](https://docs.djangoproject.com/en/stable/topics/settings/ "(in Django v4.1)") and [Django URL Dispatcher](https://docs.djangoproject.com/en/stable/topics/http/urls/ "(in Django v4.1)").

 What follows is a settings reference which skips many boilerplate Django settings. If you just want to get your Wagtail install up quickly without fussing with settings at the moment, see [Ready to Use Example Configuration Files](#complete-example-config).

  Middleware (`settings.py`)
--------------------------

 
```
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.contrib.redirects.middleware.RedirectMiddleware',
]

```
 Wagtail depends on the default set of Django middleware modules, to cover basic security and functionality such as login sessions. One additional middleware module is provided:

 **`RedirectMiddleware`**  
 Wagtail provides a simple interface for adding arbitrary redirects to your site and this module makes it happen.

   Apps (`settings.py`)
--------------------

 
```
INSTALLED_APPS = [

  'myapp',  # your own app

  'wagtail.contrib.forms',
  'wagtail.contrib.redirects',
  'wagtail.embeds',
  'wagtail.sites',
  'wagtail.users',
  'wagtail.snippets',
  'wagtail.documents',
  'wagtail.images',
  'wagtail.search',
  'wagtail.admin',
  'wagtail',

  'taggit',
  'modelcluster',

  'django.contrib.auth',
  'django.contrib.contenttypes',
  'django.contrib.sessions',
  'django.contrib.messages',
  'django.contrib.staticfiles',
]

```
 Wagtail requires several Django app modules, third-party apps, and defines several apps of its own. Wagtail was built to be modular, so many Wagtail apps can be omitted to suit your needs. Your own app (here `myapp`) is where you define your models, templates, static assets, template tags, and other custom functionality for your site.

  ### Wagtail Apps

 **`wagtail`**  
 The core functionality of Wagtail, such as the `Page` class, the Wagtail tree, and model fields.

 **`wagtail.admin`**  
 The administration interface for Wagtail, including page edit handlers.

 **`wagtail.documents`**  
 The Wagtail document content type.

 **`wagtail.snippets`**  
 Editing interface for non-Page models and objects. See [Snippets](../topics/snippets#snippets).

 **`wagtail.users`**  
 User editing interface.

 **`wagtail.images`**  
 The Wagtail image content type.

 **`wagtail.embeds`**  
 Module governing oEmbed and Embedly content in Wagtail rich text fields. See .

 **`wagtail.search`**  
 Search framework for Page content. See [Search](../topics/search/index#wagtailsearch).

 **`wagtail.sites`**  
 Management UI for Wagtail sites.

 **`wagtail.contrib.redirects`**  
 Admin interface for creating arbitrary redirects on your site.

 **`wagtail.contrib.forms`**  
 Models for creating forms on your pages and viewing submissions. See [Form builder](../reference/contrib/forms/index#form-builder).

   ### Third-Party Apps

 **`taggit`**  
 Tagging framework for Django. This is used internally within Wagtail for image and document tagging and is available for your own models as well. See [Tagging](../reference/pages/model_recipes#tagging) for a Wagtail model recipe or the [Taggit Documentation](https://django-taggit.readthedocs.io/en/stable/).

 **`modelcluster`**  
 Extension of Django ForeignKey relation functionality, which is used in Wagtail pages for on-the-fly related object creation. For more information, see [Inline Panels and Model Clusters](../reference/pages/panels#inline-panels) or [the django-modelcluster github project page](https://github.com/wagtail/django-modelcluster).

    URL Patterns
------------

 
```
from django.contrib import admin

from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls

urlpatterns = [
    path('django-admin/', admin.site.urls),

    path('admin/', include(wagtailadmin_urls)),
    path('documents/', include(wagtaildocs_urls)),

    # Optional URL for including your own vanilla Django urls/views
    re_path(r'', include('myapp.urls')),

    # For anything not caught by a more specific rule above, hand over to
    # Wagtail's serving mechanism
    re_path(r'', include(wagtail_urls)),
]

```
 This block of code for your project’s `urls.py` does a few things:

 * Load the vanilla Django admin interface to `/django-admin/`
* Load the Wagtail admin and its various apps
* Dispatch any vanilla Django apps you’re using other than Wagtail which require their own URL configuration (this is optional, since Wagtail might be all you need)
* Lets Wagtail handle any further URL dispatching.

 That’s not everything you might want to include in your project’s URL configuration, but it’s what’s necessary for Wagtail to flourish.

   Ready to Use Example Configuration Files
----------------------------------------

 These two files should reside in your project directory (`myproject/myproject/`).

  ### `settings.py`

 
```
import os

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

DEBUG = True

# Application definition

INSTALLED_APPS = [
    'myapp',

    'wagtail.contrib.forms',
    'wagtail.contrib.redirects',
    'wagtail.embeds',
    'wagtail.sites',
    'wagtail.users',
    'wagtail.snippets',
    'wagtail.documents',
    'wagtail.images',
    'wagtail.search',
    'wagtail.admin',
    'wagtail',

    'taggit',
    'modelcluster',

    '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.contrib.redirects.middleware.RedirectMiddleware',
]

ROOT_URLCONF = 'myproject.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 = 'myproject.wsgi.application'

# Database

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'myprojectdb',
        'USER': 'postgres',
        'PASSWORD': '',
        'HOST': '',  # Set to empty string for localhost.
        'PORT': '',  # Set to empty string for default.
        'CONN_MAX_AGE': 600,  # number of seconds database connections should persist for
    }
}

# Internationalization

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


# Static files (CSS, JavaScript, Images)

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

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

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

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


ADMINS = [
    # ('Your Name', 'your_email@example.com'),
]
MANAGERS = ADMINS

# Default to dummy email backend. Configure dev/production/local backend
# as per https://docs.djangoproject.com/en/stable/topics/email/#email-backends
EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'

# Hosts/domain names that are valid for this site; required if DEBUG is False
ALLOWED_HOSTS = []

# Make this unique, and don't share it with anybody.
SECRET_KEY = 'change-me'

EMAIL_SUBJECT_PREFIX = '[Wagtail] '

INTERNAL_IPS = ('127.0.0.1', '10.0.2.2')

# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See https://docs.djangoproject.com/en/stable/topics/logging for
# more details on how to customise your logging configuration.
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse'
        }
    },
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    'loggers': {
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': True,
        },
    }
}


# WAGTAIL SETTINGS

# This is the human-readable name of your Wagtail install
# which welcomes users upon login to the Wagtail admin.
WAGTAIL_SITE_NAME = 'My Project'

# Replace the search backend
#WAGTAILSEARCH_BACKENDS = {
#  'default': {
#    'BACKEND': 'wagtail.search.backends.elasticsearch5',
#    'INDEX': 'myapp'
#  }
#}

# Wagtail email notifications from address
# WAGTAILADMIN_NOTIFICATION_FROM_EMAIL = 'wagtail@myhost.io'

# Wagtail email notification format
# WAGTAILADMIN_NOTIFICATION_USE_HTML = True

# Reverse the default case-sensitive handling of tags
TAGGIT_CASE_INSENSITIVE = True

```
   ### `urls.py`

 
```
from django.urls import include, path, re_path
from django.conf.urls.static import static
from django.views.generic.base import RedirectView
from django.contrib import admin
from django.conf import settings
import os.path

from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls


urlpatterns = [
    path('django-admin/', admin.site.urls),

    path('admin/', include(wagtailadmin_urls)),
    path('documents/', include(wagtaildocs_urls)),

    # For anything not caught by a more specific rule above, hand over to
    # Wagtail's serving mechanism
    re_path(r'', include(wagtail_urls)),
]


if settings.DEBUG:
    from django.contrib.staticfiles.urls import staticfiles_urlpatterns

    urlpatterns += staticfiles_urlpatterns() # tell gunicorn where static files are in dev mode
    urlpatterns += static(settings.MEDIA_URL + 'images/', document_root=os.path.join(settings.MEDIA_ROOT, 'images'))
    urlpatterns += [
        path('favicon.ico', RedirectView.as_view(url=settings.STATIC_URL + 'myapp/images/favicon.ico'))
    ]

```
  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/add_to_django_project.html>

Performance
===========

 Wagtail is designed for speed, both in the editor interface and on the front-end, but if you want even better performance or you need to handle very high volumes of traffic, here are some tips on eking out the most from your installation.

  Editor interface
----------------

 We have tried to minimise external dependencies for a working installation of Wagtail, in order to make it as simple as possible to get going. However, a number of default settings can be configured for better performance:

  ### Cache

 We recommend [Redis](https://redis.io/) as a fast, persistent cache. Install Redis through your package manager (on Debian or Ubuntu: `sudo apt-get install redis-server`), add `django-redis` to your `requirements.txt`, and enable it as a cache backend:

 
```
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/dbname',
        # for django-redis < 3.8.0, use:
        # 'LOCATION': '127.0.0.1:6379',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    }
}

```
   ### Caching image renditions

 If you define a cache named ‘renditions’ (typically alongside your ‘default’ cache), Wagtail will cache image rendition lookups, which may improve the performance of pages which include many images.

 
```
CACHES = {
    'default': {...},
    'renditions': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '127.0.0.1:11211',
        'TIMEOUT': 600,
        'OPTIONS': {
            'MAX_ENTRIES': 1000
        }
    }
}

```
   ### Image URLs

 If all you need is the URL to an image (such as for use in meta tags or other tag attributes), it is likely more efficient to use the [image serve view](images/image_serve_view#using-images-outside-wagtail) and `{% image_url %}` tag:

 
```
<meta property="og:image" content="{% image_url page.hero_image width-600 %}" />

```
 Rather than finding or creating the rendition in the page request, the image serve view offloads this to a separate view, which only creates the rendition when the user requests the image (or returning an existing rendition if it already exists). This can drastically speed up page loads with many images. This may increase the number of requests handled by Wagtail if you’re using an external storage backend (for example Amazon S3).

 Another side benefit is it prevents errors during conversation from causing page errors. If an image is too large for Willow to handle (the size of an image can be constrained with [`WAGTAILIMAGES_MAX_IMAGE_PIXELS`](../reference/settings#wagtailimages-max-image-pixels)), Willow may crash. As the resize is done outside the page load, the image will be missing, but the rest of the page content will remain.

 The same can be achieved in Python using [`generate_image_url`](images/image_serve_view#dynamic-image-urls).

   ### Search

 Wagtail has strong support for [Elasticsearch](https://www.elastic.co) - both in the editor interface and for users of your site - but can fall back to a database search if Elasticsearch isn’t present. Elasticsearch is faster and more powerful than the Django ORM for text search, so we recommend installing it or using a hosted service like [Searchly](http://www.searchly.com/).

 For details on configuring Wagtail for Elasticsearch, see [Elasticsearch Backend](../topics/search/backends#wagtailsearch-backends-elasticsearch).

   ### Database

 Wagtail is tested on PostgreSQL, SQLite and MySQL. It may work on some third-party database backends as well, but this is not guaranteed. We recommend PostgreSQL for production use.

   ### Templates

 The overhead from reading and compiling templates adds up. Django wraps its default loaders with [cached template loader](https://docs.djangoproject.com/en/stable/ref/templates/api/#django.template.loaders.cached.Loader "(in Django v4.1)") which stores the compiled `Template` in memory and returns it for subsequent requests. The cached loader is automatically enabled when `DEBUG` is `False`. If you are using custom loaders, update your settings to use it:

 
```
TEMPLATES = [{
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'DIRS': [os.path.join(BASE_DIR, 'templates')],
    'OPTIONS': {
        'loaders': [
            ('django.template.loaders.cached.Loader', [
                'django.template.loaders.filesystem.Loader',
                'django.template.loaders.app_directories.Loader',
                'path.to.custom.Loader',
            ]),
        ],
    },
}]

```
    Public users
------------

  ### Caching proxy

 To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both [Varnish](https://varnish-cache.org/) and [Squid](http://www.squid-cache.org/) have been tested in production. Hosted proxies like [Cloudflare](https://www.cloudflare.com/) should also work well.

 Wagtail supports automatic cache invalidation for Varnish/Squid. See [Frontend cache invalidator](../reference/contrib/frontendcache#frontend-cache-purging) for more information.

   ### Image attributes

 For some images, it may be beneficial to lazy load images, so the rest of the page can continue to load. It can be configured site-wide [Adding default attributes to all images](../topics/images#adding-default-attributes-to-images) or per-image [More control over the img tag](../topics/images#image-tag-alt). For more details you can read about the [`loading='lazy'` attribute](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#images_and_iframes) and the [`'decoding='async'` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-decoding) or this [web.dev article on lazy loading images](https://web.dev/lazy-loading-images/).

 This optimisation is already handled for you for images in the admin site.

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/performance.html>

Internationalisation
====================

 * [Multi-language content](#multi-language-content)


	+ [Overview](#overview)
	+ [Wagtail’s approach to multi-lingual content](#wagtail-s-approach-to-multi-lingual-content)
	
	
		- [Page structure](#page-structure)
		- [How locales and translations are recorded in the database](#how-locales-and-translations-are-recorded-in-the-database)
		- [Translated homepages](#translated-homepages)
		- [Language detection and routing](#language-detection-and-routing)
		- [Locales](#locales)
	+ [Configuration](#configuration)
	
	
		- [Enabling internationalisation](#enabling-internationalisation)
		- [Configuring available languages](#configuring-available-languages)
		- [Enabling the locale management UI (optional)](#enabling-the-locale-management-ui-optional)
		- [Adding a language prefix to URLs](#adding-a-language-prefix-to-urls)
		- [User language auto-detection](#user-language-auto-detection)
		- [Custom routing/language detection](#custom-routing-language-detection)
	+ [Recipes for internationalised sites](#recipes-for-internationalised-sites)
	
	
		- [Language/region selector](#language-region-selector)
		- [API filters for headless sites](#api-filters-for-headless-sites)
		- [Translatable snippets](#translatable-snippets)
	+ [Translation workflow](#translation-workflow)
	
	
		- [Wagtail Localize](#wagtail-localize)
* [Alternative internationalisation plugins](#alternative-internationalisation-plugins)
* [Wagtail admin translations](#wagtail-admin-translations)
* [Change Wagtail admin language on a per-user basis](#change-wagtail-admin-language-on-a-per-user-basis)
* [Changing the primary language of your Wagtail installation](#changing-the-primary-language-of-your-wagtail-installation)

  Multi-language content
----------------------

  ### Overview

 Out of the box, Wagtail assumes all content will be authored in a single language. This document describes how to configure Wagtail for authoring content in multiple languages.

  Note

 Wagtail provides the infrastructure for creating and serving content in multiple languages. There are two options for managing translations across different languages in the admin interface: [wagtail.contrib.simple\_translation](../reference/contrib/simple_translation#simple-translation) or the more advanced [wagtail-localize](https://github.com/wagtail/wagtail-localize) (third-party package).

  This document only covers the internationalisation of content managed by Wagtail. For information on how to translate static content in template files, JavaScript code, etc, refer to the [Django internationalisation docs](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/). Or, if you are building a headless site, refer to the docs of the frontend framework you are using.

   ### Wagtail’s approach to multi-lingual content

 This section provides an explanation of Wagtail’s internationalisation approach. If you’re in a hurry, you can skip to [Configuration](#configuration).

 In summary:

 * Wagtail stores content in a separate page tree for each locale
* It has a built-in `Locale` model and all pages are linked to a `Locale` with the `locale` foreign key field
* It records which pages are translations of each other using a shared UUID stored in the `translation_key` field
* It automatically routes requests through translations of the site’s homepage
* It uses Django’s `i18n_patterns` and `LocaleMiddleware` for language detection

  #### Page structure

 Wagtail stores content in a separate page tree for each locale.

 For example, if you have two sites in two locales, then you will see four homepages at the top level of the page hierarchy in the explorer.

 This approach has some advantages for the editor experience as well:

 * There is no default language for editing, so content can be authored in any language and then translated to any other.
* Translations of a page are separate pages so they can be published at different times.
* Editors can be given permission to edit content in one locale and not others.

   #### How locales and translations are recorded in the database

 All pages (and any snippets that have translation enabled) have a `locale` and `translation_key` field:

 * `locale` is a foreign key to the `Locale` model
* `translation_key` is a UUID that’s used to find translations of a piece of content. Translations of the same page/snippet share the same value in this field

 These two fields have a ‘unique together’ constraint so you can’t have more than one translation in the same locale.

   #### Translated homepages

 When you set up a site in Wagtail, you select the site’s homepage in the ‘root page’ field and all requests to that site’s root URL will be routed to that page.

 Multi-lingual sites have a separate homepage for each locale that exist as siblings in the page tree. Wagtail finds the other homepages by looking for translations of the site’s ‘root page’.

 This means that to make a site available in another locale, you just need to translate and publish its homepage in that new locale.

 If Wagtail can’t find a homepage that matches the user’s language, it will fall back to the page that is selected as the ‘root page’ on the site record, so you can use this field to specify the default language of your site.

   #### Language detection and routing

 For detecting the user’s language and adding a prefix to the URLs (`/en/`, `/fr-fr/`, for example), Wagtail is designed to work with Django’s built-in internationalisation utilities such as `i18n_patterns` and `LocaleMiddleware`. This means that Wagtail should work seamlessly with any other internationalised Django applications on your site.

   #### Locales

 The locales that are enabled on a site are recorded in the `Locale` model in `wagtailcore`. This model has just two fields: ID and `language_code` which stores the [BCP-47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) that represents this locale.

 The locale records can be set up with an [optional management UI](#enabling-locale-management) or created in the shell. The possible values of the `language_code` field are controlled by the `WAGTAIL_CONTENT_LANGUAGES` setting.

  Note

 Read this if you’ve changed `LANGUAGE_CODE` before enabling internationalisation

 On initial migration, Wagtail creates a `Locale` record for the language that was set in the `LANGUAGE_CODE` setting at the time the migration was run. All pages will be assigned to this `Locale` when Wagtail’s internationalisation is disabled.

 If you have changed the `LANGUAGE_CODE` setting since updating to Wagtail 2.11, you will need to manually update the record in the `Locale` model too before enabling internationalisation, as your existing content will be assigned to the old code.

     ### Configuration

 In this section, we will go through the minimum configuration required to enable content to be authored in multiple languages.

 * [Enabling internationalisation](#enabling-internationalisation)
* [Configuring available languages](#configuring-available-languages)
* [Enabling the locale management UI (optional)](#enabling-the-locale-management-ui-optional)
* [Adding a language prefix to URLs](#adding-a-language-prefix-to-urls)
* [User language auto-detection](#user-language-auto-detection)
* [Custom routing/language detection](#custom-routing-language-detection)

  #### Enabling internationalisation

 To enable internationalisation in both Django and Wagtail, set the following settings to `True`:

 
```
# my_project/settings.py

USE_I18N = True
WAGTAIL_I18N_ENABLED = True

```
 In addition, you might also want to enable Django’s localisation support. This will make dates and numbers display in the user’s local format:

 
```
# my_project/settings.py

USE_L10N = True

```
   #### Configuring available languages

 Next we need to configure the available languages. There are two settings for this that are each used for different purposes:

 * `LANGUAGES` - This sets which languages are available on the frontend of the site.
* `WAGTAIL_CONTENT_LANGUAGES` - This sets which the languages Wagtail content can be authored in.

 You can set both of these settings to the exact same value. For example, to enable English, French, and Spanish:

 
```
# my_project/settings.py

WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
    ('en', "English"),
    ('fr', "French"),
    ('es', "Spanish"),
]

```
  Note

 Whenever `WAGTAIL_CONTENT_LANGUAGES` is changed, the `Locale` model needs to be updated as well to match.

 This can either be done with a data migration or with the optional locale management UI described in the next section.

  You can also set these to different values. You might want to do this if you want to have some programmatic localisation (like date formatting or currency, for example) but use the same Wagtail content in multiple regions:

 
```
# my_project/settings.py

LANGUAGES = [
    ('en-GB', "English (Great Britain)"),
    ('en-US', "English (United States)"),
    ('en-CA', "English (Canada)"),
    ('fr-FR', "French (France)"),
    ('fr-CA', "French (Canada)"),
]

WAGTAIL_CONTENT_LANGUAGES = [
    ('en-GB', "English"),
    ('fr-FR', "French"),
]

```
 When configured like this, the site will be available in all the different locales in the first list, but there will only be two language trees in Wagtail.

 All the `en-` locales will use the “English” language tree, and the `fr-` locales will use the “French” language tree. The differences between each locale in a language would be programmatic. For example: which date/number format to use, and what currency to display prices in.

   #### Enabling the locale management UI (optional)

 An optional locale management app exists to allow a Wagtail administrator to set up the locales from the Wagtail admin interface.

 To enable it, add `wagtail.locales` into `INSTALLED_APPS`:

 
```
# my_project/settings.py

INSTALLED_APPS = [
    # ...
    'wagtail.locales',
    # ...
]

```
   #### Adding a language prefix to URLs

 To allow all of the page trees to be served at the same domain, we need to add a URL prefix for each language.

 To implement this, we can use Django’s built-in [i18n\_patterns](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#language-prefix-in-url-patterns) function, which adds a language prefix to all of the URL patterns passed into it. This activates the language code specified in the URL and Wagtail takes this into account when it decides how to route the request.

 In your project’s `urls.py` add Wagtail’s core URLs (and any other URLs you want to be translated) into an `i18n_patterns` block:

 
```
# /my_project/urls.py

# ...

from django.conf.urls.i18n import i18n_patterns

# Non-translatable URLs
# Note: if you are using the Wagtail API or sitemaps,
# these should not be added to `i18n_patterns` either
urlpatterns = [
    path('django-admin/', admin.site.urls),

    path('admin/', include(wagtailadmin_urls)),
    path('documents/', include(wagtaildocs_urls)),
]

# Translatable URLs
# These will be available under a language code prefix. For example /en/search/
urlpatterns += i18n_patterns(
    path('search/', search_views.search, name='search'),
    path("", include(wagtail_urls)),
)

```
  ##### Bypass language prefix for the default language

 If you want your default language to have URLs that resolve normally without a language prefix, you can set the `prefix_default_language` parameter of `i18n_patterns` to `False`. For example, if you have your languages configured like this:

 
```
# myproject/settings.py

# ...

LANGUAGE_CODE = 'en'
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
    ('en', "English"),
    ('fr', "French"),
]

# ...

```
 And your `urls.py` configured like this:

 
```
# myproject/urls.py
# ...

# These URLs will be available under a language code prefix only for languages that
# are not set as default in LANGUAGE_CODE.

urlpatterns += i18n_patterns(
    path('search/', search_views.search, name='search'),
    path("", include(wagtail_urls)),
    prefix_default_language=False,
)

```
 Your URLs will now be prefixed only for the French version of your website, for example:

 
```
- /search/
- /fr/search/

```
    #### User language auto-detection

 After wrapping your URL patterns with `i18n_patterns`, your site will now respond on URL prefixes. But now it won’t respond on the root path.

 To fix this, we need to detect the user’s browser language and redirect them to the best language prefix. The recommended approach to do this is with Django’s `LocaleMiddleware`:

 
```
# my_project/settings.py

MIDDLEWARE = [
    # ...
    'django.middleware.locale.LocaleMiddleware',
    # ...
]

```
   #### Custom routing/language detection

 You don’t strictly have to use `i18n_patterns` or `LocaleMiddleware` for this and you can write your own logic if you need to.

 All Wagtail needs is the language to be activated (using Django’s `django.utils.translation.activate` function) before the `wagtail.views.serve` view is called.

    ### Recipes for internationalised sites

  #### Language/region selector

 Perhaps the most important bit of internationalisation-related UI you can add to your site is a selector to allow users to switch between different languages.

 If you’re not convinced that you need this, have a look at <https://www.w3.org/International/questions/qa-site-conneg#yyyshortcomings> for some rationale.

  ##### Basic example

 Here is a basic example of how to add links between translations of a page.

 This example, however, will only include languages defined in `WAGTAIL_CONTENT_LANGUAGES` and not any extra languages that might be defined in `LANGUAGES`. For more information on what both of these settings mean, see [Configuring available languages](#configuring-available-languages).

 If both settings are set to the same value, this example should work well for you, otherwise skip to the next section that has a more complicated example which takes this into account.

 
```
{# make sure these are at the top of the file #}
{% load i18n wagtailcore_tags %}

{% if page %}
    {% for translation in page.get_translations.live %}
        {% get_language_info for translation.locale.language_code as lang %}
        <a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
            {{ lang.name_local }}
        </a>
    {% endfor %}
{% endif %}

```
 Let’s break this down:

 
```
{% if page %}
    ...
{% endif %}

```
 If this is part of a shared base template it may be used in situations where no page object is available, such as 404 error responses, so check that we have a page before proceeding.

 
```
{% for translation in page.get_translations.live %}
    ...
{% endfor %}

```
 This `for` block iterates through all published translations of the current page.

 
```
{% get_language_info for translation.locale.language_code as lang %}

```
 This is a Django built-in tag that gets info about the language of the translation. For more information, see [get\_language\_info() in the Django docs](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#django.utils.translation.get_language_info).

 
```
<a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
    {{ lang.name_local }}
</a>

```
 This adds a link to the translation. We use `{{ lang.name_local }}` to display the name of the locale in its own language. We also add `rel` and `hreflang` attributes to the `<a>` tag for SEO.

   ##### Handling locales that share content

 Rather than iterating over pages, this example iterates over all of the configured languages and finds the page for each one. This works better than the [Basic example](#basic-example) above on sites that have extra Django `LANGUAGES` that share the same Wagtail content.

 For this example to work, you firstly need to add Django’s [django.template.context\_processors.i18n](https://docs.djangoproject.com/en/3.1/ref/templates/api/#django-template-context-processors-i18n) context processor to your `TEMPLATES` setting:

 
```
# myproject/settings.py

TEMPLATES = [
    {
        # ...
        'OPTIONS': {
            'context_processors': [
                # ...
                'django.template.context_processors.i18n',
            ],
        },
    },
]

```
 Now for the example itself:

 
```
{% for language_code, language_name in LANGUAGES %}
    {% get_language_info for language_code as lang %}

    {% language language_code %}
        <a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
            {{ lang.name_local }}
        </a>
    {% endlanguage %}
{% endfor %}

```
 Let’s break this down too:

 
```
{% for language_code, language_name in LANGUAGES %}
    ...
{% endfor %}

```
 This `for` block iterates through all of the configured languages on the site. The `LANGUAGES` variable comes from the `django.template.context_processors.i18n` context processor.

 
```
{% get_language_info for language_code as lang %}

```
 Does exactly the same as the previous example.

 
```
{% language language_code %}
    ...
{% endlanguage %}

```
 This `language` tag comes from Django’s `i18n` tag library. It changes the active language for just the code contained within it.

 
```
<a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
    {{ lang.name_local }}
</a>

```
 The only difference with the `<a>` tag here from the `<a>` tag in the previous example is how we’re getting the page’s URL: `{% pageurl page.localized %}`.

 All page instances in Wagtail have a `.localized` attribute which fetches the translation of the page in the current active language. This is why we activated the language previously.

 Another difference here is that if the same translated page is shared in two locales, Wagtail will generate the correct URL for the page based on the current active locale. This is the key difference between this example and the previous one as the previous one can only get the URL of the page in its default locale.

    #### API filters for headless sites

 For headless sites, the Wagtail API supports two extra filters for internationalised sites:

 * `?locale=` Filters pages by the given locale
* `?translation_of=` Filters pages to only include translations of the given page ID

 For more information, see [Special filters for internationalized sites](api/v2/usage#apiv2-i18n-filters).

   #### Translatable snippets

 You can make a snippet translatable by making it inherit from `wagtail.models.TranslatableMixin`. For example:

 
```
# myapp/models.py

from django.db import models

from wagtail.models import TranslatableMixin
from wagtail.snippets.models import register_snippet


@register_snippet
class Advert(TranslatableMixin, models.Model):
    name = models.CharField(max_length=255)

```
 The `TranslatableMixin` model adds the `locale` and `translation_key` fields to the model.

  ##### Making snippets with existing data translatable

 For snippets with existing data, it’s not possible to just add `TranslatableMixin`, make a migration, and run it. This is because the `locale` and `translation_key` fields are both required and `translation_key` needs a unique value for each instance.

 To migrate the existing data properly, we firstly need to use `BootstrapTranslatableMixin`, which excludes these constraints, then add a data migration to set the two fields, then switch to `TranslatableMixin`.

 This is only needed if there are records in the database. So if the model is empty, you can go straight to adding `TranslatableMixin` and skip this.

  ###### Step 1: Add `BootstrapTranslatableMixin` to the model

 This will add the two fields without any constraints:

 
```
# myapp/models.py

from django.db import models

from wagtail.models import BootstrapTranslatableMixin
from wagtail.snippets.models import register_snippet


@register_snippet
class Advert(BootstrapTranslatableMixin, models.Model):
    name = models.CharField(max_length=255)

    # if the model has a Meta class, ensure it inherits from
    # BootstrapTranslatableMixin.Meta too
    class Meta(BootstrapTranslatableMixin.Meta):
        verbose_name = 'adverts'

```
 Run `python manage.py makemigrations myapp` to generate the schema migration.

   ###### Step 2: Create a data migration

 Create a data migration with the following command:

 
```
python manage.py makemigrations myapp --empty

```
 This will generate a new empty migration in the app’s `migrations` folder. Edit that migration and add a `BootstrapTranslatableModel` for each model to bootstrap in that app:

 
```

from django.db import migrations
from wagtail.models import BootstrapTranslatableModel

class Migration(migrations.Migration):
    dependencies = [
        ('myapp', '0002_bootstraptranslations'),
    ]

    # Add one operation for each model to bootstrap here
    # Note: Only include models that are in the same app!
    operations = [
        BootstrapTranslatableModel('myapp.Advert'),
    ]

```
 Repeat this for any other apps that contain a model to be bootstrapped.

   ###### Step 3: Change `BootstrapTranslatableMixin` to `TranslatableMixin`

 Now that we have a migration that fills in the required fields, we can swap out `BootstrapTranslatableMixin` for `TranslatableMixin` that has all the constraints:

 
```
# myapp/models.py

from wagtail.models import TranslatableMixin  # Change this line

@register_snippet
class Advert(TranslatableMixin, models.Model):  # Change this line
    name = models.CharField(max_length=255)

    class Meta(TranslatableMixin.Meta):  # Change this line, if present
        verbose_name = 'adverts'

```
   ###### Step 4: Run `makemigrations` to generate schema migrations, then migrate!

 Run `makemigrations` to generate the schema migration that adds the constraints into the database, then run `migrate` to run all of the migrations:

 
```
python manage.py makemigrations myapp
python manage.py migrate

```
 When prompted to select a fix for the nullable field ‘locale’ being changed to non-nullable, select the option “Ignore for now” (as this has been handled by the data migration).

      ### Translation workflow

 As mentioned at the beginning, Wagtail does supply `wagtail.contrib.simple_translation`.

 The simple\_translation module provides a user interface that allows users to copy pages and translatable snippets into another language.

 * Copies are created in the source language (not translated)
* Copies of pages are in draft status

 Content editors need to translate the content and publish the pages.

 To enable add `"wagtail.contrib.simple_translation"` to `INSTALLED_APPS` and run `python manage.py migrate` to create the `submit_translation` permissions. In the Wagtail admin, go to settings and give some users or groups the “Can submit translations” permission.

  Note

 Simple Translation is optional. It can be switched out by third-party packages. Like the more advanced [wagtail-localize](https://github.com/wagtail/wagtail-localize).

   #### Wagtail Localize

 As part of the initial work on implementing internationalisation for Wagtail core, we also created a translation package called `wagtail-localize`. This supports translating pages within Wagtail, using PO files, machine translation, and external integration with translation services.

 Github: <https://github.com/wagtail/wagtail-localize>

     Alternative internationalisation plugins
----------------------------------------

 Before official multi-language support was added into Wagtail, site implementors had to use external plugins. These have not been replaced by Wagtail’s own implementation as they use slightly different approaches, one of them might fit your use case better:

 * [Wagtailtrans](https://github.com/wagtail/wagtailtrans)
* [wagtail-modeltranslation](https://github.com/infoportugal/wagtail-modeltranslation)

 For a comparison of these options, see AccordBox’s blog post [How to support multi-language in Wagtail CMS](https://www.accordbox.com/blog/how-support-multi-language-wagtail-cms/).

   Wagtail admin translations
--------------------------

 The Wagtail admin backend has been translated into many different languages. You can find a list of currently available translations on Wagtail’s [Transifex page](https://explore.transifex.com/torchbox/wagtail/). (Note: if you’re using an old version of Wagtail, this page may not accurately reflect what languages you have available).

 If your language isn’t listed on that page, you can easily contribute new languages or correct mistakes. Sign up and submit changes to [Transifex](https://explore.transifex.com/torchbox/wagtail/). Translation updates are typically merged into an official release within one month of being submitted.

   Change Wagtail admin language on a per-user basis
-------------------------------------------------

 Logged-in users can set their preferred language from `/admin/account/`. By default, Wagtail provides a list of languages that have a >= 90% translation coverage. It is possible to override this list via the [WAGTAILADMIN\_PERMITTED\_LANGUAGES](../reference/settings#wagtailadmin-permitted-languages) setting.

 In case there is zero or one language permitted, the form will be hidden.

 If there is no language selected by the user, the `LANGUAGE_CODE` will be used.

   Changing the primary language of your Wagtail installation
----------------------------------------------------------

 The default language of Wagtail is `en-us` (American English). You can change this by tweaking a couple of Django settings:

 * Make sure [USE\_I18N](https://docs.djangoproject.com/en/stable/ref/settings/#use-i18n) is set to `True`
* Set [LANGUAGE\_CODE](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LANGUAGE_CODE) to your websites’ primary language

 If there is a translation available for your language, the Wagtail admin backend should now be in the language you’ve chosen.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/i18n.html>

Customising Wagtail
===================

 * [Customising the editing interface](page_editing_interface)
	+ [Customising the tabbed interface](page_editing_interface#customising-the-tabbed-interface)
	+ [Rich Text (HTML)](page_editing_interface#rich-text-html)
	+ [Customising generated forms](page_editing_interface#customising-generated-forms)
* [Customising admin templates](admin_templates)
	+ [Custom branding](admin_templates#custom-branding)
	+ [Custom user interface fonts](admin_templates#custom-user-interface-fonts)
	+ [Custom user interface colours](admin_templates#custom-user-interface-colours)
	+ [Specifying a site or page in the branding](admin_templates#specifying-a-site-or-page-in-the-branding)
	+ [Extending the login form](admin_templates#extending-the-login-form)
	+ [Extending the password reset request form](admin_templates#extending-the-password-reset-request-form)
	+ [Extending client-side components](admin_templates#extending-client-side-components)
* [Custom user models](custom_user_models)
	+ [Custom user forms example](custom_user_models#custom-user-forms-example)
* [How to build custom StreamField blocks](streamfield_blocks)
	+ [Custom editing interfaces for `StructBlock`](streamfield_blocks#custom-editing-interfaces-for-structblock)
	+ [Additional JavaScript on `StructBlock` forms](streamfield_blocks#additional-javascript-on-structblock-forms)
	+ [Additional methods and properties on `StructBlock` values](streamfield_blocks#additional-methods-and-properties-on-structblock-values)
	+ [Custom block types](streamfield_blocks#custom-block-types)
	+ [Handling block definitions within migrations](streamfield_blocks#handling-block-definitions-within-migrations)



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/customisation/index.html>

Embedded content
================

 Wagtail supports generating embed code from URLs to content on external providers such as Youtube or Twitter. By default, Wagtail will fetch the embed code directly from the relevant provider’s site using the oEmbed protocol.

 Wagtail has a built-in list of the most common providers and this list can be changed [with a setting](#customising-embed-providers). Wagtail also supports fetching embed code using [Embedly](#embedly) and [custom embed finders](#custom-embed-finders).

  Embedding content on your site
------------------------------

 Wagtail’s embeds module should work straight out of the box for most providers. You can use any of the following methods to call the module:

  ### Rich text

 Wagtail’s default rich text editor has a “media” icon that allows embeds to be placed into rich text. You don’t have to do anything to enable this; just make sure the rich text field’s content is being passed through the `|richtext` filter in the template as this is what calls the embeds module to fetch and nest the embed code.

   ### 
`EmbedBlock` StreamField block type

 The `EmbedBlock` block type allows embeds to be placed into a `StreamField`.

 The `max_width` and `max_height` arguments are sent to the provider when fetching the embed code.

 For example:

 
```
from wagtail.embeds.blocks import EmbedBlock

class MyStreamField(blocks.StreamBlock):
    ...

    embed = EmbedBlock(max_width=800, max_height=400)

```
   ### 
`{% embed %}` tag

 Syntax: `{% embed <url> [max_width=<max width>] %}`

 You can nest embeds into a template by passing the URL and an optional `max_width` argument to the `{% embed %}` tag.

 The `max_width` argument is sent to the provider when fetching the embed code.

 
```
{% load wagtailembeds_tags %}

{# Embed a YouTube video #}
{% embed 'https://www.youtube.com/watch?v=Ffu-2jEdLPw' %}

{# This tag can also take the URL from a variable #}
{% embed page.video_url %}

```
   ### From Python

 You can also call the internal `get_embed` function that takes a URL string and returns an `Embed` object (see model documentation below). This also takes a `max_width` keyword argument that is sent to the provider when fetching the embed code.

 
```
from wagtail.embeds.embeds import get_embed
from wagtail.embeds.exceptions import EmbedException

try:
    embed = get_embed('https://www.youtube.com/watch?v=Ffu-2jEdLPw')

    print(embed.html)
except EmbedException:
    # Cannot find embed
    pass

```
    Configuring embed “finders”
---------------------------

 Embed finders are the modules within Wagtail that are responsible for producing embed code from a URL.

 Embed finders are configured using the `WAGTAILEMBEDS_FINDERS` setting. This is a list of finder configurations that are each run in order until one of them successfully returns an embed:

 The default configuration is:

 
```
WAGTAILEMBEDS_FINDERS = [
    {
        'class': 'wagtail.embeds.finders.oembed'
    }
]

```
  ### oEmbed (default)

 The default embed finder fetches the embed code directly from the content provider using the oEmbed protocol. Wagtail has a built-in list of providers which are all enabled by default. You can find that provider list at the following link:

 <https://github.com/wagtail/wagtail/blob/main/wagtail/embeds/oembed_providers.py>

  #### Customising the provider list

 You can limit which providers may be used by specifying the list of providers in the finder configuration.

 For example, this configuration will only allow content to be nested from Vimeo and Youtube. It also adds a custom provider:

 
```
from wagtail.embeds.oembed_providers import youtube, vimeo

# Add a custom provider
# Your custom provider must support oEmbed for this to work. You should be
# able to find these details in the provider's documentation.
# - 'endpoint' is the URL of the oEmbed endpoint that Wagtail will call
# - 'urls' specifies which patterns
my_custom_provider = {
    'endpoint': 'https://customvideosite.com/oembed',
    'urls': [
        '^http(?:s)?://(?:www\\.)?customvideosite\\.com/[^#?/]+/videos/.+$',
    ]
}

WAGTAILEMBEDS_FINDERS = [
    {
        'class': 'wagtail.embeds.finders.oembed',
        'providers': [youtube, vimeo, my_custom_provider],
    }
]

```
   #### Customising an individual provider

 Multiple finders can be chained together. This can be used for customising the configuration for one provider without affecting the others.

 For example, this is how you can instruct Youtube to return videos in HTTPS (which must be done explicitly for YouTube):

 
```
from wagtail.embeds.oembed_providers import youtube


WAGTAILEMBEDS_FINDERS = [
    # Fetches YouTube videos but puts ``?scheme=https`` in the GET parameters
    # when calling YouTube's oEmbed endpoint
    {
        'class': 'wagtail.embeds.finders.oembed',
        'providers': [youtube],
        'options': {'scheme': 'https'}
    },

    # Handles all other oEmbed providers the default way
    {
        'class': 'wagtail.embeds.finders.oembed',
    }
]

```
   #### How Wagtail uses multiple finders

 If multiple providers can handle a URL (for example, a YouTube video was requested using the configuration above), the topmost finder is chosen to perform the request.

 Wagtail will not try to run any other finder, even if the chosen one didn’t return an embed.

    ### Facebook and Instagram

 As of October 2020, Facebook deprecated their public oEmbed APIs. If you would like to embed Facebook or Instagram posts in your site, you will need to use the new authenticated APIs. This requires you to set up a Facebook Developer Account and create a Facebook App that includes the *oEmbed Product*. Instructions for creating the necessary app are in the requirements sections of the [Facebook](https://developers.facebook.com/docs/plugins/oembed) and [Instagram](https://developers.facebook.com/docs/instagram/oembed) documentation.

 As of June 2021, the *oEmbed Product* has been replaced with the *oEmbed Read* feature. In order to embed Facebook and Instagram posts your app must activate the *oEmbed Read* feature. Furthermore the app must be reviewed and accepted by Facebook. You can find the announcement in the [API changelog](https://developers.facebook.com/docs/graph-api/changelog/version11.0/#oembed).

 Apps that activated the oEmbed Product before June 8, 2021 need to activate the oEmbed Read feature and review their app before September 7, 2021.

 Once you have your app access tokens (App ID and App Secret), add the Facebook and/or Instagram finders to your `WAGTAILEMBEDS_FINDERS` setting and configure them with the App ID and App Secret from your app:

 
```
WAGTAILEMBEDS_FINDERS = [
    {
        'class': 'wagtail.embeds.finders.facebook',
        'app_id': 'YOUR FACEBOOK APP_ID HERE',
        'app_secret': 'YOUR FACEBOOK APP_SECRET HERE',
    },
    {
        'class': 'wagtail.embeds.finders.instagram',
        'app_id': 'YOUR INSTAGRAM APP_ID HERE',
        'app_secret': 'YOUR INSTAGRAM APP_SECRET HERE',
    },

    # Handles all other oEmbed providers the default way
    {
        'class': 'wagtail.embeds.finders.oembed',
    }
]

```
 By default, Facebook and Instagram embeds include some JavaScript that is necessary to fully render the embed. In certain cases, this might not be something you want - for example, if you have multiple Facebook embeds, this would result in multiple script tags. By passing `'omitscript': True` in the configuration, you can indicate that these script tags should be omitted from the embed HTML. Note that you will then have to take care of loading this script yourself.

   ### Embed.ly

 [Embed.ly](https://embed.ly) is a paid-for service that can also provide embeds for sites that do not implement the oEmbed protocol.

 They also provide some helpful features such as giving embeds a consistent look and a common video playback API which is useful if your site allows videos to be hosted on different providers and you need to implement custom controls for them.

 Wagtail has built in support for fetching embeds from Embed.ly. To use it, first pip install the `Embedly` [python package](https://pypi.org/project/Embedly/).

 Now add an embed finder to your `WAGTAILEMBEDS_FINDERS` setting that uses the `wagtail.embeds.finders.oembed` class and pass it your API key:

 
```
WAGTAILEMBEDS_FINDERS = [
    {
        'class': 'wagtail.embeds.finders.embedly',
        'key': 'YOUR EMBED.LY KEY HERE'
    }
]

```
   ### Custom embed finder classes

 For complete control, you can create a custom finder class.

 Here’s a stub finder class that could be used as a skeleton; please read the docstrings for details of what each method does:

 
```
from wagtail.embeds.finders.base import EmbedFinder


class ExampleFinder(EmbedFinder):
    def __init__(self, **options):
        pass

    def accept(self, url):
        """
        Returns True if this finder knows how to fetch an embed for the URL.

        This should not have any side effects (no requests to external servers)
        """
        pass

    def find_embed(self, url, max_width=None):
        """
        Takes a URL and max width and returns a dictionary of information about the
        content to be used for embedding it on the site.

        This is the part that may make requests to external APIs.
        """
        # TODO: Perform the request

        return {
            'title': "Title of the content",
            'author_name': "Author name",
            'provider_name': "Provider name (such as YouTube, Vimeo, etc)",
            'type': "Either 'photo', 'video', 'link' or 'rich'",
            'thumbnail_url': "URL to thumbnail image",
            'width': width_in_pixels,
            'height': height_in_pixels,
            'html': "<h2>The Embed HTML</h2>",
        }

```
 Once you’ve implemented all of those methods, you just need to add it to your `WAGTAILEMBEDS_FINDERS` setting:

 
```
WAGTAILEMBEDS_FINDERS = [
    {
        'class': 'path.to.your.finder.class.here',
        # Any other options will be passed as kwargs to the __init__ method
    }
]

```
    The `Embed` model
-----------------

  ### Deleting embeds

 As long as your embeds configuration is not broken, deleting items in the `Embed` model should be perfectly safe to do. Wagtail will automatically repopulate the records that are being used on the site.

 You may want to do this if you’ve changed from oEmbed to Embedly or vice-versa as the embed code they generate may be slightly different and lead to inconsistency on your site.

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/embeds.html>

Deploying Wagtail
=================

  On your server
--------------

 Wagtail is straightforward to deploy on modern Linux-based distributions, and should run with any of the combinations detailed in Django’s [deployment documentation](https://docs.djangoproject.com/en/stable/howto/deployment/ "(in Django v4.1)"). See the section on <performance> for the non-Python services we recommend.

   On Divio Cloud
--------------

 [Divio Cloud](https://www.divio.com/) is a Dockerised cloud hosting platform for Python/Django that allows you to launch and deploy Wagtail projects in minutes. With a free account, you can create a Wagtail project. Choose from a:

 * [site based on the Wagtail Bakery project](https://www.divio.com/wagtail/), or
* [brand new Wagtail project](https://control.divio.com/control/project/create) (see the [how to get started notes](https://docs.divio.com/en/latest/introduction/wagtail/)).

 Divio Cloud also hosts a [live Wagtail Bakery demo](https://www.divio.com/wagtail/) (no account required).

   On PythonAnywhere
-----------------

 [PythonAnywhere](https://www.pythonanywhere.com/) is a Platform-as-a-Service (PaaS) focused on Python hosting and development. It allows developers to quickly develop, host, and scale applications in a cloud environment. Starting with a free plan they also provide MySQL and PostgreSQL databases as well as very flexible and affordable paid plans, so there’s all you need to host a Wagtail site. To get quickly up and running you may use the [wagtail-pythonanywhere-quickstart](https://github.com/texperience/wagtail-pythonanywhere-quickstart).

   On Google Cloud
---------------

 [Google Cloud](https://cloud.google.com) is an Infrastructure-as-a-Service (IaaS) that offers multiple managed products, supported by Python client libraries, to help you build, deploy, and monitor your applications. You can deploy Wagtail, or any Django application, in a number of ways, including on [App Engine](https://www.youtube.com/watch?v=uD9PTag2-PQ) or [Cloud Run](https://codelabs.developers.google.com/codelabs/cloud-run-wagtail/#0).

   On alwaysdata
-------------

 [alwaysdata](https://www.alwaysdata.com/) is a Platform-as-a-Service (PaaS) providing Public and Private Cloud offers. Starting with a free plan they provide MySQL/PostgreSQL databases, emails, free SSL certificates, included backups, etc.

 To get your Wagtail application running you may:

 * [Install Wagtail from alwaysdata Marketplace](https://www.alwaysdata.com/en/marketplace/wagtail/)
* [Configure a Django application](https://help.alwaysdata.com/en/languages/python/django/)

   On other PAASs and IAASs
------------------------

 We know of Wagtail sites running on [Heroku](https://spapas.github.io/2014/02/13/wagtail-tutorial/), Digital Ocean and elsewhere. If you have successfully installed Wagtail on your platform or infrastructure, please [contribute](../contributing/index) your notes to this documentation!

   Deployment tips
---------------

  ### Static files

 As with all Django projects, static files are only served by the Django application server during development, when running through the `manage.py runserver` command. In production, these need to be handled separately at the web server level. See [Django’s documentation on deploying static files](https://docs.djangoproject.com/en/stable/howto/static-files/deployment/ "(in Django v4.1)").

 The JavaScript and CSS files used by the Wagtail admin frequently change between releases of Wagtail - it’s important to avoid serving outdated versions of these files due to browser or server-side caching, as this can cause hard-to-diagnose issues. We recommend enabling [ManifestStaticFilesStorage](https://docs.djangoproject.com/en/stable/ref/contrib/staticfiles/#django.contrib.staticfiles.storage.ManifestStaticFilesStorage "(in Django v4.1)") in the `STATICFILES_STORAGE` setting - this ensures that different versions of files are assigned distinct URLs.

   ### User Uploaded Files

 Wagtail follows [Django’s conventions for managing uploaded files](https://docs.djangoproject.com/en/stable/topics/files/ "(in Django v4.1)"). So by default, Wagtail uses Django’s built-in `FileSystemStorage` class which stores files on your site’s server, in the directory specified by the `MEDIA_ROOT` setting. Alternatively, Wagtail can be configured to store uploaded images and documents on a cloud storage service such as Amazon S3; this is done through the [DEFAULT\_FILE\_STORAGE](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_FILE_STORAGE) setting in conjunction with an add-on package such as [django-storages](https://django-storages.readthedocs.io/).

 When using `FileSystemStorage`, image urls are constructed starting from the path specified by the `MEDIA_URL`. In most cases, you should configure your web server to serve image files directly (without passing through Django/Wagtail). When using one of the cloud storage backends, images urls go directly to the cloud storage file url. If you would like to serve your images from a separate asset server or CDN, you can [configure the image serve view](images/image_serve_view#image-serve-view-redirect-action) to redirect instead.

 Document serving is controlled by the [WAGTAILDOCS\_SERVE\_METHOD](../reference/settings#wagtaildocs-serve-method) method. When using `FileSystemStorage`, documents are stored in a `documents` subdirectory within your site’s `MEDIA_ROOT`. If all your documents are public, you can set the `WAGTAILDOCS_SERVE_METHOD` to `direct` and configure your web server to serve the files itself. However, if you use Wagtail’s Collection Privacy settings to restrict access to some or all of your documents, you may or may not want to configure your web server to serve the documents directly. The default setting is `redirect` which allows Wagtail to perform any configured privacy checks before offloading serving the actual document to your web server or CDN. This means that Wagtail constructs document links that pass through Wagtail, but the final url in the user’s browser is served directly by your web server. If a user bookmarks this url, they will be able to access the file without passing through Wagtail’s privacy checks. If this is not acceptable, you may want to set the `WAGTAILDOCS_SERVE_METHOD` to `serve_view` and configure your web server so it will not serve document files itself. If you are serving documents from the cloud and need to enforce privacy settings, you should make sure the documents are not publicly accessible using the cloud service’s file url.

   ### Cloud storage

 Be aware that setting up remote storage will not entirely offload file handling tasks from the application server - some Wagtail functionality requires files to be read back by the application server. In particular, original image files need to be read back whenever a new resized rendition is created, and documents may be configured to be served through a Django view in order to enforce permission checks (see [WAGTAILDOCS\_SERVE\_METHOD](../reference/settings#wagtaildocs-serve-method)).

 Note that the django-storages Amazon S3 backends (`storages.backends.s3boto.S3BotoStorage` and `storages.backends.s3boto3.S3Boto3Storage`) **do not correctly handle duplicate filenames** in their default configuration. When using these backends, `AWS_S3_FILE_OVERWRITE` must be set to `False`.

 If you are also serving Wagtail’s static files from remote storage (using Django’s [STATICFILES\_STORAGE](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-STATICFILES_STORAGE) setting), you’ll need to ensure that it is configured to serve [CORS HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), as current browsers will reject remotely-hosted font files that lack a valid header. For Amazon S3, refer to the documentation [Setting Bucket and Object Access Permissions](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/set-permissions.html), or (for the `storages.backends.s3boto.S3Boto3Storage` backend only) add the following to your Django settings:

 
```
AWS_S3_OBJECT_PARAMETERS = {
    "ACL": "public-read"
}

```
 The `ACL` parameter accepts a list of predefined configurations for Amazon S3. For more information, refer to the documentation [Canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl).

 For Google Cloud Storage, create a `cors.json` configuration:

 
```
[
    {
        "origin": ["*"],
        "responseHeader": ["Content-Type"],
        "method": ["GET"],
        "maxAgeSeconds": 3600
    }
]

```
 Then, apply this CORS configuration to the storage bucket:

 
```
gsutil cors set cors.json gs://$GS_BUCKET_NAME

```
 For other storage services, refer to your provider’s documentation, or the documentation for the Django storage backend library you’re using.

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/deploying.html>

Private pages
=============

 Users with publish permission on a page can set it to be private by clicking the ‘Privacy’ control in the top right corner of the page explorer or editing interface. This sets a restriction on who is allowed to view the page and its sub-pages. Several different kinds of restriction are available:

 * **Accessible to logged-in users:** The user must log in to view the page. All user accounts are granted access, regardless of permission level.
* **Accessible with the following password:** The user must enter the given password to view the page. This is appropriate for situations where you want to share a page with a trusted group of people, but giving them individual user accounts would be overkill. The same password is shared between all users, and this works independently of any user accounts that exist on the site.
* **Accessible to users in specific groups:** The user must be logged in, and a member of one or more of the specified groups, in order to view the page.

 Similarly, documents can be made private by placing them in a collection with appropriate privacy settings (see: [Image / document permissions](../topics/permissions#image-document-permissions)).

 Private pages and documents work on Wagtail out of the box - the site implementer does not need to do anything to set them up. However, the default “log in” and “password required” forms are only bare-bones HTML pages, and site implementers may wish to replace them with a page customised to their site design.

  Setting up a login page
-----------------------

 The basic login page can be customised by setting `WAGTAIL_FRONTEND_LOGIN_TEMPLATE` to the path of a template you wish to use:

 
```
WAGTAIL_FRONTEND_LOGIN_TEMPLATE = 'myapp/login.html'

```
 Wagtail uses Django’s standard `django.contrib.auth.views.LoginView` view here, and so the context variables available on the template are as detailed in [Django’s login view documentation](https://docs.djangoproject.com/en/stable/topics/auth/default/#django.contrib.auth.views.LoginView "(in Django v4.1)").

 If the stock Django login view is not suitable - for example, you wish to use an external authentication system, or you are integrating Wagtail into an existing Django site that already has a working login view - you can specify the URL of the login view via the `WAGTAIL_FRONTEND_LOGIN_URL` setting:

 
```
WAGTAIL_FRONTEND_LOGIN_URL = '/accounts/login/'

```
 To integrate Wagtail into a Django site with an existing login mechanism, setting `WAGTAIL_FRONTEND_LOGIN_URL = LOGIN_URL` will usually be sufficient.

   Setting up a global “password required” page
--------------------------------------------

 By setting `PASSWORD_REQUIRED_TEMPLATE` in your Django settings file, you can specify the path of a template which will be used for all “password required” forms on the site (except for page types that specifically override it - see below):

 
```
PASSWORD_REQUIRED_TEMPLATE = 'myapp/password_required.html'

```
 This template will receive the same set of context variables that the blocked page would pass to its own template via `get_context()` - including `page` to refer to the page object itself - plus the following additional variables (which override any of the page’s own context variables of the same name):

 * **form** - A Django form object for the password prompt; this will contain a field named `password` as its only visible field. A number of hidden fields may also be present, so the page must loop over `form.hidden_fields` if not using one of Django’s rendering helpers such as `form.as_p`.
* **action\_url** - The URL that the password form should be submitted to, as a POST request.

 A basic template suitable for use as `PASSWORD_REQUIRED_TEMPLATE` might look like this:

 
```
<!DOCTYPE HTML>
<html>
    <head>
        <title>Password required</title>
    </head>
    <body>
        <h1>Password required</h1>
        <p>You need a password to access this page.</p>
        <form action="{{ action_url }}" method="POST">
            {% csrf_token %}

            {{ form.non_field_errors }}

            <div>
                {{ form.password.errors }}
                {{ form.password.label_tag }}
                {{ form.password }}
            </div>

            {% for field in form.hidden_fields %}
                {{ field }}
            {% endfor %}
            <input type="submit" value="Continue" />
        </form>
    </body>
</html>

```
 Password restrictions on documents use a separate template, specified through the setting `DOCUMENT_PASSWORD_REQUIRED_TEMPLATE`; this template also receives the context variables `form` and `action_url` as described above.

   Setting a “password required” page for a specific page type
-----------------------------------------------------------

 The attribute `password_required_template` can be defined on a page model to use a custom template for the “password required” view, for that page type only. For example, if a site had a page type for displaying embedded videos along with a description, it might choose to use a custom “password required” template that displays the video description as usual, but shows the password form in place of the video embed.

 
```
class VideoPage(Page):
    ...

    password_required_template = 'video/password_required.html'

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/privacy.html>

Customising the editing interface
=================================

  Customising the tabbed interface
--------------------------------

 As standard, Wagtail organises panels for pages into three tabs: ‘Content’, ‘Promote’ and ‘Settings’. For snippets Wagtail puts all panels into one page. Depending on the requirements of your site, you may wish to customise this for specific page types or snippets - for example, adding an additional tab for sidebar content. This can be done by specifying an `edit_handler` attribute on the page or snippet model. For example:

 
```
from wagtail.admin.panels import TabbedInterface, ObjectList

class BlogPage(Page):
    # field definitions omitted

    content_panels = [
        FieldPanel('title', classname="title"),
        FieldPanel('date'),
        FieldPanel('body'),
    ]
    sidebar_content_panels = [
        FieldPanel('advert'),
        InlinePanel('related_links', heading="Related links", label="Related link"),
    ]

    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading='Content'),
        ObjectList(sidebar_content_panels, heading='Sidebar content'),
        ObjectList(Page.promote_panels, heading='Promote'),
        ObjectList(Page.settings_panels, heading='Settings'),
    ])

```
   Rich Text (HTML)
----------------

 Wagtail provides a general-purpose WYSIWYG editor for creating rich text content (HTML) and embedding media such as images, video, and documents. To include this in your models, use the `RichTextField` function when defining a model field:

 
```
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel


class BookPage(Page):
    body = RichTextField()

    content_panels = Page.content_panels + [
        FieldPanel('body'),
    ]

```
 `RichTextField` inherits from Django’s basic `TextField` field, so you can pass any field parameters into `RichTextField` as if using a normal Django field. Its `max_length` will ignore any rich text formatting. This field does not need a special panel and can be defined with `FieldPanel`.

 However, template output from `RichTextField` is special and needs to be filtered in order to preserve embedded content. See [Rich text (filter)](../../topics/writing_templates#rich-text-filter).

  ### Limiting features in a rich text field

 By default, the rich text editor provides users with a wide variety of options for text formatting and inserting embedded content such as images. However, we may wish to restrict a rich text field to a more limited set of features - for example:

 * The field might be intended for a short text snippet, such as a summary to be pulled out on index pages, where embedded images or videos would be inappropriate;
* When page content is defined using [StreamField](../../topics/streamfield), elements such as headings, images and videos are usually given their own block types, alongside a rich text block type used for ordinary paragraph text; in this case, allowing headings and images to also exist within the rich text content is redundant (and liable to result in inconsistent designs).

 This can be achieved by passing a `features` keyword argument to `RichTextField`, with a list of identifiers for the features you wish to allow:

 
```
body = RichTextField(features=['h2', 'h3', 'bold', 'italic', 'link'])

```
 The feature identifiers provided on a default Wagtail installation are as follows:

 * `h1`, `h2`, `h3`, `h4`, `h5`, `h6` - heading elements
* `bold`, `italic` - bold / italic text
* `ol`, `ul` - ordered / unordered lists
* `hr` - horizontal rules
* `link` - page, external and email links
* `document-link` - links to documents
* `image` - embedded images
* `embed` - embedded media (see [Embedded content](../embeds#embedded-content))

 We have few additional feature identifiers as well. They are not enabled by default, but you can use them in your list of identifiers. These are as follows:

 * `code` - inline code
* `superscript`, `subscript`, `strikethrough` - text formatting
* `blockquote` - blockquote

 The process for creating new features is described in the following pages:

 * [Rich text internals](../../extending/rich_text_internals)
* [Extending the Draftail Editor](../../extending/extending_draftail)

 You can also provide a setting for naming a group of rich text features. See [WAGTAILADMIN\_RICH\_TEXT\_EDITORS](../../reference/settings#wagtailadmin-rich-text-editors).

   ### Image Formats in the Rich Text Editor

 On loading, Wagtail will search for any app with the file `image_formats.py` and execute the contents. This provides a way to customise the formatting options shown to the editor when inserting images in the `RichTextField` editor.

 As an example, add a “thumbnail” format:

 
```
# image_formats.py
from wagtail.images.formats import Format, register_image_format

register_image_format(Format('thumbnail', 'Thumbnail', 'richtext-image thumbnail', 'max-120x120'))

```
 To begin, import the `Format` class, `register_image_format` function, and optionally `unregister_image_format` function. To register a new `Format`, call the `register_image_format` with the `Format` object as the argument. The `Format` class takes the following constructor arguments:

 **`name`**  
 The unique key used to identify the format. To unregister this format, call `unregister_image_format` with this string as the only argument.

 **`label`**  
 The label used in the chooser form when inserting the image into the `RichTextField`.

 **`classnames`**  
 The string to assign to the `class` attribute of the generated `<img>` tag.

  Note

 Any class names you provide must have CSS rules matching them written separately, as part of the front end CSS code. Specifying a `classnames` value of `left` will only ensure that class is output in the generated markup, it won’t cause the image to align itself left.

  **`filter_spec`**  
 The string specification to create the image rendition. For more, see [How to use images in templates](../../topics/images#image-tag).

 To unregister, call `unregister_image_format` with the string of the `name` of the `Format` as the only argument.

  Warning

 Unregistering `Format` objects will cause errors viewing or editing pages that reference them.

     Customising generated forms
---------------------------

 Wagtail automatically generates forms using the panels configured on the model. By default, this form subclasses [WagtailAdminModelForm](#wagtail.admin.forms.WagtailAdminModelForm "wagtail.admin.forms.WagtailAdminModelForm"), or [WagtailAdminPageForm](#wagtail.admin.forms.WagtailAdminPageForm "wagtail.admin.forms.WagtailAdminPageForm"). for pages. A custom base form class can be configured by setting the `base_form_class` attribute on any model. Custom forms for snippets must subclass [WagtailAdminModelForm](#wagtail.admin.forms.WagtailAdminModelForm "wagtail.admin.forms.WagtailAdminModelForm"), and custom forms for pages must subclass [WagtailAdminPageForm](#wagtail.admin.forms.WagtailAdminPageForm "wagtail.admin.forms.WagtailAdminPageForm").

 This can be used to add non-model fields to the form, to automatically generate field content, or to add custom validation logic for your models:

 
```
from django import forms
from django.db import models
import geocoder  # not in Wagtail, for example only - https://geocoder.readthedocs.io/
from wagtail.admin.panels import FieldPanel
from wagtail.admin.forms import WagtailAdminPageForm
from wagtail.models import Page


class EventPageForm(WagtailAdminPageForm):
    address = forms.CharField()

    def clean(self):
        cleaned_data = super().clean()

        # Make sure that the event starts before it ends
        start_date = cleaned_data['start_date']
        end_date = cleaned_data['end_date']
        if start_date and end_date and start_date > end_date:
            self.add_error('end_date', 'The end date must be after the start date')

        return cleaned_data

    def save(self, commit=True):
        page = super().save(commit=False)

        # Update the duration field from the submitted dates
        page.duration = (page.end_date - page.start_date).days

        # Fetch the location by geocoding the address
        page.location = geocoder.arcgis(self.cleaned_data['address'])

        if commit:
            page.save()
        return page


class EventPage(Page):
    start_date = models.DateField()
    end_date = models.DateField()
    duration = models.IntegerField()
    location = models.CharField(max_length=255)

    content_panels = [
        FieldPanel('title'),
        FieldPanel('start_date'),
        FieldPanel('end_date'),
        FieldPanel('address'),
    ]
    base_form_class = EventPageForm

```
 Wagtail will generate a new subclass of this form for the model, adding any fields defined in `panels` or `content_panels`. Any fields already defined on the model will not be overridden by these automatically added fields, so the form field for a model field can be overridden by adding it to the custom form.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/customisation/page_editing_interface.html>

Customising admin templates
===========================

 In your projects with Wagtail, you may wish to replace elements such as the Wagtail logo within the admin interface with your own branding. This can be done through Django’s template inheritance mechanism.

 You need to create a `templates/wagtailadmin/` folder within one of your apps - this may be an existing one, or a new one created for this purpose, for example, `dashboard`. This app must be registered in `INSTALLED_APPS` before `wagtail.admin`:

 
```
INSTALLED_APPS = (
    # ...

    'dashboard',

    'wagtail',
    'wagtail.admin',

    # ...
)

```
  Custom branding
---------------

 The template blocks that are available to customise the branding in the admin interface are as follows:

  ### `branding_logo`

 To replace the default logo, create a template file `dashboard/templates/wagtailadmin/base.html` that overrides the block `branding_logo`:

 
```
{% extends "wagtailadmin/base.html" %}
{% load static %}

{% block branding_logo %}
    <img src="{% static 'images/custom-logo.svg' %}" alt="Custom Project" width="80" />
{% endblock %}

```
 The logo also appears in the following pages and can be replaced with its template file:

 * **login page** - create a template file `dashboard/templates/wagtailadmin/login.html` that overwrites the `branding_logo` block.
* **404 error page** - create a template file `dashboard/templates/wagtailadmin/404.html` that overrides the `branding_logo` block.
* **wagtail userbar** - create a template file `dashboard/templates/wagtailadmin/userbar/base.html` that overwrites the `branding_logo` block.

   ### `branding_favicon`

 To replace the favicon displayed when viewing admin pages, create a template file `dashboard/templates/wagtailadmin/admin_base.html` that overrides the block `branding_favicon`:

 
```
{% extends "wagtailadmin/admin_base.html" %}
{% load static %}

{% block branding_favicon %}
    <link rel="shortcut icon" href="{% static 'images/favicon.ico' %}" />
{% endblock %}

```
   ### `branding_title`

 To replace the title prefix (which is ‘Wagtail’ by default), create a template file `dashboard/templates/wagtailadmin/admin_base.html` that overrides the block `branding_title`:

 
```
{% extends "wagtailadmin/admin_base.html" %}

{% block branding_title %}Frank's CMS{% endblock %}

```
   ### `branding_login`

 To replace the login message, create a template file `dashboard/templates/wagtailadmin/login.html` that overrides the block `branding_login`:

 
```
{% extends "wagtailadmin/login.html" %}

{% block branding_login %}Sign in to Frank's Site{% endblock %}

```
   ### `branding_welcome`

 To replace the welcome message on the dashboard, create a template file `dashboard/templates/wagtailadmin/home.html` that overrides the block `branding_welcome`:

 
```
{% extends "wagtailadmin/home.html" %}

{% block branding_welcome %}Welcome to Frank's Site{% endblock %}

```
    Custom user interface fonts
---------------------------

 To customise the font families used in the admin user interface, inject a CSS file using the hook [insert\_global\_admin\_css](../../reference/hooks#insert-global-admin-css) and override the variables within the `:root` selector:

 
```
:root {
    --w-font-sans: Papyrus;
    --w-font-mono: Courier;
}

```
   Custom user interface colours
-----------------------------

  Warning

 The default Wagtail colours conform to the WCAG2.1 AA level colour contrast requirements. When customising the admin colours you should test the contrast using tools like [Axe](https://www.deque.com/axe/browser-extensions/).

  To customise the colours used in the admin user interface, inject a CSS file using the hook [insert\_global\_admin\_css](../../reference/hooks#insert-global-admin-css) and set the desired variables within the `:root` selector. There are two ways to customisation options: either set each colour separately (for example `--w-color-primary: #2E1F5E;`); or separately set [HSL](https://en.wikipedia.org/wiki/HSL_and_HSV) (`--w-color-primary-hue`, `--w-color-primary-saturation`, `--w-color-primary-lightness`) variables so all shades are customised at once. For example, setting `--w-color-secondary-hue: 180;` will customise all of the secondary shades at once.

 
Make sure to test any customisations against our [Contrast Grid](https://contrast-grid.eightshapes.com/?version=1.1.0&es-color-form__tile-size=compact&es-color-form__show-contrast=aaa&es-color-form__show-contrast=aa&es-color-form__show-contrast=aa18&background-colors=%23000000%2C%20black%0D%0A%23F6F6F8%2C%20grey-50%0D%0A%23E0E0E0%2C%20grey-100%0D%0A%23C8C8C8%2C%20grey-150%0D%0A%23929292%2C%20grey-200%0D%0A%235C5C5C%2C%20grey-400%0D%0A%23262626%2C%20grey-600%0D%0A%23FFFFFF%2C%20white%0D%0A%23261A4E%2C%20primary-200%0D%0A%232E1F5E%2C%20primary%0D%0A%23F2FCFC%2C%20secondary-50%0D%0A%2380D7D8%2C%20secondary-75%0D%0A%2300B0B1%2C%20secondary-100%0D%0A%23005B5E%2C%20secondary-400%0D%0A%23004345%2C%20secondary-600%0D%0A%23007D7E%2C%20secondary%0D%0A%23E2F5FC%2C%20info-50%0D%0A%231F7E9A%2C%20info-100%0D%0A%23E0FBF4%2C%20positive-50%0D%0A%231B8666%2C%20positive-100%0D%0A%23FAECD5%2C%20warning-50%0D%0A%23FAA500%2C%20warning-100%0D%0A%23FDE9E9%2C%20critical-50%0D%0A%23FD5765%2C%20critical-100%0D%0A%23CD4444%2C%20critical-200&foreground-colors=%23000000%2C%20black%0D%0A%23F6F6F8%2C%20grey-50%0D%0A%23E0E0E0%2C%20grey-100%0D%0A%23C8C8C8%2C%20grey-150%0D%0A%23929292%2C%20grey-200%0D%0A%235C5C5C%2C%20grey-400%0D%0A%23262626%2C%20grey-600%0D%0A%23FFFFFF%2C%20white%0D%0A%23261A4E%2C%20primary-200%0D%0A%232E1F5E%2C%20primary%0D%0A%23F2FCFC%2C%20secondary-50%0D%0A%2380D7D8%2C%20secondary-75%0D%0A%2300B0B1%2C%20secondary-100%0D%0A%23005B5E%2C%20secondary-400%0D%0A%23004345%2C%20secondary-600%0D%0A%23007D7E%2C%20secondary%0D%0A%231F7E9A%2C%20info-100%0D%0A%231B8666%2C%20positive-100%0D%0A%23FAA500%2C%20warning-100%0D%0A%23FD5765%2C%20critical-100%0D%0A%23CD4444%2C%20critical-200). Try out your own customisations with this interactive style editor:




|  | Variable | Usage |
| --- | --- | --- |
|  | `--w-color-black` | Shadows only |
|  | `--w-color-grey-600` | Body copy, user content |
|  | `--w-color-grey-400` | Help text, placeholders, meta text, neutral state indicators |
|  | `--w-color-grey-200` | Dividers, button borders |
|  | `--w-color-grey-150` | Field borders |
|  | `--w-color-grey-100` | Dividers, panel borders |
|  | `--w-color-grey-50` | Background for panels, row highlights |
|  | `--w-color-white` | Page backgrounds, Panels, Button text |
|  | `--w-color-primary` | Wagtail branding, Panels, Headings, Buttons, Labels |
|  | `--w-color-primary-200` | Accent for elements used in conjunction with primary colour in sidebar |
|  | `--w-color-secondary` | Primary buttons, action links |
|  | `--w-color-secondary-600` | Hover states for two-tone buttons |
|  | `--w-color-secondary-400` | Two-tone buttons, hover states |
|  | `--w-color-secondary-100` | UI element highlights over dark backgrounds |
|  | `--w-color-secondary-75` | UI element highlights over dark text |
|  | `--w-color-secondary-50` | Button backgrounds, highlighted fields background |
|  | `--w-color-info-100` | Background and icons for information messages |
|  | `--w-color-info-50` | Background only, for information messages |
|  | `--w-color-positive-100` | Positive states |
|  | `--w-color-positive-50` | Background only, for positive states |
|  | `--w-color-warning-100` | Background and icons for potentially dangerous states |
|  | `--w-color-warning-50` | Background only, for potentially dangerous states |
|  | `--w-color-critical-200` | Dangerous actions or states (over light background), errors |
|  | `--w-color-critical-100` | Dangerous actions or states (over dark background) |
|  | `--w-color-critical-50` | Background only, for dangerous states |

   Specifying a site or page in the branding
-----------------------------------------

 The admin interface has a number of variables available to the renderer context that can be used to customise the branding in the admin page. These can be useful for customising the dashboard on a multitenanted Wagtail installation:

  ### `root_page`

 Returns the highest explorable page object for the currently logged in user. If the user has no explore rights, this will default to `None`.

   ### `root_site`

 Returns the name on the site record for the above root page.

   ### `site_name`

 Returns the value of `root_site`, unless it evaluates to `None`. In that case, it will return the value of `settings.WAGTAIL_SITE_NAME`.

 To use these variables, create a template file `dashboard/templates/wagtailadmin/home.html`, just as if you were overriding one of the template blocks in the dashboard, and use them as you would any other Django template variable:

 
```
{% extends "wagtailadmin/home.html" %}

{% block branding_welcome %}Welcome to the Admin Homepage for {{ root_site }}{% endblock %}

```
    Extending the login form
------------------------

 To add extra controls to the login form, create a template file `dashboard/templates/wagtailadmin/login.html`.

  ### 
`above_login` and `below_login`

 To add content above or below the login form, override these blocks:

 
```
{% extends "wagtailadmin/login.html" %}

{% block above_login %} If you are not Frank you should not be here! {% endblock %}

```
   ### `fields`

 To add extra fields to the login form, override the `fields` block. You will need to add `{{ block.super }}` somewhere in your block to include the username and password fields:

 
```
{% extends "wagtailadmin/login.html" %}

{% block fields %}
    {{ block.super }}
    <li>
        <div>
            <label for="id_two-factor-auth">Two factor auth token</label>
            <input type="text" name="two-factor-auth" id="id_two-factor-auth">
        </div>
    </li>
{% endblock %}

```
   ### `submit_buttons`

 To add extra buttons to the login form, override the `submit_buttons` block. You will need to add `{{ block.super }}` somewhere in your block to include the sign in button:

 
```
{% extends "wagtailadmin/login.html" %}

{% block submit_buttons %}
    {{ block.super }}
    <a href="{% url 'signup' %}"><button type="button" class="button">{% trans 'Sign up' %}</button></a>
{% endblock %}

```
   ### `login_form`

 To completely customise the login form, override the `login_form` block. This block wraps the whole contents of the `<form>` element:

 
```
{% extends "wagtailadmin/login.html" %}

{% block login_form %}
    <p>Some extra form content</p>
    {{ block.super }}
{% endblock %}

```
    Extending the password reset request form
-----------------------------------------

 To add extra controls to the password reset form, create a template file `dashboard/templates/wagtailadmin/account/password_reset/form.html`.

  ### 
`above_form` and `below_form`

 To add content above or below the password reset form, override these blocks:

 
```
{% extends "wagtailadmin/account/password_reset/form.html" %}

{% block above_login %} If you have not received your email within 7 days, call us. {% endblock %}

```
   ### `submit_buttons`

 To add extra buttons to the password reset form, override the `submit_buttons` block. You will need to add `{{ block.super }}` somewhere in your block if you want to include the original submit button:

 
```
{% extends "wagtailadmin/account/password_reset/form.html" %}

{% block submit_buttons %}
    <a href="{% url 'helpdesk' %}">Contact the helpdesk</a>
{% endblock %}

```
    Extending client-side components
--------------------------------

 Some of Wagtail’s admin interface is written as client-side JavaScript with [React](https://reactjs.org/). In order to customise or extend those components, you may need to use React too, as well as other related libraries. To make this easier, Wagtail exposes its React-related dependencies as global variables within the admin. Here are the available packages:

 
```
// 'focus-trap-react'
window.FocusTrapReact;
// 'react'
window.React;
// 'react-dom'
window.ReactDOM;
// 'react-transition-group/CSSTransitionGroup'
window.CSSTransitionGroup;

```
 Wagtail also exposes some of its own React components. You can reuse:

 
```
window.wagtail.components.Icon;
window.wagtail.components.Portal;

```
 Pages containing rich text editors also have access to:

 
```
// 'draft-js'
window.DraftJS;
// 'draftail'
window.Draftail;

// Wagtail’s Draftail-related APIs and components.
window.draftail;
window.draftail.DraftUtils;
window.draftail.ModalWorkflowSource;
window.draftail.ImageModalWorkflowSource;
window.draftail.EmbedModalWorkflowSource;
window.draftail.LinkModalWorkflowSource;
window.draftail.DocumentModalWorkflowSource;
window.draftail.Tooltip;
window.draftail.TooltipEntity;

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/customisation/admin_templates.html>

Custom user models
==================

  Custom user forms example
-------------------------

 This example shows how to add a text field and foreign key field to a custom user model and configure Wagtail user forms to allow the fields values to be updated.

 Create a custom user model. This must at minimum inherit from `AbstractBaseUser` and `PermissionsMixin`. In this case we extend the `AbstractUser` class and add two fields. The foreign key references another model (not shown).

 
```
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    country = models.CharField(verbose_name='country', max_length=255)
    status = models.ForeignKey(MembershipStatus, on_delete=models.SET_NULL, null=True, default=1)

```
 Add the app containing your user model to `INSTALLED_APPS` - it must be above the `'wagtail.users'` line, in order to override Wagtail’s built-in templates - and set [AUTH\_USER\_MODEL](https://docs.djangoproject.com/en/stable/topics/auth/customizing/#substituting-a-custom-user-model) to reference your model. In this example the app is called `users` and the model is `User`

 
```
AUTH_USER_MODEL = 'users.User'

```
 Create your custom user ‘create’ and ‘edit’ forms in your app:

 
```
from django import forms
from django.utils.translation import gettext_lazy as _

from wagtail.users.forms import UserEditForm, UserCreationForm

from users.models import MembershipStatus


class CustomUserEditForm(UserEditForm):
    country = forms.CharField(required=True, label=_("Country"))
    status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status"))


class CustomUserCreationForm(UserCreationForm):
    country = forms.CharField(required=True, label=_("Country"))
    status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status"))

```
 Extend the Wagtail user ‘create’ and ‘edit’ templates. These extended templates should be placed in a template directory `wagtailusers/users`.

 Template create.html:

 
```
{% extends "wagtailusers/users/create.html" %}

{% block extra_fields %}
    <li>{% include "wagtailadmin/shared/field.html" with field=form.country %}</li>
    <li>{% include "wagtailadmin/shared/field.html" with field=form.status %}</li>
{% endblock extra_fields %}

```
 Template edit.html:

 
```
{% extends "wagtailusers/users/edit.html" %}

{% block extra_fields %}
    <li>{% include "wagtailadmin/shared/field.html" with field=form.country %}</li>
    <li>{% include "wagtailadmin/shared/field.html" with field=form.status %}</li>
{% endblock extra_fields %}

```
 The `extra_fields` block allows fields to be inserted below the `last_name` field in the default templates. Other block overriding options exist to allow appending fields to the end or beginning of the existing fields, or to allow all the fields to be redefined.

 Add the wagtail settings to your project to reference the user form additions:

 
```
WAGTAIL_USER_EDIT_FORM = 'users.forms.CustomUserEditForm'
WAGTAIL_USER_CREATION_FORM = 'users.forms.CustomUserCreationForm'
WAGTAIL_USER_CUSTOM_FIELDS = ['country', 'status']

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/customisation/custom_user_models.html>

Third-party tutorials
=====================

  Warning

 The following list is a collection of tutorials and development notes from third-party developers. Some of the older links may not apply to the latest Wagtail versions.

  * [Guide to integrate Wagtail CRX with a Snipcart storefront](https://github.com/justicepelteir/wagtail_crx_snipcart_storefront) (9 October 2022)
* [Adding featured events to the HomePage with Wagtail 4.0 (video)](https://www.youtube.com/watch?v=opQ_ktAXplo) (6 October 2022)
* [Creating an interactive event budgeting tool within Wagtail](https://dev.to/lb/creating-an-interactive-event-budgeting-tool-within-wagtail-53b3) (4 October 2022)
* [Deploy Django Wagtail to Render](https://stackingtabs.medium.com/deploy-django-wagtail-to-render-7d33c4b09bf9) (23 September 2022)
* [Using a migration to apply permissions to Wagtail snippets](https://sixfeetup.com/blog/a-look-at-using-wagtail-snippets-with-django) (7 September 2022)
* [Deploying a Wagtail site to Fly.io - Part 1 of 5](https://usher.dev/posts/wagtail-on-flyio/part-1/) (30 August 2022)
* [Hosting a Wagtail site on Digital Ocean with CapRover](https://medium.com/@Gidsey/hosting-a-wagtail-site-on-digital-ocean-with-caprover-e71306e8d053) (21 July 2022)
* [Add Heading Blocks with Bookmarks in Wagtail](https://enzedonline.com/en/tech-blog/how-to-add-heading-blocks-with-bookmarks-in-wagtail/) (5 July 2022)
* [Import files into Wagtail](https://cynthiakiser.com/blog/2022/07/02/import-files-into-wagtail.html) (2 July 2022)
* [Adding MapBox Blocks to Wagtail Stream Fields](https://enzedonline.com/en/tech-blog/adding-mapbox-blocks-to-wagtail-stream-fields/) (19 June 2022)
* [5 Tips to Streamline Your Wagtail CMS Development](https://profil-software.com/blog/development/5-tips-to-streamline-your-wagtail-cms-development/) (14 June 2022)
* [Wagtail 3 Upgrade: Per Site Features](https://cynthiakiser.com/blog/2022/06/02/wagtail-3-upgrade-part-2.html) (2 June 2022)
* [Wagtail 3 Upgrade: Per User FieldPanel Permissions](https://cynthiakiser.com/blog/2022/06/01/wagtail-3-upgrade-part-1.html) (1 June 2022)
* [Upgrading to Wagtail 3.0](https://enzedonline.com/en/tech-blog/upgrading-to-wagtail-3-0/) (3 May 2022)
* [Django for E-Commerce: A Developers Guide (with Wagtail CMS Tutorial) - Updated](https://snipcart.com/blog/django-ecommerce-tutorial-wagtail-cms) (21 March 2022)
* [How to install Wagtail on Ubuntu 20.04|22.04](https://nextgentips.com/2022/03/01/how-to-install-wagtail-on-ubuntu-20-0422-04/) (1 March 2022)
* [Building a blog with Wagtail (tutorial part 1 of 2)](https://paulgrajewski.medium.com/wagtail-blog-part-1-ad0df1c59f4) (27 February 2022); [part 2 of 2](https://paulgrajewski.medium.com/wagtail-blog-part-2-3fe698e38983) (6 March 2022)
* [Creating a schematic editor within Wagtail CMS with StimulusJS](https://dev.to/lb/creating-a-schematic-editor-within-the-wagtail-cms-with-stimulusjs-n5j) (20 February 2022)
* [Adding Placeholder Text to Wagtail Forms](https://www.coderedcorp.com/blog/adding-placeholder-text-to-wagtail-forms/) (11 February 2022)
* [Deploying a Wagtail 2.16 website to Heroku](https://dev.to/kalobtaulien/deploying-a-wagtail-216-website-to-heroku-1iki) (9 February 2022)
* [Build an E-Commerce Site with Wagtail CMS, Bootstrap & Django Framework](https://dev.to/paulwababu/build-an-e-commerce-site-with-wagtail-cms-bootstrap-django-framework-4jdb) (7 February 2022)
* [Complex Custom Field Pagination in Django (Wagtail)](https://dev.awhileback.net/hacking-the-django-paginator/) (3 February 2022)
* [How to Connect Wagtail and React](https://stackingtabs.medium.com/how-to-connect-wagtail-and-react-7f6d1adf230) (31 January 2022)
* [Wagtail: Dynamically Adding Admin Menu Items](https://cynthiakiser.com/blog/2022/01/25/dynamically-adding-menu-items-in-the-wagtail-admin.html) (25 January 2022)
* [Headless Wagtail, what are the pain points? (with solutions)](https://tommasoamici.com/blog/headless-wagtail-what-are-the-pain-points) (24 January 2022)
* [A collection of UIKit components that can be used as a Wagtail StreamField block](https://pythonawesome.com/a-collection-of-uikit-components-that-can-be-used-as-a-wagtail-streamfield-block/) (14 January 2022)
* [Introduction to Wagtail CMS](https://blog.reallyroxanna.codes/introduction-to-wagtail-cms) (1 January 2022)
* [How to make Wagtail project have good coding style](https://www.accordbox.com/blog/how-to-make-wagtail-project-have-good-coding-style/) (18 December 2021)
* [Wagtail: The Django newcomer - German](https://cmsstash.de/cms-reviews/wagtail) (13 December 2021)
* [Create a Developer Portfolio with Wagtail Part 10: Dynamic Site Settings](https://engineertodeveloper.com/wagtail-dynamic-site-settings/) (3 December 2021)
* [Dockerize Wagtail CMS for your development environment](https://jortdevreeze.com/en/blog/dockerize-wagtail-cms-for-your-development-environment/) (29 November 2021)
* [How To Add an Email Newsletter to Wagtail](https://engineertodeveloper.com/how-to-add-an-email-newsletter-to-wagtail/) (25 November 2021)
* [Dealing with UNIQUE Fields on a Multi-lingual Site](https://enzedonline.com/en/tech-blog/dealing-with-unique-fields-on-a-multi-lingual-site/) (6 November 2021)
* [General Wagtail Tips & Ticks](https://github.com/spapas/wagtail-faq) (26 October 2021)
* [Branching workflows in Wagtail](https://importthis.tech/wagtail-branching-workflows) (12 October 2021)
* [Wagtail is the best python CMS in our galaxy - Russian](https://habr.com/ru/post/582898/) (12 October 2021)
* [Adding Tasks with a Checklist to Wagtail Workflows](https://dev.to/lb/adding-tasks-with-a-checklist-to-wagtail-workflows-29b8) (22 September 2021)
* [How to create a Zen (Focused) mode for the Wagtail CMS admin](https://dev.to/lb/how-to-create-a-zen-focused-mode-for-the-wagtail-cms-admin-3ipk) (5 September 2021)
* [How to Install Wagtail on Shared Hosting without Root (CPanel)](https://chemicloud.com/kb/article/install-wagtail-without-root-access/) (26 August 2021)
* [Django for E-Commerce: A Developers Guide (with Wagtail CMS Tutorial)](https://dev.to/realguillaume/django-for-e-commerce-a-developers-guide-with-wagtail-cms-tutorial-57on) (26 August 2021)
* [How to create a Kanban (Trello style) view of your ModelAdmin data in Wagtail](https://dev.to/lb/how-to-create-a-kanban-trello-style-view-of-your-modeladmin-data-in-wagtail-20eg) (20 August 2021)
* [eBook: The Definitive Guide to Next.js and Wagtail](https://www.accordbox.com/blog/ebook-the-definitive-guide-to-nextjs-and-wagtail/) (19 August 2021)
* [How to build an interactive guide for users in the Wagtail CMS admin](https://dev.to/lb/how-to-build-an-interactive-guide-for-users-in-the-wagtail-cms-admin-2dcp) (19 August 2021)
* [Add Custom User Model (with custom fields like phone no, profile picture) to django or wagtail sites](https://medium.com/@altaf008bd/wagtail-add-custom-fields-including-image-to-custom-user-model-1c976ddbc24) (16 August 2021)
* [File size limits in Nginx and animated GIF support](https://www.meagenvoss.com/blog/random-wagtail-thing-i-learned-file-size-limits-in-nginx-and-animated-gif-support/) (14 August 2021)
* [Deploying Wagtail site on Digital Ocean](https://www.craftplustech.com/blog/deploying-wagtail-site-on-digital-ocean/) (11 August 2021)
* [Multi-language Wagtail websites with XLIFF](https://www.fourdigits.nl/blog/multi-language-wagtail-websites-with-xliff/) (21 June 2021)
* [Add & Configure Mail in Django (or Wagtail) using Sendgrid](https://mpettersson.com/blog/how-to-add-and-configure-a-mail-service-in-django-or-wagtail/) (28 May 2021)
* [Advanced Django Development: How to build a professional CMS for any business? (3 part tutorial)](https://medium.com/engineerx/advanced-django-development-how-to-build-a-professional-cms-for-any-business-part-1-9859cb5b4d24) (2 April 2021)
* [Matomo Analytics with WagtailCMS](https://experiencednovice.dev/blog/matomo-analytics-with-wagtailcms/) (31 March 2021)
* [Dockerizing a Wagtail App](https://www.accordbox.com/blog/dockerizing-wagtail-app/) (16 March 2021)
* [Deploying Wagtail on CentOS8 with MariaDB/Nginx/Gunicorn](https://experiencednovice.dev/blog/deploying-wagtail-on-centos8/) (7 March 2021)
* [How to add a List of Related Fields to a Page](https://learningtofly.dev/blog/wagtail-how-to-add-a-list-of-related-fields-to-a-page) (6 March 2021)
* [Wagtail - `get_absolute_url`, without domain](https://kuttler.eu/code/wagtail-get_absolute_url-without-domain/) (3 March 2021)
* [How To Alternate Blocks in Your Django & Wagtail Templates](https://www.coderedcorp.com/blog/how-to-alternate-blocks-in-your-templates/) (19 February 2021)
* [Build a Blog With Wagtail CMS (second version)](https://www.accordbox.com/blog/build-blog-wagtail-cms-second-version-available/) (13 January 2021)
* [Migrate your Wagtail Website from wagtailtrans to the new wagtail-localize](https://www.cnc.io/en/blog/wagtailtrans-to-wagtail-localize-migration) (10 January 2021)
* [How to Use the Wagtail CMS for Django: An Overview](https://steelkiwi.com/blog/how-to-use-the-wagtail-cms-for-django-an-overview/) (21 December 2020)
* [Wagtail `modeladmin` and a dynamic panels list](https://kuttler.eu/code/wagtail-modeladmin-and-dynamic-panels-list/) (14 December 2020)
* [Install and Deploy Wagtail CMS on pythonanywhere.com](https://www.theinsidetrade.com/blog/install-and-deploy-wagtail-cms-pythonanywherecom/) (14 December 2020)
* [Overriding the admin CSS in Wagtail](https://www.yellowduck.be/posts/overriding-the-admin-css-in-wagtail/) (4 December 2020)
* [Migrating your Wagtail site to a different database engine](https://www.yellowduck.be/posts/migrating-your-wagtail-site-to-a-different-database-engine/) (3 December 2020)
* [Wagtail for Django Devs: Create a Developer Portfolio](https://dev.to/brian101co/wagtail-for-django-devs-create-a-developer-portfolio-5e75) (30 November 2020)
* [Create a Developer Portfolio with Wagtail Tutorial Series](https://engineertodeveloper.com/category/wagtail/) (11 November 2020)
* [Wagtail Instagram New oEmbed API](https://www.codista.com/en/blog/wagtail-instagram-new-oembed-api/) (5 November 2020)
* [Image upload in wagtail forms](https://dev.to/lb/image-uploads-in-wagtail-forms-39pl) (21 October 2020)
* [Adding a timeline of your wagtail Posts](https://spapas.github.io/2020/09/18/wagtail-add-posts-timeline/) (18 September 2020)
* [How to create amazing SSR website with Wagtail 2 + Vue 3](https://dev.to/robert197/how-to-create-amazing-ssr-website-with-wagtail-2-vue-3-463j) (1 September 2020)
* [Migrate Wagtail Application Database from SQLite to PostgreSQL](https://medium.com/@ochieng.grace/migrate-wagtail-application-database-from-sqlite-to-postgresql-32f705f2f5f4) (5 June 2020)
* [How to Build Scalable Websites with Wagtail and Nuxt](https://devs-group.medium.com/why-our-websites-stay-ahead-c608e3f4bea4) (14 May 2020)
* [Wagtail multi-language and internationalization](https://dev.to/codista_/wagtail-multi-language-and-internationalization-2gkf) (8 April 2020)
* [Wagtail SEO Guide](https://www.accordbox.com/blog/wagtail-seo-guide/) (30 March 2020)
* [Adding a latest-changes list to your Wagtail site](https://spapas.github.io/2020/03/27/wagtail-add-latest-changes/) (27 March 2020)
* [How to support multi-language in Wagtail CMS](https://www.accordbox.com/blog/how-support-multi-language-wagtail-cms/) (22 February 2020)
* [Deploying my Wagtail blog to Digital Ocean](https://rosederwelt.com/deploying-my-wagtail-blog-digital-ocean-pt-1/) Part 1 of a 2 part series (29 January 2020)
* [How to Create and Manage Menus of Wagtail application](https://www.accordbox.com/blog/wagtail-tutorial-12-how-create-and-manage-menus-wagtail-application/) - An updated overview of implementing menus (22 February 2020)
* [Adding a React component in Wagtail Admin](https://dev.to/lb/adding-a-react-component-in-wagtail-admin-3e) - Shows how to render an interactive timeline of published Pages (30 December 2019)
* [Wagtail API - how to customise the detail URL](https://dev.to/wagtail/wagtail-api-how-to-customize-the-detail-url-2j3l) (19 December 2020)
* [How to Add Django Models to the Wagtail Admin](https://dev.to/revsys/how-to-add-django-models-to-the-wagtail-admin-1mdi) (27 August 2019)
* [How do I Wagtail](https://foundation.mozilla.org/en/docs/how-do-i-wagtail/) - An Editor’s Guide for Mozilla’s usage of Wagtail (25 April 2019)
* [Learn Wagtail](https://learnwagtail.com/) - Regular video tutorials about all aspects of Wagtail (1 March 2019)
* [How to add buttons to ModelAdmin Index View in Wagtail CMS](https://timonweb.com/tutorials/how-to-add-buttons-to-modeladmin-index-view-in-wagtail-cms/) (23 January 2019)
* [Wagtail Tutorial Series](https://www.accordbox.com/blog/wagtail-tutorials/) (20 January 2019)
* [How to Deploy Wagtail to Google App Engine PaaS (Video)](https://www.youtube.com/watch?v=uD9PTag2-PQ) (18 December 2018)
* [How To Prevent Users From Creating Pages by Page Type](https://timonweb.com/tutorials/prevent-users-from-creating-certain-page-types-in-wagtail-cms/) (25 October 2018)
* [How to Deploy Wagtail to Jelastic PaaS](https://jelastic.com/blog/deploy-wagtail-python-cms/) (11 October 2018)
* [Basic Introduction to Setting Up Wagtail](https://medium.com/nonstopio/wagtail-an-open-source-cms-cec6b93706da) (15 August 2018)
* [E-Commerce for Django developers (with Wagtail shop tutorial)](https://snipcart.com/blog/django-ecommerce-tutorial-wagtail-cms) (5 July 2018)
* [Supporting StreamFields, Snippets and Images in a Wagtail GraphQL API](https://wagtail.org/blog/graphql-with-streamfield/) (14 June 2018)
* [Wagtail and GraphQL](https://jossingram.wordpress.com/2018/04/19/wagtail-and-graphql/) (19 April 2018)
* [Wagtail and Azure storage blob containers](https://jossingram.wordpress.com/2017/11/29/wagtail-and-azure-storage-blob-containers/) (29 November 2017)
* [Building TwilioQuest with Twilio Sync, Django [incl. Wagtail], and Vue.js](https://www.twilio.com/blog/2017/11/building-twilioquest-with-twilio-sync-django-and-vue-js.html) (6 November 2017)
* [Upgrading from Wagtail 1.0 to Wagtail 1.11](https://www.caktusgroup.com/blog/2017/07/19/upgrading-wagtail/) (19 July 2017)
* [Wagtail-Multilingual: a simple project to demonstrate how multilingual is implemented](https://github.com/cristovao-alves/Wagtail-Multilingual) (31 January 2017)
* [Wagtail: 2 Steps for Adding Pages Outside of the CMS](https://www.caktusgroup.com/blog/2016/02/15/wagtail-2-steps-adding-pages-outside-cms/) (15 February 2016)
* [Adding a Twitter Widget for Wagtail’s new StreamField](https://jossingram.wordpress.com/2015/04/02/adding-a-twitter-widget-for-wagtails-new-streamfield/) (2 April 2015)
* [Working With Wagtail: Menus](https://www.tivix.com/blog/working-with-wagtail-menus/) (22 January 2015)
* [Upgrading Wagtail to use Django 1.7 locally using vagrant](https://jossingram.wordpress.com/2014/12/10/upgrading-wagtail-to-use-django-1-7-locally-using-vagrant/) (10 December 2014)
* [Wagtail redirect page. Can link to page, URL and document](https://gist.github.com/alej0varas/e7e334643ceab6e65744) (24 September 2014)
* [Outputting JSON for a model with properties and db fields in Wagtail/Django](https://jossingram.wordpress.com/2014/09/24/outputing-json-for-a-model-with-properties-and-db-fields-in-wagtaildjango/) (24 September 2014)
* [Bi-lingual website using Wagtail CMS](https://jossingram.wordpress.com/2014/09/17/bi-lingual-website-using-wagtail-cms/) (17 September 2014)
* [Wagtail CMS – Lesser known features](https://jossingram.wordpress.com/2014/09/12/wagtail-cms-lesser-known-features/) (12 September 2014)
* [Wagtail notes: stateful on/off hallo.js plugins](https://www.coactivate.org/projects/ejucovy/blog/2014/08/09/wagtail-notes-stateful-onoff-hallojs-plugins/) (9 August 2014)
* [Add some blockquote buttons to Wagtail CMS’ WYSIWYG Editor](https://jossingram.wordpress.com/2014/07/24/add-some-blockquote-buttons-to-wagtail-cms-wysiwyg-editor/) (24 July 2014)
* [Adding Bread Crumbs to the front end in Wagtail CMS](https://jossingram.wordpress.com/2014/07/01/adding-bread-crumbs-to-the-front-end-in-wagtail-cms/) (1 July 2014)
* [Extending hallo.js using Wagtail hooks](https://gist.github.com/jeffrey-hearn/502d0914fa4a930f08ac) (9 July 2014)
* [Wagtail notes: custom tabs per page type](https://www.coactivate.org/projects/ejucovy/blog/2014/05/10/wagtail-notes-custom-tabs-per-page-type/) (10 May 2014)
* [Wagtail notes: managing redirects as pages](https://www.coactivate.org/projects/ejucovy/blog/2014/05/10/wagtail-notes-managing-redirects-as-pages/) (10 May 2014)
* [Wagtail notes: dynamic templates per page](https://www.coactivate.org/projects/ejucovy/blog/2014/05/10/wagtail-notes-dynamic-templates-per-page/) (10 May 2014)
* [Wagtail notes: type-constrained PageChooserPanel](https://www.coactivate.org/projects/ejucovy/blog/2014/05/09/wagtail-notes-type-constrained-pagechooserpanel/) (9 May 2014)

 You can also find more resources from the community on [Awesome Wagtail](https://github.com/springload/awesome-wagtail).

  Tip
---

 We are working on a collection of Wagtail tutorials and best practices. Please tweet [@WagtailCMS](https://twitter.com/WagtailCMS) or [contact us directly](mailto:hello%40wagtail.org) to share your Wagtail HOWTOs, development notes or site launches.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/third_party_tutorials.html>

Testing your Wagtail site
=========================

 Wagtail comes with some utilities that simplify writing tests for your site.

  WagtailPageTestCase
-------------------

 ***class* wagtail.test.utils.WagtailPageTestCase** `WagtailPageTestCase` extends `django.test.TestCase`, adding a few new `assert` methods. You should extend this class to make use of its methods:

 
```
from wagtail.test.utils import WagtailPageTestCase
from myapp.models import MyPage

class MyPageTests(WagtailPageTestCase):
    def test_can_create_a_page(self):
        ...

```
 **assertPageIsRoutable(*page, route\_path=”/”, msg=None*)**

 Asserts that `page` can be routed to without raising a `Http404` error.

 For page types with multiple routes, you can use `route_path` to specify an alternate route to test.

 This assertion is great for getting coverage on custom routing logic for page types. Here is an example:

 
```
from wagtail.test.utils import WagtailPageTestCase
from myapp.models import EventListPage

class EventListPageRoutabilityTests(WagtailPageTestCase):
    @classmethod
    def setUpTestData(cls):
        # create page(s) for testing
        ...

    def test_default_route(self):
        self.assertPageIsRoutable(self.page)

    def test_year_archive_route(self):
        # NOTE: Despite this page type raising a 404 when no events exist for
        # the specified year, routing should still be successful
        self.assertPageIsRoutable(self.page, "archive/year/1984/")


```
 **assertPageIsRenderable(*page, route\_path=”/”, query\_data=None, post\_data=None, user=None, accept\_404=False, accept\_redirect=False, msg=None*)**

 Asserts that `page` can be rendered without raising a fatal error.

 For page types with multiple routes, you can use `route_path` to specify a partial path to be added to the page’s regular `url`.

 When `post_data` is provided, the test makes a `POST` request with `post_data` in the request body. Otherwise, a `GET` request is made.

 When supplied, `query_data` is always converted to a querystring and added to the request URL.

 When `user` is provided, the test is conducted with them as the active user.

 By default, the assertion will fail if the request to the page URL results in a 301, 302 or 404 HTTP response. If you are testing a page/route where a 404 response is expected, you can use `accept_404=True` to indicate this, and the assertion will pass when encountering a 404 response. Likewise, if you are testing a page/route where a redirect response is expected, you can use `accept_redirect=True` to indicate this, and the assertion will pass when encountering 301 or 302 response.

 This assertion is great for getting coverage on custom rendering logic for page types. Here is an example:

 
```
def test_default_route_rendering(self):
    self.assertPageIsRenderable(self.page)

def test_year_archive_route_with_zero_matches(self):
    # NOTE: Should raise a 404 when no events exist for the specified year
    self.assertPageIsRenderable(self.page, "archive/year/1984/", accept_404=True)

def test_month_archive_route_with_zero_matches(self):
    # NOTE: Should redirect to year-specific view when no events exist for the specified month
    self.assertPageIsRenderable(self.page, "archive/year/1984/07/", accept_redirect=True)

```
 **assertPageIsEditable(*page, post\_data=None, user=None, msg=None*)**

 Asserts that the page edit view works for `page` without raising a fatal error.

 When `user` is provided, the test is conducted with them as the active user. Otherwise, a superuser is created and used for the test.

 After a successful `GET` request, a `POST` request is made with field data in the request body. If `post_data` is provided, that will be used for this purpose. If not, this data will be extracted from the `GET` response HTML.

 This assertion is great for getting coverage on custom fields, panel configuration and custom validation logic. Here is an example:

 
```
def test_editability(self):
    self.assertPageIsEditable(self.page)

def test_editability_on_post(self):
    self.assertPageIsEditable(
        self.page,
        post_data={
            "title": "Fabulous events",
            "slug": "events",
            "show_featured": True,
            "show_expired": False,
            "action-publish": "",
        }
    )

```
 **assertPageIsPreviewable(*page, mode=””, post\_data=None, user=None, msg=None*)**

 Asserts that the page preview view can be loaded for `page` without raising a fatal error.

 For page types that support different preview modes, you can use `mode` to specify the preview mode to be tested.

 When `user` is provided, the test is conducted with them as the active user. Otherwise, a superuser is created and used for the test.

 To load the preview, the test client needs to make a `POST` request including all required field data in the request body. If `post_data` is provided, that will be used for this purpose. If not, the method will attempt to extract this data from the page edit view.

 This assertion is great for getting coverage on custom preview modes, or getting reassurance that custom rendering logic is compatible with Wagtail’s preview mode. Here is an example:

 
```
def test_general_previewability(self):
    self.assertPageIsPreviewable(self.page)

def test_archive_previewability(self):
    self.assertPageIsPreviewable(self.page, mode="year-archive")

```
 **assertCanCreateAt(*parent\_model, child\_model, msg=None*)** Assert a particular child Page type can be created under a parent Page type. `parent_model` and `child_model` should be the Page classes being tested.

 
```
def test_can_create_under_home_page(self):
    # You can create a ContentPage under a HomePage
    self.assertCanCreateAt(HomePage, ContentPage)

```
 **assertCanNotCreateAt(*parent\_model, child\_model, msg=None*)** Assert a particular child Page type can not be created under a parent Page type. `parent_model` and `child_model` should be the Page classes being tested.

 
```
def test_cant_create_under_event_page(self):
    # You can not create a ContentPage under an EventPage
    self.assertCanNotCreateAt(EventPage, ContentPage)

```
 **assertCanCreate(*parent, child\_model, data, msg=None*)** Assert that a child of the given Page type can be created under the parent, using the supplied POST data.

 `parent` should be a Page instance, and `child_model` should be a Page subclass. `data` should be a dict that will be POSTed at the Wagtail admin Page creation method.

 
```
from wagtail.test.utils.form_data import nested_form_data, streamfield

def test_can_create_content_page(self):
    # Get the HomePage
    root_page = HomePage.objects.get(pk=2)

    # Assert that a ContentPage can be made here, with this POST data
    self.assertCanCreate(root_page, ContentPage, nested_form_data({
        'title': 'About us',
        'body': streamfield([
            ('text', 'Lorem ipsum dolor sit amet'),
        ])
    }))

```
 See [Form data helpers](#form-data-test-helpers) for a set of functions useful for constructing POST data.

 **assertAllowedParentPageTypes(*child\_model, parent\_models, msg=None*)** Test that the only page types that `child_model` can be created under are `parent_models`.

 The list of allowed parent models may differ from those set in `Page.parent_page_types`, if the parent models have set `Page.subpage_types`.

 
```
def test_content_page_parent_pages(self):
    # A ContentPage can only be created under a HomePage
    # or another ContentPage
    self.assertAllowedParentPageTypes(
        ContentPage, {HomePage, ContentPage})

    # An EventPage can only be created under an EventIndex
    self.assertAllowedParentPageTypes(
        EventPage, {EventIndex})

```
 **assertAllowedSubpageTypes(*parent\_model, child\_models, msg=None*)** Test that the only page types that can be created under `parent_model` are `child_models`.

 The list of allowed child models may differ from those set in `Page.subpage_types`, if the child models have set `Page.parent_page_types`.

 
```
def test_content_page_subpages(self):
    # A ContentPage can only have other ContentPage children
    self.assertAllowedSubpageTypes(
        ContentPage, {ContentPage})

    # A HomePage can have ContentPage and EventIndex children
    self.assertAllowedSubpageTypes(
        HomePage, {ContentPage, EventIndex})

```
   Form data helpers
-----------------

 The `assertCanCreate` method requires page data to be passed in the same format that the page edit form would submit. For complex page types, it can be difficult to construct this data structure by hand; the `wagtail.test.utils.form_data` module provides a set of helper functions to assist with this.

   Fixtures
--------

  ### Using `dumpdata`

 Creating [fixtures](https://docs.djangoproject.com/en/stable/howto/initial-data/ "(in Django v4.1)") for tests is best done by creating content in a development environment, and using Django’s [dumpdata](https://docs.djangoproject.com/en/stable/ref/django-admin/#django-admin-dumpdata) command.

 Note that by default `dumpdata` will represent `content_type` by the primary key; this may cause consistency issues when adding / removing models, as content types are populated separately from fixtures. To prevent this, use the `--natural-foreign` switch, which represents content types by `["app", "model"]` instead.

   ### Manual modification

 You could modify the dumped fixtures manually, or even write them all by hand. Here are a few things to be wary of.

  #### Custom Page models

 When creating customised Page models in fixtures, you will need to add both a `wagtailcore.page` entry, and one for your custom Page model.

 Let’s say you have a `website` module which defines a `Homepage(Page)` class. You could create such a homepage in a fixture with:

 
```
[
    {
        "model": "wagtailcore.page",
        "pk": 3,
        "fields": {
            "title": "My Customer's Homepage",
            "content_type": ["website", "homepage"],
            "depth": 2
        }
    },
    {
        "model": "website.homepage",
        "pk": 3,
        "fields": {}
    }
]

```
   #### Treebeard fields

 Filling in the `path` / `numchild` / `depth` fields is necessary in order for tree operations like `get_parent()` to work correctly. `url_path` is another field that can cause errors in some uncommon cases if it isn’t filled in.

 The [Treebeard docs](https://django-treebeard.readthedocs.io/en/latest/mp_tree.html) might help in understanding how this works.

   

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/testing.html>

Wagtail API v2 Usage Guide
==========================

 The Wagtail API module exposes a public, read only, JSON-formatted API which can be used by external clients (such as a mobile app) or the site’s frontend.

 This document is intended for developers using the API exposed by Wagtail. For documentation on how to enable the API module in your Wagtail site, see [Wagtail API v2 Configuration Guide](configuration)

 Contents

 * [Fetching content](#fetching-content)


	+ [Example response](#example-response)
	+ [Custom page fields in the API](#custom-page-fields-in-the-api)
	+ [Pagination](#pagination)
	+ [Ordering](#ordering)
	
	
		- [Random ordering](#random-ordering)
	+ [Filtering](#filtering)
	+ [Filtering by tree position (pages only)](#filtering-by-tree-position-pages-only)
	+ [Filtering pages by site](#filtering-pages-by-site)
	+ [Search](#search)
	
	
		- [Search operator](#search-operator)
	+ [Special filters for internationalized sites](#special-filters-for-internationalized-sites)
	
	
		- [Filtering pages by locale](#filtering-pages-by-locale)
		- [Getting translations of a page](#getting-translations-of-a-page)
	+ [Fields](#fields)
	
	
		- [Additional fields](#additional-fields)
		- [All fields](#all-fields)
		- [Removing fields](#removing-fields)
		- [Removing all default fields](#removing-all-default-fields)
	+ [Detail views](#detail-views)
	+ [Finding pages by HTML path](#finding-pages-by-html-path)
* [Default endpoint fields](#default-endpoint-fields)


	+ [Common fields](#common-fields)
	+ [Pages](#pages)
	+ [Images](#images)
	+ [Documents](#documents)
* [Changes since v1](#changes-since-v1)


	+ [Breaking changes](#breaking-changes)
	+ [Major features](#major-features)
	+ [Minor features](#minor-features)

  Fetching content
----------------

 To fetch content over the API, perform a `GET` request against one of the following endpoints:

 * Pages `/api/v2/pages/`
* Images `/api/v2/images/`
* Documents `/api/v2/documents/`

  Note

 The available endpoints and their URLs may vary from site to site, depending on how the API has been configured.

   ### Example response

 Each response contains the list of items (`items`) and the total count (`meta.total_count`). The total count is irrespective of pagination.

 
```
GET /api/v2/endpoint_name/

HTTP 200 OK
Content-Type: application/json

{
    "meta": {
        "total_count": "total number of results"
    },
    "items": [
        {
            "id": 1,
            "meta": {
                "type": "app_name.ModelName",
                "detail_url": "http://api.example.com/api/v2/endpoint_name/1/"
            },
            "field": "value"
        },
        {
            "id": 2,
            "meta": {
                "type": "app_name.ModelName",
                "detail_url": "http://api.example.com/api/v2/endpoint_name/2/"
            },
            "field": "different value"
        }
    ]
}

```
   ### Custom page fields in the API

 Wagtail sites contain many page types, each with their own set of fields. The `pages` endpoint will only expose the common fields by default (such as `title` and `slug`).

 To access custom page fields with the API, select the page type with the `?type` parameter. This will filter the results to only include pages of that type but will also make all the exported custom fields for that type available in the API.

 For example, to access the `published_date`, `body` and `authors` fields on the `blog.BlogPage` model in the [configuration docs](configuration#apiv2-page-fields-configuration):

 
```
GET /api/v2/pages/?type=blog.BlogPage&fields=published_date,body,authors(name)

HTTP 200 OK
Content-Type: application/json

{
    "meta": {
        "total_count": 10
    },
    "items": [
        {
            "id": 1,
            "meta": {
                "type": "blog.BlogPage",
                "detail_url": "http://api.example.com/api/v2/pages/1/",
                "html_url": "http://www.example.com/blog/my-blog-post/",
                "slug": "my-blog-post",
                "first_published_at": "2016-08-30T16:52:00Z"
            },
            "title": "Test blog post",
            "published_date": "2016-08-30",
            "authors": [
                {
                    "id": 1,
                    "meta": {
                        "type": "blog.BlogPageAuthor",
                    },
                    "name": "Karl Hobley"
                }
            ]
        },

        ...
    ]
}

```
  Note

 Only fields that have been explicitly exported by the developer may be used in the API. This is done by adding a `api_fields` attribute to the page model. You can read about configuration [here](configuration#apiv2-page-fields-configuration).

  This doesn’t apply to images/documents as there is only one model exposed in those endpoints. But for projects that have customized image/document models, the `api_fields` attribute can be used to export any custom fields into the API.

   ### Pagination

 The number of items in the response can be changed by using the `?limit` parameter (default: 20) and the number of items to skip can be changed by using the `?offset` parameter.

 For example:

 
```
GET /api/v2/pages/?offset=20&limit=20

HTTP 200 OK
Content-Type: application/json

{
    "meta": {
        "total_count": 50
    },
    "items": [
        pages 20 - 40 will be listed here.
    ]
}

```
  Note

 There may be a maximum value for the `?limit` parameter. This can be modified in your project settings by setting `WAGTAILAPI_LIMIT_MAX` to either a number (the new maximum value) or `None` (which disables maximum value check).

    ### Ordering

 The results can be ordered by any field by setting the `?order` parameter to the name of the field to order by.

 
```
GET /api/v2/pages/?order=title

HTTP 200 OK
Content-Type: application/json

{
    "meta": {
        "total_count": 50
    },
    "items": [
        pages will be listed here in ascending title order (a-z)
    ]
}

```
 The results will be ordered in ascending order by default. This can be changed to descending order by prefixing the field name with a `-` sign.

 
```
GET /api/v2/pages/?order=-title

HTTP 200 OK
Content-Type: application/json

{
    "meta": {
        "total_count": 50
    },
    "items": [
        pages will be listed here in descending title order (z-a)
    ]
}

```
  Note

 Ordering is case-sensitive so lowercase letters are always ordered after uppercase letters when in ascending order.

   #### Random ordering

 Passing `random` into the `?order` parameter will make results return in a random order. If there is no caching, each request will return results in a different order.

 
```
GET /api/v2/pages/?order=random

HTTP 200 OK
Content-Type: application/json

{
    "meta": {
        "total_count": 50
    },
    "items": [
        pages will be listed here in random order
    ]
}

```
  Note

 It’s not possible to use `?offset` while ordering randomly because consistent random ordering cannot be guaranteed over multiple requests (so requests for subsequent pages may return results that also appeared in previous pages).

     ### Filtering

 Any field may be used in an exact match filter. Use the filter name as the parameter and the value to match against.

 For example, to find a page with the slug “about”:

 
```
GET /api/v2/pages/?slug=about

HTTP 200 OK
Content-Type: application/json

{
    "meta": {
        "total_count": 1
    },
    "items": [
        {
            "id": 10,
            "meta": {
                "type": "standard.StandardPage",
                "detail_url": "http://api.example.com/api/v2/pages/10/",
                "html_url": "http://www.example.com/about/",
                "slug": "about",
                "first_published_at": "2016-08-30T16:52:00Z"
            },
            "title": "About"
        },
    ]
}

```
   ### Filtering by tree position (pages only)

 Pages can additionally be filtered by their relation to other pages in the tree.

 The `?child_of` filter takes the id of a page and filters the list of results to contain only direct children of that page.

 For example, this can be useful for constructing the main menu, by passing the id of the homepage to the filter:

 
```
GET /api/v2/pages/?child_of=2&show_in_menus=true

HTTP 200 OK
Content-Type: application/json

{
    "meta": {
        "total_count": 5
    },
    "items": [
        {
            "id": 3,
            "meta": {
                "type": "blog.BlogIndexPage",
                "detail_url": "http://api.example.com/api/v2/pages/3/",
                "html_url": "http://www.example.com/blog/",
                "slug": "blog",
                "first_published_at": "2016-09-21T13:54:00Z"
            },
            "title": "About"
        },
        {
            "id": 10,
            "meta": {
                "type": "standard.StandardPage",
                "detail_url": "http://api.example.com/api/v2/pages/10/",
                "html_url": "http://www.example.com/about/",
                "slug": "about",
                "first_published_at": "2016-08-30T16:52:00Z"
            },
            "title": "About"
        },

        ...
    ]
}

```
 The `?ancestor_of` filter takes the id of a page and filters the list to only include ancestors of that page (parent, grandparent etc.) all the way down to the site’s root page.

 For example, when combined with the `type` filter it can be used to find the particular `blog.BlogIndexPage` a `blog.BlogPage` belongs to. By itself, it can be used to to construct a breadcrumb trail from the current page back to the site’s root page.

 The `?descendant_of` filter takes the id of a page and filter the list to only include descendants of that page (children, grandchildren etc.).

   ### Filtering pages by site

  New in version 4.0.

  By default, the API will look for the site based on the hostname of the request. In some cases, you might want to query pages belonging to a different site. The `?site=` filter is used to filter the listing to only include pages that belong to a specific site. The filter requires the configured hostname of the site. If you have multiple sites using the same hostname but a different port number, it’s possible to filter by port number using the format `hostname:port`. For example:

 
```
GET /api/v2/pages/?site=demo-site.local
GET /api/v2/pages/?site=demo-site.local:8080

```
   ### Search

 Passing a query to the `?search` parameter will perform a full-text search on the results.

 The query is split into “terms” (by word boundary), then each term is normalized (lowercased and unaccented).

 For example: `?search=James+Joyce`

  #### Search operator

 The `search_operator` specifies how multiple terms in the query should be handled. There are two possible values:

 * `and` - All terms in the search query (excluding stop words) must exist in each result
* `or` - At least one term in the search query must exist in each result

 The `or` operator is generally better than `and` as it allows the user to be inexact with their query and the ranking algorithm will make sure that irrelevant results are not returned at the top of the page.

 The default search operator depends on whether the search engine being used by the site supports ranking. If it does (Elasticsearch), the operator will default to `or`. Otherwise (database), it will default to `and`.

 For the same reason, it’s also recommended to use the `and` operator when using `?search` in conjunction with `?order` (as this disables ranking).

 For example: `?search=James+Joyce&order=-first_published_at&search_operator=and`

    ### Special filters for internationalized sites

 When `WAGTAIL_I18N_ENABLED` is set to `True` (see [Enabling internationalisation](../../i18n#enabling-internationalisation) for more details) two new filters are made available on the pages endpoint.

  #### Filtering pages by locale

 The `?locale=` filter is used to filter the listing to only include pages in the specified locale. For example:

 
```
GET /api/v2/pages/?locale=en-us

HTTP 200 OK
Content-Type: application/json

{
    "meta": {
        "total_count": 5
    },
    "items": [
        {
            "id": 10,
            "meta": {
                "type": "standard.StandardPage",
                "detail_url": "http://api.example.com/api/v2/pages/10/",
                "html_url": "http://www.example.com/usa-page/",
                "slug": "usa-page",
                "first_published_at": "2016-08-30T16:52:00Z",
                "locale": "en-us"
            },
            "title": "American page"
        },

        ...
    ]
}

```
   #### Getting translations of a page

 The `?translation_of` filter is used to filter the listing to only include pages that are a translation of the specified page ID. For example:

 
```
GET /api/v2/pages/?translation_of=10

HTTP 200 OK
Content-Type: application/json

{
    "meta": {
        "total_count": 2
    },
    "items": [
        {
            "id": 11,
            "meta": {
                "type": "standard.StandardPage",
                "detail_url": "http://api.example.com/api/v2/pages/11/",
                "html_url": "http://www.example.com/gb-page/",
                "slug": "gb-page",
                "first_published_at": "2016-08-30T16:52:00Z",
                "locale": "en-gb"
            },
            "title": "British page"
        },
        {
            "id": 12,
            "meta": {
                "type": "standard.StandardPage",
                "detail_url": "http://api.example.com/api/v2/pages/12/",
                "html_url": "http://www.example.com/fr-page/",
                "slug": "fr-page",
                "first_published_at": "2016-08-30T16:52:00Z",
                "locale": "fr"
            },
            "title": "French page"
        },
    ]
}

```
    ### Fields

 By default, only a subset of the available fields are returned in the response. The `?fields` parameter can be used to both add additional fields to the response and remove default fields that you know you won’t need.

  #### Additional fields

 Additional fields can be added to the response by setting `?fields` to a comma-separated list of field names you want to add.

 For example, `?fields=body,feed_image` will add the `body` and `feed_image` fields to the response.

 This can also be used across relationships. For example, `?fields=body,feed_image(width,height)` will nest the `width` and `height` of the image in the response.

   #### All fields

 Setting `?fields` to an asterisk (`*`) will add all available fields to the response. This is useful for discovering what fields have been exported.

 For example: `?fields=*`

   #### Removing fields

 Fields you know that you do not need can be removed by prefixing the name with a `-` and adding it to `?fields`.

 For example, `?fields=-title,body` will remove `title` and add `body`.

 This can also be used with the asterisk. For example, `?fields=*,-body` adds all fields except for `body`.

   #### Removing all default fields

 To specify exactly the fields you need, you can set the first item in fields to an underscore (`_`) which removes all default fields.

 For example, `?fields=_,title` will only return the title field.

    ### Detail views

 You can retrieve a single object from the API by appending its id to the end of the URL. For example:

 * Pages `/api/v2/pages/1/`
* Images `/api/v2/images/1/`
* Documents `/api/v2/documents/1/`

 All exported fields will be returned in the response by default. You can use the `?fields` parameter to customize which fields are shown.

 For example: `/api/v2/pages/1/?fields=_,title,body` will return just the `title` and `body` of the page with the id of 1.

   ### Finding pages by HTML path

 You can find an individual page by its HTML path using the `/api/v2/pages/find/?html_path=<path>` view.

 This will return either a `302` redirect response to that page’s detail view, or a `404` not found response.

 For example: `/api/v2/pages/find/?html_path=/` always redirects to the homepage of the site

    Default endpoint fields
-----------------------

  ### Common fields

 These fields are returned by every endpoint.

 **`id` (number)** The unique ID of the object

  Note

 Except for page types, every other content type has its own id space so you must combine this with the `type` field in order to get a unique identifier for an object.

  **`type` (string)** The type of the object in `app_label.ModelName` format

 **`detail_url` (string)** The URL of the detail view for the object

   ### Pages

 **`title` (string)** **`meta.slug` (string)** **`meta.show_in_menus` (boolean)** **`meta.seo_title` (string)** **`meta.search_description` (string)** **`meta.first_published_at` (date/time)** These values are taken from their corresponding fields on the page

 **`meta.html_url` (string)** If the site has an HTML frontend that’s generated by Wagtail, this field will be set to the URL of this page

 **`meta.parent`** Nests some information about the parent page (only available on detail views)

 **`meta.alias_of` (dictionary)** If the page marked as an alias return original page id and full url

   ### Images

 **`title` (string)** The value of the image’s title field. Within Wagtail, this is used in the image’s `alt` HTML attribute.

 **`width` (number)** **`height` (number)** The size of the original image file

 **`meta.tags` (list of strings)** A list of tags associated with the image

   ### Documents

 **`title` (string)** The value of the document’s title field

 **`meta.tags` (list of strings)** A list of tags associated with the document

 **`meta.download_url` (string)** A URL to the document file

    Changes since v1
----------------

  ### Breaking changes

 * The results list in listing responses has been renamed to `items` (was previously either `pages`, `images` or `documents`)

   ### Major features

 * The `fields` parameter has been improved to allow removing fields, adding all fields and customising nested fields

   ### Minor features

 * `html_url`, `slug`, `first_published_at`, `expires_at` and `show_in_menus` fields have been added to the pages endpoint
* `download_url` field has been added to the documents endpoint
* Multiple page types can be specified in `type` parameter on pages endpoint
* `true` and `false` may now be used when filtering boolean fields
* `order` can now be used in conjunction with `search`
* `search_operator` parameter was added

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/api/v2/usage.html>

How to build a site with AMP support
====================================

 This recipe document describes a method for creating an [AMP](https://amp.dev/) version of a Wagtail site and hosting it separately to the rest of the site on a URL prefix. It also describes how to make Wagtail render images with the `<amp-img>` tag when a user is visiting a page on the AMP version of the site.

  Overview
--------

 In the next section, we will add a new URL entry that points at Wagtail’s internal `serve()` view which will have the effect of rendering the whole site again under the `/amp` prefix.

 Then, we will add some utilities that will allow us to track whether the current request is in the `/amp` prefixed version of the site without needing a request object.

 After that, we will add a template context processor to allow us to check from within templates which version of the site is being rendered.

 Then, finally, we will modify the behaviour of the `{% image %}` tag to make it render `<amp-img>` tags when rendering the AMP version of the site.

   Creating the second page tree
-----------------------------

 We can render the whole site at a different prefix by duplicating the Wagtail URL in the project `urls.py` file and giving it a prefix. This must be before the default URL from Wagtail, or it will try to find `/amp` as a page:

 
```
# <project>/urls.py

urlpatterns += [
    # Add this line just before the default ``include(wagtail_urls)`` line
    path('amp/', include(wagtail_urls)),

    path('', include(wagtail_urls)),
]

```
 If you now open `http://localhost:8000/amp/` in your browser, you should see the homepage.

   Making pages aware of “AMP mode”
--------------------------------

 All the pages will now render under the `/amp` prefix, but right now there isn’t any difference between the AMP version and the normal version.

 To make changes, we need to add a way to detect which URL was used to render the page. To do this, we will have to wrap Wagtail’s `serve()` view and set a thread-local to indicate to all downstream code that AMP mode is active.

  Note

 Why a thread-local?

 (feel free to skip this part if you’re not interested)

 Modifying the `request` object would be the most common way to do this. However, the image tag rendering is performed in a part of Wagtail that does not have access to the request.

 Thread-locals are global variables that can have a different value for each running thread. As each thread only handles one request at a time, we can use it as a way to pass around data that is specific to that request without having to pass the request object everywhere.

 Django uses thread-locals internally to track the currently active language for the request.

 Python implements thread-local data through the `threading.local` class, but as of Django 3.x, multiple requests can be handled in a single thread and so thread-locals will no longer be unique to a single request. Django therefore provides `asgiref.Local` as a drop-in replacement.

  Now let’s create that thread-local and some utility functions to interact with it, save this module as `amp_utils.py` in an app in your project:

 
```
# <app>/amp_utils.py

from contextlib import contextmanager
from asgiref.local import Local

_amp_mode_active = Local()

@contextmanager
def activate_amp_mode():
    """
    A context manager used to activate AMP mode
    """
    _amp_mode_active.value = True
    try:
        yield
    finally:
        del _amp_mode_active.value

def amp_mode_active():
    """
    Returns True if AMP mode is currently active
    """
    return hasattr(_amp_mode_active, 'value')

```
 This module defines two functions:

 * `activate_amp_mode` is a context manager which can be invoked using Python’s `with` syntax. In the body of the `with` statement, AMP mode would be active.
* `amp_mode_active` is a function that returns `True` when AMP mode is active.

 Next, we need to define a view that wraps Wagtail’s builtin `serve` view and invokes the `activate_amp_mode` context manager:

 
```
# <app>/amp_views.py

from django.template.response import SimpleTemplateResponse
from wagtail.views import serve as wagtail_serve

from .amp_utils import activate_amp_mode

def serve(request, path):
    with activate_amp_mode():
        response = wagtail_serve(request, path)

        # Render template responses now while AMP mode is still active
        if isinstance(response, SimpleTemplateResponse):
            response.render()

        return response

```
 Then we need to create a `amp_urls.py` file in the same app:

 
```
# <app>/amp_urls.py

from django.urls import re_path
from wagtail.urls import serve_pattern

from . import amp_views

urlpatterns = [
    re_path(serve_pattern, amp_views.serve, name='wagtail_amp_serve')
]

```
 Finally, we need to update the project’s main `urls.py` to use this new URLs file for the `/amp` prefix:

 
```
# <project>/urls.py

from myapp import amp_urls as wagtail_amp_urls

urlpatterns += [
    # Change this line to point at your amp_urls instead of Wagtail's urls
    path('amp/', include(wagtail_amp_urls)),

    re_path(r'', include(wagtail_urls)),
]

```
 After this, there shouldn’t be any noticeable difference to the AMP version of the site.

   Write a template context processor so that AMP state can be checked in templates
--------------------------------------------------------------------------------

 This is optional, but worth doing so we can confirm that everything is working so far.

 Add a `amp_context_processors.py` file into your app that contains the following:

 
```
# <app>/amp_context_processors.py

from .amp_utils import amp_mode_active

def amp(request):
    return {
        'amp_mode_active': amp_mode_active(),
    }

```
 Now add the path to this context processor to the `['OPTIONS']['context_processors']` key of the `TEMPLATES` setting:

 
```
# Either <project>/settings.py or <project>/settings/base.py

TEMPLATES = [
    {
        ...

        'OPTIONS': {
            'context_processors': [
                ...
                # Add this after other context processors
                'myapp.amp_context_processors.amp',
            ],
        },
    },
]

```
 You should now be able to use the `amp_mode_active` variable in templates. For example:

 
```
{% if amp_mode_active %}
    AMP MODE IS ACTIVE!
{% endif %}

```
   Using a different page template when AMP mode is active
-------------------------------------------------------

 You’re probably not going to want to use the same templates on the AMP site as you do on the normal web site. Let’s add some logic in to make Wagtail use a separate template whenever a page is served with AMP enabled.

 We can use a mixin, which allows us to re-use the logic on different page types. Add the following to the bottom of the amp\_utils.py file that you created earlier:

 
```
# <app>/amp_utils.py

import os.path

...

class PageAMPTemplateMixin:

    @property
    def amp_template(self):
        # Get the default template name and insert `_amp` before the extension
        name, ext = os.path.splitext(self.template)
        return name + '_amp' + ext

    def get_template(self, request):
        if amp_mode_active():
            return self.amp_template

        return super().get_template(request)

```
 Now add this mixin to any page model, for example:

 
```
# <app>/models.py

from .amp_utils import PageAMPTemplateMixin

class MyPageModel(PageAMPTemplateMixin, Page):
    ...

```
 When AMP mode is active, the template at `app_label/mypagemodel_amp.html` will be used instead of the default one.

 If you have a different naming convention, you can override the `amp_template` attribute on the model. For example:

 
```
# <app>/models.py

from .amp_utils import PageAMPTemplateMixin

class MyPageModel(PageAMPTemplateMixin, Page):
    amp_template = 'my_custom_amp_template.html'

```
   Overriding the `{% image %}` tag to output `<amp-img>` tags
-----------------------------------------------------------

 Finally, let’s change Wagtail’s `{% image %}` tag, so it renders an `<amp-img>` tags when rendering pages with AMP enabled. We’ll make the change on the `Rendition` model itself so it applies to both images rendered with the `{% image %}` tag and images rendered in rich text fields as well.

 Doing this with a [Custom image model](images/custom_image_model#custom-image-model) is easier, as you can override the `img_tag` method on your custom `Rendition` model to return a different tag.

 For example:

 
```
from django.forms.utils import flatatt
from django.utils.safestring import mark_safe

from wagtail.images.models import AbstractRendition

...

class CustomRendition(AbstractRendition):
    def img_tag(self, extra_attributes):
        attrs = self.attrs_dict.copy()
        attrs.update(extra_attributes)

        if amp_mode_active():
            return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
        else:
            return mark_safe('<img{}>'.format(flatatt(attrs)))

    ...

```
 Without a custom image model, you will have to monkey-patch the builtin `Rendition` model. Add this anywhere in your project where it would be imported on start:

 
```
from django.forms.utils import flatatt
from django.utils.safestring import mark_safe

from wagtail.images.models import Rendition

def img_tag(rendition, extra_attributes={}):
    """
    Replacement implementation for Rendition.img_tag

    When AMP mode is on, this returns an <amp-img> tag instead of an <img> tag
    """
    attrs = rendition.attrs_dict.copy()
    attrs.update(extra_attributes)

    if amp_mode_active():
        return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
    else:
        return mark_safe('<img{}>'.format(flatatt(attrs)))

Rendition.img_tag = img_tag

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/amp.html>

Accessibility considerations
============================

 Accessibility for CMS-driven websites is a matter of [modeling content appropriately](#content-modeling), [creating accessible templates](#accessibility-in-templates), and [authoring accessible content](#authoring-accessible-content) with readability and accessibility guidelines in mind.

 Wagtail generally puts developers in control of content modeling and front-end markup, but there are a few areas to be aware of nonetheless, and ways to help authors be aware of readability best practices. Note there is much more to building accessible websites than we cover here – see our list of [accessibility resources](#accessibility-resources) for more information.

 * [Content modeling](#content-modeling)
* [Accessibility in templates](#accessibility-in-templates)
* [Authoring accessible content](#authoring-accessible-content)
* [Accessibility resources](#accessibility-resources)

  Content modeling
----------------

 As part of defining your site’s models, here are areas to pay special attention to:

  ### Alt text for images

 The default behaviour for Wagtail images is to use the `title` field as the alt text ([#4945](https://github.com/wagtail/wagtail/issues/4945)). This is inappropriate, as it’s not communicated in the CMS interface, and the image upload form uses the image’s filename as the title by default.

 Ideally, always add an optional “alt text” field wherever an image is used, alongside the image field:

 * For normal fields, add an alt text field to your image’s panel.
* For StreamField, add an extra field to your image block.
* For rich text – Wagtail already makes it possible to customise alt text for rich text images.

 When defining the alt text fields, make sure they are optional so editors can choose to not write any alt text for decorative images. Take the time to provide `help_text` with appropriate guidance. For example, linking to [established resources on alt text](https://axesslab.com/alt-texts/).

  Note

 Should I add an alt text field on the Image model for my site?

 It’s better than nothing to have a dedicated alt field on the Image model ([#5789](https://github.com/wagtail/wagtail/pull/5789)), and may be appropriate for some websites, but we recommend to have it inline with the content because ideally alt text should be written for the context the image is used in:

 * If the alt text’s content is already part of the rest of the page, ideally the image should not repeat the same content.
* Ideally, the alt text should be written based on the context the image is displayed in.
* An image might be decorative in some cases but not in others. For example, thumbnails in page listings can often be considered decorative.

  See [RFC 51: Contextual alt text](https://github.com/wagtail/rfcs/pull/51) for a long-term solution to this problem.

   ### Embeds title

 Missing embed titles are common failures in accessibility audits of Wagtail websites. In some cases, Wagtail embeds’ iframe doesn’t have a `title` attribute set. This is generally a problem with OEmbed providers like YouTube ([#5982](https://github.com/wagtail/wagtail/issues/5982)). This is very problematic for screen reader users, who rely on the title to understand what the embed is, and whether to interact with it or not.

 If your website relies on embeds that have are missing titles, make sure to either:

 * Add the OEmbed *title* field as a `title` on the `iframe`.
* Add a custom mandatory Title field to your embeds, and add it as the `iframe`’s `title`.

   ### Available heading levels

 Wagtail makes it very easy for developers to control which heading levels should be available for any given content, via [rich text features](customisation/page_editing_interface#rich-text-features) or custom StreamField blocks. In both cases, take the time to restrict what heading levels are available so the pages’ document outline is more likely to be logical and sequential. Consider using the following restrictions:

 * Disallow `h1` in rich text. There should only be one `h1` tag per page, which generally maps to the page’s `title`.
* Limit heading levels to `h2` for the main content of a page. Add `h3` only if deemed necessary. Avoid other levels as a general rule.
* For content that is displayed in a specific section of the page, limit heading levels to those directly below the section’s main heading.

 If managing headings via StreamField, make sure to apply the same restrictions there.

   ### Bold and italic formatting in rich text

 By default, Wagtail stores its bold formatting as a `b` tag, and italic as `i` ([#4665](https://github.com/wagtail/wagtail/issues/4665)). While those tags don’t necessarily always have correct semantics (`strong` and `em` are more ubiquitous), there isn’t much consequence for screen reader users, as by default screen readers do not announce content differently based on emphasis.

 If this is a concern to you, you can change which tags are used when saving content with [rich text format converters](../extending/rich_text_internals#rich-text-format-converters). In the future, [rich text rewrite handlers](../extending/rich_text_internals#rich-text-rewrite-handlers) should also support this being done without altering the storage format ([#4223](https://github.com/wagtail/wagtail/issues/4223)).

   ### TableBlock

 The [TableBlock](../reference/contrib/table_block) default implementation makes it too easy for end-users to miss they need either row or column headers ([#5989](https://github.com/wagtail/wagtail/issues/5989%3E)). Make sure to always have either row headers or column headers set. Always add a Caption, so screen reader users navigating the site’s tables know where they are.

    Accessibility in templates
--------------------------

 Here are common gotchas to be aware of to make the site’s templates as accessible as possible,

  ### Alt text in templates

 See the [content modelling](#content-modeling) section above. Additionally, make sure to [customise images’ alt text](../topics/images#image-tag-alt), either setting it to the relevant field, or to an empty string for decorative images, or images where the alt text would be a repeat of other content. Even when your images have alt text coming directly from the image model, you still need to decide whether there should be alt text for the particular context the image is used in. For example, avoid alt text in listings where the alt text just repeats the listing items’ title.

   ### Empty heading tags

 In both rich text and custom StreamField blocks, it’s sometimes easy for editors to create a heading block but not add any content to it. If this is a problem for your site,

 * Add validation rules to those fields, making sure the page can’t be saved with the empty headings, for example by using the [StreamField](../topics/streamfield) `CharBlock` which is required by default.
* Consider adding similar validation rules for rich text fields ([#6526](https://github.com/wagtail/wagtail/issues/6526)).

 Additionally, you can hide empty heading blocks with CSS:

 
```
h1:empty,
h2:empty,
h3:empty,
h4:empty,
h5:empty,
h6:empty {
    display: none;
}

```
   ### Forms

 The [Form builder](../reference/contrib/forms/index#form-builder) uses Django’s forms API. Here are considerations specific to forms in templates:

 * Avoid rendering helpers such as `as_table`, `as_ul`, `as_p`, which can make forms harder to navigate for screen reader users or cause HTML validation issues (see Django ticket [#32339](https://code.djangoproject.com/ticket/32339)).
* Make sure to visually distinguish required and optional fields.
* Take the time to group related fields together in `fieldset`, with an appropriate `legend`, in particular for radios and checkboxes (see Django ticket [#32338](https://code.djangoproject.com/ticket/32338)).
* If relevant, use the appropriate `autocomplete` and `autocapitalize` attributes.
* For Date and Datetime fields, make sure to display the expected format or an example value (see Django ticket [#32340](https://code.djangoproject.com/ticket/32340)). Or use [input type=”date”](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date).
* For Number fields, consider whether `input type="number"` really is appropriate, or whether there may be [better alternatives such as inputmode](https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/).

 Make sure to test your forms’ implementation with assistive technologies, and review [official W3C guidance on accessible forms development](https://www.w3.org/WAI/tutorials/forms/) for further information.

    Authoring accessible content
----------------------------

 Here are things you can do to help authors create accessible content.

  ### wagtail-accessibility

 [wagtail-accessibility](https://github.com/neon-jungle/wagtail-accessibility) is a third-party package which adds [tota11y](https://khan.github.io/tota11y/) to Wagtail previews. This makes it easy for authors to run basic accessibility checks – validating the page’s heading outline, or link text.

   ### help\_text and HelpPanel

 Occasional Wagtail users may not be aware of your site’s content guidelines, or best practices of writing for the web. Use fields’ `help_text` and `HelpPanel` (see [Panel types](../reference/pages/panels)).

   ### Readability

 Readability is fundamental to accessibility. One of the ways to improve text content is to have a clear target for reading level / reading age, which can be assessed with [wagtail-readinglevel](https://github.com/vixdigital/wagtail-readinglevel) as a score displayed in rich text fields.

    Accessibility resources
-----------------------

 We focus on considerations specific to Wagtail websites, but there is much more to accessibility. Here are valuable resources to learn more, for developers but also designers and authors:

 * [W3C Accessibility Fundamentals](https://www.w3.org/WAI/fundamentals/)
* [The A11Y Project](https://www.a11yproject.com/)
* [US GSA – Accessibility for Teams](https://accessibility.digital.gov/)
* [UK GDS – Dos and don’ts on designing for accessibility](https://accessibility.blog.gov.uk/2016/09/02/dos-and-donts-on-designing-for-accessibility/)
* [Accessibility Developer Guide](https://www.accessibility-developer-guide.com/)

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/accessibility_considerations.html>

About StreamField BoundBlocks and values
========================================

 All StreamField block types accept a `template` parameter to determine how they will be rendered on a page. However, for blocks that handle basic Python data types, such as `CharBlock` and `IntegerBlock`, there are some limitations on where the template will take effect, since those built-in types (`str`, `int` and so on) cannot be ‘taught’ about their template rendering. As an example of this, consider the following block definition:

 
```
class HeadingBlock(blocks.CharBlock):
    class Meta:
        template = 'blocks/heading.html'

```
 where `blocks/heading.html` consists of:

 
```
<h1>{{ value }}</h1>

```
 This gives us a block that behaves as an ordinary text field, but wraps its output in `<h1>` tags whenever it is rendered:

 
```
class BlogPage(Page):
    body = StreamField([
        # ...
        ('heading', HeadingBlock()),
        # ...
    ], use_json_field=True)

```
 
```
{% load wagtailcore_tags %}

{% for block in page.body %}
    {% if block.block_type == 'heading' %}
        {% include_block block %}  {# This block will output its own <h1>...</h1> tags. #}
    {% endif %}
{% endfor %}

```
 This kind of arrangement - a value that supposedly represents a plain text string, but has its own custom HTML representation when output on a template - would normally be a very messy thing to achieve in Python, but it works here because the items you get when iterating over a StreamField are not actually the ‘native’ values of the blocks. Instead, each item is returned as an instance of `BoundBlock` - an object that represents the pairing of a value and its block definition. By keeping track of the block definition, a `BoundBlock` always knows which template to render. To get to the underlying value - in this case, the text content of the heading - you would need to access `block.value`. Indeed, if you were to output `{% include_block block.value %}` on the page, you would find that it renders as plain text, without the `<h1>` tags.

 (More precisely, the items returned when iterating over a StreamField are instances of a class `StreamChild`, which provides the `block_type` property as well as `value`.)

 Experienced Django developers may find it helpful to compare this to the `BoundField` class in Django’s forms framework, which represents the pairing of a form field value with its corresponding form field definition, and therefore knows how to render the value as an HTML form field.

 Most of the time, you won’t need to worry about these internal details; Wagtail will use the template rendering wherever you would expect it to. However, there are certain cases where the illusion isn’t quite complete - namely, when accessing children of a `ListBlock` or `StructBlock`. In these cases, there is no `BoundBlock` wrapper, and so the item cannot be relied upon to know its own template rendering. For example, consider the following setup, where our `HeadingBlock` is a child of a StructBlock:

 
```
class EventBlock(blocks.StructBlock):
    heading = HeadingBlock()
    description = blocks.TextBlock()
    # ...

    class Meta:
        template = 'blocks/event.html'

```
 In `blocks/event.html`:

 
```
{% load wagtailcore_tags %}

<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
    {% include_block value.heading %}
    - {% include_block value.description %}
</div>

```
 In this case, `value.heading` returns the plain string value rather than a `BoundBlock`; this is necessary because otherwise the comparison in `{% if value.heading == 'Party!' %}` would never succeed. This in turn means that `{% include_block value.heading %}` renders as the plain string, without the `<h1>` tags. To get the HTML rendering, you need to explicitly access the `BoundBlock` instance through `value.bound_blocks.heading`:

 
```
{% load wagtailcore_tags %}

<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
    {% include_block value.bound_blocks.heading %}
    - {% include_block value.description %}
</div>

```
 In practice, it would probably be more natural and readable to make the `<h1>` tag explicit in the EventBlock’s template:

 
```
{% load wagtailcore_tags %}

<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
    <h1>{{ value.heading }}</h1>
    - {% include_block value.description %}
</div>

```
 This limitation does not apply to StructBlock and StreamBlock values as children of a StructBlock, because Wagtail implements these as complex objects that know their own template rendering, even when not wrapped in a `BoundBlock`. For example, if a StructBlock is nested in another StructBlock, as in:

 
```
class EventBlock(blocks.StructBlock):
    heading = HeadingBlock()
    description = blocks.TextBlock()
    guest_speaker = blocks.StructBlock([
        ('first_name', blocks.CharBlock()),
        ('surname', blocks.CharBlock()),
        ('photo', ImageChooserBlock()),
    ], template='blocks/speaker.html')

```
 then `{% include_block value.guest_speaker %}` within the EventBlock’s template will pick up the template rendering from `blocks/speaker.html` as intended.

 In summary, interactions between BoundBlocks and plain values work according to the following rules:

 1. When iterating over the value of a StreamField or StreamBlock (as in `{% for block in page.body %}`), you will get back a sequence of BoundBlocks.
2. If you have a BoundBlock instance, you can access the plain value as `block.value`.
3. Accessing a child of a StructBlock (as in `value.heading`) will return a plain value; to retrieve the BoundBlock instead, use `value.bound_blocks.heading`.
4. Likewise, accessing children of a ListBlock (for example `for item in value`) will return plain values; to retrieve BoundBlocks instead, use `value.bound_blocks`.
5. StructBlock and StreamBlock values always know how to render their own templates, even if you only have the plain value rather than the BoundBlock.

  Changed in version 2.16: The value of a ListBlock now provides a `bound_blocks` property; previously it was a plain Python list of child values.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/boundblocks_and_values.html>

Multi-site, multi-instance and multi-tenancy
============================================

 This page gives background information on how to run multiple Wagtail sites (with the same source code).

 * [Multi-site](#multi-site)
* [Multi-instance](#multi-instance)
* [Multi-tenancy](#multi-tenancy)

  Multi-site
----------

 Multi-site is a Wagtail project configuration where content creators go into a single admin interface and manage the content of multiple websites. Permission to manage specific content, and restricting access to other content, is possible to some extent.

 Multi-site configuration is a single code base, on a single server, connecting to a single database. Media is stored in a single media root directory. Content can be shared between sites.

 Wagtail supports multi-site out of the box: Wagtail comes with a [site model](../reference/pages/model_reference#wagtail.models.Site "wagtail.models.Site"). The site model contains a hostname, port, and root page field. When a URL is requested, the request comes in, the domain name and port are taken from the request object to look up the correct site object. The root page is used as starting point to resolve the URL and serve the correct page.

 Wagtail also comes with [site settings](../reference/contrib/settings#site-settings). *Site settings* are ‘singletons’ that let you store additional information on a site. For example, social media settings, a field to upload a logo, or a choice field to select a theme.

 Model objects can be linked to a site by placing a foreign key field on the model pointing to the site object. A request object can be used to look up the current site. This way, content belonging to a specific site can be served.

 User, groups, and permissions can be configured in such a way that content creators can only manage the pages, images, and documents of a specific site. Wagtail can have multiple *site objects* and multiple *page trees*. Permissions can be linked to a specific page tree or a subsection thereof. Collections are used to categorize images and documents. A collection can be restricted to users who are in a specific group.

 Some projects require content editors to have permissions on specific sites and restrict access to other sites. Splitting *all* content per site and guaranteeing that no content ‘leaks’ is difficult to realize in a multi-site project. If you require full separation of content, then multi-instance might be a better fit…

   Multi-instance
--------------

 Multi-instance is a Wagtail project configuration where a single set of project files is used by multiple websites. Each website has its own settings file, and a dedicated database and media directory. Each website runs in its own server process. This guarantees the *total separation* of *all content*.

 Assume the domains a.com and b.com. Settings files can be `base.py`, `acom.py`, and `bcom.py`. The base settings will contain all settings like normal. The contents of site-specific settings override the base settings:

 
```
# settings/acom.py

from base import \* # noqa

ALLOWED_HOSTS = ['a.com']
DATABASES["NAME"] = "acom"
DATABASES["PASSWORD"] = "password-for-acom"
MEDIA_DIR = BASE_DIR / "acom-media"

```
 Each site can be started with its own settings file. In development `./manage.py runserver --settings settings.acom`. In production, for example with uWSGI, specify the correct settings with `env = DJANGO_SETTINGS_MODULE=settings.acom`.

 Because each site has its own database and media folder, nothing can ‘leak’ to another site. But this also means that content cannot be shared between sites as one can do when using the multi-site option.

 In this configuration, multiple sites share the same, single set of project files. Deployment would update the single set of project files and reload each instance.

 This multi-instance configuration isn’t that different from deploying the project code several times. However, having a single set of project files, and only differentiate with settings files, is the closest Wagtail can get to true multi-tenancy. Every site is identical, content is separated, including user management. ‘Adding a new tenant’ is adding a new settings file and running a new instance.

 In a multi-instance configuration, each instance requires a certain amount of server resources (CPU and memory). That means adding sites will increase server load. This only scales up to a certain point.

   Multi-tenancy
-------------

 Multi-tenancy is a project configuration in which a single instance of the software serves multiple tenants. A tenant is a group of users who have access and permission to a single site. Multitenant software is designed to provide every tenant its configuration, data, and user management.

 Wagtail supports *multi-site*, where user management and content are shared. Wagtail can run *multi-instance* where there is full separation of content at the cost of running multiple instances. Multi-tenancy combines the best of both worlds: a single instance, and the full separation of content per site and user management.

 Wagtail does not support full multi-tenancy at this moment. But it is on our radar, we would like to improve Wagtail to add multi-tenancy - while still supporting the existing multi-site option. If you have ideas or like to contribute, join us on [Slack](../support#slack) in the multi-tenancy channel.

 Wagtail currently has the following features to support multi-tenancy:

 * A Site model mapping a hostname to a root page
* Permissions to allow groups of users to manage:


	+ arbitrary sections of the page tree
	+ sections of the collection tree (coming soon)
	+ one or more collections of documents and images
* The page API is automatically scoped to the host used for the request

 But several features do not currently support multi-tenancy:

 * Snippets are global pieces of content so not suitable for multi-tenancy but any model that can be registered as a snippet can also be managed via the Wagtail model admin. You can add a site\_id to the model and then use the model admin get\_queryset method to determine which site can manage each object. The built-in snippet choosers can be replaced by [modelchooser](https://pypi.org/project/wagtail-modelchooser/) that allows filtering the queryset to restrict which sites may display which objects.
* Site, site setting, user, and group management. At the moment, your best bet is to only allow superusers to manage these objects.
* Workflows and workflow tasks
* Site history
* Redirects

 Permission configuration for built-in models like Sites, Site settings and Users is not site-specific, so any user with permission to edit a single entry can edit them all. This limitation can be mostly circumvented by only allowing superusers to manage these models.

 Python, Django, and Wagtail allow you to override, extend and customise functionality. Here are some ideas that may help you create a multi-tenancy solution for your site:

 * Django allows to override templates, this also works in the Wagtail admin.
* A custom user model can be used to link users to a specific site.
* Custom admin views can provide more restrictive user management.

 We welcome interested members of the Wagtail community to contribute code and ideas.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/multi_site_multi_instance_multi_tenancy.html>

How to build custom StreamField blocks
======================================

  Custom editing interfaces for `StructBlock`
-------------------------------------------

 To customise the styling of a `StructBlock` as it appears in the page editor, you can specify a `form_classname` attribute (either as a keyword argument to the `StructBlock` constructor, or in a subclass’s `Meta`) to override the default value of `struct-block`:

 
```
class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    class Meta:
        icon = 'user'
        form_classname = 'person-block struct-block'

```
 You can then provide custom CSS for this block, targeted at the specified classname, by using the [insert\_editor\_css](../../reference/hooks#insert-editor-css) hook.

  Note

 Wagtail’s editor styling has some built in styling for the `struct-block` class and other related elements. If you specify a value for `form_classname`, it will overwrite the classes that are already applied to `StructBlock`, so you must remember to specify the `struct-block` as well.

  For more extensive customisations that require changes to the HTML markup as well, you can override the `form_template` attribute in `Meta` to specify your own template path. The following variables are available on this template:

 **`children`**  
 An `OrderedDict` of `BoundBlock`s for all of the child blocks making up this `StructBlock`.

 **`help_text`**  
 The help text for this block, if specified.

 **`classname`** The class name passed as `form_classname` (defaults to `struct-block`).

 **`block_definition`** The `StructBlock` instance that defines this block.

 **`prefix`** The prefix used on form fields for this block instance, guaranteed to be unique across the form.

 To add additional variables, you can override the block’s `get_form_context` method:

 
```
class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    def get_form_context(self, value, prefix='', errors=None):
        context = super().get_form_context(value, prefix=prefix, errors=errors)
        context['suggested_first_names'] = ['John', 'Paul', 'George', 'Ringo']
        return context

    class Meta:
        icon = 'user'
        form_template = 'myapp/block_forms/person.html'

```
 A form template for a StructBlock must include the output of `render_form` for each child block in the `children` dict, inside a container element with a `data-contentpath` attribute equal to the block’s name. This attribute is used by the commenting framework to attach comments to the correct fields. The StructBlock’s form template is also responsible for rendering labels for each field, but this (and all other HTML markup) can be customised as you see fit. The template below replicates the default StructBlock form rendering:

 
```
{% load wagtailadmin_tags  %}

<div class="{{ classname }}">
    {% if help_text %}
        <span>
            <div class="help">
                {% icon name="help" class_name="default" %}
                {{ help_text }}
            </div>
        </span>
    {% endif %}

    {% for child in children.values %}
        <div class="w-field" data-field data-contentpath="{{ child.block.name }}">
            {% if child.block.label %}
                <label class="w-field__label" {% if child.id_for_label %}for="{{ child.id_for_label }}"{% endif %}>{{ child.block.label }}{% if child.block.required %}<span class="w-required-mark">*</span>{% endif %}</label>
            {% endif %}
            {{ child.render_form }}
        </div>
    {% endfor %}
</div>

```
   Additional JavaScript on `StructBlock` forms
--------------------------------------------

 Often it may be desirable to attach custom JavaScript behaviour to a StructBlock form. For example, given a block such as:

 
```
class AddressBlock(StructBlock):
    street = CharBlock()
    town = CharBlock()
    state = CharBlock(required=False)
    country = ChoiceBlock(choices=[
        ('us', 'United States'),
        ('ca', 'Canada'),
        ('mx', 'Mexico'),
    ])

```
 we may wish to disable the ‘state’ field when a country other than United States is selected. Since new blocks can be added dynamically, we need to integrate with StreamField’s own front-end logic to ensure that our custom JavaScript code is executed when a new block is initialised.

 StreamField uses the [telepath](https://wagtail.github.io/telepath/) library to map Python block classes such as `StructBlock` to a corresponding JavaScript implementation. These JavaScript implementations can be accessed through the `window.wagtailStreamField.blocks` namespace, as the following classes:

 * `FieldBlockDefinition`
* `ListBlockDefinition`
* `StaticBlockDefinition`
* `StreamBlockDefinition`
* `StructBlockDefinition`

 First, we define a telepath adapter for `AddressBlock`, so that it uses our own JavaScript class in place of the default `StructBlockDefinition`. This can be done in the same module as the `AddressBlock` definition:

 
```
from wagtail.blocks.struct_block import StructBlockAdapter
from wagtail.telepath import register
from django import forms
from django.utils.functional import cached_property

class AddressBlockAdapter(StructBlockAdapter):
    js_constructor = 'myapp.blocks.AddressBlock'

    @cached_property
    def media(self):
        structblock_media = super().media
        return forms.Media(
            js=structblock_media._js + ['js/address-block.js'],
            css=structblock_media._css
        )

register(AddressBlockAdapter(), AddressBlock)

```
 Here `'myapp.blocks.AddressBlock'` is the identifier for our JavaScript class that will be registered with the telepath client-side code, and `'js/address-block.js'` is the file that defines it (as a path within any static file location recognised by Django). This implementation subclasses StructBlockDefinition and adds our custom code to the `render` method:

 
```
class AddressBlockDefinition extends window.wagtailStreamField.blocks
    .StructBlockDefinition {
    render(placeholder, prefix, initialState, initialError) {
        const block = super.render(
            placeholder,
            prefix,
            initialState,
            initialError,
        );

        const stateField = document.getElementById(prefix + '-state');
        const countryField = document.getElementById(prefix + '-country');
        const updateStateInput = () => {
            if (countryField.value == 'us') {
                stateField.removeAttribute('disabled');
            } else {
                stateField.setAttribute('disabled', true);
            }
        };
        updateStateInput();
        countryField.addEventListener('change', updateStateInput);

        return block;
    }
}
window.telepath.register('myapp.blocks.AddressBlock', AddressBlockDefinition);

```
   Additional methods and properties on `StructBlock` values
---------------------------------------------------------

 When rendering StreamField content on a template, StructBlock values are represented as `dict`-like objects where the keys correspond to the names of the child blocks. Specifically, these values are instances of the class `wagtail.blocks.StructValue`.

 Sometimes, it’s desirable to make additional methods or properties available on this object. For example, given a StructBlock that represents either an internal or external link:

 
```
class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

```
 you may want to make a `url` property available, that returns either the page URL or external URL depending which one was filled in. A common mistake is to define this property on the block class itself:

 
```
class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

    @property
    def url(self):  # INCORRECT - will not work
        return self.external_url or self.page.url

```
 This does not work because the value as seen in the template is not an instance of `LinkBlock`. `StructBlock` instances only serve as specifications for the block’s behaviour, and do not hold block data in their internal state - in this respect, they are similar to Django’s form widget objects (which provide methods for rendering a given value as a form field, but do not hold on to the value itself).

 Instead, you should define a subclass of `StructValue` that implements your custom property or method. Within this method, the block’s data can be accessed as `self['page']` or `self.get('page')`, since `StructValue` is a dict-like object.

 
```
from wagtail.blocks import StructValue


class LinkStructValue(StructValue):
    def url(self):
        external_url = self.get('external_url')
        page = self.get('page')
        return external_url or page.url

```
 Once this is defined, set the block’s `value_class` option to instruct it to use this class rather than a plain StructValue:

 
```
class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

    class Meta:
        value_class = LinkStructValue

```
 Your extended value class methods will now be available in your template:

 
```
{% for block in page.body %}
    {% if block.block_type == 'link' %}
        <a href="{{ link.value.url }}">{{ link.value.text }}</a>
    {% endif %}
{% endfor %}

```
   Custom block types
------------------

 If you need to implement a custom UI, or handle a datatype that is not provided by Wagtail’s built-in block types (and cannot be built up as a structure of existing fields), it is possible to define your own custom block types. For further guidance, refer to the source code of Wagtail’s built-in block classes.

 For block types that simply wrap an existing Django form field, Wagtail provides an abstract class `wagtail.blocks.FieldBlock` as a helper. Subclasses should set a `field` property that returns the form field object:

 
```
class IPAddressBlock(FieldBlock):
    def __init__(self, required=True, help_text=None, **kwargs):
        self.field = forms.GenericIPAddressField(required=required, help_text=help_text)
        super().__init__(**kwargs)

```
 Since the StreamField editing interface needs to create blocks dynamically, certain complex widget types will need additional JavaScript code to define how to render and populate them on the client-side. If a field uses a widget type that does not inherit from one of the classes inheriting from `django.forms.widgets.Input`, `django.forms.Textarea`, `django.forms.Select` or `django.forms.RadioSelect`, or has customised client-side behaviour to the extent where it is not possible to read or write its data simply by accessing the form element’s `value` property, you will need to provide a JavaScript handler object, implementing the methods detailed on [Form widget client-side API](../../reference/streamfield/widget_api#streamfield-widget-api).

   Handling block definitions within migrations
--------------------------------------------

 As with any model field in Django, any changes to a model definition that affect a StreamField will result in a migration file that contains a ‘frozen’ copy of that field definition. Since a StreamField definition is more complex than a typical model field, there is an increased likelihood of definitions from your project being imported into the migration – which would cause problems later on if those definitions are moved or deleted.

 To mitigate this, StructBlock, StreamBlock and ChoiceBlock implement additional logic to ensure that any subclasses of these blocks are deconstructed to plain instances of StructBlock, StreamBlock and ChoiceBlock – in this way, the migrations avoid having any references to your custom class definitions. This is possible because these block types provide a standard pattern for inheritance, and know how to reconstruct the block definition for any subclass that follows that pattern.

 If you subclass any other block class, such as `FieldBlock`, you will need to either keep that class definition in place for the lifetime of your project, or implement a [custom deconstruct method](https://docs.djangoproject.com/en/stable/topics/migrations/#custom-deconstruct-method "(in Django v4.1)") that expresses your block entirely in terms of classes that are guaranteed to remain in place. Similarly, if you customise a StructBlock, StreamBlock or ChoiceBlock subclass to the point where it can no longer be expressed as an instance of the basic block type – for example, if you add extra arguments to the constructor – you will need to provide your own `deconstruct` method.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/customisation/streamfield_blocks.html>

Wagtail API
===========

 The API module provides a public-facing, JSON-formatted API to allow retrieving content as raw field data. This is useful for cases like serving content to non-web clients (such as a mobile phone app) or pulling content out of Wagtail for use in another site.

 See [RFC 8: Wagtail API](https://github.com/wagtail/rfcs/blob/main/text/008-wagtail-api.md#12---stable-and-unstable-versions) for full details on our stabilisation policy.

 * [Wagtail API v2 Configuration Guide](v2/configuration)
	+ [Basic configuration](v2/configuration#basic-configuration)
	+ [Additional settings](v2/configuration#additional-settings)
* [Wagtail API v2 Usage Guide](v2/usage)
	+ [Fetching content](v2/usage#fetching-content)
	+ [Default endpoint fields](v2/usage#default-endpoint-fields)
	+ [Changes since v1](v2/usage#changes-since-v1)



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/api/index.html>

Wagtail API v2 Configuration Guide
==================================

 This section of the docs will show you how to set up a public API for your Wagtail site.

 Even though the API is built on Django REST Framework, you do not need to install this manually as it is already a dependency of Wagtail.

  Basic configuration
-------------------

  ### Enable the app

 Firstly, you need to enable Wagtail’s API app so Django can see it. Add `wagtail.api.v2` to `INSTALLED_APPS` in your Django project settings:

 
```
# settings.py

INSTALLED_APPS = [
    ...

    'wagtail.api.v2',

    ...
]

```
 Optionally, you may also want to add `rest_framework` to `INSTALLED_APPS`. This would make the API browsable when viewed from a web browser but is not required for basic JSON-formatted output.

   ### Configure endpoints

 Next, it’s time to configure which content will be exposed on the API. Each content type (such as pages, images and documents) has its own endpoint. Endpoints are combined by a router, which provides the url configuration you can hook into the rest of your project.

 Wagtail provides three endpoint classes you can use:

 * Pages `wagtail.api.v2.views.PagesAPIViewSet`
* Images `wagtail.images.api.v2.views.ImagesAPIViewSet`
* Documents `wagtail.documents.api.v2.views.DocumentsAPIViewSet`

 You can subclass any of these endpoint classes to customize their functionality. Additionally, there is a base endpoint class you can use for adding different content types to the API: `wagtail.api.v2.views.BaseAPIViewSet`

 For this example, we will create an API that includes all three builtin content types in their default configuration:

 
```
# api.py

from wagtail.api.v2.views import PagesAPIViewSet
from wagtail.api.v2.router import WagtailAPIRouter
from wagtail.images.api.v2.views import ImagesAPIViewSet
from wagtail.documents.api.v2.views import DocumentsAPIViewSet

# Create the router. "wagtailapi" is the URL namespace
api_router = WagtailAPIRouter('wagtailapi')

# Add the three endpoints using the "register_endpoint" method.
# The first parameter is the name of the endpoint (such as pages, images). This
# is used in the URL of the endpoint
# The second parameter is the endpoint class that handles the requests
api_router.register_endpoint('pages', PagesAPIViewSet)
api_router.register_endpoint('images', ImagesAPIViewSet)
api_router.register_endpoint('documents', DocumentsAPIViewSet)

```
 Next, register the URLs so Django can route requests into the API:

 
```
# urls.py

from .api import api_router

urlpatterns = [
    ...

    path('api/v2/', api_router.urls),

    ...

    # Ensure that the api_router line appears above the default Wagtail page serving route
    re_path(r'^', include(wagtail_urls)),
]

```
 With this configuration, pages will be available at `/api/v2/pages/`, images at `/api/v2/images/` and documents at `/api/v2/documents/`

   ### Adding custom page fields

 It’s likely that you would need to export some custom fields over the API. This can be done by adding a list of fields to be exported into the `api_fields` attribute for each page model.

 For example:

 
```
# blog/models.py

from wagtail.api import APIField

class BlogPageAuthor(Orderable):
    page = models.ForeignKey('blog.BlogPage', on_delete=models.CASCADE, related_name='authors')
    name = models.CharField(max_length=255)

    api_fields = [
        APIField('name'),
    ]


class BlogPage(Page):
    published_date = models.DateTimeField()
    body = RichTextField()
    feed_image = models.ForeignKey('wagtailimages.Image', on_delete=models.SET_NULL, null=True, ...)
    private_field = models.CharField(max_length=255)

    # Export fields over the API
    api_fields = [
        APIField('published_date'),
        APIField('body'),
        APIField('feed_image'),
        APIField('authors'),  # This will nest the relevant BlogPageAuthor objects in the API response
    ]

```
 This will make `published_date`, `body`, `feed_image` and a list of `authors` with the `name` field available in the API. But to access these fields, you must select the `blog.BlogPage` type using the `?type` [parameter in the API itself](usage#apiv2-custom-page-fields).

   ### Custom serializers

 [Serializers](https://www.django-rest-framework.org/api-guide/fields/) are used to convert the database representation of a model into JSON format. You can override the serializer for any field using the `serializer` keyword argument:

 
```
from rest_framework.fields import DateField

class BlogPage(Page):
    ...

    api_fields = [
        # Change the format of the published_date field to "Thursday 06 April 2017"
        APIField('published_date', serializer=DateField(format='%A %d %B %Y')),
        ...
    ]

```
 Django REST framework’s serializers can all take a [source](https://www.django-rest-framework.org/api-guide/fields/#source) argument allowing you to add API fields that have a different field name or no underlying field at all:

 
```
from rest_framework.fields import DateField

class BlogPage(Page):
    ...

    api_fields = [
        # Date in ISO8601 format (the default)
        APIField('published_date'),

        # A separate published_date_display field with a different format
        APIField('published_date_display', serializer=DateField(format='%A %d %B %Y', source='published_date')),
        ...
    ]

```
 This adds two fields to the API (other fields omitted for brevity):

 
```
{
    "published_date": "2017-04-06",
    "published_date_display": "Thursday 06 April 2017"
}

```
   ### Images in the API

 The `ImageRenditionField` serializer allows you to add renditions of images into your API. It requires an image filter string specifying the resize operations to perform on the image. It can also take the `source` keyword argument described above.

 For example:

 
```
from wagtail.api import APIField
from wagtail.images.api.fields import ImageRenditionField

class BlogPage(Page):
    ...

    api_fields = [
        # Adds information about the source image (eg, title) into the API
        APIField('feed_image'),

        # Adds a URL to a rendered thumbnail of the image to the API
        APIField('feed_image_thumbnail', serializer=ImageRenditionField('fill-100x100', source='feed_image')),
        ...
    ]

```
 This would add the following to the JSON:

 
```
{
    "feed_image": {
        "id": 45529,
        "meta": {
            "type": "wagtailimages.Image",
            "detail_url": "http://www.example.com/api/v2/images/12/",
            "download_url": "/media/images/a_test_image.jpg",
            "tags": []
        },
        "title": "A test image",
        "width": 2000,
        "height": 1125
    },
    "feed_image_thumbnail": {
        "url": "/media/images/a_test_image.fill-100x100.jpg",
        "full_url": "http://www.example.com/media/images/a_test_image.fill-100x100.jpg",
        "width": 100,
        "height": 100,
        "alt": "image alt text"
    }
}

```
 Note: `download_url` is the original uploaded file path, whereas `feed_image_thumbnail['url']` is the url of the rendered image. When you are using another storage backend, such as S3, `download_url` will return a URL to the image if your media files are properly configured.

    Additional settings
-------------------

  ### `WAGTAILAPI_BASE_URL`

 (required when using frontend cache invalidation)

 This is used in two places, when generating absolute URLs to document files and invalidating the cache.

 Generating URLs to documents will fall back the the current request’s hostname if this is not set. Cache invalidation cannot do this, however, so this setting must be set when using this module alongside the `wagtailfrontendcache` module.

   ### `WAGTAILAPI_SEARCH_ENABLED`

 (default: True)

 Setting this to false will disable full text search. This applies to all endpoints.

   ### `WAGTAILAPI_LIMIT_MAX`

 (default: 20)

 This allows you to change the maximum number of results a user can request at a time. This applies to all endpoints. Set to `None` for no limit.

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/api/v2/configuration.html>

How to use a redirect with Form builder to prevent double submission
====================================================================

 It is common for form submission HTTP responses to be a `302 Found` temporary redirection to a new page. By default `wagtail.contrib.forms.models.FormPage` success responses don’t do this, meaning there is a risk that users will refresh the success page and re-submit their information.

 Instead of rendering the `render_landing_page` content in the POST response, we will redirect to a `route` of the `FormPage` instance at a child URL path. The content will still be managed within the same form page’s admin. This approach uses the additional contrib module `wagtail.contrib.routable_page`.

 An alternative approach is to redirect to an entirely different page, which does not require the `routable_page` module. See [Custom landing page redirect](../reference/contrib/forms/customisation#form-builder-custom-landing-page-redirect).

 Make sure `"wagtail.contrib.routable_page"` is added to `INSTALLED_APPS`, see [RoutablePageMixin](../reference/contrib/routablepage#routable-page-mixin) documentation.

 
```
from django.shortcuts import redirect

from wagtail.contrib.forms.models import AbstractEmailForm
from wagtail.contrib.routable_page.models import RoutablePageMixin, path


class FormPage(RoutablePageMixin, AbstractEmailForm):

    # fields, content_panels, …

    @path("")
    def index_route(self, request, *args, **kwargs):
        """Serve the form, and validate it on POST"""
        return super(AbstractEmailForm, self).serve(request, *args, **kwargs)

    def render_landing_page(self, request, form_submission, *args, **kwargs):
        """Redirect instead to self.thank_you route"""
        url = self.reverse_subpage("thank_you")
        # If a form_submission instance is available, append the ID to URL.
        if form_submission:
            url += "?id=%s" % form_submission.id
        return redirect(self.url + url, permanent=False)

    @path("thank-you/")
    def thank_you(self, request):
        """Return the superclass's landing page, after redirect."""
        form_submission = None
        try:
            submission_id = int(request.GET["id"])
        except (KeyError, TypeError):
            pass
        else:
            submission_class = self.get_submission_class()
            try:
                form_submission = submission_class.objects.get(id=submission_id)
            except submission_class.DoesNotExist:
                pass

        return super().render_landing_page(request, form_submission)

```


 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/advanced_topics/formbuilder_routablepage_redirect.html>

Extending Wagtail
=================

 The Wagtail admin interface is a suite of Django apps, and so the familiar concepts from Django development - views, templates, URL routes and so on - can be used to add new functionality to Wagtail. Numerous [third-party packages](https://wagtail.org/packages/) can be installed to extend Wagtail’s capabilities.

 This section describes the various mechanisms that can be used to integrate your own code into Wagtail’s admin interface.

 * [Creating admin views](admin_views)
	+ [Defining a view](admin_views#defining-a-view)
	+ [Registering a URL route](admin_views#registering-a-url-route)
	+ [Adding a template](admin_views#adding-a-template)
	+ [Adding a menu item](admin_views#adding-a-menu-item)
	+ [Adding a group of menu items](admin_views#adding-a-group-of-menu-items)
* [Generic views](generic_views)
	+ [ModelViewSet](generic_views#modelviewset)
	+ [ChooserViewSet](generic_views#chooserviewset)
	+ [Chooser viewsets for non-model datasources](generic_views#chooser-viewsets-for-non-model-datasources)
* [Template components](template_components)
	+ [`render_html()`](template_components#render_html)
	+ [`media`](template_components#media)
	+ [Creating components](template_components#creating-components)
	+ [Passing context to the template](template_components#passing-context-to-the-template)
	+ [Adding media definitions](template_components#adding-media-definitions)
	+ [Using components on your own templates](template_components#using-components-on-your-own-templates)
* [Using forms in admin views](forms)
	+ [Defining admin form widgets](forms#defining-admin-form-widgets)
	+ [Panels](forms#panels)
* [Adding reports](adding_reports)
	+ [Defining your report](adding_reports#defining-your-report)
	+ [Spreadsheet exports](adding_reports#spreadsheet-exports)
	+ [Customising templates](adding_reports#customising-templates)
	+ [Adding a menu item and admin URL](adding_reports#adding-a-menu-item-and-admin-url)
	+ [The full code](adding_reports#the-full-code)
* [Adding new Task types](custom_tasks)
	+ [Task models](custom_tasks#task-models)
	+ [Custom TaskState models](custom_tasks#custom-taskstate-models)
	+ [Customising behaviour](custom_tasks#customising-behaviour)
	+ [Adding notifications](custom_tasks#adding-notifications)
* [Audit log](audit_log)
	+ [`log()`](audit_log#log)
	+ [Log actions provided by Wagtail](audit_log#log-actions-provided-by-wagtail)
	+ [Log context](audit_log#log-context)
	+ [Log models](audit_log#log-models)
* [Customising the user account settings form](custom_account_settings)
	+ [Adding new panels](custom_account_settings#adding-new-panels)
	+ [Operating on the `UserProfile` model](custom_account_settings#operating-on-the-userprofile-model)
	+ [Creating new tabs](custom_account_settings#creating-new-tabs)
	+ [Customising the template](custom_account_settings#customising-the-template)
* [Customising group edit/create views](customising_group_views)
	+ [Custom edit/create forms](customising_group_views#custom-edit-create-forms)
* [Rich text internals](rich_text_internals)
	+ [Data format](rich_text_internals#data-format)
	+ [The feature registry](rich_text_internals#the-feature-registry)
	+ [Rewrite handlers](rich_text_internals#rewrite-handlers)
	+ [Editor widgets](rich_text_internals#editor-widgets)
	+ [Editor plugins](rich_text_internals#editor-plugins)
	+ [Format converters](rich_text_internals#format-converters)
* [Extending the Draftail Editor](extending_draftail)
	+ [Creating new inline styles](extending_draftail#creating-new-inline-styles)
	+ [Creating new blocks](extending_draftail#creating-new-blocks)
	+ [Creating new entities](extending_draftail#creating-new-entities)
	+ [Integration of the Draftail widgets](extending_draftail#integration-of-the-draftail-widgets)
* [Adding custom bulk actions](custom_bulk_actions)
	+ [Registering a custom bulk action](custom_bulk_actions#registering-a-custom-bulk-action)
	+ [Adding bulk actions to the page explorer](custom_bulk_actions#adding-bulk-actions-to-the-page-explorer)
	+ [Adding bulk actions to the Images listing](custom_bulk_actions#adding-bulk-actions-to-the-images-listing)
	+ [Adding bulk actions to the documents listing](custom_bulk_actions#adding-bulk-actions-to-the-documents-listing)
	+ [Adding bulk actions to the user listing](custom_bulk_actions#adding-bulk-actions-to-the-user-listing)
	+ [Adding bulk actions to the snippets listing](custom_bulk_actions#adding-bulk-actions-to-the-snippets-listing)



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/index.html>

Creating admin views
====================

 The most common use for adding custom views to the Wagtail admin is to provide an interface for managing a Django model. The [Index](../reference/contrib/modeladmin/index) app makes this simple, providing ready-made views for listing, creating and editing objects with minimal configuration.

 For other kinds of admin view that don’t fit this pattern, you can write your own Django views and register them as part of the Wagtail admin through [hooks](../reference/hooks#admin-hooks). In this example, we’ll implement a view that displays a calendar for the current year, using [the calendar module](https://docs.python.org/3/library/calendar.html) from Python’s standard library.

  Defining a view
---------------

 Within a Wagtail project, create a new `wagtailcalendar` app with `./manage.py startapp wagtailcalendar` and add it to your project’s `INSTALLED_APPS`. (In this case we’re using the name ‘wagtailcalendar’ to avoid clashing with the standard library’s `calendar` module - in general there is no need to use a ‘wagtail’ prefix.)

 Edit `views.py` as follows - note that this is a plain Django view with no Wagtail-specific code.

 
```
import calendar

from django.http import HttpResponse
from django.utils import timezone


def index(request):
    current_year = timezone.now().year
    calendar_html = calendar.HTMLCalendar().formatyear(current_year)

    return HttpResponse(calendar_html)

```
   Registering a URL route
-----------------------

 At this point, the standard practice for a Django project would be to add a URL route for this view to your project’s top-level URL config module. However, in this case we want the view to only be available to logged-in users, and to appear within the `/admin/` URL namespace which is managed by Wagtail. This is done through the [Register Admin URLs](../reference/hooks#register-admin-urls) hook.

 On startup, Wagtail looks for a `wagtail_hooks` submodule within each installed app. In this submodule, you can define functions to be run at various points in Wagtail’s operation, such as building the URL config for the admin, and constructing the main menu.

 Create a `wagtail_hooks.py` file within the `wagtailcalendar` app containing the following:

 
```
from django.urls import path
from wagtail import hooks

from .views import index


@hooks.register('register_admin_urls')
def register_calendar_url():
    return [
        path('calendar/', index, name='calendar'),
    ]

```
 The calendar will now be visible at the URL `/admin/calendar/`.

 ![A calendar, presented in unstyled HTML]

   Adding a template
-----------------

 Currently this view is outputting a plain HTML fragment. Let’s insert this into the usual Wagtail admin page furniture, by creating a template that extends Wagtail’s base template `"wagtailadmin/base.html"`.

  Note

 The base template and HTML structure are not considered astable part of Wagtail’s API, and may change in futurereleases.

  Update `views.py` as follows:

 
```
import calendar
from django.shortcuts import render
from django.utils import timezone

def index(request):
    current_year = timezone.now().year
    calendar_html = calendar.HTMLCalendar().formatyear(current_year)

    return render(request, 'wagtailcalendar/index.html', {
        'current_year': current_year,
        'calendar_html': calendar_html,
    })

```
 Now create a `templates/wagtailcalendar/` folder within the `wagtailcalendar` app, containing `index.html` as follows:

 
```
{% extends "wagtailadmin/base.html" %}
{% block titletag %}{{ current_year }} calendar{% endblock %}

{% block extra_css %}
    {{ block.super }}
    <style>
        table.month {
            margin: 20px;
        }
        table.month td, table.month th {
            padding: 5px;
        }
    </style>
{% endblock %}

{% block content %}
    {% include "wagtailadmin/shared/header.html" with title="Calendar" icon="date" %}

    <div class="nice-padding">
        {{ calendar_html|safe }}
    </div>
{% endblock %}

```
 Here we are overriding three of the blocks defined in the base template: `titletag` (which sets the content of the HTML `<title>` tag), `extra_css` (which allows us to provide additional CSS styles specific to this page), and `content` (for the main content area of the page). We’re also including the standard header bar component, and setting a title and icon. For a list of the recognised icon identifiers, see the [style guide](../contributing/styleguide#styleguide).

 Revisiting `/admin/calendar/` will now show the calendar within the Wagtail admin page furniture.

 ![A calendar, shown within the Wagtail admin interface]

   Adding a menu item
------------------

 Our calendar view is now complete, but there’s no way to reach it from the rest of the admin backend. To add an item to the sidebar menu, we’ll use another hook, [Register Admin Menu Item](../reference/hooks#register-admin-menu-item). Update `wagtail_hooks.py` as follows:

 
```
from django.urls import path, reverse

from wagtail.admin.menu import MenuItem
from wagtail import hooks

from .views import index


@hooks.register('register_admin_urls')
def register_calendar_url():
    return [
        path('calendar/', index, name='calendar'),
    ]


@hooks.register('register_admin_menu_item')
def register_calendar_menu_item():
    return MenuItem('Calendar', reverse('calendar'), icon_name='date')

```
 A ‘Calendar’ item will now appear in the menu.

 ![Wagtail admin sidebar menu, showing a "Calendar" menu item with a date icon]

   Adding a group of menu items
----------------------------

 Sometimes you want to group custom views together in a single menu item in the sidebar. Let’s create another view to display only the current calendar month:

 
```
import calendar
from django.shortcuts import render
from django.utils import timezone


def index(request):
    current_year = timezone.now().year
    calendar_html = calendar.HTMLCalendar().formatyear(current_year)

    return render(request, 'wagtailcalendar/index.html', {
        'current_year': current_year,
        'calendar_html': calendar_html,
    })

def month(request):
    current_year = timezone.now().year
    current_month = timezone.now().month
    calendar_html = calendar.HTMLCalendar().formatmonth(current_year, current_month)

    return render(request, 'wagtailcalendar/index.html', {
        'current_year': current_year,
        'calendar_html': calendar_html,
    })

```
 We also need to update `wagtail_hooks.py` to register our URL in the admin interface:

 
```
from django.urls import path
from wagtail import hooks

from .views import index, month


@hooks.register('register_admin_urls')
def register_calendar_url():
    return [
        path('calendar/', index, name='calendar'),
        path('calendar/month/', month, name='calendar-month'),
    ]

```
 The calendar will now be visible at the URL `/admin/calendar/month/`.

 ![A single calender month]

 Finally we can alter our `wagtail_hooks.py` to include a group of custom menu items. This is similar to adding a single item but involves importing two more classes, `Menu` and `SubmenuMenuItem`.

 
```
from django.urls import path, reverse

from wagtail.admin.menu import Menu, MenuItem, SubmenuMenuItem
from wagtail import hooks


from .views import index, month


@hooks.register('register_admin_urls')
def register_calendar_url():
    return [
        path('calendar/', index, name='calendar'),
        path('calendar/month/', month, name='calendar-month'),
    ]


@hooks.register('register_admin_menu_item')
def register_calendar_menu_item():
    submenu = Menu(items=[
        MenuItem('Calendar', reverse('calendar'), icon_name='date'),
        MenuItem('Current month', reverse('calendar-month'), icon_name='date'),
    ])

    return SubmenuMenuItem('Calendar', submenu, icon_name='date')

```
 The ‘Calendar’ item will now appear as a group of menu items. When expanded, the ‘Calendar’ item will now show our two custom menu items.

 ![Wagtail admin sidebar menu, showing an expanded "Calendar" group menu item with a date icon, showing two child menu items, 'Calendar' and 'Month'.]

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/admin_views.html>

Generic views
=============

 Wagtail provides a number of generic views for handling common tasks such as creating / editing model instances, and chooser modals. Since these often involve several related views with shared properties (such as the model that we’re working with, and its associated icon) Wagtail also implements the concept of a *viewset*, which allows a bundle of views to be defined collectively, and their URLs to be registered with the admin app as a single operation through the `register_admin_viewset` hook.

  ModelViewSet
------------

 The `wagtail.admin.viewsets.model.ModelViewSet` class provides the views for listing, creating, editing and deleting model instances. For example, if we have the following model:

 
```
from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)

    def __str__(self):
        return "%s %s" % (self.first_name, self.last_name)

```
 The following definition (to be placed in the same app’s `views.py`) will generate a set of views for managing Person instances:

 
```
from wagtail.admin.viewsets.model import ModelViewSet
from .models import Person


class PersonViewSet(ModelViewSet):
    model = Person
    form_fields = ["first_name", "last_name"]
    icon = "user"


person_viewset = PersonViewSet("person")  # defines /admin/person/ as the base URL

```
 This viewset can then be registered with the Wagtail admin to make it available under the URL `/admin/person/`, by adding the following to `wagtail_hooks.py`:

 
```
from wagtail import hooks

from .views import person_viewset


@hooks.register("register_admin_viewset")
def register_viewset():
    return person_viewset

```
 Various additional attributes are available to customise the viewset - see [Viewsets](../reference/viewsets).

   ChooserViewSet
--------------

 The `wagtail.admin.viewsets.chooser.ChooserViewSet` class provides the views that make up a modal chooser interface, allowing users to select from a list of model instances to populate a ForeignKey field. Using the same `Person` model, the following definition (to be placed in `views.py`) will generate the views for a person chooser modal:

 
```
from wagtail.admin.viewsets.chooser import ChooserViewSet


class PersonChooserViewSet(ChooserViewSet):
    # The model can be specified as either the model class or an "app_label.model_name" string;
    # using a string avoids circular imports when accessing the StreamField block class (see below)
    model = "myapp.Person"

    icon = "user"
    choose_one_text = "Choose a person"
    choose_another_text = "Choose another person"
    edit_item_text = "Edit this person"
    form_fields = ["first_name", "last_name"]  # fields to show in the "Create" tab


person_chooser_viewset = PersonChooserViewSet("person_chooser")

```
 Again this can be registered with the `register_admin_viewset` hook:

 
```
from wagtail import hooks

from .views import person_chooser_viewset


@hooks.register("register_admin_viewset")
def register_viewset():
    return person_chooser_viewset

```
 Registering a chooser viewset will also set up a chooser widget to be used whenever a ForeignKey field to that model appears in a `WagtailAdminModelForm` - see [Using forms in admin views](forms). In particular, this means that a panel definition such as `FieldPanel("author")`, where `author` is a foreign key to the `Person` model, will automatically use this chooser interface. The chooser widget class can also be retrieved directly (for use in ordinary Django forms, for example) as the `widget_class` property on the viewset. For example, placing the following code in `widgets.py` will make the chooser widget available to be imported with `from myapp.widgets import PersonChooserWidget`:

 
```
from .views import person_chooser_viewset

PersonChooserWidget = person_chooser_viewset.widget_class

```
 The viewset also makes a StreamField chooser block class available, through the method `get_block_class`. Placing the following code in `blocks.py` will make a chooser block available for use in StreamField definitions by importing `from myapp.blocks import PersonChooserBlock`:

 
```
from .views import person_chooser_viewset

PersonChooserBlock = person_chooser_viewset.get_block_class(
    name="PersonChooserBlock", module_path="myapp.blocks"
)

```
   Chooser viewsets for non-model datasources
------------------------------------------

 While the generic chooser views are primarily designed to use Django models as the data source, choosers based on other sources such as REST API endpoints can be implemented by overriding the individual methods that deal with data retrieval.

 Within `wagtail.admin.views.generic.chooser`:

 * `BaseChooseView.get_object_list()` - returns a list of records to be displayed in the chooser. (In the default implementation, this is a Django QuerySet, and the records are model instances.)
* `BaseChooseView.columns` - a list of `wagtail.admin.ui.tables.Column` objects specifying the fields of the record to display in the final table
* `BaseChooseView.apply_object_list_ordering(objects)` - given a list of records as returned from `get_object_list`, returns the list with the desired ordering applied
* `ChosenViewMixin.get_object(pk)` - returns the record identified by the given primary key
* `ChosenResponseMixin.get_chosen_response_data(item)` - given a record, returns the dictionary of data that will be passed back to the chooser widget to populate it (consisting of items `id` and `title`, unless the chooser widget’s JavaScript has been customised)

 Within `wagtail.admin.widgets`:

 * `BaseChooser.get_instance(value)` - given a value that may be a record, a primary key or None, returns the corresponding record or None
* `BaseChooser.get_value_data_from_instance(item)` - given a record, returns the dictionary of data that will populate the chooser widget (consisting of items `id` and `title`, unless the widget’s JavaScript has been customised)

 For example, the following code will implement a chooser that runs against a JSON endpoint for the User model at `http://localhost:8000/api/users/`, set up with Django REST Framework using the default configuration and no pagination:

 
```
from django.views.generic.base import View
import requests

from wagtail.admin.ui.tables import Column, TitleColumn
from wagtail.admin.views.generic.chooser import (
    BaseChooseView, ChooseViewMixin, ChooseResultsViewMixin, ChosenResponseMixin, ChosenViewMixin, CreationFormMixin
)
from wagtail.admin.viewsets.chooser import ChooserViewSet
from wagtail.admin.widgets import BaseChooser


class BaseUserChooseView(BaseChooseView):
    @property
    def columns(self):
        return [
            TitleColumn(
                "title",
                label="Title",
                accessor='username',
                id_accessor='id',
                url_name=self.chosen_url_name,
                link_attrs={"data-chooser-modal-choice": True},
            ),
            Column(
                "email", label="Email", accessor="email"
            )
        ]

    def get_object_list(self):
        r = requests.get("http://localhost:8000/api/users/")
        r.raise_for_status()
        results = r.json()
        return results

    def apply_object_list_ordering(self, objects):
        return objects


class UserChooseView(ChooseViewMixin, CreationFormMixin, BaseUserChooseView):
    pass


class UserChooseResultsView(ChooseResultsViewMixin, CreationFormMixin, BaseUserChooseView):
    pass


class UserChosenViewMixin(ChosenViewMixin):
    def get_object(self, pk):
        r = requests.get("http://localhost:8000/api/users/%d/" % int(pk))
        r.raise_for_status()
        return r.json()


class UserChosenResponseMixin(ChosenResponseMixin):
    def get_chosen_response_data(self, item):
        return {
            "id": item["id"],
            "title": item["username"],
        }


class UserChosenView(UserChosenViewMixin, UserChosenResponseMixin, View):
    pass


class BaseUserChooserWidget(BaseChooser):
    def get_instance(self, value):
        if value is None:
            return None
        elif isinstance(value, dict):
            return value
        else:
            r = requests.get("http://localhost:8000/api/users/%d/" % int(value))
            r.raise_for_status()
            return r.json()

    def get_value_data_from_instance(self, instance):
        return {
            "id": instance["id"],
            "title": instance["username"],
        }


class UserChooserViewSet(ChooserViewSet):
    icon = "user"
    choose_one_text = "Choose a user"
    choose_another_text = "Choose another user"
    edit_item_text = "Edit this user"

    choose_view_class = UserChooseView
    choose_results_view_class = UserChooseResultsView
    chosen_view_class = UserChosenView
    base_widget_class = BaseUserChooserWidget


user_chooser_viewset = UserChooserViewSet("user_chooser", url_prefix="user-chooser")

```
 If the data source implements its own pagination - meaning that the pagination mechanism built into the chooser should be bypassed - the `BaseChooseView.get_results_page(request)` method can be overridden instead of `get_object_list`. This should return an instance of `django.core.paginator.Page`. For example, if the API in the above example followed the conventions of the Wagtail API, implementing pagination with `offset` and `limit` URL parameters and returning a dict consisting of `meta` and `results`, the `BaseUserChooseView` implementation could be modified as follows:

 
```
from django.core.paginator import Page, Paginator

class APIPaginator(Paginator):
    """
    Customisation of Django's Paginator class for use when we don't want it to handle
    slicing on the result set, but still want it to generate the page numbering based
    on a known result count.
    """
    def __init__(self, count, per_page, **kwargs):
        self._count = int(count)
        super().__init__([], per_page, **kwargs)

    @property
    def count(self):
        return self._count

class BaseUserChooseView(BaseChooseView):
    @property
    def columns(self):
        return [
            TitleColumn(
                "title",
                label="Title",
                accessor='username',
                id_accessor='id',
                url_name=self.chosen_url_name,
                link_attrs={"data-chooser-modal-choice": True},
            ),
            Column(
                "email", label="Email", accessor="email"
            )
        ]

    def get_results_page(self, request):
        try:
            page_number = int(request.GET.get('p', 1))
        except ValueError:
            page_number = 1

        r = requests.get("http://localhost:8000/api/users/", params={
            'offset': (page_number - 1) * self.per_page,
            'limit': self.per_page,
        })
        r.raise_for_status()
        result = r.json()
        paginator = APIPaginator(result['meta']['total_count'], self.per_page)
        page = Page(result['items'], page_number, paginator)
        return page

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/generic_views.html>

Template components
===================

 Working with objects that know how to render themselves as elements on an HTML template is a common pattern seen throughout the Wagtail admin. For example, the admin homepage is a view provided by the central `wagtail.admin` app, but brings together information panels sourced from various other modules of Wagtail, such as images and documents (potentially along with others provided by third-party packages). These panels are passed to the homepage via the [`construct_homepage_panels`](../reference/hooks#construct-homepage-panels) hook, and each one is responsible for providing its own HTML rendering. In this way, the module providing the panel has full control over how it appears on the homepage.

 Wagtail implements this pattern using a standard object type known as a **component**. A component is a Python object that provides the following methods and properties:

 Given a context dictionary from the calling template (which may be a [`Context`](https://docs.djangoproject.com/en/stable/ref/templates/api/#django.template.Context "(in Django v4.1)") object or a plain `dict` of context variables), returns the string representation to be inserted into the template. This will be subject to Django’s HTML escaping rules, so a return value consisting of HTML should typically be returned as a [`SafeString`](https://docs.djangoproject.com/en/stable/ref/utils/#module-django.utils.safestring "(in Django v4.1)") instance.

 A (possibly empty) [form media](https://docs.djangoproject.com/en/stable/topics/forms/media/ "(in Django v4.1)") object defining JavaScript and CSS resources used by the component.

  Note

 Any object implementing this API can be considered a valid component; it does not necessarily have to inherit from the `Component` class described below, and user code that works with components should not assume this (for example, it must not use `isinstance` to check whether a given value is a component).

   Creating components
-------------------

 The preferred way to create a component is to define a subclass of `wagtail.admin.ui.components.Component` and specify a `template_name` attribute on it. The rendered template will then be used as the component’s HTML representation:

 
```
from wagtail.admin.ui.components import Component

class WelcomePanel(Component):
    template_name = 'my_app/panels/welcome.html'


my_welcome_panel = WelcomePanel()

```
 `my_app/templates/my_app/panels/welcome.html`:

 
```
<h1>Welcome to my app!</h1>

```
 For simple cases that don’t require a template, the `render_html` method can be overridden instead:

 
```
from django.utils.html import format_html
from wagtail.admin.components import Component

class WelcomePanel(Component):
    def render_html(self, parent_context):
        return format_html("<h1>{}</h1>", "Welcome to my app!")

```
   Passing context to the template
-------------------------------

 The `get_context_data` method can be overridden to pass context variables to the template. As with `render_html`, this receives the context dictionary from the calling template:

 
```
from wagtail.admin.ui.components import Component

class WelcomePanel(Component):
    template_name = 'my_app/panels/welcome.html'

    def get_context_data(self, parent_context):
        context = super().get_context_data(parent_context)
        context['username'] = parent_context['request'].user.username
        return context

```
 `my_app/templates/my_app/panels/welcome.html`:

 
```
<h1>Welcome to my app, {{ username }}!</h1>

```
   Adding media definitions
------------------------

 Like Django form widgets, components can specify associated JavaScript and CSS resources using either an inner `Media` class or a dynamic `media` property:

 
```
class WelcomePanel(Component):
    template_name = 'my_app/panels/welcome.html'

    class Media:
        css = {
            'all': ('my_app/css/welcome-panel.css',)
        }

```
   Using components on your own templates
--------------------------------------

 The `wagtailadmin_tags` tag library provides a `{% component %}` tag for including components on a template. This takes care of passing context variables from the calling template to the component (which would not be the case for a basic `{{ ... }}` variable tag). For example, given the view:

 
```
from django.shortcuts import render

def welcome_page(request):
    panels = [
        WelcomePanel(),
    ]

    render(request, 'my_app/welcome.html', {
        'panels': panels,
    })

```
 the `my_app/welcome.html` template could render the panels as follows:

 
```
{% load wagtailadmin_tags %}
{% for panel in panels %}
    {% component panel %}
{% endfor %}

```
 Note that it is your template’s responsibility to output any media declarations defined on the components. For a Wagtail admin view, this is best done by constructing a media object for the whole page within the view, passing this to the template, and outputting it via the base template’s `extra_js` and `extra_css` blocks:

 
```
from django.forms import Media
from django.shortcuts import render

def welcome_page(request):
    panels = [
        WelcomePanel(),
    ]

    media = Media()
    for panel in panels:
        media += panel.media

    render(request, 'my_app/welcome.html', {
        'panels': panels,
        'media': media,
    })

```
 `my_app/welcome.html`:

 
```
{% extends "wagtailadmin/base.html" %}
{% load wagtailadmin_tags %}

{% block extra_js %}
    {{ block.super }}
    {{ media.js }}
{% endblock %}

{% block extra_css %}
    {{ block.super }}
    {{ media.css }}
{% endblock %}

{% block content %}
    {% for panel in panels %}
        {% component panel %}
    {% endfor %}
{% endblock %}

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/template_components.html>

Using forms in admin views
==========================

 [Django’s forms framework](https://docs.djangoproject.com/en/stable/topics/forms/) can be used within Wagtail admin views just like in any other Django app. However, Wagtail also provides various admin-specific form widgets, such as date/time pickers and choosers for pages, documents, images and snippets. By constructing forms using `wagtail.admin.forms.models.WagtailAdminModelForm` as the base class instead of `django.forms.models.ModelForm`, the most appropriate widget will be selected for each model field. For example, given the model and form definition:

 
```
from django.db import models

from wagtail.admin.forms.models import WagtailAdminModelForm
from wagtail.images.models import Image


class FeaturedImage(models.Model):
    date = models.DateField()
    image = models.ForeignKey(Image, on_delete=models.CASCADE)


class FeaturedImageForm(WagtailAdminModelForm):
    class Meta:
        model = FeaturedImage

```
 the `date` and `image` fields on the form will use a date picker and image chooser widget respectively.

  Defining admin form widgets
---------------------------

 If you have implemented a form widget of your own, you can configure `WagtailAdminModelForm` to select it for a given model field type. This is done by calling the `wagtail.admin.forms.models.register_form_field_override` function, typically in an `AppConfig.ready` method.

 For example, if the app `wagtail.videos` implements a `Video` model and a `VideoChooser` form widget, the following AppConfig definition will ensure that `WagtailAdminModelForm` selects `VideoChooser` as the form widget for any foreign keys pointing to `Video`:

 
```
from django.apps import AppConfig
from django.db.models import ForeignKey


class WagtailVideosAppConfig(AppConfig):
    name = 'wagtail.videos'
    label = 'wagtailvideos'

    def ready(self):
        from wagtail.admin.forms.models import register_form_field_override
        from .models import Video
        from .widgets import VideoChooser
        register_form_field_override(ForeignKey, to=Video, override={'widget': VideoChooser})

```
 Wagtail’s edit views for pages, snippets and ModelAdmin use `WagtailAdminModelForm` as standard, so this change will take effect across the Wagtail admin; a foreign key to `Video` on a page model will automatically use the `VideoChooser` widget, with no need to specify this explicitly.

   Panels
------

 Panels (also known as edit handlers until Wagtail 3.0) are Wagtail’s mechanism for specifying the content and layout of a model form without having to write a template. They are used for the editing interface for pages and snippets, as well as the [ModelAdmin](../reference/contrib/modeladmin/index) and [site settings](../reference/contrib/settings) contrib modules.

 See [Panel types](../reference/pages/panels) for the set of panel types provided by Wagtail. All panels inherit from the base class `wagtail.admin.panels.Panel`. A single panel object (usually `ObjectList` or `TabbedInterface`) exists at the top level and is the only one directly accessed by the view code; panels containing child panels inherit from the base class `wagtail.admin.panels.PanelGroup` and take care of recursively calling methods on their child panels where appropriate.

 A view performs the following steps to render a model form through the panels mechanism:

 * The top-level panel object for the model is retrieved. Usually this is done by looking up the model’s `edit_handler` property and falling back on an `ObjectList` consisting of children given by the model’s `panels` property. However, it may come from elsewhere - for example, the ModelAdmin module allows defining it on the ModelAdmin configuration object.
* If the `PanelsGroup`s permissions do not allow a user to see this panel, then nothing more will be done.
* The view calls `bind_to_model` on the top-level panel, passing the model class, and this returns a clone of the panel with a `model` property. As part of this process the `on_model_bound` method is invoked on each child panel, to allow it to perform additional initialisation that requires access to the model (for example, this is where `FieldPanel` retrieves the model field definition).
* The view then calls `get_form_class` on the top-level panel to retrieve a ModelForm subclass that can be used to edit the model. This proceeds as follows:


	+ Retrieve a base form class from the model’s `base_form_class` property, falling back on `wagtail.admin.forms.WagtailAdminModelForm`
	+ Call `get_form_options` on each child panel - which returns a dictionary of properties including `fields` and `widgets` - and merge the results into a single dictionary
	+ Construct a subclass of the base form class, with the options dict forming the attributes of the inner `Meta` class.
* An instance of the form class is created as per a normal Django form view.
* The view then calls `get_bound_panel` on the top-level panel, passing `instance`, `form` and `request` as keyword arguments. This returns a `BoundPanel` object, which follows [the template component API](template_components). Finally, the `BoundPanel` object (and its media definition) is rendered onto the template.

 New panel types can be defined by subclassing `wagtail.admin.panels.Panel` - see [Panel API](../reference/panel_api).

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/forms.html>

Adding reports
==============

 Reports are views with listings of pages matching a specific query. They can also export these listings in spreadsheet format. They are found in the *Reports* submenu: by default, the *Locked pages* report is provided, allowing an overview of locked pages on the site.

 It is possible to create your own custom reports in the Wagtail admin. Two base classes are provided: `wagtail.admin.views.reports.ReportView`, which provides basic listing and spreadsheet export functionality, and `wagtail.admin.views.reports.PageReportView`, which additionally provides a default set of fields suitable for page listings. For this example, we’ll add a report which shows any pages with unpublished changes.

 
```
# <project>/views.py
from wagtail.admin.views.reports import PageReportView


class UnpublishedChangesReportView(PageReportView):
    pass

```
  Defining your report
--------------------

 The most important attributes and methods to customise to define your report are:

 This retrieves the queryset of pages for your report. For our example:

 
```
# <project>/views.py

from wagtail.admin.views.reports import PageReportView
from wagtail.models import Page


class UnpublishedChangesReportView(PageReportView):

    def get_queryset(self):
        return Page.objects.filter(has_unpublished_changes=True)

```
 (string)

 The template used to render your report. For `ReportView`, this defaults to `"wagtailadmin/reports/base_report.html"`, which provides an empty report page layout; for `PageReportView`, this defaults to `"wagtailadmin/reports/base_page_report.html"` which provides a listing based on the explorer views, displaying action buttons, as well as the title, time of the last update, status, and specific type of any pages. In this example, we’ll change this to a new template in a later section.

 (string)

 The name of your report, which will be displayed in the header. For our example, we’ll set it to `"Pages with unpublished changes"`.

 (string)

 The name of the icon, using the standard Wagtail icon names. For example, the locked pages view uses `"locked"`, and for our example report, we’ll set it to `'doc-empty-inverse'`.

   Spreadsheet exports
-------------------

 (list)

 A list of the fields/attributes for each model which are exported as columns in the spreadsheet view. For `ReportView`, this is empty by default, and for `PageReportView`, it corresponds to the listing fields: the title, time of the last update, status, and specific type of any pages. For our example, we might want to know when the page was last published, so we’ll set `list_export` as follows:

 `list_export = PageReportView.list_export + ['last_published_at']`

 (dictionary)

 A dictionary of any fields/attributes in `list_export` for which you wish to manually specify a heading for the spreadsheet column, and their headings. If unspecified, the heading will be taken from the field `verbose_name` if applicable, and the attribute string otherwise. For our example, `last_published_at` will automatically get a heading of `"Last Published At"`, but a simple “Last Published” looks neater. We’ll add that by setting `export_headings`:

 `export_headings = dict(last_published_at='Last Published', **PageReportView.export_headings)`

 (dictionary)

 A dictionary of `(value_class_1, value_class_2, ...)` tuples mapping to `{export_format: preprocessing_function}` dictionaries, allowing custom preprocessing functions to be applied when exporting field values of specific classes (or their subclasses). If unspecified (and `ReportView.custom_field_preprocess` also does not specify a function), `force_str` will be used. To prevent preprocessing, set the preprocessing\_function to `None`.

 (dictionary)

 A dictionary of `field_name` strings mapping to `{export_format: preprocessing_function}` dictionaries, allowing custom preprocessing functions to be applied when exporting field values of specific classes (or their subclasses). This will take priority over functions specified in `ReportView.custom_value_preprocess`. If unspecified (and `ReportView.custom_value_preprocess` also does not specify a function), `force_str` will be used. To prevent preprocessing, set the preprocessing\_function to `None`.

   Customising templates
---------------------

 For this example “pages with unpublished changes” report, we’ll add an extra column to the listing template, showing the last publication date for each page. To do this, we’ll extend two templates: `wagtailadmin/reports/base_page_report.html`, and `wagtailadmin/reports/listing/_list_page_report.html`.

 
```
{# <project>/templates/reports/unpublished_changes_report.html #}

{% extends 'wagtailadmin/reports/base_page_report.html' %}

{% block listing %}
    {% include 'reports/include/_list_unpublished_changes.html' %}
{% endblock %}

{% block no_results %}
    <p>No pages with unpublished changes.</p>
{% endblock %}

```
 
```
{# <project>/templates/reports/include/_list_unpublished_changes.html #}

{% extends 'wagtailadmin/reports/listing/_list_page_report.html' %}

{% block extra_columns %}
    <th>Last Published</th>
{% endblock %}

{% block extra_page_data %}
    <td valign="top">
        {{ page.last_published_at }}
    </td>
{% endblock %}

```
 Finally, we’ll set `UnpublishedChangesReportView.template_name` to this new template: `'reports/unpublished_changes_report.html'`.

   Adding a menu item and admin URL
--------------------------------

 To add a menu item for your new report to the *Reports* submenu, you will need to use the `register_reports_menu_item` hook (see: [Register Reports Menu Item](../reference/hooks#register-reports-menu-item)). To add an admin url for the report, you will need to use the `register_admin_urls` hook (see: [Register Admin URLs](../reference/hooks#register-admin-urls)). This can be done as follows:

 
```
# <project>/wagtail_hooks.py

from django.urls import path, reverse

from wagtail.admin.menu import AdminOnlyMenuItem
from wagtail import hooks

from .views import UnpublishedChangesReportView

@hooks.register('register_reports_menu_item')
def register_unpublished_changes_report_menu_item():
    return AdminOnlyMenuItem("Pages with unpublished changes", reverse('unpublished_changes_report'), classnames='icon icon-' + UnpublishedChangesReportView.header_icon, order=700)

@hooks.register('register_admin_urls')
def register_unpublished_changes_report_url():
    return [
        path('reports/unpublished-changes/', UnpublishedChangesReportView.as_view(), name='unpublished_changes_report'),
    ]

```
 Here, we use the `AdminOnlyMenuItem` class to ensure our report icon is only shown to superusers. To make the report visible to all users, you could replace this with `MenuItem`.

   The full code
-------------

 
```
# <project>/views.py

from wagtail.admin.views.reports import PageReportView
from wagtail.models import Page


class UnpublishedChangesReportView(PageReportView):

    header_icon = 'doc-empty-inverse'
    template_name = 'reports/unpublished_changes_report.html'
    title = "Pages with unpublished changes"

    list_export = PageReportView.list_export + ['last_published_at']
    export_headings = dict(last_published_at='Last Published', **PageReportView.export_headings)

    def get_queryset(self):
        return Page.objects.filter(has_unpublished_changes=True)

```
 
```
# <project>/wagtail_hooks.py

from django.urls import path, reverse

from wagtail.admin.menu import AdminOnlyMenuItem
from wagtail import hooks

from .views import UnpublishedChangesReportView

@hooks.register('register_reports_menu_item')
def register_unpublished_changes_report_menu_item():
    return AdminOnlyMenuItem("Pages with unpublished changes", reverse('unpublished_changes_report'), classnames='icon icon-' + UnpublishedChangesReportView.header_icon, order=700)

@hooks.register('register_admin_urls')
def register_unpublished_changes_report_url():
    return [
        path('reports/unpublished-changes/', UnpublishedChangesReportView.as_view(), name='unpublished_changes_report'),
    ]

```
 
```
{# <project>/templates/reports/unpublished_changes_report.html #}

{% extends 'wagtailadmin/reports/base_page_report.html' %}

{% block listing %}
    {% include 'reports/include/_list_unpublished_changes.html' %}
{% endblock %}

{% block no_results %}
    <p>No pages with unpublished changes.</p>
{% endblock %}

```
 
```
{# <project>/templates/reports/include/_list_unpublished_changes.html #}

{% extends 'wagtailadmin/reports/listing/_list_page_report.html' %}

{% block extra_columns %}
    <th>Last Published</th>
{% endblock %}

{% block extra_page_data %}
    <td valign="top">
        {{ page.last_published_at }}
    </td>
{% endblock %}

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/adding_reports.html>

Adding new Task types
=====================

 The Workflow system allows users to create tasks, which represent stages of moderation.

 Wagtail provides one built in task type: `GroupApprovalTask`, which allows any user in specific groups to approve or reject moderation.

 However, it is possible to add your own task types in code. Instances of your custom task can then be created in the `Tasks` section of the Wagtail Admin.

  Task models
-----------

 All custom tasks must be models inheriting from `wagtailcore.Task`. In this set of examples, we’ll set up a task which can be approved by only one specific user.

 
```
# <project>/models.py

from wagtail.models import Task


class UserApprovalTask(Task):
    pass

```
 Subclassed Tasks follow the same approach as Pages: they are concrete models, with the specific subclass instance accessible by calling `Task.specific()`.

 You can now add any custom fields. To make these editable in the admin, add the names of the fields into the `admin_form_fields` attribute:

 For example:

 
```
# <project>/models.py

from django.conf import settings
from django.db import models
from wagtail.models import Task


class UserApprovalTask(Task):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=False)

    admin_form_fields = Task.admin_form_fields + ['user']

```
 Any fields that shouldn’t be edited after task creation - for example, anything that would fundamentally change the meaning of the task in any history logs - can be added to `admin_form_readonly_on_edit_fields`. For example:

 
```
# <project>/models.py

from django.conf import settings
from django.db import models
from wagtail.models import Task


class UserApprovalTask(Task):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=False)

    admin_form_fields = Task.admin_form_fields + ['user']

    # prevent editing of `user` after the task is created
    # by default, this attribute contains the 'name' field to prevent tasks from being renamed
    admin_form_readonly_on_edit_fields = Task.admin_form_readonly_on_edit_fields + ['user']

```
 Wagtail will choose a default form widget to use based on the field type. But you can override the form widget using the `admin_form_widgets` attribute:

 
```
# <project>/models.py

from django.conf import settings
from django.db import models
from wagtail.models import Task

from .widgets import CustomUserChooserWidget


class UserApprovalTask(Task):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=False)

    admin_form_fields = Task.admin_form_fields + ['user']

    admin_form_widgets = {
        'user': CustomUserChooserWidget,
    }

```
   Custom TaskState models
-----------------------

 You might also need to store custom state information for the task: for example, a rating left by an approving user. Normally, this is done on an instance of `TaskState`, which is created when a page starts the task. However, this can also be subclassed equivalently to `Task`:

 
```
# <project>/models.py

from wagtail.models import TaskState


class UserApprovalTaskState(TaskState):
    pass

```
 Your custom task must then be instructed to generate an instance of your custom task state on start instead of a plain `TaskState` instance:

 
```
# <project>/models.py

from django.conf import settings
from django.db import models
from wagtail.models import Task, TaskState


class UserApprovalTaskState(TaskState):
    pass


class UserApprovalTask(Task):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=False)

    admin_form_fields = Task.admin_form_fields + ['user']

    task_state_class = UserApprovalTaskState

```
   Customising behaviour
---------------------

 Both `Task` and `TaskState` have a number of methods which can be overridden to implement custom behaviour. Here are some of the most useful:

 `Task.user_can_access_editor(page, user)`, `Task.user_can_lock(page, user)`, `Task.user_can_unlock(page, user)`:

 These methods determine if users usually without permissions can access the editor, lock, or unlock the page, by returning True or False. Note that returning `False` will not prevent users who would normally be able to perform those actions. For example, for our `UserApprovalTask`:

 
```
def user_can_access_editor(self, page, user):
    return user == self.user

```
 `Task.page_locked_for_user(page, user)`:

 This returns `True` if the page should be locked and uneditable by the user. It is used by `GroupApprovalTask` to lock the page to any users not in the approval group.

 
```
def page_locked_for_user(self, page, user):
    return user != self.user

```
 `Task.get_actions(page, user)`:

 This returns a list of `(action_name, action_verbose_name, action_requires_additional_data_from_modal)` tuples, corresponding to the actions available for the task in the edit view menu. `action_requires_additional_data_from_modal` should be a boolean, returning `True` if choosing the action should open a modal for additional data input - for example, entering a comment.

 For example:

 
```
def get_actions(self, page, user):
    if user == self.user:
        return [
            ('approve', "Approve", False),
            ('reject', "Reject", False),
            ('cancel', "Cancel", False),
        ]
    else:
        return []

```
 `Task.get_form_for_action(action)`:

 Returns a form to be used for additional data input for the given action modal. By default, returns `TaskStateCommentForm`, with a single comment field. The form data returned in `form.cleaned_data` must be fully serializable as JSON.

 `Task.get_template_for_action(action)`:

 Returns the name of a custom template to be used in rendering the data entry modal for that action.

 `Task.on_action(task_state, user, action_name, **kwargs)`:

 This performs the actions specified in `Task.get_actions(page, user)`: it is passed an action name, for example `approve`, and the relevant task state. By default, it calls `approve` and `reject` methods on the task state when the corresponding action names are passed through. Any additional data entered in a modal (see `get_form_for_action` and `get_actions`) is supplied as kwargs.

 For example, let’s say we wanted to add an additional option: cancelling the entire workflow:

 
```
def on_action(self, task_state, user, action_name):
    if action_name == 'cancel':
        return task_state.workflow_state.cancel(user=user)
    else:
        return super().on_action(task_state, user, workflow_state)

```
 `Task.get_task_states_user_can_moderate(user, **kwargs)`:

 This returns a QuerySet of `TaskStates` (or subclasses) the given user can moderate - this is currently used to select pages to display on the user’s dashboard.

 For example:

 
```
def get_task_states_user_can_moderate(self, user, **kwargs):
    if user == self.user:
        # get all task states linked to the (base class of) current task
        return TaskState.objects.filter(status=TaskState.STATUS_IN_PROGRESS, task=self.task_ptr)
    else:
        return TaskState.objects.none()

```
 `Task.get_description()`

 A class method that returns the human-readable description for the task.

 For example:

 
```
@classmethod
def get_description(cls):
    return _("Members of the chosen Wagtail Groups can approve this task")

```
   Adding notifications
--------------------

 Wagtail’s notifications are sent by `wagtail.admin.mail.Notifier` subclasses: callables intended to be connected to a signal.

 By default, email notifications are sent upon workflow submission, approval and rejection, and upon submission to a group approval task.

 As an example, we’ll add email notifications for when our new task is started.

 
```
# <project>/mail.py

from wagtail.admin.mail import EmailNotificationMixin, Notifier
from wagtail.models import TaskState

from .models import UserApprovalTaskState


class BaseUserApprovalTaskStateEmailNotifier(EmailNotificationMixin, Notifier):
    """A base notifier to send updates for UserApprovalTask events"""

    def __init__(self):
        # Allow UserApprovalTaskState and TaskState to send notifications
        super().__init__((UserApprovalTaskState, TaskState))

    def can_handle(self, instance, **kwargs):
        if super().can_handle(instance, **kwargs) and isinstance(instance.task.specific, UserApprovalTask):
            # Don't send notifications if a Task has been cancelled and then resumed - when page was updated to a new revision
            return not TaskState.objects.filter(workflow_state=instance.workflow_state, task=instance.task, status=TaskState.STATUS_CANCELLED).exists()
        return False

    def get_context(self, task_state, **kwargs):
        context = super().get_context(task_state, **kwargs)
        context['page'] = task_state.workflow_state.page
        context['task'] = task_state.task.specific
        return context

    def get_recipient_users(self, task_state, **kwargs):

        # Send emails to the user assigned to the task
        approving_user = task_state.task.specific.user

        recipients = {approving_user}

        return recipients


class UserApprovalTaskStateSubmissionEmailNotifier(BaseUserApprovalTaskStateEmailNotifier):
    """A notifier to send updates for UserApprovalTask submission events"""

    notification = 'submitted'

```
 Similarly, you could define notifier subclasses for approval and rejection notifications.

 Next, you need to instantiate the notifier, and connect it to the `task_submitted` signal.

 
```
# <project>/signal_handlers.py

from wagtail.signals import task_submitted
from .mail import UserApprovalTaskStateSubmissionEmailNotifier


task_submission_email_notifier = UserApprovalTaskStateSubmissionEmailNotifier()

def register_signal_handlers():
    task_submitted.connect(user_approval_task_submission_email_notifier, dispatch_uid='user_approval_task_submitted_email_notification')

```
 `register_signal_handlers()` should then be run on loading the app: for example, by adding it to the `ready()` method in your `AppConfig`.

 
```
# <project>/apps.py
from django.apps import AppConfig


class MyAppConfig(AppConfig):
    name = 'myappname'
    label = 'myapplabel'
    verbose_name = 'My verbose app name'

    def ready(self):
        from .signal_handlers import register_signal_handlers
        register_signal_handlers()

```
  Note

 In Django versions before 3.2 your `AppConfig` subclass needs to be set as `default_app_config` in `<project>/__init__.py`. See the [relevant section in the Django docs](https://docs.djangoproject.com/en/3.1/ref/applications/#for-application-authors) for the version you are using.

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/custom_tasks.html>

Audit log
=========

 Wagtail provides a mechanism to log actions performed on its objects. Common activities such as page creation, update, deletion, locking and unlocking, revision scheduling and privacy changes are automatically logged at the model level.

 The Wagtail admin uses the action log entries to provide a site-wide and page specific history of changes. It uses a registry of ‘actions’ that provide additional context for the logged action.

 The audit log-driven Page history replaces the revisions list page, but provide a filter for revision-specific entries.

  Note

 The audit log does not replace revisions.

  The `wagtail.log_actions.log` function can be used to add logging to your own code.

  Note

 When adding logging, you need to log the action or actions that happen to the object. For example, if the user creates and publishes a page, there should be a “create” entry and a “publish” entry. Or, if the user copies a published page and chooses to keep it published, there should be a “copy” and a “publish” entry for new page.

  
```

    # mypackage/views.py
    from wagtail.log_actions import log

    def copy_for_translation(page):
        # ...
        page.copy(log_action='mypackage.copy_for_translation')

    def my_method(request, page):
        # ..
        # Manually log an action
        data = {
            'make': {'it': 'so'}
        }
        log(
            instance=page, action='mypackage.custom_action', data=data
        )

```
  Changed in version 2.15: The `log` function was added. Previously, logging was only implemented for pages, and invoked through the `PageLogEntry.objects.log_action` method.

   Log actions provided by Wagtail
-------------------------------

 

| Action | Notes |
| --- | --- |
| `wagtail.create` | The object was created |
| `wagtail.edit` | The object was edited (for pages, saved as draft) |
| `wagtail.delete` | The object was deleted. Will only surface in the Site History for administrators |
| `wagtail.publish` | The page was published |
| `wagtail.publish.schedule` | Draft is scheduled for publishing |
| `wagtail.publish.scheduled` | Draft published via `publish_scheduled` management command |
| `wagtail.schedule.cancel` | Draft scheduled for publishing cancelled via “Cancel scheduled publish” |
| `wagtail.unpublish` | The page was unpublished |
| `wagtail.unpublish.scheduled` | Page unpublished via `publish_scheduled` management command |
| `wagtail.lock` | Page was locked |
| `wagtail.unlock` | Page was unlocked |
| `wagtail.moderation.approve` | The revision was approved for moderation |
| `wagtail.moderation.reject` | The revision was rejected |
| `wagtail.rename` | A page was renamed |
| `wagtail.revert` | The page was reverted to a previous draft |
| `wagtail.copy` | The page was copied to a new location |
| `wagtail.copy_for_translation` | The page was copied into a new locale for translation |
| `wagtail.move` | The page was moved to a new location |
| `wagtail.reorder` | The order of the page under it’s parent was changed |
| `wagtail.view_restriction.create` | The page was restricted |
| `wagtail.view_restriction.edit` | The page restrictions were updated |
| `wagtail.view_restriction.delete` | The page restrictions were removed |
| `wagtail.workflow.start` | The page was submitted for moderation in a Workflow |
| `wagtail.workflow.approve` | The draft was approved at a Workflow Task |
| `wagtail.workflow.reject` | The draft was rejected, and changes requested at a Workflow Task |
| `wagtail.workflow.resume` | The draft was resubmitted to the workflow |
| `wagtail.workflow.cancel` | The workflow was cancelled |

   Log context
-----------

 The `wagtail.log_actions` module provides a context manager to simplify code that logs a large number of actions, such as import scripts:

 
```
from wagtail.log_actions import LogContext

with LogContext(user=User.objects.get(username='admin')):
    # ...
    log(page, 'wagtail.edit')
    # ...
    log(page, 'wagtail.publish')

```
 All `log` calls within the block will then be attributed to the specified user, and assigned a common UUID. A log context is created automatically for views within the Wagtail admin.

   Log models
----------

 Logs are stored in the database via the models `wagtail.models.PageLogEntry` (for actions on Page instances) and `wagtail.models.ModelLogEntry` (for actions on all other models). Page logs are stored in their own model to ensure that reports can be filtered according to the current user’s permissions, which could not be done efficiently with a generic foreign key.

 If your own models have complex reporting requirements that would make `ModelLogEntry` unsuitable, you can configure them to be logged to their own log model; this is done by subclassing the abstract `wagtail.models.BaseLogEntry` model, and registering that model with the log registry’s `register_model` method:

 
```
from myapp.models import Sprocket, SprocketLogEntry
# here SprocketLogEntry is a subclass of BaseLogEntry

@hooks.register('register_log_actions')
def sprocket_log_model(actions):
    actions.register_model(Sprocket, SprocketLogEntry)

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/audit_log.html>

Customising the user account settings form
==========================================

 This document describes how to customise the user account settings form that can be found by clicking “Account settings” at the bottom of the main menu.

  Adding new panels
-----------------

 Each panel on this form is a separate model form which can operate on an instance of either the user model, or the `wagtail.users.models.UserProfile`.

  ### Basic example

 Here is an example of how to add a new form that operates on the user model:

 
```
# forms.py

from django import forms
from django.contrib.auth import get_user_model

class CustomSettingsForm(forms.ModelForm):

    class Meta:
        model = get_user_model()
        fields = [...]

```
 
```
# hooks.py

from wagtail.admin.views.account import BaseSettingsPanel
from wagtail import hooks
from .forms import CustomSettingsForm

@hooks.register('register_account_settings_panel')
class CustomSettingsPanel(BaseSettingsPanel):
    name = 'custom'
    title = "My custom settings"
    order = 500
    form_class = CustomSettingsForm
    form_object = 'user'

```
 The attributes are as follows:

 * `name` - A unique name for the panel. All form fields are prefixed with this name, so it must be lowercase and cannot contain symbols -
* `title` - The heading that is displayed to the user
* `order` - Used to order panels on a tab. The builtin Wagtail panels start at `100` and increase by `100` for each panel.
* `form_class` - A `ModelForm` subclass that operates on a user or a profile
* `form_object` - Set to `user` to operate on the user, and `profile` to operate on the profile
* `tab` (optional) - Set which tab the panel appears on.
* `template_name` (optional) - Override the default template used for rendering the panel

    Operating on the `UserProfile` model
------------------------------------

 To add a panel that alters data on the user’s `wagtail.users.models.UserProfile` instance, set `form_object` to `'profile'`:

 
```
# forms.py

from django import forms
from wagtail.users.models import UserProfile

class CustomProfileSettingsForm(forms.ModelForm):

    class Meta:
        model = UserProfile
        fields = [...]

```
 
```
# hooks.py

from wagtail.admin.views.account import BaseSettingsPanel
from wagtail import hooks
from .forms import CustomProfileSettingsForm

@hooks.register('register_account_settings_panel')
class CustomSettingsPanel(BaseSettingsPanel):
    name = 'custom'
    title = "My custom settings"
    order = 500
    form_class = CustomProfileSettingsForm
    form_object = 'profile'

```
   Creating new tabs
-----------------

 You can define a new tab using the `SettingsTab` class:

 
```
# hooks.py

from wagtail.admin.views.account import BaseSettingsPanel, SettingsTab
from wagtail import hooks
from .forms import CustomSettingsForm

custom_tab = SettingsTab('custom', "Custom settings", order=300)

@hooks.register('register_account_settings_panel')
class CustomSettingsPanel(BaseSettingsPanel):
    name = 'custom'
    title = "My custom settings"
    tab = custom_tab
    order = 100
    form_class = CustomSettingsForm

```
 `SettingsTab` takes three arguments:

 * `name` - A slug to use for the tab (this is placed after the `#` when linking to a tab)
* `title` - The display name of the title
* `order` - The order of the tab. The builtin Wagtail tabs start at `100` and increase by `100` for each tab

   Customising the template
------------------------

 You can provide a custom template for the panel by specifying a template name:

 
```
# hooks.py

from wagtail.admin.views.account import BaseSettingsPanel
from wagtail import hooks
from .forms import CustomSettingsForm

@hooks.register('register_account_settings_panel')
class CustomSettingsPanel(BaseSettingsPanel):
    name = 'custom'
    title = "My custom settings"
    order = 500
    form_class = CustomSettingsForm
    template_name = 'myapp/admin/custom_settings.html'

```
 
```
{# templates/myapp/admin/custom_settings.html #}

{# This is the default template Wagtail uses, which just renders the form #}

{% block content %}
    {% for field in form %}
        {% include "wagtailadmin/shared/field.html" with field=field %}
    {% endfor %}
{% endblock %}

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/custom_account_settings.html>

Customising group edit/create views
===================================

 The views for managing groups within the app are collected into a ‘viewset’ class, which acts as a single point of reference for all shared components of those views, such as forms. By subclassing the viewset, it is possible to override those components and customise the behaviour of the group management interface.

  Custom edit/create forms
------------------------

 This example shows how to customise forms on the ‘edit group’ and ‘create group’ views in the Wagtail admin.

 Let’s say you need to connect Active Directory groups with Django groups. We create a model for Active Directory groups as follows:

 
```
from django.contrib.auth.models import Group
from django.db import models


class ADGroup(models.Model):
    guid = models.CharField(verbose_name="GUID", max_length=64, db_index=True, unique=True)
    name = models.CharField(verbose_name="Group", max_length=255)
    domain = models.CharField(verbose_name="Domain", max_length=255, db_index=True)
    description = models.TextField(verbose_name="Description", blank=True, null=True)
    roles = models.ManyToManyField(Group, verbose_name="Role", related_name="adgroups", blank=True)

class Meta:
    verbose_name = "AD group"
    verbose_name_plural = "AD groups"

```
 However, there is no role field on the Wagtail group ‘edit’ or ‘create’ view. To add it, inherit from `wagtail.users.forms.GroupForm` and add a new field:

 
```
from django import forms

from wagtail.users.forms import GroupForm as WagtailGroupForm

from .models import ADGroup


class GroupForm(WagtailGroupForm):
    adgroups = forms.ModelMultipleChoiceField(
        label="AD groups",
        required=False,
        queryset=ADGroup.objects.order_by("name"),
    )

    class Meta(WagtailGroupForm.Meta):
        fields = WagtailGroupForm.Meta.fields + ("adgroups",)

    def __init__(self, initial=None, instance=None, **kwargs):
        if instance is not None:
            if initial is None:
                initial = {}
            initial["adgroups"] = instance.adgroups.all()
        super().__init__(initial=initial, instance=instance, **kwargs)

    def save(self, commit=True):
        instance = super().save()
        instance.adgroups.set(self.cleaned_data["adgroups"])
        return instance

```
 Now add your custom form into the group viewset by inheriting the default Wagtail `GroupViewSet` class and overriding the `get_form_class` method.

 
```
from wagtail.users.views.groups import GroupViewSet as WagtailGroupViewSet

from .forms import GroupForm


class GroupViewSet(WagtailGroupViewSet):
    def get_form_class(self, for_update=False):
        return GroupForm

```
 Add the field to the group ‘edit’/’create’ templates:

 
```
{% extends "wagtailusers/groups/edit.html" %}
{% load wagtailusers_tags wagtailadmin_tags i18n %}

{% block extra_fields %}
    <li>{% include "wagtailadmin/shared/field.html" with field=form.adgroups %}</li>
{% endblock extra_fields %}

```
 Finally we configure the `wagtail.users` application to use the custom viewset, by setting up a custom `AppConfig` class. Within your project folder (which will be the package containing the top-level settings and urls modules), create `apps.py` (if it does not exist already) and add:

 
```
from wagtail.users.apps import WagtailUsersAppConfig


class CustomUsersAppConfig(WagtailUsersAppConfig):
    group_viewset = "myapplication.someapp.viewsets.GroupViewSet"

```
 Replace `wagtail.users` in `settings.INSTALLED_APPS` with the path to `CustomUsersAppConfig`.

 
```
INSTALLED_APPS = [
    ...,
    "myapplication.apps.CustomUsersAppConfig",
    # "wagtail.users",
    ...,
]

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/customising_group_views.html>

Rich text internals
===================

 At first glance, Wagtail’s rich text capabilities appear to give editors direct control over a block of HTML content. In reality, it’s necessary to give editors a representation of rich text content that is several steps removed from the final HTML output, for several reasons:

 * The editor interface needs to filter out certain kinds of unwanted markup; this includes malicious scripting, font styles pasted from an external word processor, and elements which would break the validity or consistency of the site design (for example, pages will generally reserve the `<h1>` element for the page title, and so it would be inappropriate to allow users to insert their own additional `<h1>` elements through rich text).
* Rich text fields can specify a `features` argument to further restrict the elements permitted in the field - see [Rich Text Features](../advanced_topics/customisation/page_editing_interface#rich-text-features).
* Enforcing a subset of HTML helps to keep presentational markup out of the database, making the site more maintainable, and making it easier to repurpose site content (including, potentially, producing non-HTML output such as [LaTeX](https://www.latex-project.org/)).
* Elements such as page links and images need to preserve metadata such as the page or image ID, which is not present in the final HTML representation.

 This requires the rich text content to go through a number of validation and conversion steps; both between the editor interface and the version stored in the database, and from the database representation to the final rendered HTML.

 For this reason, extending Wagtail’s rich text handling to support a new element is more involved than simply saying (for example) “enable the `<blockquote>` element”, since various components of Wagtail - both client and server-side - need to agree on how to handle that feature, including how it should be exposed in the editor interface, how it should be represented within the database, and (if appropriate) how it should be translated when rendered on the front-end.

 The components involved in Wagtail’s rich text handling are described below.

  Data format
-----------

 Rich text data (as handled by [RichTextField](../advanced_topics/customisation/page_editing_interface#rich-text), and `RichTextBlock` within [StreamField](../topics/streamfield)) is stored in the database in a format that is similar, but not identical, to HTML. For example, a link to a page might be stored as:

 
```
<p><a linktype="page" id="3">Contact us</a> for more information.</p>

```
 Here, the `linktype` attribute identifies a rule that shall be used to rewrite the tag. When rendered on a template through the `|richtext` filter (see [rich text filter](../topics/writing_templates#rich-text-filter)), this is converted into valid HTML:

 
```
<p><a href="/contact-us/">Contact us</a> for more information.</p>

```
 In the case of `RichTextBlock`, the block’s value is a `RichText` object which performs this conversion automatically when rendered as a string, so the `|richtext` filter is not necessary.

 Likewise, an image inside rich text content might be stored as:

 
```
<embed embedtype="image" id="10" alt="A pied wagtail" format="left" />

```
 which is converted into an `img` element when rendered:

 
```
<img
    alt="A pied wagtail"
    class="richtext-image left"
    height="294"
    src="/media/images/pied-wagtail.width-500_ENyKffb.jpg"
    width="500"
/>

```
 Again, the `embedtype` attribute identifies a rule that shall be used to rewrite the tag. All tags other than `<a linktype="...">` and `<embed embedtype="..." />` are left unchanged in the converted HTML.

 A number of additional constraints apply to `<a linktype="...">` and `<embed embedtype="..." />` tags, to allow the conversion to be performed efficiently via string replacement:

 * The tag name and attributes must be lower-case
* Attribute values must be quoted with double-quotes
* `embed` elements must use XML self-closing tag syntax (those that end in `/>` instead of a closing `</embed>` tag)
* The only HTML entities permitted in attribute values are `&lt;`, `&gt;`, `&amp;` and `&quot;`

   The feature registry
--------------------

 Any app within your project can define extensions to Wagtail’s rich text handling, such as new `linktype` and `embedtype` rules. An object known as the *feature registry* serves as a central source of truth about how rich text should behave. This object can be accessed through the [Register Rich Text Features](../reference/hooks#register-rich-text-features) hook, which is called on startup to gather all definitions relating to rich text:

 
```

    # my_app/wagtail_hooks.py

    from wagtail import hooks

    @hooks.register('register_rich_text_features')
    def register_my_feature(features):
        # add new definitions to 'features' here

```
   Rewrite handlers
----------------

 Rewrite handlers are classes that know how to translate the content of rich text tags like `<a linktype="...">` and `<embed embedtype="..." />` into front-end HTML. For example, the `PageLinkHandler` class knows how to convert the rich text tag `<a linktype="page" id="123">` into the HTML tag `<a href="/path/to/page/123">`.

 Rewrite handlers can also provide other useful information about rich text tags. For example, given an appropriate tag, `PageLinkHandler` can be used to extract which page is being referred to. This can be useful for downstream code that may want information about objects being referenced in rich text.

 You can create custom rewrite handlers to support your own new `linktype` and `embedtype` tags. New handlers must be Python classes that inherit from either `wagtail.richtext.LinkHandler` or `wagtail.richtext.EmbedHandler`. Your new classes should override at least some of the following methods (listed here for `LinkHandler`, although `EmbedHandler` has an identical signature):

 Below is an example custom rewrite handler that implements these methods to add support for rich text linking to user email addresses. It supports the conversion of rich text tags like `<a linktype="user" username="wagtail">` to valid HTML like `<a href="mailto:hello@wagtail.org">`. This example assumes that equivalent front-end functionality has been added to allow users to insert these kinds of links into their rich text editor.

 
```
from django.contrib.auth import get_user_model
from wagtail.rich_text import LinkHandler

class UserLinkHandler(LinkHandler):
    identifier = 'user'

    @staticmethod
    def get_model():
        return get_user_model()

    @classmethod
    def get_instance(cls, attrs):
        model = cls.get_model()
        return model.objects.get(username=attrs['username'])

    @classmethod
    def expand_db_attributes(cls, attrs):
        user = cls.get_instance(attrs)
        return '<a href="mailto:%s">' % user.email

```
  ### Registering rewrite handlers

 Rewrite handlers must also be registered with the feature registry via the [register rich text features](../reference/hooks#register-rich-text-features) hook. Independent methods for registering both link handlers and embed handlers are provided.

 This method allows you to register a custom handler deriving from `wagtail.rich_text.LinkHandler`, and adds it to the list of link handlers available during rich text conversion.

 
```
# my_app/wagtail_hooks.py

from wagtail import hooks
from my_app.handlers import MyCustomLinkHandler

@hooks.register('register_rich_text_features')
def register_link_handler(features):
    features.register_link_type(MyCustomLinkHandler)

```
 It is also possible to define link rewrite handlers for Wagtail’s built-in `external` and `email` links, even though they do not have a predefined `linktype`. For example, if you want external links to have a `rel="nofollow"` attribute for SEO purposes:

 
```
from django.utils.html import escape
from wagtail import hooks
from wagtail.rich_text import LinkHandler

class NoFollowExternalLinkHandler(LinkHandler):
    identifier = 'external'

    @classmethod
    def expand_db_attributes(cls, attrs):
        href = attrs["href"]
        return '<a href="%s" rel="nofollow">' % escape(href)

@hooks.register('register_rich_text_features')
def register_external_link(features):
    features.register_link_type(NoFollowExternalLinkHandler)

```
 Similarly you can use `email` linktype to add a custom rewrite handler for email links (for example to obfuscate emails in rich text).

 This method allows you to register a custom handler deriving from `wagtail.rich_text.EmbedHandler`, and adds it to the list of embed handlers available during rich text conversion.

 
```
# my_app/wagtail_hooks.py

from wagtail import hooks
from my_app.handlers import MyCustomEmbedHandler

@hooks.register('register_rich_text_features')
def register_embed_handler(features):
    features.register_embed_type(MyCustomEmbedHandler)

```
    Editor widgets
--------------

 The editor interface used on rich text fields can be configured with the [WAGTAILADMIN\_RICH\_TEXT\_EDITORS](../reference/settings#wagtailadmin-rich-text-editors) setting. Wagtail provides an implementation: `wagtail.admin.rich_text.DraftailRichTextArea` (the [Draftail](https://www.draftail.org/) editor based on [Draft.js](https://draftjs.org/)).

 It is possible to create your own rich text editor implementation. At minimum, a rich text editor is a Django ***class* django.forms.Widget** subclass whose constructor accepts an `options` keyword argument (a dictionary of editor-specific configuration options sourced from the `OPTIONS` field in `WAGTAILADMIN_RICH_TEXT_EDITORS`), and which consumes and produces string data in the HTML-like format described above.

 Typically, a rich text widget also receives a `features` list, passed from either `RichTextField` / `RichTextBlock` or the `features` option in `WAGTAILADMIN_RICH_TEXT_EDITORS`, which defines the features available in that instance of the editor (see [rich text features](../advanced_topics/customisation/page_editing_interface#rich-text-features)). To opt in to supporting features, set the attribute `accepts_features = True` on your widget class; the widget constructor will then receive the feature list as a keyword argument `features`.

 There is a standard set of recognised feature identifiers as listed under [rich text features](../advanced_topics/customisation/page_editing_interface#rich-text-features), but this is not a definitive list; feature identifiers are only defined by convention, and it is up to each editor widget to determine which features it will recognise, and adapt its behaviour accordingly. Individual editor widgets might implement fewer or more features than the default set, either as built-in functionality or through a plugin mechanism if the editor widget has one.

 For example, a third-party Wagtail extension might introduce `table` as a new rich text feature, and provide implementations for the Draftail editor (which provides a plugin mechanism). In this case, the third-party extension will not be aware of your custom editor widget, and so the widget will not know how to handle the `table` feature identifier. Editor widgets should silently ignore any feature identifiers that they do not recognise.

 The `default_features` attribute of the feature registry is a list of feature identifiers to be used whenever an explicit feature list has not been given in `RichTextField` / `RichTextBlock` or `WAGTAILADMIN_RICH_TEXT_EDITORS`. This list can be modified within the `register_rich_text_features` hook to make new features enabled by default, and retrieved by calling `get_default_features()`.

 
```
@hooks.register('register_rich_text_features')
def make_h1_default(features):
    features.default_features.append('h1')

```
 Outside of the `register_rich_text_features` hook - for example, inside a widget class - the feature registry can be imported as the object `wagtail.rich_text.features`. A possible starting point for a rich text editor with feature support would be:

 
```
from django.forms import widgets
from wagtail.rich_text import features

class CustomRichTextArea(widgets.TextArea):
    accepts_features = True

    def __init__(self, *args, **kwargs):
        self.options = kwargs.pop('options', None)

        self.features = kwargs.pop('features', None)
        if self.features is None:
            self.features = features.get_default_features()

        super().__init__(*args, **kwargs)

```
   Editor plugins
--------------

 Rich text editors often provide a plugin mechanism to allow extending the editor with new functionality. The `register_editor_plugin` method provides a standardised way for `register_rich_text_features` hooks to define plugins to be pulled in to the editor when a given rich text feature is enabled.

 `register_editor_plugin` is passed an editor name (a string uniquely identifying the editor widget - Wagtail uses the identifier `draftail` for the built-in editor), a feature identifier, and a plugin definition object. This object is specific to the editor widget and can be any arbitrary value, but will typically include a [Django form media](https://docs.djangoproject.com/en/stable/topics/forms/media/ "(in Django v4.1)") definition referencing the plugin’s JavaScript code - which will then be merged into the editor widget’s own media definition - along with any relevant configuration options to be passed when instantiating the editor.

 Within the editor widget, the plugin definition for a given feature can be retrieved via the `get_editor_plugin` method, passing the editor’s own identifier string and the feature identifier. This will return `None` if no matching plugin has been registered.

 For details of the plugin formats for Wagtail’s built-in editors, see [Extending the Draftail Editor](extending_draftail).

   Format converters
-----------------

 Editor widgets will often be unable to work directly with Wagtail’s rich text format, and require conversion to their own native format. For Draftail, this is a JSON-based format known as ContentState (see [How Draft.js Represents Rich Text Data](https://rajaraodv.medium.com/how-draft-js-represents-rich-text-data-eeabb5f25cf2)). Editors based on HTML’s `contentEditable` mechanism require valid HTML, and so Wagtail uses a convention referred to as “editor HTML”, where the additional data required on link and embed elements is stored in `data-` attributes, for example: `<a href="/contact-us/" data-linktype="page" data-id="3">Contact us</a>`.

 Wagtail provides two utility classes, `wagtail.admin.rich_text.converters.contentstate.ContentstateConverter` and `wagtail.admin.rich_text.converters.editor_html.EditorHTMLConverter`, to perform conversions between rich text format and the native editor formats. These classes are independent of any editor widget, and distinct from the rewriting process that happens when rendering rich text onto a template.

 Both classes accept a `features` list as an argument to their constructor, and implement two methods, `from_database_format(data)` which converts Wagtail rich text data to the editor’s format, and `to_database_format(data)` which converts editor data to Wagtail rich text format.

 As with editor plugins, the behaviour of a converter class can vary according to the feature list passed to it. In particular, it can apply whitelisting rules to ensure that the output only contains HTML elements corresponding to the currently active feature set. The feature registry provides a `register_converter_rule` method to allow `register_rich_text_features` hooks to define conversion rules that will be activated when a given feature is enabled.

 `register_editor_plugin` is passed a converter name (a string uniquely identifying the converter class - Wagtail uses the identifiers `contentstate` and `editorhtml`), a feature identifier, and a rule definition object. This object is specific to the converter and can be any arbitrary value.

 For details of the rule definition format for the `contentstate` converter, see [Extending the Draftail Editor](extending_draftail).

 Within a converter class, the rule definition for a given feature can be retrieved via the `get_converter_rule` method, passing the converter’s own identifier string and the feature identifier. This will return `None` if no matching rule has been registered.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/rich_text_internals.html>

Extending the Draftail Editor
=============================

 Wagtail’s rich text editor is built with [Draftail](https://www.draftail.org/), and its functionality can be extended through plugins.

 Plugins come in three types:

 * Inline styles – To format a portion of a line, for example `bold`, `italic`, `monospace`.
* Blocks – To indicate the structure of the content, for example `blockquote`, `ol`.
* Entities – To enter additional data/metadata, for example `link` (with a URL), `image` (with a file).

 All of these plugins are created with a similar baseline, which we can demonstrate with one of the simplest examples – a custom feature for an inline style of `mark`. Place the following in a `wagtail_hooks.py` file in any installed app:

 
```
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
from wagtail.admin.rich_text.converters.html_to_contentstate import InlineStyleElementHandler
from wagtail import hooks

# 1. Use the register_rich_text_features hook.
@hooks.register('register_rich_text_features')
def register_mark_feature(features):
    """
    Registering the `mark` feature, which uses the `MARK` Draft.js inline style type,
    and is stored as HTML with a `<mark>` tag.
    """
    feature_name = 'mark'
    type_ = 'MARK'
    tag = 'mark'

    # 2. Configure how Draftail handles the feature in its toolbar.
    control = {
        'type': type_,
        'label': '☆',
        'description': 'Mark',
        # This isn’t even required – Draftail has predefined styles for MARK.
        # 'style': {'textDecoration': 'line-through'},
    }

    # 3. Call register_editor_plugin to register the configuration for Draftail.
    features.register_editor_plugin(
        'draftail', feature_name, draftail_features.InlineStyleFeature(control)
    )

    # 4.configure the content transform from the DB to the editor and back.
    db_conversion = {
        'from_database_format': {tag: InlineStyleElementHandler(type_)},
        'to_database_format': {'style_map': {type_: tag}},
    }

    # 5. Call register_converter_rule to register the content transformation conversion.
    features.register_converter_rule('contentstate', feature_name, db_conversion)

    # 6. (optional) Add the feature to the default features list to make it available
    # on rich text fields that do not specify an explicit 'features' list
    features.default_features.append('mark')

```
 These steps will always be the same for all Draftail plugins. The important parts are to:

 * Consistently use the feature’s Draft.js type or Wagtail feature names where appropriate.
* Give enough information to Draftail so it knows how to make a button for the feature, and how to render it (more on this later).
* Configure the conversion to use the right HTML element (as they are stored in the DB).

 For detailed configuration options, head over to the [Draftail documentation](https://www.draftail.org/docs/formatting-options) to see all of the details. Here are some parts worth highlighting about controls:

 * The `type` is the only mandatory piece of information.
* To display the control in the toolbar, combine `icon`, `label` and `description`.
* The controls’ `icon` can be a string to use an icon font with CSS classes, say `'icon': 'fas fa-user',`. It can also be an array of strings, to use SVG paths, or SVG symbol references for example `'icon': ['M100 100 H 900 V 900 H 100 Z'],`. The paths need to be set for a 1024x1024 viewbox.

  Creating new inline styles
--------------------------

 In addition to the initial example, inline styles take a `style` property to define what CSS rules will be applied to text in the editor. Be sure to read the [Draftail documentation](https://www.draftail.org/docs/formatting-options) on inline styles.

 Finally, the DB to/from conversion uses an `InlineStyleElementHandler` to map from a given tag (`<mark>` in the example above) to a Draftail type, and the inverse mapping is done with [Draft.js exporter configuration](https://github.com/springload/draftjs_exporter) of the `style_map`.

   Creating new blocks
-------------------

 Blocks are nearly as simple as inline styles:

 
```
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler

@hooks.register('register_rich_text_features')
def register_help_text_feature(features):
    """
    Registering the `help-text` feature, which uses the `help-text` Draft.js block type,
    and is stored as HTML with a `<div class="help-text">` tag.
    """
    feature_name = 'help-text'
    type_ = 'help-text'

    control = {
        'type': type_,
        'label': '?',
        'description': 'Help text',
        # Optionally, we can tell Draftail what element to use when displaying those blocks in the editor.
        'element': 'div',
    }

    features.register_editor_plugin(
        'draftail', feature_name, draftail_features.BlockFeature(control, css={'all': ['help-text.css']})
    )

    features.register_converter_rule('contentstate', feature_name, {
        'from_database_format': {'div[class=help-text]': BlockElementHandler(type_)},
        'to_database_format': {'block_map': {type_: {'element': 'div', 'props': {'class': 'help-text'}}}},
    })

```
 Here are the main differences:

 * We can configure an `element` to tell Draftail how to render those blocks in the editor.
* We register the plugin with `BlockFeature`.
* We set up the conversion with `BlockElementHandler` and `block_map`.

 Optionally, we can also define styles for the blocks with the `Draftail-block--help-text` (`Draftail-block--<block type>`) CSS class.

 That’s it! The extra complexity is that you may need to write CSS to style the blocks in the editor.

   Creating new entities
---------------------

  Warning

 This is an advanced feature. Please carefully consider whether you really need this.

  Entities aren’t simply formatting buttons in the toolbar. They usually need to be much more versatile, communicating to APIs or requesting further user input. As such,

 * You will most likely need to write a **hefty dose of JavaScript**, some of it with React.
* The API is very **low-level**. You will most likely need some **Draft.js knowledge**.
* Custom UIs in rich text can be brittle. Be ready to spend time **testing in multiple browsers**.

 The good news is that having such a low-level API will enable third-party Wagtail plugins to innovate on rich text features, proposing new kinds of experiences. But in the meantime, consider implementing your UI through [StreamField](../topics/streamfield) instead, which has a battle-tested API meant for Django developers.

 Here are the main requirements to create a new entity feature:

 * Like for inline styles and blocks, register an editor plugin.
* The editor plugin must define a `source`: a React component responsible for creating new entity instances in the editor, using the Draft.js API.
* The editor plugin also needs a `decorator` (for inline entities) or `block` (for block entities): a React component responsible for displaying entity instances within the editor.
* Like for inline styles and blocks, set up the to/from DB conversion.
* The conversion usually is more involved, since entities contain data that needs to be serialised to HTML.

 To write the React components, Wagtail exposes its own React, Draft.js and Draftail dependencies as global variables. Read more about this in [ectending clientside components](../advanced_topics/customisation/admin_templates#extending-clientside-components). To go further, please look at the [Draftail documentation](https://www.draftail.org/docs/formatting-options) as well as the [Draft.js exporter documentation](https://github.com/springload/draftjs_exporter).

 Here is a detailed example to showcase how those tools are used in the context of Wagtail. For the sake of our example, we can imagine a news team working at a financial newspaper. They want to write articles about the stock market, refer to specific stocks anywhere inside of their content (for example “$TSLA” tokens in a sentence), and then have their article automatically enriched with the stock’s information (a link, a number, a sparkline).

 The editor toolbar could contain a “stock chooser” that displays a list of available stocks, then inserts the user’s selection as a textual token. For our example, we will just pick a stock at random:

 ![Draftail entity stock source](https://docs.wagtail.org/en/stable/extending/_static/images/draftail_entity_stock_source.gif)

 Those tokens are then saved in the rich text on publish. When the news article is displayed on the site, we then insert live market data coming from an API next to each token:

 ![Draftail entity stock rendering]

 In order to achieve this, we start with registering the rich text feature like for inline styles and blocks:

 
```
@hooks.register('register_rich_text_features')
def register_stock_feature(features):
    features.default_features.append('stock')
    """
    Registering the `stock` feature, which uses the `STOCK` Draft.js entity type,
    and is stored as HTML with a `<span data-stock>` tag.
    """
    feature_name = 'stock'
    type_ = 'STOCK'

    control = {
        'type': type_,
        'label': '$',
        'description': 'Stock',
    }

    features.register_editor_plugin(
        'draftail', feature_name, draftail_features.EntityFeature(
            control,
            js=['stock.js'],
            css={'all': ['stock.css']}
        )
    )

    features.register_converter_rule('contentstate', feature_name, {
        # Note here that the conversion is more complicated than for blocks and inline styles.
        'from_database_format': {'span[data-stock]': StockEntityElementHandler(type_)},
        'to_database_format': {'entity_decorators': {type_: stock_entity_decorator}},
    })

```
 The `js` and `css` keyword arguments on `EntityFeature` can be used to specify additional JS and CSS files to load when this feature is active. Both are optional. Their values are added to a `Media` object, more documentation on these objects is available in the [Django Form Assets documentation](https://docs.djangoproject.com/en/stable/topics/forms/media/ "(in Django v4.1)").

 Since entities hold data, the conversion to/from database format is more complicated. We have to create the two handlers:

 
```
from draftjs_exporter.dom import DOM
from wagtail.admin.rich_text.converters.html_to_contentstate import InlineEntityElementHandler

def stock_entity_decorator(props):
    """
    Draft.js ContentState to database HTML.
    Converts the STOCK entities into a span tag.
    """
    return DOM.create_element('span', {
        'data-stock': props['stock'],
    }, props['children'])


class StockEntityElementHandler(InlineEntityElementHandler):
    """
    Database HTML to Draft.js ContentState.
    Converts the span tag into a STOCK entity, with the right data.
    """
    mutability = 'IMMUTABLE'

    def get_attribute_data(self, attrs):
        """
        Take the `stock` value from the `data-stock` HTML attribute.
        """
        return { 'stock': attrs['data-stock'] }

```
 Note how they both do similar conversions, but use different APIs. `to_database_format` is built with the [Draft.js exporter](https://github.com/springload/draftjs_exporter) components API, whereas `from_database_format` uses a Wagtail API.

 The next step is to add JavaScript to define how the entities are created (the `source`), and how they are displayed (the `decorator`). Within `stock.js`, we define the source component:

 
```
const React = window.React;
const Modifier = window.DraftJS.Modifier;
const EditorState = window.DraftJS.EditorState;

const DEMO_STOCKS = ['AMD', 'AAPL', 'TWTR', 'TSLA', 'BTC'];

// Not a real React component – just creates the entities as soon as it is rendered.
class StockSource extends React.Component {
    componentDidMount() {
        const { editorState, entityType, onComplete } = this.props;

        const content = editorState.getCurrentContent();
        const selection = editorState.getSelection();

        const randomStock =
            DEMO_STOCKS[Math.floor(Math.random() * DEMO_STOCKS.length)];

        // Uses the Draft.js API to create a new entity with the right data.
        const contentWithEntity = content.createEntity(
            entityType.type,
            'IMMUTABLE',
            {
                stock: randomStock,
            },
        );
        const entityKey = contentWithEntity.getLastCreatedEntityKey();

        // We also add some text for the entity to be activated on.
        const text = `$${randomStock}`;

        const newContent = Modifier.replaceText(
            content,
            selection,
            text,
            null,
            entityKey,
        );
        const nextState = EditorState.push(
            editorState,
            newContent,
            'insert-characters',
        );

        onComplete(nextState);
    }

    render() {
        return null;
    }
}

```
 This source component uses data and callbacks provided by [Draftail](https://www.draftail.org/docs/api). It also uses dependencies from global variables – see [Extending clientside components](../advanced_topics/customisation/admin_templates#extending-clientside-components).

 We then create the decorator component:

 
```
const Stock = (props) => {
    const { entityKey, contentState } = props;
    const data = contentState.getEntity(entityKey).getData();

    return React.createElement(
        'a',
        {
            role: 'button',
            onMouseUp: () => {
                window.open(`https://finance.yahoo.com/quote/${data.stock}`);
            },
        },
        props.children,
    );
};

```
 This is a straightforward React component. It does not use JSX since we do not want to have to use a build step for our JavaScript.

 Finally, we register the JS components of our plugin:

 
```
window.draftail.registerPlugin({
    type: 'STOCK',
    source: StockSource,
    decorator: Stock,
});

```
 And that’s it! All of this setup will finally produce the following HTML on the site’s front-end:

 
```
<p>
    Anyone following Elon Musk’s <span data-stock="TSLA">$TSLA</span> should
    also look into <span data-stock="BTC">$BTC</span>.
</p>

```
 To fully complete the demo, we can add a bit of JavaScript to the front-end in order to decorate those tokens with links and a little sparkline.

 
```
document.querySelectorAll('[data-stock]').forEach((elt) => {
    const link = document.createElement('a');
    link.href = `https://finance.yahoo.com/quote/${elt.dataset.stock}`;
    link.innerHTML = `${elt.innerHTML}<svg width="50" height="20" stroke-width="2" stroke="blue" fill="rgba(0, 0, 255, .2)"><path d="M4 14.19 L 4 14.19 L 13.2 14.21 L 22.4 13.77 L 31.59 13.99 L 40.8 13.46 L 50 11.68 L 59.19 11.35 L 68.39 10.68 L 77.6 7.11 L 86.8 7.85 L 96 4" fill="none"></path><path d="M4 14.19 L 4 14.19 L 13.2 14.21 L 22.4 13.77 L 31.59 13.99 L 40.8 13.46 L 50 11.68 L 59.19 11.35 L 68.39 10.68 L 77.6 7.11 L 86.8 7.85 L 96 4 V 20 L 4 20 Z" stroke="none"></path></svg>`;

    elt.innerHTML = '';
    elt.appendChild(link);
});

```
 Custom block entities can also be created (have a look at the separate [Draftail documentation](https://www.draftail.org/docs/blocks)), but these are not detailed here since [StreamField](../topics/streamfield#streamfield-topic) is the go-to way to create block-level rich text in Wagtail.

   Integration of the Draftail widgets
-----------------------------------

 To further customise how the Draftail widgets are integrated into the UI, there are additional extension points for CSS and JS:

 * In JavaScript, use the `[data-draftail-input]` attribute selector to target the input which contains the data, and `[data-draftail-editor-wrapper]` for the element which wraps the editor.
* The editor instance is bound on the input field for imperative access. Use `document.querySelector('[data-draftail-input]').draftailEditor`.
* In CSS, use the classes prefixed with `Draftail-`.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/extending_draftail.html>

Adding custom bulk actions
==========================

 This document describes how to add custom bulk actions to different listings.

  Registering a custom bulk action
--------------------------------

 
```
from wagtail.admin.views.bulk_action import BulkAction
from wagtail import hooks


@hooks.register('register_bulk_action')
class CustomDeleteBulkAction(BulkAction):
    display_name = _("Delete")
    aria_label = _("Delete selected objects")
    action_type = "delete"
    template_name = "/path/to/confirm_bulk_delete.html"
    models = [...]

    @classmethod
    def execute_action(cls, objects, **kwargs):
        for obj in objects:
            do_something(obj)
        return num_parent_objects, num_child_objects  # return the count of updated objects

```
 The attributes are as follows:

 * `display_name` - The label that will be displayed on the button in the user interface
* `aria_label` - The `aria-label` attribute that will be applied to the button in the user interface
* `action_type` - A unique identifier for the action. Will be required in the url for bulk actions
* `template_name` - The path to the confirmation template
* `models` - A list of models on which the bulk action can act
* `action_priority` (optional) - A number that is used to determine the placement of the button in the list of buttons
* `classes` (optional) - A set of CSS classnames that will be used on the button in the user interface

 An example for a confirmation template is as follows:

 
```
<!-- /path/to/confirm_bulk_delete.html -->

{% extends 'wagtailadmin/bulk_actions/confirmation/base.html' %}
{% load i18n wagtailadmin_tags %}

{% block titletag %}{% blocktrans trimmed count counter=items|length %}Delete 1 item{% plural %}Delete {{ counter }} items{% endblocktrans %}{% endblock %}

{% block header %}
    {% trans "Delete" as del_str %}
    {% include "wagtailadmin/shared/header.html" with title=del_str icon="doc-empty-inverse" %}
{% endblock header %}

{% block items_with_access %}
        {% if items %}
        <p>{% trans "Are you sure you want to delete these items?" %}</p>
        <ul>
            {% for item in items %}
            <li>
                <a href="" target="_blank" rel="noreferrer">{{ item.item.title }}</a>
            </li>
            {% endfor %}
        </ul>
        {% endif %}
{% endblock items_with_access %}

{% block items_with_no_access %}

{% blocktrans trimmed asvar no_access_msg count counter=items_with_no_access|length %}You don't have permission to delete this item{% plural %}You don't have permission to delete these items{% endblocktrans %}
{% include './list_items_with_no_access.html' with items=items_with_no_access no_access_msg=no_access_msg %}

{% endblock items_with_no_access %}

{% block form_section %}
{% if items %}
    {% trans 'Yes, delete' as action_button_text %}
    {% trans "No, don't delete" as no_action_button_text %}
    {% include 'wagtailadmin/bulk_actions/confirmation/form.html' with action_button_class="serious" %}
{% else %}
    {% include 'wagtailadmin/bulk_actions/confirmation/go_back.html' %}
{% endif %}
{% endblock form_section %}

```
 
```
<!-- ./list_items_with_no_access.html -->
{% extends 'wagtailadmin/bulk_actions/confirmation/list_items_with_no_access.html' %}
{% load i18n %}

{% block per_item %}
    {% if item.can_edit %}
    <a href="{% url 'wagtailadmin_pages:edit' item.item.id %}" target="_blank" rel="noreferrer">{{ item.item.title }}</a>
    {% else %}
    {{ item.item.title }}
    {% endif %}
{% endblock per_item %}

```
 The `execute_action` classmethod is the only method that must be overridden for the bulk action to work properly. It takes a list of objects as the only required argument, and a bunch of keyword arguments that can be supplied by overriding the `get_execution_context` method. For example.

 
```
@classmethod
def execute_action(cls, objects, **kwargs):
    # the kwargs here is the output of the get_execution_context method
    user = kwargs.get('user', None)
    num_parent_objects, num_child_objects = 0, 0
    # you could run the action per object or run them in bulk using django's bulk update and delete methods
    for obj in objects:
        num_child_objects += obj.get_children().count()
        num_parent_objects += 1
        obj.delete(user=user)
        num_parent_objects += 1
    return num_parent_objects, num_child_objects

```
 The `get_execution_context` method can be overridden to provide context to the `execute_action`

 
```
def get_execution_context(self):
    return { 'user': self.request.user }

```
 The `get_context_data` method can be overridden to pass additional context to the confirmation template.

 
```
def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    context['new_key'] = some_value
    return context

```
 Thes `check_perm` method can be overridden to check if an object has some permission or not. objects for which the `check_perm` returns `False` will be available in the context under the key `'items_with_no_access'`.

 
```
def check_perm(self, obj):
    return obj.has_perm('some_perm')  # returns True or False

```
 The success message shown on the admin can be customised by overriding the `get_success_message` method.

 
```
def get_success_message(self, num_parent_objects, num_child_objects):
    return _("{} objects, including {} child objects have been updated".format(num_parent_objects, num_child_objects))

```
   Adding bulk actions to the page explorer
----------------------------------------

 When creating a custom bulk action class for pages, subclass from `wagtail.admin.views.pages.bulk_actions.page_bulk_action.PageBulkAction` instead of `wagtail.admin.views.bulk_action.BulkAction`

  ### Basic example

 
```
from wagtail.admin.views.pages.bulk_actions.page_bulk_action import PageBulkAction
from wagtail import hooks


@hooks.register('register_bulk_action')
class CustomPageBulkAction(PageBulkAction):
    ...

```
    Adding bulk actions to the Images listing
-----------------------------------------

 When creating a custom bulk action class for images, subclass from `wagtail.images.views.bulk_actions.image_bulk_action.ImageBulkAction` instead of `wagtail.admin.views.bulk_action.BulkAction`

  ### Basic example

 
```
from wagtail.images.views.bulk_actions.image_bulk_action import ImageBulkAction
from wagtail import hooks


@hooks.register('register_bulk_action')
class CustomImageBulkAction(ImageBulkAction):
    ...

```
    Adding bulk actions to the documents listing
--------------------------------------------

 When creating a custom bulk action class for documents, subclass from `wagtail.documents.views.bulk_actions.document_bulk_action.DocumentBulkAction` instead of `wagtail.admin.views.bulk_action.BulkAction`

  ### Basic example

 
```
from wagtail.documents.views.bulk_actions.document_bulk_action import DocumentBulkAction
from wagtail import hooks


@hooks.register('register_bulk_action')
class CustomDocumentBulkAction(DocumentBulkAction):
    ...

```
    Adding bulk actions to the user listing
---------------------------------------

 When creating a custom bulk action class for users, subclass from `wagtail.users.views.bulk_actions.user_bulk_action.UserBulkAction` instead of `wagtail.admin.views.bulk_action.BulkAction`

  ### Basic example

 
```
from wagtail.users.views.bulk_actions.user_bulk_action import UserBulkAction
from wagtail import hooks


@hooks.register('register_bulk_action')
class CustomUserBulkAction(UserBulkAction):
    ...

```
    Adding bulk actions to the snippets listing
-------------------------------------------

 When creating a custom bulk action class for snippets, subclass from `wagtail.snippets.bulk_actions.snippet_bulk_action.SnippetBulkAction` instead of `wagtail.admin.views.bulk_action.BulkAction`

  ### Basic example

 
```
from wagtail.snippets.bulk_actions.snippet_bulk_action import SnippetBulkAction
from wagtail import hooks


@hooks.register('register_bulk_action')
class CustomSnippetBulkAction(SnippetBulkAction):
    # ...

```
 If you want to apply an action only to certain snippets, override the `models` list in the action class

 
```
from wagtail.snippets.bulk_actions.snippet_bulk_action import SnippetBulkAction
from wagtail import hooks


@hooks.register('register_bulk_action')
class CustomSnippetBulkAction(SnippetBulkAction):
    models = [SnippetA, SnippetB]
    # ...

```
  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/extending/custom_bulk_actions.html>

Reference
=========

 * [Pages](pages/index)
	+ [Theory](pages/theory)
	+ [Recipes](pages/model_recipes)
	+ [Panel types](pages/panels)
	+ [Model Reference](pages/model_reference)
	+ [Page QuerySet reference](pages/queryset_reference)
* [StreamField reference](streamfield/index)
	+ [StreamField block reference](streamfield/blocks)
	+ [Form widget client-side API](streamfield/widget_api)
* [Contrib modules](contrib/index)
	+ [Settings](contrib/settings)
	+ [Form builder](contrib/forms/index)
	+ [Sitemap generator](contrib/sitemaps)
	+ [Frontend cache invalidator](contrib/frontendcache)
	+ [`RoutablePageMixin`](contrib/routablepage)
	+ [`ModelAdmin`](contrib/modeladmin/index)
	+ [Promoted search results](contrib/searchpromotions)
	+ [Simple translation](contrib/simple_translation)
	+ [TableBlock](contrib/table_block)
	+ [Typed table block](contrib/typed_table_block)
	+ [Redirects](contrib/redirects)
	+ [Legacy richtext](contrib/legacy_richtext)
* [Management commands](management_commands)
* [Hooks](hooks)
* [Signals](signals)
* [Settings](settings)
* [The project template](project_template)
* [Jinja2 template support](jinja2)
* [Panel API](panel_api)
* [Viewsets](viewsets)



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/index.html>

Pages
=====

 Wagtail requires a little careful setup to define the types of content that you want to present through your website. The basic unit of content in Wagtail is the :class:`~wagtail.models.Page`, and all of your page-level content will inherit basic webpage-related properties from it. But for the most part, you will be defining content yourself, through the construction of Django models using Wagtail’s `Page` as a base.

 Wagtail organises content created from your models in a tree, which can have any structure and combination of model objects in it. Wagtail doesn’t prescribe ways to organise and interrelate your content, but here we’ve sketched out some strategies for organising your models.

 The presentation of your content, the actual webpages, includes the normal use of the Django template system. We’ll cover additional functionality that Wagtail provides at the template level later on.

 * [Theory](theory)
	+ [Introduction to Trees](theory#introduction-to-trees)
	+ [Anatomy of a Wagtail Request](theory#anatomy-of-a-wagtail-request)
	+ [Scheduled Publishing](theory#scheduled-publishing)
* [Recipes](model_recipes)
	+ [Overriding the `serve()` Method](model_recipes#overriding-the-serve-method)
	+ [Tagging](model_recipes#tagging)
* [Panel types](panels)
	+ [Built-in Fields and Choosers](panels#built-in-fields-and-choosers)
	+ [Field Customisation](panels#field-customisation)
	+ [Inline Panels and Model Clusters](panels#inline-panels-and-model-clusters)
* [Model Reference](model_reference)
	+ [`Page`](model_reference#page)
	+ [`Site`](model_reference#site)
	+ [`Locale`](model_reference#locale)
	+ [`TranslatableMixin`](model_reference#translatablemixin)
	+ [`PreviewableMixin`](model_reference#previewablemixin)
	+ [`RevisionMixin`](model_reference#revisionmixin)
	+ [`DraftStateMixin`](model_reference#draftstatemixin)
	+ [`Revision`](model_reference#revision)
	+ [`GroupPagePermission`](model_reference#grouppagepermission)
	+ [`PageViewRestriction`](model_reference#pageviewrestriction)
	+ [`Orderable` (abstract)](model_reference#orderable-abstract)
	+ [`Workflow`](model_reference#workflow)
	+ [`WorkflowState`](model_reference#workflowstate)
	+ [`Task`](model_reference#task)
	+ [`TaskState`](model_reference#taskstate)
	+ [`WorkflowTask`](model_reference#workflowtask)
	+ [`WorkflowPage`](model_reference#workflowpage)
	+ [`BaseLogEntry`](model_reference#baselogentry)
	+ [`PageLogEntry`](model_reference#pagelogentry)
	+ [`Comment`](model_reference#comment)
	+ [`CommentReply`](model_reference#commentreply)
	+ [`PageSubscription`](model_reference#pagesubscription)
* [Page QuerySet reference](queryset_reference)
	+ [Examples](queryset_reference#examples)
	+ [Reference](queryset_reference#module-wagtail.query)



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/pages/index.html>

Theory
======

  Introduction to Trees
---------------------

 If you’re unfamiliar with trees as an abstract data type, you might want to [review the concepts involved](https://en.wikipedia.org/wiki/Tree_(data_structure)).

 As a web developer, though, you probably already have a good understanding of trees as filesystem directories or paths. Wagtail pages can create the same structure, as each page in the tree has its own URL path, like so:

 
```
/
    people/
        nien-nunb/
        laura-roslin/
    events/
        captain-picard-day/
        winter-wrap-up/

```
 The Wagtail admin interface uses the tree to organise content for editing, letting you navigate up and down levels in the tree through its Explorer menu. This method of organisation is a good place to start in thinking about your own Wagtail models.

  ### Nodes and Leaves

 It might be handy to think of the `Page`-derived models you want to create as being one of two node types: parents and leaves. Wagtail isn’t prescriptive in this approach, but it’s a good place to start if you’re not experienced in structuring your own content types.

  #### Nodes

 Parent nodes on the Wagtail tree probably want to organise and display a browse-able index of their descendants. A blog, for instance, needs a way to show a list of individual posts.

 A Parent node could provide its own function returning its descendant objects.

 
```
class EventPageIndex(Page):
    # ...
    def events(self):
        # Get list of live event pages that are descendants of this page
        events = EventPage.objects.live().descendant_of(self)

        # Filter events list to get ones that are either
        # running now or start in the future
        events = events.filter(date_from__gte=date.today())

        # Order by date
        events = events.order_by('date_from')

        return events

```
 This example makes sure to limit the returned objects to pieces of content which make sense, specifically ones which have been published through Wagtail’s admin interface (`live()`) and are children of this node (`descendant_of(self)`). By setting a `subpage_types` class property in your model, you can specify which models are allowed to be set as children, and by setting a `parent_page_types` class property, you can specify which models are allowed to be parents of this page model. Wagtail will allow any `Page`-derived model by default. Regardless, it’s smart for a parent model to provide an index filtered to make sense.

   #### Leaves

 Leaves are the pieces of content itself, a page which is consumable, and might just consist of a bunch of properties. A blog page leaf might have some body text and an image. A person page leaf might have a photo, a name, and an address.

 It might be helpful for a leaf to provide a way to back up along the tree to a parent, such as in the case of breadcrumbs navigation. The tree might also be deep enough that a leaf’s parent won’t be included in general site navigation.

 The model for the leaf could provide a function that traverses the tree in the opposite direction and returns an appropriate ancestor:

 
```
class EventPage(Page):
    # ...
    def event_index(self):
        # Find closest ancestor which is an event index
        return self.get_ancestors().type(EventIndexPage).last()

```
 If defined, `subpage_types` and `parent_page_types` will also limit the parent models allowed to contain a leaf. If not, Wagtail will allow any combination of parents and leafs to be associated in the Wagtail tree. Like with index pages, it’s a good idea to make sure that the index is actually of the expected model to contain the leaf.

   #### Other Relationships

 Your `Page`-derived models might have other interrelationships which extend the basic Wagtail tree or depart from it entirely. You could provide functions to navigate between siblings, such as a “Next Post” link on a blog page (`post->post->post`). It might make sense for subtrees to interrelate, such as in a discussion forum (`forum->post->replies`) Skipping across the hierarchy might make sense, too, as all objects of a certain model class might interrelate regardless of their ancestors (`events = EventPage.objects.all`). It’s largely up to the models to define their interrelations, the possibilities are really endless.

     Anatomy of a Wagtail Request
----------------------------

 For going beyond the basics of model definition and interrelation, it might help to know how Wagtail handles requests and constructs responses. In short, it goes something like:

 1. Django gets a request and routes through Wagtail’s URL dispatcher definitions
2. Wagtail checks the hostname of the request to determine which `Site` record will handle this request.
3. Starting from the root page of that site, Wagtail traverses the page tree, calling the `route()` method and letting each page model decide whether it will handle the request itself or pass it on to a child page.
4. The page responsible for handling the request returns a `RouteResult` object from `route()`, which identifies the page along with any additional `args`/`kwargs` to be passed to `serve()`.
5. Wagtail calls `serve()`, which constructs a context using `get_context()`
6. `serve()` finds a template to pass it to using `get_template()`
7. A response object is returned by `serve()` and Django responds to the requester.

 You can apply custom behaviour to this process by overriding `Page` class methods such as `route()` and `serve()` in your own models. For examples, see [Recipes](model_recipes#page-model-recipes).

   Scheduled Publishing
--------------------

 Page publishing can be scheduled through the *Set schedule* feature in the *Status* side panel of the *Edit* page. This allows you to set up initial page publishing or a page update in advance. In order for pages to go live at the scheduled time, you should set up the [publish\_scheduled](../management_commands#publish-scheduled) management command.

 The basic workflow is as follows:

 * Scheduling is done by setting the *go-live at* field of the page and clicking *Publish*.
* Scheduling a revision for a page that is not currently live means that page will go live when the scheduled time comes.
* Scheduling a revision for a page that is already live means that revision will be published when the time comes.
* If the page has a scheduled revision and you set another revision to publish immediately (i.e. clicking *Publish* with the *go-live at* field unset), the scheduled revision will be unscheduled.
* If the page has a scheduled revision and you schedule another revision to publish (i.e. clicking *Publish* with the *go-live at* field set), the existing scheduled revision will be unscheduled and the new revision will be scheduled instead.

 Note that you have to click *Publish* after setting the *go-live at* field for the revision to be scheduled. Saving a draft revision with the *go-live at* field without clicking *Publish* will not schedule it to be published.

 The *History* view for a given page will show which revision is scheduled and when it is scheduled for. A scheduled revision in the list will also provide an *Unschedule* button to cancel it.

 In addition to scheduling a page to be published, it is also possible to schedule a page to be unpublished by setting the *expire at* field. However, unlike with publishing, the unpublishing schedule is applied to the live page instance rather than a specific revision. This means that any change to the *expire at* field will only be effective once the associated revision is published (i.e. when the changes are applied to the live instance). To illustrate:

 * Scheduling is done by setting the *expire at* field of the page and clicking *Publish*. If the *go-live at* field is also set, then the unpublishing schedule will only be applied after the revision goes live.
* Consider a live page that is scheduled to be unpublished on e.g. 14 June. Then sometime before the schedule, consider that a new revision is scheduled to be published on a date that’s **earlier** than the unpublishing schedule, e.g. 9 June. When the new revision goes live on 9 June, the *expire at* field contained in the new revision will replace the existing unpublishing schedule. This means:


	+ If the new revision contains a different *expire at* field (e.g. 17 June), the new revision will go live on 9 June and the page will not be unpublished on 14 June but it will be unpublished on 17 June.
	+ If the new revision has the *expire at* field unset, the new revision will go live on 9 June and the unpublishing schedule will be unset, thus the page will not be unpublished.
* Consider another live page that is scheduled to be unpublished on e.g. 14 June. Then sometime before the schedule, consider that a new revision is scheduled to be published on a date that’s **later** than the unpublishing schedule, e.g. 21 June. The new revision will not take effect until it goes live on 21 June, so the page will still be unpublished on 14 June. This means:


	+ If the new revision contains a different *expire at* field (e.g. 25 June), the page will be unpublished on 14 June, the new revision will go live on 21 June and the page will be unpublished again on 25 June.
	+ If the new revision has the *expire at* field unset, the page will be unpublished on 14 June and the new revision will go live on 21 June.

 The same scheduling mechanism also applies to snippets with [`DraftStateMixin`](model_reference#wagtail.models.DraftStateMixin "wagtail.models.DraftStateMixin") applied. For more details, see [Saving draft changes of snippets](../../topics/snippets#wagtailsnippets-saving-draft-changes-of-snippets).

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/pages/theory.html>

Recipes
=======

  Overriding the serve() Method
-----------------------------

 Wagtail defaults to serving [`Page`](model_reference#wagtail.models.Page "wagtail.models.Page")-derived models by passing a reference to the page object to a Django HTML template matching the model’s name, but suppose you wanted to serve something other than HTML? You can override the [`serve()`](model_reference#wagtail.models.Page.serve "wagtail.models.Page.serve") method provided by the [`Page`](model_reference#wagtail.models.Page "wagtail.models.Page") class and handle the Django request and response more directly.

 Consider this example of an `EventPage` object which is served as an iCal file if the `format` variable is set in the request:

 
```
class EventPage(Page):
    ...

    def serve(self, request):
        if "format" in request.GET:
            if request.GET['format'] == 'ical':
                # Export to ical format
                response = HttpResponse(
                    export_event(self, 'ical'),
                    content_type='text/calendar',
                )
                response['Content-Disposition'] = 'attachment; filename=' + self.slug + '.ics'
                return response
            else:
                # Unrecognised format error
                message = 'Could not export event\n\nUnrecognised format: ' + request.GET['format']
                return HttpResponse(message, content_type='text/plain')
        else:
            # Display event page as usual
            return super().serve(request)

```
 [`serve()`](model_reference#wagtail.models.Page.serve "wagtail.models.Page.serve") takes a Django request object and returns a Django response object. Wagtail returns a `TemplateResponse` object with the template and context which it generates, which allows middleware to function as intended, so keep in mind that a simpler response object like a `HttpResponse` will not receive these benefits.

 With this strategy, you could use Django or Python utilities to render your model in JSON or XML or any other format you’d like.

  ### Adding Endpoints with Custom [`route()`](model_reference#wagtail.models.Page.route "wagtail.models.Page.route") Methods

  Note

 A much simpler way of adding more endpoints to pages is provided by the [RoutablePageMixin](../contrib/routablepage#routable-page-mixin) mixin.

  Wagtail routes requests by iterating over the path components (separated with a forward slash `/`), finding matching objects based on their slug, and delegating further routing to that object’s model class. The Wagtail source is very instructive in figuring out what’s happening. This is the default `route()` method of the `Page` class:

 
```
class Page(...):
    ...

    def route(self, request, path_components):
        if path_components:
            # request is for a child of this page
            child_slug = path_components[0]
            remaining_components = path_components[1:]

            # find a matching child or 404
            try:
                subpage = self.get_children().get(slug=child_slug)
            except Page.DoesNotExist:
                raise Http404

            # delegate further routing
            return subpage.specific.route(request, remaining_components)

        else:
            # request is for this very page
            if self.live:
                # Return a RouteResult that will tell Wagtail to call
                # this page's serve() method
                return RouteResult(self)
            else:
                # the page matches the request, but isn't published, so 404
                raise Http404

```
 [`route()`](model_reference#wagtail.models.Page.route "wagtail.models.Page.route") takes the current object (`self`), the `request` object, and a list of the remaining `path_components` from the request URL. It either continues delegating routing by calling [`route()`](model_reference#wagtail.models.Page.route "wagtail.models.Page.route") again on one of its children in the Wagtail tree, or ends the routing process by returning a `RouteResult` object or raising a 404 error.

 The `RouteResult` object (defined in wagtail.url\_routing) encapsulates all the information Wagtail needs to call a page’s [`serve()`](model_reference#wagtail.models.Page.serve "wagtail.models.Page.serve") method and return a final response: this information consists of the page object, and any additional `args`/`kwargs` to be passed to [`serve()`](model_reference#wagtail.models.Page.serve "wagtail.models.Page.serve").

 By overriding the [`route()`](model_reference#wagtail.models.Page.route "wagtail.models.Page.route") method, we could create custom endpoints for each object in the Wagtail tree. One use case might be using an alternate template when encountering the `print/` endpoint in the path. Another might be a REST API which interacts with the current object. Just to see what’s involved, lets make a simple model which prints out all of its child path components.

 First, `models.py`:

 
```
from django.shortcuts import render
from wagtail.url_routing import RouteResult
from django.http.response import Http404
from wagtail.models import Page

# ...

class Echoer(Page):

    def route(self, request, path_components):
        if path_components:
            # tell Wagtail to call self.serve() with an additional 'path_components' kwarg
            return RouteResult(self, kwargs={'path_components': path_components})
        else:
            if self.live:
                # tell Wagtail to call self.serve() with no further args
                return RouteResult(self)
            else:
                raise Http404

    def serve(self, path_components=[]):
        return render(request, self.template, {
            'page': self,
            'echo': ' '.join(path_components),
            })

```
 This model, `Echoer`, doesn’t define any properties, but does subclass `Page` so objects will be able to have a custom title and slug. The template just has to display our `{{ echo }}` property.

 Now, once creating a new `Echoer` page in the Wagtail admin titled “Echo Base,” requests such as:

 
```
http://127.0.0.1:8000/echo-base/tauntaun/kennel/bed/and/breakfast/

```
 Will return:

 
```
tauntaun kennel bed and breakfast

```
 Be careful if you’re introducing new required arguments to the `serve()` method - Wagtail still needs to be able to display a default view of the page for previewing and moderation, and by default will attempt to do this by calling `serve()` with a request object and no further arguments. If your `serve()` method does not accept that as a method signature, you will need to override the page’s `serve_preview()` method to call `serve()` with suitable arguments:

 
```
def serve_preview(self, request, mode_name):
    return self.serve(request, variant='radiant')

```
    Tagging
-------

 Wagtail provides tagging capabilities through the combination of two Django modules, [django-taggit](https://django-taggit.readthedocs.io/)  (which provides a general-purpose tagging implementation) and [django-modelcluster](https://github.com/wagtail/django-modelcluster) (which extends django-taggit’s `TaggableManager` to allow tag relations to be managed in memory without writing to the database - necessary for handling previews and revisions). To add tagging to a page model, you’ll need to define a ‘through’ model inheriting from `TaggedItemBase` to set up the many-to-many relationship between django-taggit’s `Tag` model and your page model, and add a `ClusterTaggableManager` accessor to your page model to present this relation as a single tag field.

 In this example, we set up tagging on `BlogPage` through a `BlogPageTag` model:

 
```
# models.py

from modelcluster.fields import ParentalKey
from modelcluster.contrib.taggit import ClusterTaggableManager
from taggit.models import TaggedItemBase

class BlogPageTag(TaggedItemBase):
    content_object = ParentalKey('demo.BlogPage', on_delete=models.CASCADE, related_name='tagged_items')

class BlogPage(Page):
    ...
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)

    promote_panels = Page.promote_panels + [
        ...
        FieldPanel('tags'),
    ]

```
 Wagtail’s admin provides a nice interface for inputting tags into your content, with typeahead tag completion and friendly tag icons.

 We can now make use of the many-to-many tag relationship in our views and templates. For example, we can set up the blog’s index page to accept a `?tag=...` query parameter to filter the `BlogPage` listing by tag:

 
```
from django.shortcuts import render

class BlogIndexPage(Page):
    ...
    def get_context(self, request):
        context = super().get_context(request)

        # Get blog entries
        blog_entries = BlogPage.objects.child_of(self).live()

        # Filter by tag
        tag = request.GET.get('tag')
        if tag:
            blog_entries = blog_entries.filter(tags__name=tag)

        context['blog_entries'] = blog_entries
        return context

```
 Here, `blog_entries.filter(tags__name=tag)` follows the `tags` relation on `BlogPage`, to filter the listing to only those pages with a matching tag name before passing this to the template for rendering. We can now update the `blog_page.html` template to show a list of tags associated with the page, with links back to the filtered index page:

 
```
{% for tag in page.tags.all %}
    <a href="{% pageurl page.blog_index %}?tag={{ tag }}">{{ tag }}</a>
{% endfor %}

```
 Iterating through `page.tags.all` will display each tag associated with `page`, while the links back to the index make use of the filter option added to the `BlogIndexPage` model. A Django query could also use the `tagged_items` related name field to get `BlogPage` objects associated with a tag.

 The same approach can be used to add tagging to non-page models managed through [Snippets](../../topics/snippets#snippets) and [ModelAdmin](../contrib/modeladmin/index). In this case, the model must inherit from `modelcluster.models.ClusterableModel` to be compatible with `ClusterTaggableManager`.

  ### Custom tag models

 In the above example, any newly-created tags will be added to django-taggit’s default `Tag` model, which will be shared by all other models using the same recipe as well as Wagtail’s image and document models. In particular, this means that the autocompletion suggestions on tag fields will include tags previously added to other models. To avoid this, you can set up a custom tag model inheriting from `TagBase`, along with a ‘through’ model inheriting from `ItemBase`, which will provide an independent pool of tags for that page model.

 
```
from django.db import models
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import TagBase, ItemBase

class BlogTag(TagBase):
    class Meta:
        verbose_name = "blog tag"
        verbose_name_plural = "blog tags"


class TaggedBlog(ItemBase):
    tag = models.ForeignKey(
        BlogTag, related_name="tagged_blogs", on_delete=models.CASCADE
    )
    content_object = ParentalKey(
        to='demo.BlogPage',
        on_delete=models.CASCADE,
        related_name='tagged_items'
    )

class BlogPage(Page):
    ...
    tags = ClusterTaggableManager(through='demo.TaggedBlog', blank=True)

```
 Within the admin, the tag field will automatically recognise the custom tag model being used, and will offer autocomplete suggestions taken from that tag model.

   ### Disabling free tagging

 By default, tag fields work on a “free tagging” basis: editors can enter anything into the field, and upon saving, any tag text not recognised as an existing tag will be created automatically. To disable this behaviour, and only allow editors to enter tags that already exist in the database, custom tag models accept a `free_tagging = False` option:

 
```
from taggit.models import TagBase
from wagtail.snippets.models import register_snippet

@register_snippet
class BlogTag(TagBase):
    free_tagging = False

    class Meta:
        verbose_name = "blog tag"
        verbose_name_plural = "blog tags"

```
 Here we have registered `BlogTag` as a snippet, to provide an interface for administrators (and other users with the appropriate permissions) to manage the allowed set of tags. With the `free_tagging = False` option set, editors can no longer enter arbitrary text into the tag field, and must instead select existing tags from the autocomplete dropdown.

   ### Managing tags with Wagtail’s `ModelAdmin`

 In order to manage all the tags used in a project, you can a use the `ModelAdmin` to add the `Tag` model to the Wagtail admin. This will allow you to have a tag admin interface within the main menu in which you can add, edit or delete your tags.

 Tags that are removed from a content don’t get deleted from the `Tag` model and will still be shown in typeahead tag completion. So having a tag interface is a great way to completely get rid of tags you don’t need.

 To add the tag interface, add the following block of code to a `wagtail_hooks.py` file within any your project’s apps:

 
```
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from wagtail.admin.edit_handlers import FieldPanel
from taggit.models import Tag


class TagsModelAdmin(ModelAdmin):
    Tag.panels = [FieldPanel("name")]  # only show the name field
    model = Tag
    menu_label = "Tags"
    menu_icon = "tag"  # change as required
    menu_order = 200  # will put in 3rd place (000 being 1st, 100 2nd)
    list_display = ["name", "slug"]
    search_fields = ("name",)

modeladmin_register(TagsModelAdmin)

```
 A `Tag` model has a `name` and `slug` required fields. If you decide to add a tag, it is recommended to only display the `name` field panel as the slug field is autofilled when the `name` field is filled and you don’t need to enter the same name in both the fields.

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/pages/model_recipes.html>

Panel types
===========

  Built-in Fields and Choosers
----------------------------

 Django’s field types are automatically recognised and provided with an appropriate widget for input. Just define that field the normal Django way and pass the field name into [`FieldPanel`](#wagtail.admin.panels.FieldPanel "wagtail.admin.panels.FieldPanel") when defining your panels. Wagtail will take care of the rest.

 Here are some Wagtail-specific types that you might include as fields in your models.

  ### FieldPanel

   ### StreamFieldPanel

   ### MultiFieldPanel

   ### InlinePanel

 This is a powerful but complex feature which will take some space to cover, so we’ll skip over it for now. For a full explanation on the usage of `InlinePanel`, see [Inline Panels and Model Clusters](#inline-panels).

  #### Collapsing InlinePanels to save space

 Note that you can use `classname="collapsed"` to load the panel collapsed under its heading in order to save space in the Wagtail admin.

    ### FieldRowPanel

   ### HelpPanel

   ### PageChooserPanel

   ### ImageChooserPanel

   ### FormSubmissionsPanel

   ### DocumentChooserPanel

   ### SnippetChooserPanel

    Field Customisation
-------------------

 By adding CSS classes to your panel definitions or adding extra parameters to your field definitions, you can control much of how your fields will display in the Wagtail page editing interface. Wagtail’s page editing interface takes much of its behaviour from Django’s admin, so you may find many options for customisation covered there. (See [Django model field reference](https://docs.djangoproject.com/en/stable/ref/models/fields/ "(in Django v4.1)")).

  ### Titles

 Use `classname="title"` to make Page’s built-in title field stand out with more vertical padding.

   ### Collapsible

  Changed in version 4.0: All panels are now collapsible by default.

  Using `classname="collapsed"` will load the editor page with the panel collapsed under its heading.

 
```
    content_panels = [
        MultiFieldPanel(
            [
                FieldPanel('cover'),
                FieldPanel('book_file'),
                FieldPanel('publisher'),
            ],
            heading="Collection of Book Fields",
            classname="collapsed"
        ),
    ]

```
   ### Placeholder Text

 By default, Wagtail uses the field’s label as placeholder text. To change it, pass to the FieldPanel a widget with a placeholder attribute set to your desired text. You can select widgets from [Django’s form widgets](https://docs.djangoproject.com/en/stable/ref/forms/widgets/ "(in Django v4.1)"), or any of the Wagtail’s widgets found in `wagtail.admin.widgets`.

 For example, to customise placeholders for a Book model exposed via ModelAdmin:

 
```
# models.py
from django import forms            # the default Django widgets live here
from wagtail.admin import widgets   # to use Wagtail's special datetime widget

class Book(models.Model):
    title = models.CharField(max_length=256)
    release_date = models.DateField()
    price = models.DecimalField(max_digits=5, decimal_places=2)

    # you can create them separately
    title_widget = forms.TextInput(
        attrs = {
            'placeholder': 'Enter Full Title'
        }
    )
    # using the correct widget for your field type and desired effect
    date_widget = widgets.AdminDateInput(
        attrs = {
            'placeholder': 'dd-mm-yyyy'
        }
    )

    panels = [
        FieldPanel('title', widget=title_widget), # then add them as a variable
        FieldPanel('release_date', widget=date_widget),
        FieldPanel('price', widget=forms.NumberInput(attrs={'placeholder': 'Retail price on release'})) # or directly inline
    ]

```
   ### Required Fields

 To make input or chooser selection mandatory for a field, add [`blank=False`](https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.Field.blank "(in Django v4.1)") to its model definition.

   ### Hiding Fields

 Without a panel definition, a default form field (without label) will be used to represent your fields. If you intend to hide a field on the Wagtail page editor, define the field with [`editable=False`](https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.Field.editable "(in Django v4.1)").

    Inline Panels and Model Clusters
--------------------------------

 The `django-modelcluster` module allows for streamlined relation of extra models to a Wagtail page via a ForeignKey-like relationship called `ParentalKey`. Normally, your related objects “cluster” would need to be created beforehand (or asynchronously) before being linked to a Page; however, objects related to a Wagtail page via `ParentalKey` can be created on-the-fly and saved to a draft revision of a `Page` object.

 Let’s look at the example of adding related links to a [`Page`](model_reference#wagtail.models.Page "wagtail.models.Page")-derived model. We want to be able to add as many as we like, assign an order, and do all of this without leaving the page editing screen.

 
```
from wagtail.models import Orderable, Page
from modelcluster.fields import ParentalKey

# The abstract model for related links, complete with panels
class RelatedLink(models.Model):
    title = models.CharField(max_length=255)
    link_external = models.URLField("External link", blank=True)

    panels = [
        FieldPanel('title'),
        FieldPanel('link_external'),
    ]

    class Meta:
        abstract = True

# The real model which combines the abstract model, an
# Orderable helper class, and what amounts to a ForeignKey link
# to the model we want to add related links to (BookPage)
class BookPageRelatedLinks(Orderable, RelatedLink):
    page = ParentalKey('demo.BookPage', on_delete=models.CASCADE, related_name='related_links')

class BookPage(Page):
    # ...

    content_panels = Page.content_panels + [
        InlinePanel('related_links', heading="Related Links", label="Related link"),
    ]

```
 The `RelatedLink` class is a vanilla Django abstract model. The `BookPageRelatedLinks` model extends it with capability for being ordered in the Wagtail interface via the `Orderable` class as well as adding a `page` property which links the model to the `BookPage` model we’re adding the related links objects to. Finally, in the panel definitions for `BookPage`, we’ll add an [`InlinePanel`](#wagtail.admin.panels.InlinePanel "wagtail.admin.panels.InlinePanel") to provide an interface for it all. Let’s look again at the parameters that [`InlinePanel`](#wagtail.admin.panels.InlinePanel "wagtail.admin.panels.InlinePanel") accepts:

 
```
InlinePanel(relation_name, panels=None, heading='', label='', help_text='', min_num=None, max_num=None)

```
 The `relation_name` is the `related_name` label given to the cluster’s `ParentalKey` relation. You can add the `panels` manually or make them part of the cluster model. `heading` and `help_text` provide a heading and caption, respectively, for the Wagtail editor. `label` sets the text on the add button and child panels, and is used as the heading when `heading` is not present. Finally, `min_num` and `max_num` allow you to set the minimum/maximum number of forms that the user must submit.

 For another example of using model clusters, see [Tagging](model_recipes#tagging).

 For more on `django-modelcluster`, visit [the django-modelcluster github project page](https://github.com/wagtail/django-modelcluster)

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/pages/panels.html>

Model Reference
===============

 wagtail.models is split into submodules for maintainability. All definitions intended as public should be imported here (with ‘noqa’ comments as required) and outside code should continue to import them from wagtail.models (e.g. `from wagtail.models import Site`, not `from wagtail.models.sites import Site`.)

 Submodules should take care to keep the direction of dependencies consistent; where possible they should implement low-level generic functionality which is then imported by higher-level models such as Page.

 This document contains reference information for the model classes inside the `wagtailcore` module.

  `Page`
------

  ### Database fields

   ### Methods and properties

 In addition to the model fields provided, `Page` has many properties and methods that you may wish to reference, use, or override in creating your own models.

  Note

 See also [django-treebeard](https://django-treebeard.readthedocs.io/en/latest/index.html)’s `node API <https://django-treebeard.readthedocs.io/en/latest/api.html>. `Page` is a subclass of [materialized path tree](https://django-treebeard.readthedocs.io/en/latest/mp_tree.html) nodes.

     `Site`
------

 The `Site` model is useful for multi-site installations as it allows an administrator to configure which part of the tree to use for each hostname that the server responds on.

 The [`find_for_request()`](#wagtail.models.Site.find_for_request "wagtail.models.Site.find_for_request") function returns the Site object that will handle the given HTTP request.

  ### Database fields

   ### Methods and properties

    `Locale`
--------

 The `Locale` model defines the set of languages and/or locales that can be used on a site. Each `Locale` record corresponds to a “language code” defined in the :ref:`wagtail_content_languages_setting` setting.

 Wagtail will initially set up one `Locale` to act as the default language for all existing content. This first locale will automatically pick the value from `WAGTAIL_CONTENT_LANGUAGES` that most closely matches the site primary language code defined in `LANGUAGE_CODE`. If the primary language code is changed later, Wagtail will **not** automatically create a new `Locale` record or update an existing one.

 Before internationalisation is enabled, all pages use this primary `Locale` record. This is to satisfy the database constraints, and makes it easier to switch internationalisation on at a later date.

  ### Changing `WAGTAIL_CONTENT_LANGUAGES`

 Languages can be added or removed from `WAGTAIL_CONTENT_LANGUAGES` over time.

 Before removing an option from `WAGTAIL_CONTENT_LANGUAGES`, it’s important that the `Locale` record is updated to a use a different content language or is deleted. Any `Locale` instances that have invalid content languages are automatically filtered out from all database queries making them unable to be edited or viewed.

   ### Methods and properties

    `TranslatableMixin`
-------------------

 `TranslatableMixin` is an abstract model that can be added to any non-page Django model to make it translatable. Pages already include this mixin, so there is no need to add it.

  ### Database fields

 The `locale` and `translation_key` fields have a unique key constraint to prevent the object being translated into a language more than once.

   ### Methods and properties

    `PreviewableMixin`
------------------

 `PreviewableMixin` is a mixin class that can be added to any non-page Django model to allow previewing its instances. Pages already include this mixin, so there is no need to add it.

  New in version 4.0: The class is added to allow snippets to have live preview in the editor. See [Making snippets previewable](../../topics/snippets#wagtailsnippets-making-snippets-previewable) for more details.

   ### Methods and properties

    `RevisionMixin`
---------------

 `RevisionMixin` is an abstract model that can be added to any non-page Django model to allow saving revisions of its instances. Pages already include this mixin, so there is no need to add it.

  New in version 4.0: The model is added to allow snippets to save revisions, revert to a previous revision, and compare changes between revisions. See [Saving revisions of snippets](../../topics/snippets#wagtailsnippets-saving-revisions-of-snippets) for more details.

   ### Database fields

   ### Methods and properties

    `DraftStateMixin`
-----------------

 `DraftStateMixin` is an abstract model that can be added to any non-page Django model to allow its instances to have unpublished changes. This mixin requires [`RevisionMixin`](#wagtail.models.RevisionMixin "wagtail.models.RevisionMixin") to be applied. Pages already include this mixin, so there is no need to add it.

  New in version 4.0: The model is added to allow snippets to have changes that are not immediately reflected to the instance. See [Saving draft changes of snippets](../../topics/snippets#wagtailsnippets-saving-draft-changes-of-snippets) for more details.

   ### Database fields

   ### Methods and properties

    `Revision`
----------

 Every time a page is edited, a new `Revision` is created and saved to the database. It can be used to find the full history of all changes that have been made to a page and it also provides a place for new changes to be kept before going live.

 * Revisions can be created from any instance of [`RevisionMixin`](#wagtail.models.RevisionMixin "wagtail.models.RevisionMixin") by calling its [`save_revision()`](#wagtail.models.RevisionMixin.save_revision "wagtail.models.RevisionMixin.save_revision") method.
* The content of the page is JSON-serialisable and stored in the [`content`](#wagtail.models.Revision.content "wagtail.models.Revision.content") field.
* You can retrieve a `Revision` as an instance of the object’s model by calling the [`as_object()`](#wagtail.models.Revision.as_object "wagtail.models.Revision.as_object") method.

  Changed in version 4.0: The model has been renamed from `PageRevision` to `Revision` and it now references the `Page` model using a [`GenericForeignKey`](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#django.contrib.contenttypes.fields.GenericForeignKey "(in Django v4.1)").

   ### Database fields

   ### Managers

   ### Methods and properties

    `GroupPagePermission`
---------------------

  ### Database fields

    `PageViewRestriction`
---------------------

  ### Database fields

    
`Orderable` (abstract)
-----------------------

  ### Database fields

    `Workflow`
----------

 Workflows represent sequences of tasks which much be approved for an action to be performed on a page - typically publication.

  ### Database fields

   ### Methods and properties

    `WorkflowState`
---------------

 Workflow states represent the status of a started workflow on a page.

  ### Database fields

   ### Methods and properties

    `Task`
------

 Tasks represent stages in a workflow which must be approved for the workflow to complete successfully.

  ### Database fields

   ### Methods and properties

    `TaskState`
-----------

 Task states store state information about the progress of a task on a particular page revision.

  ### Database fields

   ### Methods and properties

    `WorkflowTask`
--------------

 Represents the ordering of a task in a specific workflow.

  ### Database fields

    `WorkflowPage`
--------------

 Represents the assignment of a workflow to a page and its descendants.

  ### Database fields

    `BaseLogEntry`
--------------

 An abstract base class that represents a record of an action performed on an object.

  ### Database fields

   ### Methods and properties

    `PageLogEntry`
--------------

 Represents a record of an action performed on an [`Page`](#wagtail.models.Page "wagtail.models.Page"), subclasses [`BaseLogEntry`](#wagtail.models.BaseLogEntry "wagtail.models.BaseLogEntry").

  ### Database fields

    `Comment`
---------

 Represents a comment on a page.

  ### Database fields

    `CommentReply`
--------------

 Represents a reply to a comment thread.

  ### Database fields

    `PageSubscription`
------------------

 Represents a user’s subscription to email notifications about page events. Currently only used for comment notifications.

  ### Database fields

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/pages/model_reference.html>

Page QuerySet reference
=======================

 All models that inherit from [`Page`](model_reference#wagtail.models.Page "wagtail.models.Page") are given some extra QuerySet methods accessible from their `.objects` attribute.

  Examples
--------

  ### Selecting only live pages

 
```
live_pages = Page.objects.live()

```
   ### Selecting published EventPages that are descendants of events\_index

 
```
events = EventPage.objects.live().descendant_of(events_index)

```
   ### Getting a list of menu items

 
```
# This gets a QuerySet of live children of the homepage with ``show_in_menus`` set
menu_items = homepage.get_children().live().in_menu()

```
    Reference
---------

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/pages/queryset_reference.html>

StreamField reference
=====================

 * [StreamField block reference](blocks)
	+ [`wagtail.fields.StreamField`](blocks#wagtail.fields.StreamField)
	+ [Block options](blocks#block-options)
	+ [Field block types](blocks#field-block-types)
	+ [Structural block types](blocks#structural-block-types)
* [Form widget client-side API](widget_api)
	+ [`render()`](widget_api#render)
	+ [`idForLabel`](widget_api#idForLabel)
	+ [`getValue()`](widget_api#getValue)
	+ [`getState()`](widget_api#getState)
	+ [`setState()`](widget_api#setState)
	+ [`focus()`](widget_api#focus)

  [StreamField block reference](blocks#streamfield-block-reference)
-----------------------------------------------------------------

 Details the block types provided by Wagtail for use in StreamField and how they can be combined into new block types.

   [Form widget client-side API](widget_api#streamfield-widget-api)
----------------------------------------------------------------

 Defines the JavaScript API that must be implemented for any form widget used within a StreamField block.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/streamfield/index.html>

StreamField block reference
===========================

 This document details the block types provided by Wagtail for use in [StreamField](../../advanced_topics/testing#wagtail.test.utils.form_data.streamfield "wagtail.test.utils.form_data.streamfield"), and how they can be combined into new block types.

  Changed in version 3.0: The required `use_json_field` argument is added.

  
```
body = StreamField([
    ('heading', blocks.CharBlock(form_classname="title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
], block_counts={
    'heading': {'min_num': 1},
    'image': {'max_num': 5},
}, use_json_field=True)

```
  Block options
-------------

 All block definitions accept the following optional keyword arguments:

 * `default`


	+ The default value that a new ‘empty’ block should receive.
* `label`


	+ The label to display in the editor interface when referring to this block - defaults to a prettified version of the block name (or, in a context where no name is assigned - such as within a `ListBlock` - the empty string).
* `icon`


	+ The name of the icon to display for this block type in the menu of available block types. For a list of icon names, see the Wagtail style guide, which can be enabled by adding `wagtail.contrib.styleguide` to your project’s `INSTALLED_APPS`.
* `template`


	+ The path to a Django template that will be used to render this block on the front end. See [Template rendering](../../topics/streamfield#streamfield-template-rendering)
* `group`


	+ The group used to categorize this block. Any blocks with the same group name will be shown together in the editor interface with the group name as a heading.

   Field block types
-----------------

   Structural block types
----------------------

 Since `StreamField` accepts an instance of `StreamBlock` as a parameter, in place of a list of block types, this makes it possible to re-use a common set of block types without repeating definitions:

 
```
class HomePage(Page):
    carousel = StreamField(
        CarouselBlock(max_num=10, block_counts={'video': {'max_num': 2}}),
        use_json_field=True
    )

```
 `StreamBlock` accepts the following additional options as either keyword arguments or `Meta` properties:

 
```
body = StreamField([
    # ...
    ('event_promotions', blocks.StreamBlock([
        ('hashtag', blocks.CharBlock()),
        ('post_date', blocks.DateBlock()),
    ], form_classname='event-promotions')),
], use_json_field=True)

```
 
```
class EventPromotionsBlock(blocks.StreamBlock):
    hashtag = blocks.CharBlock()
    post_date = blocks.DateBlock()

    class Meta:
        form_classname = 'event-promotions'

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/streamfield/blocks.html>

Form widget client-side API
===========================

 In order for the StreamField editing interface to dynamically create form fields, any Django form widgets used within StreamField blocks must have an accompanying JavaScript implementation, defining how the widget is rendered client-side and populated with data, and how to extract data from that field. Wagtail provides this implementation for widgets inheriting from `django.forms.widgets.Input`, `django.forms.Textarea`, `django.forms.Select` and `django.forms.RadioSelect`. For any other widget types, or ones which require custom client-side behaviour, you will need to provide your own implementation.

 The [telepath](https://wagtail.github.io/telepath/) library is used to set up mappings between Python widget classes and their corresponding JavaScript implementations. To create a mapping, define a subclass of `wagtail.widget_adapters.WidgetAdapter` and register it with `wagtail.telepath.register`.

 
```
from wagtail.telepath import register
   from wagtail.widget_adapters import WidgetAdapter

   class FancyInputAdapter(WidgetAdapter):
       # Identifier matching the one registered on the client side
       js_constructor = 'myapp.widgets.FancyInput'

       # Arguments passed to the client-side object
       def js_args(self, widget):
           return [
               # Arguments typically include the widget's HTML representation
               # and label ID rendered with __NAME__ and __ID__ placeholders,
               # for use in the client-side render() method
               widget.render('__NAME__', None, attrs={'id': '__ID__'}),
               widget.id_for_label('__ID__'),
               widget.extra_options,
           ]

       class Media:
           # JS / CSS includes required in addition to the widget's own media;
           # generally this will include the client-side adapter definition
           js = ['myapp/js/fancy-input-adapter.js']


   register(FancyInputAdapter(), FancyInput)

```
 The JavaScript object associated with a widget instance should provide a single method:

 A widget’s state will often be the same as the form field’s value, but may contain additional data beyond what is processed in the form submission. For example, a page chooser widget consists of a hidden form field containing the page ID, and a read-only label showing the page title: in this case, the page ID by itself does not provide enough information to render the widget, and so the state is defined as a dictionary with `id` and `title` items.

 The value returned by `render` is a ‘bound widget’ object allowing this widget instance’s data to be accessed. This object should implement the following attributes and methods:



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/streamfield/widget_api.html>

Contrib modules
===============

 Wagtail ships with a variety of extra optional modules.

 * [Settings](settings)
	+ [Installation](settings#installation)
	+ [Defining settings](settings#defining-settings)
	+ [Edit handlers](settings#edit-handlers)
	+ [Appearance](settings#appearance)
	+ [Using the settings](settings#using-the-settings)
	+ [Utilising `select_related` to improve efficiency](settings#utilising-select-related-to-improve-efficiency)
	+ [Utilising the `page_url` setting shortcut](settings#utilising-the-page-url-setting-shortcut)
* [Form builder](forms/index)
	+ [Usage](forms/index#usage)
	+ [Displaying form submission information](forms/index#displaying-form-submission-information)
	+ [Index](forms/index#index)
* [Sitemap generator](sitemaps)
	+ [Basic configuration](sitemaps#basic-configuration)
	+ [Setting the hostname](sitemaps#setting-the-hostname)
	+ [Customising](sitemaps#customising)
	+ [Serving multiple sitemaps](sitemaps#serving-multiple-sitemaps)
* [Frontend cache invalidator](frontendcache)
	+ [Setting it up](frontendcache#setting-it-up)
	+ [Advanced usage](frontendcache#advanced-usage)
* [`RoutablePageMixin`](routablepage)
	+ [Installation](routablepage#installation)
	+ [The basics](routablepage#the-basics)
	+ [The `RoutablePageMixin` class](routablepage#module-wagtail.contrib.routable_page.models)
	+ [The `routablepageurl` template tag](routablepage#the-routablepageurl-template-tag)
* [`ModelAdmin`](modeladmin/index)
	+ [Summary of features](modeladmin/index#summary-of-features)
	+ [Want to know more about customising `ModelAdmin`?](modeladmin/index#want-to-know-more-about-customising-modeladmin)
* [Promoted search results](searchpromotions)
	+ [Installation](searchpromotions#installation)
	+ [Usage](searchpromotions#usage)
* [Simple translation](simple_translation)
	+ [Basic configuration](simple_translation#basic-configuration)
	+ [Page tree synchronisation](simple_translation#page-tree-synchronisation)
* [TableBlock](table_block)
	+ [Installation](table_block#installation)
	+ [Basic Usage](table_block#basic-usage)
	+ [Advanced Usage](table_block#advanced-usage)
* [Typed table block](typed_table_block)
	+ [Installation](typed_table_block#installation)
	+ [Usage](typed_table_block#usage)
* [Redirects](redirects)
	+ [Installation](redirects#installation)
	+ [Usage](redirects#usage)
	+ [Automatic redirect creation](redirects#automatic-redirect-creation)
	+ [Management commands](redirects#management-commands)
	+ [The `Redirect` class](redirects#module-wagtail.contrib.redirects.models)
* [Legacy richtext](legacy_richtext)

  [Settings](settings)
--------------------

 Settings that are editable by administrators within the Wagtail admin - either site-specific or generic across all sites.

   [Form builder](forms/index)
---------------------------

 Allows forms to be created by admins and provides an interface for browsing form submissions.

   [Sitemap generator](sitemaps)
-----------------------------

 Provides a view that generates a Google XML sitemap of your public Wagtail content.

   [Frontend cache invalidator](frontendcache)
-------------------------------------------

 A module for automatically purging pages from a cache (Varnish, Squid, Cloudflare or Cloudfront) when their content is changed.

   [RoutablePageMixin](routablepage)
---------------------------------

 Provides a way of embedding Django URLconfs into pages.

   [ModelAdmin](modeladmin/index)
------------------------------

 A module allowing for more customisable representation and management of custom models in Wagtail’s admin area.

   [Promoted search results](searchpromotions)
-------------------------------------------

 A module for managing “Promoted Search Results”

   [Simple translation](simple_translation)
----------------------------------------

 A module for copying translatables (pages and snippets) to another language.

   [TableBlock](table_block)
-------------------------

 Provides a TableBlock for adding HTML tables to pages.

   [Typed table block](typed_table_block)
--------------------------------------

 Provides a StreamField block for authoring tables, where cells can be any block type including rich text.

   [Redirects](redirects)
----------------------

 Provides a way to manage redirects.

   [Legacy richtext](legacy_richtext)
----------------------------------

 Provides the legacy richtext wrapper (`<div class="rich-text"></div>`).

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/index.html>

Form builder
============

 The `wagtailforms` module allows you to set up single-page forms, such as a ‘Contact us’ form, as pages of a Wagtail site. It provides a set of base models that site implementers can extend to create their own `FormPage` type with their own site-specific templates. Once a page type has been set up in this way, editors can build forms within the usual page editor, consisting of any number of fields. Form submissions are stored for later retrieval through a new ‘Forms’ section within the Wagtail admin interface; in addition, they can be optionally e-mailed to an address specified by the editor.

  Note

 **wagtailforms is not a replacement for** [Django’s form support](https://docs.djangoproject.com/en/stable/topics/forms/ "(in Django v4.1)"). It is designed as a way for page authors to build general-purpose data collection forms without having to write code. If you intend to build a form that assigns specific behaviour to individual fields (such as creating user accounts), or needs a custom HTML layout, you will almost certainly be better served by a standard Django form, where the fields are fixed in code rather than defined on-the-fly by a page author. See the [wagtail-form-example project](https://github.com/gasman/wagtail-form-example/commits/master) for an example of integrating a Django form into a Wagtail page.

   Usage
-----

 Add `wagtail.contrib.forms` to your `INSTALLED_APPS`:

 
```
INSTALLED_APPS = [
    ...
    'wagtail.contrib.forms',
]

```
 Within the `models.py` of one of your apps, create a model that extends `wagtail.contrib.forms.models.AbstractEmailForm`:

 
```
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import (
    FieldPanel, FieldRowPanel,
    InlinePanel, MultiFieldPanel
)
from wagtail.fields import RichTextField
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField


class FormField(AbstractFormField):
    page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')


class FormPage(AbstractEmailForm):
    intro = RichTextField(blank=True)
    thank_you_text = RichTextField(blank=True)

    content_panels = AbstractEmailForm.content_panels + [
        FieldPanel('intro'),
        InlinePanel('form_fields', label="Form fields"),
        FieldPanel('thank_you_text'),
        MultiFieldPanel([
            FieldRowPanel([
                FieldPanel('from_address', classname="col6"),
                FieldPanel('to_address', classname="col6"),
            ]),
            FieldPanel('subject'),
        ], "Email"),
    ]

```
 `AbstractEmailForm` defines the fields `to_address`, `from_address` and `subject`, and expects `form_fields` to be defined. Any additional fields are treated as ordinary page content - note that `FormPage` is responsible for serving both the form page itself and the landing page after submission, so the model definition should include all necessary content fields for both of those views.

 Date and datetime values in a form response will be formatted with the [SHORT\_DATE\_FORMAT](https://docs.djangoproject.com/en/3.0/ref/settings/#short-date-format) and [SHORT\_DATETIME\_FORMAT](https://docs.djangoproject.com/en/3.0/ref/settings/#short-datetime-format) respectively. (see [Custom render\_email method](customisation#form-builder-render-email) for how to customise the email content).

 If you do not want your form page type to offer form-to-email functionality, you can inherit from AbstractForm instead of `AbstractEmailForm`, and omit the `to_address`, `from_address` and `subject` fields from the `content_panels` definition.

 You now need to create two templates named `form_page.html` and `form_page_landing.html` (where `form_page` is the underscore-formatted version of the class name). `form_page.html` differs from a standard Wagtail template in that it is passed a variable `form`, containing a Django `Form` object, in addition to the usual `page` variable. A very basic template for the form would thus be:

 
```
{% load wagtailcore_tags %}
<html>
    <head>
        <title>{{ page.title }}</title>
    </head>
    <body>
        <h1>{{ page.title }}</h1>
        {{ page.intro|richtext }}
        <form action="{% pageurl page %}" method="POST">
            {% csrf_token %}
            {{ form.as_p }}
            <input type="submit">
        </form>
    </body>
</html>

```
 `form_page_landing.html` is a standard Wagtail template, displayed after the user makes a successful form submission, `form_submission` will be available in this template. If you want to dynamically override the landing page template, you can do so with the `get_landing_page_template` method (in the same way that you would with `get_template`).

   Displaying form submission information
--------------------------------------

 `FormSubmissionsPanel` can be added to your page’s panel definitions to display the number of form submissions and the time of the most recent submission, along with a quick link to access the full submission data:

 
```
from wagtail.contrib.forms.panels import FormSubmissionsPanel

class FormPage(AbstractEmailForm):
    # ...

    content_panels = AbstractEmailForm.content_panels + [
        FormSubmissionsPanel(),
        FieldPanel('intro'),
        # ...
    ]

```
   Index
-----

 * [Form builder customisation](customisation)

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/forms/index.html>

Form builder customisation
==========================

 For a basic usage example see [form builder usage](index#form-builder-usage).

  Custom `related_name` for form fields
-------------------------------------

 If you want to change `related_name` for form fields (by default `AbstractForm` and `AbstractEmailForm` expect `form_fields` to be defined), you will need to override the `get_form_fields` method. You can do this as shown below.

 
```
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import (
    FieldPanel, FieldRowPanel,
    InlinePanel, MultiFieldPanel
)
from wagtail.fields import RichTextField
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField


class FormField(AbstractFormField):
    page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='custom_form_fields')


class FormPage(AbstractEmailForm):
    intro = RichTextField(blank=True)
    thank_you_text = RichTextField(blank=True)

    content_panels = AbstractEmailForm.content_panels + [
        FieldPanel('intro'),
        InlinePanel('custom_form_fields', label="Form fields"),
        FieldPanel('thank_you_text'),
        MultiFieldPanel([
            FieldRowPanel([
                FieldPanel('from_address', classname="col6"),
                FieldPanel('to_address', classname="col6"),
            ]),
            FieldPanel('subject'),
        ], "Email"),
    ]

    def get_form_fields(self):
        return self.custom_form_fields.all()

```
   Custom form submission model
----------------------------

 If you need to save additional data, you can use a custom form submission model. To do this, you need to:

 * Define a model that extends `wagtail.contrib.forms.models.AbstractFormSubmission`.
* Override the `get_submission_class` and `process_form_submission` methods in your page model.

 Example:

 
```
import json

from django.conf import settings
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import (
    FieldPanel, FieldRowPanel,
    InlinePanel, MultiFieldPanel
)
from wagtail.fields import RichTextField
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission


class FormField(AbstractFormField):
    page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')


class FormPage(AbstractEmailForm):
    intro = RichTextField(blank=True)
    thank_you_text = RichTextField(blank=True)

    content_panels = AbstractEmailForm.content_panels + [
        FieldPanel('intro'),
        InlinePanel('form_fields', label="Form fields"),
        FieldPanel('thank_you_text'),
        MultiFieldPanel([
            FieldRowPanel([
                FieldPanel('from_address', classname="col6"),
                FieldPanel('to_address', classname="col6"),
            ]),
            FieldPanel('subject'),
        ], "Email"),
    ]

    def get_submission_class(self):
        return CustomFormSubmission

    def process_form_submission(self, form):
        self.get_submission_class().objects.create(
            form_data=form.cleaned_data,
            page=self, user=form.user
        )


class CustomFormSubmission(AbstractFormSubmission):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

```
   Add custom data to CSV export
-----------------------------

 If you want to add custom data to the CSV export, you will need to:

 * Override the `get_data_fields` method in page model.
* Override `get_data` in the submission model.

 The example below shows how to add a username to the CSV export. Note that this code also changes the submissions list view.

 
```
import json

from django.conf import settings
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import (
    FieldPanel, FieldRowPanel,
    InlinePanel, MultiFieldPanel
)
from wagtail.fields import RichTextField
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission


class FormField(AbstractFormField):
    page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')


class FormPage(AbstractEmailForm):
    intro = RichTextField(blank=True)
    thank_you_text = RichTextField(blank=True)

    content_panels = AbstractEmailForm.content_panels + [
        FieldPanel('intro'),
        InlinePanel('form_fields', label="Form fields"),
        FieldPanel('thank_you_text'),
        MultiFieldPanel([
            FieldRowPanel([
                FieldPanel('from_address', classname="col6"),
                FieldPanel('to_address', classname="col6"),
            ]),
            FieldPanel('subject'),
        ], "Email"),
    ]

    def get_data_fields(self):
        data_fields = [
            ('username', 'Username'),
        ]
        data_fields += super().get_data_fields()

        return data_fields

    def get_submission_class(self):
        return CustomFormSubmission

    def process_form_submission(self, form):
        self.get_submission_class().objects.create(
            form_data=form.cleaned_data,
            page=self, user=form.user
        )


class CustomFormSubmission(AbstractFormSubmission):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

    def get_data(self):
        form_data = super().get_data()
        form_data.update({
            'username': self.user.username,
        })

        return form_data

```
   Check that a submission already exists for a user
-------------------------------------------------

 If you want to prevent users from filling in a form more than once, you need to override the `serve` method in your page model.

 Example:

 
```
import json

from django.conf import settings
from django.db import models
from django.shortcuts import render
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import (
    FieldPanel, FieldRowPanel,
    InlinePanel, MultiFieldPanel
)
from wagtail.fields import RichTextField
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField, AbstractFormSubmission


class FormField(AbstractFormField):
    page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')


class FormPage(AbstractEmailForm):
    intro = RichTextField(blank=True)
    thank_you_text = RichTextField(blank=True)

    content_panels = AbstractEmailForm.content_panels + [
        FieldPanel('intro'),
        InlinePanel('form_fields', label="Form fields"),
        FieldPanel('thank_you_text'),
        MultiFieldPanel([
            FieldRowPanel([
                FieldPanel('from_address', classname="col6"),
                FieldPanel('to_address', classname="col6"),
            ]),
            FieldPanel('subject'),
        ], "Email"),
    ]

    def serve(self, request, *args, **kwargs):
        if self.get_submission_class().objects.filter(page=self, user__pk=request.user.pk).exists():
            return render(
                request,
                self.template,
                self.get_context(request)
            )

        return super().serve(request, *args, **kwargs)

    def get_submission_class(self):
        return CustomFormSubmission

    def process_form_submission(self, form):
        self.get_submission_class().objects.create(
            form_data=form.cleaned_data,
            page=self, user=form.user
        )


class CustomFormSubmission(AbstractFormSubmission):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

    class Meta:
        unique_together = ('page', 'user')

```
 Your template should look like this:

 
```
{% load wagtailcore_tags %}
<html>
    <head>
        <title>{{ page.title }}</title>
    </head>
    <body>
        <h1>{{ page.title }}</h1>

        {% if user.is_authenticated and user.is_active or request.is_preview %}
            {% if form %}
                <div>{{ page.intro|richtext }}</div>
                <form action="{% pageurl page %}" method="POST">
                    {% csrf_token %}
                    {{ form.as_p }}
                    <input type="submit">
                </form>
            {% else %}
                <div>You can fill in the from only one time.</div>
            {% endif %}
        {% else %}
            <div>To fill in the form, you must to log in.</div>
        {% endif %}
    </body>
</html>

```
   Multi-step form
---------------

 The following example shows how to create a multi-step form.

 
```
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from django.shortcuts import render
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import (
    FieldPanel, FieldRowPanel,
    InlinePanel, MultiFieldPanel
)
from wagtail.fields import RichTextField
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField


class FormField(AbstractFormField):
    page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')


class FormPage(AbstractEmailForm):
    intro = RichTextField(blank=True)
    thank_you_text = RichTextField(blank=True)

    content_panels = AbstractEmailForm.content_panels + [
        FieldPanel('intro'),
        InlinePanel('form_fields', label="Form fields"),
        FieldPanel('thank_you_text'),
        MultiFieldPanel([
            FieldRowPanel([
                FieldPanel('from_address', classname="col6"),
                FieldPanel('to_address', classname="col6"),
            ]),
            FieldPanel('subject'),
        ], "Email"),
    ]

    def get_form_class_for_step(self, step):
        return self.form_builder(step.object_list).get_form_class()

    def serve(self, request, *args, **kwargs):
        """
        Implements a simple multi-step form.

        Stores each step into a session.
        When the last step was submitted correctly, saves whole form into a DB.
        """

        session_key_data = 'form_data-%s' % self.pk
        is_last_step = False
        step_number = request.GET.get('p', 1)

        paginator = Paginator(self.get_form_fields(), per_page=1)
        try:
            step = paginator.page(step_number)
        except PageNotAnInteger:
            step = paginator.page(1)
        except EmptyPage:
            step = paginator.page(paginator.num_pages)
            is_last_step = True

        if request.method == 'POST':
            # The first step will be submitted with step_number == 2,
            # so we need to get a form from previous step
            # Edge case - submission of the last step
            prev_step = step if is_last_step else paginator.page(step.previous_page_number())

            # Create a form only for submitted step
            prev_form_class = self.get_form_class_for_step(prev_step)
            prev_form = prev_form_class(request.POST, page=self, user=request.user)
            if prev_form.is_valid():
                # If data for step is valid, update the session
                form_data = request.session.get(session_key_data, {})
                form_data.update(prev_form.cleaned_data)
                request.session[session_key_data] = form_data

                if prev_step.has_next():
                    # Create a new form for a following step, if the following step is present
                    form_class = self.get_form_class_for_step(step)
                    form = form_class(page=self, user=request.user)
                else:
                    # If there is no next step, create form for all fields
                    form = self.get_form(
                        request.session[session_key_data],
                        page=self, user=request.user
                    )

                    if form.is_valid():
                        # Perform validation again for whole form.
                        # After successful validation, save data into DB,
                        # and remove from the session.
                        form_submission = self.process_form_submission(form)
                        del request.session[session_key_data]
                        # render the landing page
                        return self.render_landing_page(request, form_submission, *args, **kwargs)
            else:
                # If data for step is invalid
                # we will need to display form again with errors,
                # so restore previous state.
                form = prev_form
                step = prev_step
        else:
            # Create empty form for non-POST requests
            form_class = self.get_form_class_for_step(step)
            form = form_class(page=self, user=request.user)

        context = self.get_context(request)
        context['form'] = form
        context['fields_step'] = step
        return render(
            request,
            self.template,
            context
        )

```
 Your template for this form page should look like this:

 
```
{% load wagtailcore_tags %}
<html>
    <head>
        <title>{{ page.title }}</title>
    </head>
    <body>
        <h1>{{ page.title }}</h1>

        <div>{{ page.intro|richtext }}</div>
        <form action="{% pageurl page %}?p={{ fields_step.number|add:"1" }}" method="POST">
            {% csrf_token %}
            {{ form.as_p }}
            <input type="submit">
        </form>
    </body>
</html>

```
 Note that the example shown before allows the user to return to a previous step, or to open a second step without submitting the first step. Depending on your requirements, you may need to add extra checks.

   Show results
------------

 If you are implementing polls or surveys, you may want to show results after submission. The following example demonstrates how to do this.

 First, you need to collect results as shown below:

 
```
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import (
    FieldPanel, FieldRowPanel,
    InlinePanel, MultiFieldPanel
)
from wagtail.fields import RichTextField
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField


class FormField(AbstractFormField):
    page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')


class FormPage(AbstractEmailForm):
    intro = RichTextField(blank=True)
    thank_you_text = RichTextField(blank=True)

    content_panels = AbstractEmailForm.content_panels + [
        FieldPanel('intro'),
        InlinePanel('form_fields', label="Form fields"),
        FieldPanel('thank_you_text'),
        MultiFieldPanel([
            FieldRowPanel([
                FieldPanel('from_address', classname="col6"),
                FieldPanel('to_address', classname="col6"),
            ]),
            FieldPanel('subject'),
        ], "Email"),
    ]

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)

        # If you need to show results only on landing page,
        # you may need check request.method

        results = dict()
        # Get information about form fields
        data_fields = [
            (field.clean_name, field.label)
            for field in self.get_form_fields()
        ]

        # Get all submissions for current page
        submissions = self.get_submission_class().objects.filter(page=self)
        for submission in submissions:
            data = submission.get_data()

            # Count results for each question
            for name, label in data_fields:
                answer = data.get(name)
                if answer is None:
                    # Something wrong with data.
                    # Probably you have changed questions
                    # and now we are receiving answers for old questions.
                    # Just skip them.
                    continue

                if type(answer) is list:
                    # Answer is a list if the field type is 'Checkboxes'
                    answer = u', '.join(answer)

                question_stats = results.get(label, {})
                question_stats[answer] = question_stats.get(answer, 0) + 1
                results[label] = question_stats

        context.update({
            'results': results,
        })
        return context

```
 Next, you need to transform your template to display the results:

 
```
{% load wagtailcore_tags %}
<html>
    <head>
        <title>{{ page.title }}</title>
    </head>
    <body>
        <h1>{{ page.title }}</h1>

        <h2>Results</h2>
        {% for question, answers in results.items %}
            <h3>{{ question }}</h3>
            {% for answer, count in answers.items %}
                <div>{{ answer }}: {{ count }}</div>
            {% endfor %}
        {% endfor %}

        <div>{{ page.intro|richtext }}</div>
        <form action="{% pageurl page %}" method="POST">
            {% csrf_token %}
            {{ form.as_p }}
            <input type="submit">
        </form>
    </body>
</html>

```
 You can also show the results on the landing page.

   Custom landing page redirect
----------------------------

 You can override the `render_landing_page` method on your `FormPage` to change what is rendered when a form submits.

 In this example below we have added a `thank_you_page` field that enables custom redirects after a form submits to the selected page.

 When overriding the `render_landing_page` method, we check if there is a linked `thank_you_page` and then redirect to it if it exists.

 Finally, we add a URL param of `id` based on the `form_submission` if it exists.

 
```
from django.shortcuts import redirect
from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel
from wagtail.contrib.forms.models import AbstractEmailForm

class FormPage(AbstractEmailForm):

    # intro, thank_you_text, ...

    thank_you_page = models.ForeignKey(
        'wagtailcore.Page',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
    )

    def render_landing_page(self, request, form_submission=None, *args, **kwargs):
        if self.thank_you_page:
            url = self.thank_you_page.url
            # if a form_submission instance is available, append the id to URL
            # when previewing landing page, there will not be a form_submission instance
            if form_submission:
                url += '?id=%s' % form_submission.id
            return redirect(url, permanent=False)
        # if no thank_you_page is set, render default landing page
        return super().render_landing_page(request, form_submission, *args, **kwargs)

    content_panels = AbstractEmailForm.content_panels + [
        FieldPanel('intro'),
        InlinePanel('form_fields'),
        FieldPanel('thank_you_text'),
        FieldPanel('thank_you_page'),
        MultiFieldPanel([
            FieldRowPanel([
                FieldPanel('from_address', classname='col6'),
                FieldPanel('to_address', classname='col6'),
            ]),
            FieldPanel('subject'),
        ], 'Email'),
    ]

```
   Customise form submissions listing in Wagtail Admin
---------------------------------------------------

 The Admin listing of form submissions can be customised by setting the attribute `submissions_list_view_class` on your FormPage model.

 The list view class must be a subclass of `SubmissionsListView` from `wagtail.contrib.forms.views`, which is a child class of Django’s class based [`ListView`](https://docs.djangoproject.com/en/stable/ref/class-based-views/generic-display/#django.views.generic.list.ListView "(in Django v4.1)").

 Example:

 
```
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
from wagtail.contrib.forms.views import SubmissionsListView


class CustomSubmissionsListView(SubmissionsListView):
    paginate_by = 50  # show more submissions per page, default is 20
    ordering = ('submit_time',)  # order submissions by oldest first, normally newest first
    ordering_csv = ('-submit_time',)  # order csv export by newest first, normally oldest first

    # override the method to generate csv filename
    def get_csv_filename(self):
        """ Returns the filename for CSV file with page slug at start"""
        filename = super().get_csv_filename()
        return self.form_page.slug + '-' + filename


class FormField(AbstractFormField):
    page = ParentalKey('FormPage', related_name='form_fields')


class FormPage(AbstractEmailForm):
    """Form Page with customised submissions listing view"""

    # set custom view class as class attribute
    submissions_list_view_class = CustomSubmissionsListView

    intro = RichTextField(blank=True)
    thank_you_text = RichTextField(blank=True)

    # content_panels = ...

```
   Adding a custom field type
--------------------------

 First, make the new field type available in the page editor by changing your `FormField` model.

 * Create a new set of choices which includes the original `FORM_FIELD_CHOICES` along with new field types you want to make available.
* Each choice must contain a unique key and a human readable name of the field, for example `('slug', 'URL Slug')`
* Override the `field_type` field in your `FormField` model with `choices` attribute using these choices.
* You will need to run `./manage.py makemigrations` and `./manage.py migrate` after this step.

 Then, create and use a new form builder class.

 * Define a new form builder class that extends the `FormBuilder` class.
* Add a method that will return a created Django form field for the new field type.
* Its name must be in the format: `create_<field_type_key>_field`, for example `create_slug_field`
* Override the `form_builder` attribute in your form page model to use your new form builder class.

 Example:

 
```
from django import forms
from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.contrib.forms.forms import FormBuilder
from wagtail.contrib.forms.models import (
    AbstractEmailForm, AbstractFormField, FORM_FIELD_CHOICES)


class FormField(AbstractFormField):
    # extend the built in field type choices
    # our field type key will be 'ipaddress'
    CHOICES = FORM_FIELD_CHOICES + (('ipaddress', 'IP Address'),)

    page = ParentalKey('FormPage', related_name='form_fields')
    # override the field_type field with extended choices
    field_type = models.CharField(
        verbose_name='field type',
        max_length=16,
        # use the choices tuple defined above
        choices=CHOICES
    )


class CustomFormBuilder(FormBuilder):
    # create a function that returns an instanced Django form field
    # function name must match create_<field_type_key>_field
    def create_ipaddress_field(self, field, options):
        # return `forms.GenericIPAddressField(**options)` not `forms.SlugField`
        # returns created a form field with the options passed in
        return forms.GenericIPAddressField(**options)


class FormPage(AbstractEmailForm):
    # intro, thank_you_text, edit_handlers, etc...

    # use custom form builder defined above
    form_builder = CustomFormBuilder

```
   Custom `render_email` method
----------------------------

 If you want to change the content of the email that is sent when a form submits you can override the `render_email` method.

 To do this, you need to:

 * Ensure you have your form model defined that extends `wagtail.contrib.forms.models.AbstractEmailForm`.
* Override the `render_email` method in your page model.

 Example:

 
```
from datetime import date
# ... additional wagtail imports
from wagtail.contrib.forms.models import AbstractEmailForm


class FormPage(AbstractEmailForm):
    # ... fields, content_panels, etc

    def render_email(self, form):
        # Get the original content (string)
        email_content = super().render_email(form)

        # Add a title (not part of original method)
        title = '{}: {}'.format('Form', self.title)

        content = [title, '', email_content, '']

        # Add a link to the form page
        content.append('{}: {}'.format('Submitted Via', self.full_url))

        # Add the date the form was submitted
        submitted_date_str = date.today().strftime('%x')
        content.append('{}: {}'.format('Submitted on', submitted_date_str))

        # Content is joined with a new line to separate each text line
        content = '\n'.join(content)

        return content

```
   Custom `send_mail` method
-------------------------

 If you want to change the subject or some other part of how an email is sent when a form submits you can override the `send_mail` method.

 To do this, you need to:

 * Ensure you have your form model defined that extends `wagtail.contrib.forms.models.AbstractEmailForm`.
* In your models.py file, import the `wagtail.admin.mail.send_mail` function.
* Override the `send_mail` method in your page model.

 Example:

 
```
from datetime import date
# ... additional wagtail imports
from wagtail.admin.mail import send_mail
from wagtail.contrib.forms.models import AbstractEmailForm


class FormPage(AbstractEmailForm):
    # ... fields, content_panels, etc

    def send_mail(self, form):
        # `self` is the FormPage, `form` is the form's POST data on submit

        # Email addresses are parsed from the FormPage's addresses field
        addresses = [x.strip() for x in self.to_address.split(',')]

        # Subject can be adjusted (adding submitted date), be sure to include the form's defined subject field
        submitted_date_str = date.today().strftime('%x')
        subject = f"{self.subject} - {submitted_date_str}"

        send_mail(subject, self.render_email(form), addresses, self.from_address,)

```
   Custom `clean_name` generation
------------------------------

 * Each time a new `FormField` is added a `clean_name` also gets generated based on the user entered `label`.
* `AbstractFormField` has a method `get_field_clean_name` to convert the label into a HTML valid `lower_snake_case` ASCII string using the [AnyAscii](https://pypi.org/project/anyascii/) library which can be overridden to generate a custom conversion.
* The resolved `clean_name` is also used as the form field name in rendered HTML forms.
* Ensure that any conversion will be unique enough to not create conflicts within your `FormPage` instance.
* This method gets called on creation of new fields only and as such will not have access to its own `Page` or `pk`. This does not get called when labels are edited as modifying the `clean_name` after any form responses are submitted will mean those field responses will not be retrieved.
* This method gets called for form previews and also validation of duplicate labels.

 
```
    import uuid

    from django.db import models
    from modelcluster.fields import ParentalKey

    # ... other field and edit_handler imports
    from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField


    class FormField(AbstractFormField):
        page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')

        def get_field_clean_name(self):
            clean_name = super().get_field_clean_name()
            id = str(uuid.uuid4())[:8] # short uuid
            return f"{id}_{clean_name}"


    class FormPage(AbstractEmailForm):
        # ... page definitions

```
   Using `FormMixin` or `EmailFormMixin` to use with other `Page` subclasses
-------------------------------------------------------------------------

 If you need to add form behaviour while extending an additional class, you can use the base mixins instead of the abstract modals.

 
```
from wagtail.models import Page
from wagtail.contrib.forms.models import EmailFormMixin, FormMixin


class BasePage(Page):
    """
    A shared base page used throughout the project.
    """

    # ...

class FormPage(FormMixin, BasePage):
    intro = RichTextField(blank=True)
    # ...

class EmailFormPage(EmailFormMixin, FormMixin, BasePage):
    intro = RichTextField(blank=True)
    # ...


```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/forms/customisation.html>

ModelAdmin
==========

 The `modeladmin` module allows you to add any model in your project to the Wagtail admin. You can create customisable listing pages for a model, including plain Django models, and add navigation elements so that a model can be accessed directly from the Wagtail admin. Simply extend the `ModelAdmin` class, override a few attributes to suit your needs, register it with Wagtail using an easy one-line `modeladmin_register` method (you can copy and paste from the examples below), and you’re good to go. Your model doesn’t need to extend `Page` or be registered as a `Snippet`, and it won’t interfere with any of the existing admin functionality that Wagtail provides.

  Summary of features
-------------------

 * A customisable list view, allowing you to control what values are displayed for each row, available options for result filtering, default ordering, spreadsheet downloads and more.
* Access your list views from the Wagtail admin menu easily with automatically generated menu items, with automatic ‘active item’ highlighting. Control the label text and icons used with easy-to-change attributes on your class.
* An additional `ModelAdminGroup` class, that allows you to group your related models, and list them together in their own submenu, for a more logical user experience.
* Simple, robust **add** and **edit** views for your non-Page models that use the panel configurations defined on your model using Wagtail’s edit panels.
* For Page models, the system directs to Wagtail’s existing add and edit views, and returns you back to the correct list page, for a seamless experience.
* Full respect for permissions assigned to your Wagtail users and groups. Users will only be able to do what you want them to!
* All you need to easily hook your `ModelAdmin` classes into Wagtail, taking care of URL registration, menu changes, and registering any missing model permissions, so that you can assign them to Groups.
* **Built to be customisable** - While `modeladmin` provides a solid experience out of the box, you can easily use your own templates, and the `ModelAdmin` class has a large number of methods that you can override or extend, allowing you to customise the behaviour to a greater degree.

   Want to know more about customising `ModelAdmin`?
-------------------------------------------------

 * [`modeladmin` customisation primer](primer)
* [Customising the base URL path](base_url)
* [Customising the menu item](menu_item)
* [Customising `IndexView` - the listing view](indexview)
* [Customising `CreateView`, `EditView` and `DeleteView`](create_edit_delete_views)
* [Changing which fields appear in `CreateView` & `EditView`](create_edit_delete_views#changing-which-fields-appear-in-createview-editview)
* [Enabling & customising `InspectView`](inspectview)
* [Customising `ChooseParentView`](chooseparentview)
* [Additional tips and tricks](tips_and_tricks/index)

  ### Installation

 Add `wagtail.contrib.modeladmin` to your `INSTALLED_APPS`:

 
```
    INSTALLED_APPS = [
       ...
       'wagtail.contrib.modeladmin',
    ]

```
   ### How to use

   ### A simple example

 Let’s say your website is for a local library. They have a model called `Book` that appears across the site in many places. You can define a normal Django model for it, then use ModelAdmin to create a menu in Wagtail’s admin to create, view, and edit `Book` entries.

 `models.py` looks like this:

 
```
    from django.db import models
    from wagtail.admin.panels import FieldPanel

    class Book(models.Model):
        title = models.CharField(max_length=255)
        author = models.CharField(max_length=255)
        cover_photo = models.ForeignKey(
            'wagtailimages.Image',
            null=True, blank=True,
            on_delete=models.SET_NULL,
            related_name='+'
        )

        panels = [
            FieldPanel('title'),
            FieldPanel('author'),
            FieldPanel('cover_photo')
        ]

```
  Note

 You can specify panels like `MultiFieldPanel` within the `panels` attribute of the model. This lets you use Wagtail-specific layouts in an otherwise traditional Django model.

  `wagtail_hooks.py` in your app directory would look something like this:

 
```
    from wagtail.contrib.modeladmin.options import (
        ModelAdmin, modeladmin_register)
    from .models import Book


    class BookAdmin(ModelAdmin):
        model = Book
        base_url_path = 'bookadmin' # customise the URL from default to admin/bookadmin
        menu_label = 'Book'  # ditch this to use verbose_name_plural from model
        menu_icon = 'pilcrow'  # change as required
        menu_order = 200  # will put in 3rd place (000 being 1st, 100 2nd)
        add_to_settings_menu = False  # or True to add your model to the Settings sub-menu
        exclude_from_explorer = False # or True to exclude pages of this type from Wagtail's explorer view
        add_to_admin_menu = True  # or False to exclude your model from the menu
        list_display = ('title', 'author')
        list_filter = ('author',)
        search_fields = ('title', 'author')

    # Now you just need to register your customised ModelAdmin class with Wagtail
    modeladmin_register(BookAdmin)

```
   ### A more complicated example

 In addition to `Book`, perhaps we also want to add `Author` and `Genre` models to our app and display a menu item for each of them, too. Creating lots of menus can add up quickly, so it might be a good idea to group related menus together. This section show you how to create one menu called *Library* which expands to show submenus for *Book*, *Author*, and *Genre*.

 Assume we’ve defined `Book`, `Author`, and `Genre` models in `models.py`.

 `wagtail_hooks.py` in your app directory would look something like this:

 
```

    from wagtail.contrib.modeladmin.options import (
        ModelAdmin, ModelAdminGroup, modeladmin_register)
    from .models import (
        Book, Author, Genre)


    class BookAdmin(ModelAdmin):
        model = Book
        menu_label = 'Book'  # ditch this to use verbose_name_plural from model
        menu_icon = 'pilcrow'  # change as required
        list_display = ('title', 'author')
        list_filter = ('genre', 'author')
        search_fields = ('title', 'author')


    class AuthorAdmin(ModelAdmin):
        model = Author
        menu_label = 'Author'  # ditch this to use verbose_name_plural from model
        menu_icon = 'user'  # change as required
        list_display = ('first_name', 'last_name')
        list_filter = ('first_name', 'last_name')
        search_fields = ('first_name', 'last_name')


    class GenreAdmin(ModelAdmin):
        model = Genre
        menu_label = 'Genre'  # ditch this to use verbose_name_plural from model
        menu_icon = 'group'  # change as required
        list_display = ('name',)
        list_filter = ('name',)
        search_fields = ('name',)


    class LibraryGroup(ModelAdminGroup):
        menu_label = 'Library'
        menu_icon = 'folder-open-inverse'  # change as required
        menu_order = 200  # will put in 3rd place (000 being 1st, 100 2nd)
        items = (BookAdmin, AuthorAdmin, GenreAdmin)

    # When using a ModelAdminGroup class to group several ModelAdmin classes together,
    # you only need to register the ModelAdminGroup class with Wagtail:
    modeladmin_register(LibraryGroup)

```
   ### Registering multiple classes in one `wagtail_hooks.py` file

 Each time you call `modeladmin_register(MyAdmin)` it creates a new top-level menu item in Wagtail’s left sidebar. You can call this multiple times within the same `wagtail_hooks.py` file if you want. The example below will create 3 top-level menus.

 
```

    class BookAdmin(ModelAdmin):
        model = Book
        ...

    class MovieAdmin(ModelAdmin):
        model = MovieModel
        ...

    class MusicAdminGroup(ModelAdminGroup):
        menu_label = _("Music")
        items = (AlbumAdmin, ArtistAdmin)
        ...

    modeladmin_register(BookAdmin)
    modeladmin_register(MovieAdmin)
    modeladmin_register(MusicAdminGroup)

```
  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/index.html>

Customising the menu item
=========================

 You can use the following attributes and methods on the `ModelAdmin` class to alter the menu item used to represent your model in Wagtail’s admin area.

 * [`ModelAdmin.menu_label`](#modeladmin-menu-label)
* [`ModelAdmin.menu_icon`](#modeladmin-menu-icon)
* [`ModelAdmin.menu_order`](#modeladmin-menu-order)
* [`ModelAdmin.add_to_settings_menu`](#modeladmin-add-to-settings-menu)
* [`ModelAdmin.add_to_admin_menu`](#modeladmin-add-to-admin-menu)
* [`ModelAdmin.menu_item_name`](#modeladmin-menu-item-name)

  `ModelAdmin.menu_label`
-----------------------

 **Expected value**: A string.

 Set this attribute to a string value to override the label used for the menu item that appears in Wagtail’s sidebar. If not set, the menu item will use `verbose_name_plural` from your model’s `Meta` data.

   `ModelAdmin.menu_icon`
----------------------

 **Expected value**: A string matching one of Wagtail’s icon class names.

 If you want to change the icon used to represent your model, you can set the `menu_icon` attribute on your class to use one of the other icons available in Wagtail’s CMS. The same icon will be used for the menu item in Wagtail’s sidebar, and will also appear in the header on the list page and other views for your model. If not set, `'doc-full-inverse'` will be used for page-type models, and `'snippet'` for others.

 If you’re using a `ModelAdminGroup` class to group together several `ModelAdmin` classes in their own sub-menu, and want to change the menu item used to represent the group, you should override the `menu_icon` attribute on your `ModelAdminGroup` class (`'folder-open-inverse'` is the default).

   `ModelAdmin.menu_order`
-----------------------

 **Expected value**: An integer between `1` and `999`.

 If you want to change the position of the menu item for your model (or group of models) in Wagtail’s sidebar, you do that by setting `menu_order`. The value should be an integer between `1` and `999`. The lower the value, the higher up the menu item will appear.

 Wagtail’s ‘Explorer’ menu item has an order value of `100`, so supply a value greater than that if you wish to keep the explorer menu item at the top.

   `ModelAdmin.add_to_settings_menu`
---------------------------------

 **Expected value**: `True` or `False`

 If you’d like the menu item for your model to appear in Wagtail’s ‘Settings’ sub-menu instead of at the top level, add `add_to_settings_menu = True` to your `ModelAdmin` class.

 This will only work for individual `ModelAdmin` classes registered with their own `modeladmin_register` call. It won’t work for members of a `ModelAdminGroup`.

   `ModelAdmin.add_to_admin_menu`
------------------------------

 **Expected value**: `True` or `False`

 If you’d like this model admin to be excluded from the menu, set to `False`.

   `ModelAdmin.menu_item_name`
---------------------------

 **Expected value**: A string or `None`

 Passed on as the `name` parameter when initialising the `MenuItem` for this class, becoming the `name` attribute value for that instance.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/menu_item.html>

Customising CreateView, EditView and DeleteView
===============================================

  Note

 **NOTE:** `modeladmin` only provides ‘create’, ‘edit’ and ‘delete’ functionality for non page type models (models that do not extend `wagtailcore.models.Page`). If your model is a ‘page type’ model, customising any of the following will not have any effect:

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/create_edit_delete_views.html>

Settings
========

 The `wagtail.contrib.settings` module allows you to define models that hold settings which are either common across all site records, or specific to each site.

 Settings are editable by administrators within the Wagtail admin, and can be accessed in code as well as in templates.

  Installation
------------

 Add `wagtail.contrib.settings` to your `INSTALLED_APPS`:

 
```
INSTALLED_APPS += [
    'wagtail.contrib.settings',
]

```
 **Note:** If you are using `settings` within templates, you will also need to update your `TEMPLATES` settings (discussed later in this page).

   Defining settings
-----------------

 Create a model that inherits from either:

 * `BaseGenericSetting` for generic settings across all sites
* `BaseSiteSetting` for site-specific settings

 and register it using the `register_setting` decorator:

 
```
from django.db import models
from wagtail.contrib.settings.models import (
    BaseGenericSetting,
    BaseSiteSetting,
    register_setting,
)

@register_setting
class GenericSocialMediaSettings(BaseGenericSetting):
    facebook = models.URLField()

@register_setting
class SiteSpecificSocialMediaSettings(BaseSiteSetting):
    facebook = models.URLField()

```
 Links to your settings will appear in the Wagtail admin ‘Settings’ menu.

   Edit handlers
-------------

 Settings use edit handlers much like the rest of Wagtail. Add a `panels` setting to your model defining all the edit handlers required:

 
```
@register_setting
class GenericImportantPages(BaseGenericSetting):
    donate_page = models.ForeignKey(
        'wagtailcore.Page', null=True, on_delete=models.SET_NULL, related_name='+'
    )
    sign_up_page = models.ForeignKey(
        'wagtailcore.Page', null=True, on_delete=models.SET_NULL, related_name='+'
    )

    panels = [
        FieldPanel('donate_page'),
        FieldPanel('sign_up_page'),
    ]

@register_setting
class SiteSpecificImportantPages(BaseSiteSetting):
    donate_page = models.ForeignKey(
        'wagtailcore.Page', null=True, on_delete=models.SET_NULL, related_name='+'
    )
    sign_up_page = models.ForeignKey(
        'wagtailcore.Page', null=True, on_delete=models.SET_NULL, related_name='+'
    )

    panels = [
        FieldPanel('donate_page'),
        FieldPanel('sign_up_page'),
    ]

```
 You can also customise the edit handlers [like you would do for `Page` model](../../advanced_topics/customisation/page_editing_interface#customising-the-tabbed-interface) with a custom `edit_handler` attribute:

 
```
from wagtail.admin.panels import TabbedInterface, ObjectList

@register_setting
class MySettings(BaseGenericSetting):
    # ...
    first_tab_panels = [
        FieldPanel('field_1'),
    ]
    second_tab_panels = [
        FieldPanel('field_2'),
    ]

    edit_handler = TabbedInterface([
        ObjectList(first_tab_panels, heading='First tab'),
        ObjectList(second_tab_panels, heading='Second tab'),
    ])

```
   Appearance
----------

 You can change the label used in the menu by changing the `verbose_name` of your model.

 You can add an icon to the menu by passing an `icon` argument to the `register_setting` decorator:

 
```
@register_setting(icon='placeholder')
class GenericSocialMediaSettings(BaseGenericSetting):
    ...
    class Meta:
        verbose_name = "Social media settings for all sites"

@register_setting(icon='placeholder')
class SiteSpecificSocialMediaSettings(BaseSiteSetting):
    ...
    class Meta:
        verbose_name = "Site-specific social media settings"

```
 For a list of all available icons, please see the [styleguide](../../contributing/styleguide#styleguide).

   Using the settings
------------------

 Settings can be used in both Python code and in templates.

  ### Using in Python

  #### Generic settings

 If you require access to a generic setting in a view, the `BaseGenericSetting.load()` method allows you to retrieve the generic settings:

 
```
def view(request):
    social_media_settings = GenericSocialMediaSettings.load(request_or_site=request)
    ...

```
   #### Site-specific settings

 If you require access to a site-specific setting in a view, the `BaseSiteSetting.for_request()` method allows you to retrieve the site-specific settings for the current request:

 
```
def view(request):
    social_media_settings = SiteSpecificSocialMediaSettings.for_request(request=request)
    ...

```
 In places where the request is unavailable, but you know the `Site` you wish to retrieve settings for, you can use `BaseSiteSetting.for_site` instead:

 
```
def view(request):
    social_media_settings = SiteSpecificSocialMediaSettings.for_site(site=user.origin_site)
    ...

```
    ### Using in Django templates

 Add the `wagtail.contrib.settings.context_processors.settings` context processor to your settings:

 
```
TEMPLATES = [
    {
        ...

        'OPTIONS': {
            'context_processors': [
                ...

                'wagtail.contrib.settings.context_processors.settings',
            ]
        }
    }
]

```
 Then access the generic settings through `{{ settings }}`:

 
```
{{ settings.app_label.GenericSocialMediaSettings.facebook }}
{{ settings.app_label.SiteSpecificSocialMediaSettings.facebook }}

```
 **Note:** Replace `app_label` with the label of the app containing your settings model.

 If you are not in a `RequestContext`, then context processors will not have run, and the `settings` variable will not be available. To get the `settings`, use the provided `{% get_settings %}` template tag.

 
```
{% load wagtailsettings_tags %}
{% get_settings %}
{{ settings.app_label.GenericSocialMediaSettings.facebook }}
{{ settings.app_label.SiteSpecificSocialMediaSettings.facebook }}

```
 By default, the tag will create or update a `settings` variable in the context. If you want to assign to a different context variable instead, use `{% get_settings as other_variable_name %}`:

 
```
{% load wagtailsettings_tags %}
{% get_settings as wagtail_settings %}
{{ wagtail_settings.app_label.GenericSocialMediaSettings.facebook }}
{{ wagtail_settings.app_label.SiteSpecificSocialMediaSettings.facebook }}

```
   ### Using in Jinja2 templates

 Add `wagtail.contrib.settings.jinja2tags.settings` extension to your Jinja2 settings:

 
```
TEMPLATES = [
    ...

    {
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        'APP_DIRS': True,
        'OPTIONS': {
            'extensions': [
                ...

                'wagtail.contrib.settings.jinja2tags.settings',
            ],
        },
    }
]

```
 Then access the settings through the `settings()` template function:

 
```
{{ settings("app_label.GenericSocialMediaSettings").facebook }}
{{ settings("app_label.SiteSpecificSocialMediaSettings").facebook }}

```
 **Note:** Replace `app_label` with the label of the app containing your settings model.

 If there is no `request` available in the template at all, you can use the settings for the default site instead:

 
```
{{ settings("app_label.GenericSocialMediaSettings", use_default_site=True).facebook }}
{{ settings("app_label.SiteSpecificSocialMediaSettings", use_default_site=True).facebook }}

```
 **Note:** You can not reliably get the correct settings instance for the current site from this template tag if the request object is not available. This is only relevant for multi-site instances of Wagtail.

 You can store the settings instance in a variable to save some typing, if you have to use multiple values from one model:

 
```
{% with generic_social_settings=settings("app_label.GenericSocialMediaSettings") %}
    Follow us on Twitter at @{{ generic_social_settings.facebook }},
    or Instagram at @{{ generic_social_settings.instagram }}.
{% endwith %}

{% with site_social_settings=settings("app_label.SiteSpecificSocialMediaSettings") %}
    Follow us on Twitter at @{{ site_social_settings.facebook }},
    or Instagram at @{{ site_social_settings.instagram }}.
{% endwith %}

```
 Or, alternately, using the `set` tag:

 
```
{% set generic_social_settings=settings("app_label.GenericSocialMediaSettings") %}
{% set site_social_settings=settings("app_label.SiteSpecificSocialMediaSettings") %}

```
    Utilising `select_related` to improve efficiency
------------------------------------------------

 For models with foreign key relationships to other objects (for example pages), which are very often needed to output values in templates, you can set the `select_related` attribute on your model to have Wagtail utilise Django’s [QuerySet.select\_related()](https://docs.djangoproject.com/en/stable/ref/models/querysets/#select-related) method to fetch the settings object and related objects in a single query. With this, the initial query is more complex, but you will be able to freely access the foreign key values without any additional queries, making things more efficient overall.

 Building on the `GenericImportantPages` example from the previous section, the following shows how `select_related` can be set to improve efficiency:

 
```
@register_setting
class GenericImportantPages(BaseGenericSetting):

    # Fetch these pages when looking up GenericImportantPages for or a site
    select_related = ["donate_page", "sign_up_page"]

    donate_page = models.ForeignKey(
        'wagtailcore.Page', null=True, on_delete=models.SET_NULL, related_name='+'
    )
    sign_up_page = models.ForeignKey(
        'wagtailcore.Page', null=True, on_delete=models.SET_NULL, related_name='+'
    )

    panels = [
        FieldPanel('donate_page'),
        FieldPanel('sign_up_page'),
    ]

```
 With these additions, the following template code will now trigger a single database query instead of three (one to fetch the settings, and two more to fetch each page):

 
```
{% load wagtailcore_tags %}
{% pageurl settings.app_label.GenericImportantPages.donate_page %}
{% pageurl settings.app_label.GenericImportantPages.sign_up_page %}

```
   Utilising the `page_url` setting shortcut
-----------------------------------------

 If, like in the previous section, your settings model references pages, and you often need to output the URLs of those pages in your project, you can likely use the setting model’s `page_url` shortcut to do that more cleanly. For example, instead of doing the following:

 
```
{% load wagtailcore_tags %}
{% pageurl settings.app_label.GenericImportantPages.donate_page %}
{% pageurl settings.app_label.GenericImportantPages.sign_up_page %}

```
 You could write:

 
```
{{ settings.app_label.GenericImportantPages.page_url.donate_page }}
{{ settings.app_label.GenericImportantPages.page_url.sign_up_page }}

```
 Using the `page_url` shortcut has a few of advantages over using the tag:

 1. The ‘specific’ page is automatically fetched to generate the URL, so you don’t have to worry about doing this (or forgetting to do this) yourself.
2. The results are cached, so if you need to access the same page URL in more than one place (for example in a form and in footer navigation), using the `page_url` shortcut will be more efficient.
3. It’s more concise, and the syntax is the same whether using it in templates or views (or other Python code), allowing you to write more consistent code.

 When using the `page_url` shortcut, there are a couple of points worth noting:

 1. The same limitations that apply to the `{% pageurl %}` tag apply to the shortcut: If the settings are accessed from a template context where the current request is not available, all URLs returned will include the site’s scheme/domain, and URL generation will not be quite as efficient.
2. If using the shortcut in views or other Python code, the method will raise an `AttributeError` if the attribute you request from `page_url` is not an attribute on the settings object.
3. If the settings object DOES have the attribute, but the attribute returns a value of `None` (or something that is not a `Page`), the shortcut will return an empty string.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/settings.html>

Sitemap generator
=================

 This document describes how to create XML sitemaps for your Wagtail website using the `wagtail.contrib.sitemaps` module.

  Note

 As of Wagtail 1.10 the Django contrib sitemap app is used to generate sitemaps. However since Wagtail requires the Site instance to be available during the sitemap generation you will have to use the views from the `wagtail.contrib.sitemaps.views` module instead of the views provided by Django (`django.contrib.sitemaps.views`).

 The usage of these views is otherwise identical, which means that customisation and caching of the sitemaps are done using the default Django patterns. See the Django documentation for in-depth information.

   Basic configuration
-------------------

 You firstly need to add `"django.contrib.sitemaps"` to INSTALLED\_APPS in your Django settings file:

 
```
INSTALLED_APPS = [
    ...

    "django.contrib.sitemaps",
]

```
 Then, in `urls.py`, you need to add a link to the `wagtail.contrib.sitemaps.views.sitemap` view which generates the sitemap:

 
```
from wagtail.contrib.sitemaps.views import sitemap

urlpatterns = [
    ...

    path('sitemap.xml', sitemap),

    ...

    # Ensure that the 'sitemap' line appears above the default Wagtail page serving route
    re_path(r'', include(wagtail_urls)),
]

```
 You should now be able to browse to `/sitemap.xml` and see the sitemap working. By default, all published pages in your website will be added to the site map.

   Setting the hostname
--------------------

 By default, the sitemap uses the hostname defined in the Wagtail Admin’s `Sites` area. If your default site is called `localhost`, then URLs in the sitemap will look like:

 
```
<url>
    <loc>http://localhost/about/</loc>
    <lastmod>2015-09-26</lastmod>
</url>

```
 For tools like Google Search Tools to properly index your site, you need to set a valid, crawlable hostname. If you change the site’s hostname from `localhost` to `mysite.com`, `sitemap.xml` will contain the correct URLs:

 
```
<url>
    <loc>http://mysite.com/about/</loc>
    <lastmod>2015-09-26</lastmod>
</url>

```
 If you change the site’s port to `443`, the `https` scheme will be used. Find out more about [working with Sites](../pages/model_reference#site-model-ref).

   Customising
-----------

  ### URLs

 The `Page` class defines a `get_sitemap_urls` method which you can override to customise sitemaps per `Page` instance. This method must accept a request object and return a list of dictionaries, one dictionary per URL entry in the sitemap. You can exclude pages from the sitemap by returning an empty list.

 Each dictionary can contain the following:

 * **location** (required) - This is the full URL path to add into the sitemap.
* **lastmod** - A python date or datetime set to when the page was last modified.
* **changefreq**
* **priority**

 You can add more but you will need to override the `sitemap.xml` template in order for them to be displayed in the sitemap.

    Serving multiple sitemaps
-------------------------

 If you want to support the sitemap indexes from Django then you will need to use the index view from `wagtail.contrib.sitemaps.views` instead of the index view from `django.contrib.sitemaps.views`. Please see the Django documentation for further details.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/sitemaps.html>

Frontend cache invalidator
==========================

 Many websites use a frontend cache such as Varnish, Squid, Cloudflare or CloudFront to gain extra performance. The downside of using a frontend cache though is that they don’t respond well to updating content and will often keep an old version of a page cached after it has been updated.

 This document describes how to configure Wagtail to purge old versions of pages from a frontend cache whenever a page gets updated.

  Setting it up
-------------

 Firstly, add `"wagtail.contrib.frontend_cache"` to your `INSTALLED_APPS`:

 
```
INSTALLED_APPS = [
    ...

    "wagtail.contrib.frontend_cache"
]

```
 The `wagtailfrontendcache` module provides a set of signal handlers which will automatically purge the cache whenever a page is published or deleted. These signal handlers are automatically registered when the `wagtail.contrib.frontend_cache` app is loaded.

  ### Varnish/Squid

 Add a new item into the `WAGTAILFRONTENDCACHE` setting and set the `BACKEND` parameter to `wagtail.contrib.frontend_cache.backends.HTTPBackend`. This backend requires an extra parameter `LOCATION` which points to where the cache is running (this must be a direct connection to the server and cannot go through another proxy).

 
```
# settings.py

WAGTAILFRONTENDCACHE = {
    'varnish': {
        'BACKEND': 'wagtail.contrib.frontend_cache.backends.HTTPBackend',
        'LOCATION': 'http://localhost:8000',
    },
}
WAGTAILFRONTENDCACHE_LANGUAGES = []

```
 Set `WAGTAILFRONTENDCACHE_LANGUAGES` to a list of languages (typically equal to `[l[0] for l in settings.LANGUAGES]`) to also purge the urls for each language of a purging url. This setting needs `settings.USE_I18N` to be `True` to work. Its default is an empty list.

 Finally, make sure you have configured your frontend cache to accept PURGE requests:

 * [Varnish](https://varnish-cache.org/docs/3.0/tutorial/purging.html)
* [Squid](https://wiki.squid-cache.org/SquidFaq/OperatingSquid#How_can_I_purge_an_object_from_my_cache.3F)

   ### Cloudflare

 Firstly, you need to register an account with Cloudflare if you haven’t already got one. You can do this here: [Cloudflare Sign up](https://dash.cloudflare.com/sign-up).

 Add an item into the `WAGTAILFRONTENDCACHE` and set the `BACKEND` parameter to `wagtail.contrib.frontend_cache.backends.CloudflareBackend`.

 This backend can be configured to use an account-wide API key, or an API token with restricted access.

 To use an account-wide API key, find the key [as described in the Cloudflare documentation](https://developers.cloudflare.com/api/get-started/keys/#view-your-api-key) and specify `EMAIL` and `API_KEY` parameters.

 To use a limited API token, [create a token](https://developers.cloudflare.com/api/get-started/create-token/) configured with the ‘Zone, Cache Purge’ permission and specify the `BEARER_TOKEN` parameter.

 A `ZONEID` parameter will need to be set for either option. To find the `ZONEID` for your domain, read the [Cloudflare API Documentation](https://developers.cloudflare.com/fundamentals/get-started/basic-tasks/find-account-and-zone-ids/).

 With an API key:

 
```
# settings.py

WAGTAILFRONTENDCACHE = {
    'cloudflare': {
        'BACKEND': 'wagtail.contrib.frontend_cache.backends.CloudflareBackend',
        'EMAIL': 'your-cloudflare-email-address@example.com',
        'API_KEY': 'your cloudflare api key',
        'ZONEID': 'your cloudflare domain zone id',
    },
}

```
 With an API token:

 
```
# settings.py

WAGTAILFRONTENDCACHE = {
    'cloudflare': {
        'BACKEND': 'wagtail.contrib.frontend_cache.backends.CloudflareBackend',
        'BEARER_TOKEN': 'your cloudflare bearer token',
        'ZONEID': 'your cloudflare domain zone id',
    },
}

```
   ### Amazon CloudFront

 Within Amazon Web Services you will need at least one CloudFront web distribution. If you don’t have one, you can get one here: [CloudFront getting started](https://aws.amazon.com/cloudfront/)

 Add an item into the `WAGTAILFRONTENDCACHE` and set the `BACKEND` parameter to `wagtail.contrib.frontend_cache.backends.CloudfrontBackend`. This backend requires one extra parameter, `DISTRIBUTION_ID` (your CloudFront generated distribution id).

 
```
WAGTAILFRONTENDCACHE = {
    'cloudfront': {
        'BACKEND': 'wagtail.contrib.frontend_cache.backends.CloudfrontBackend',
        'DISTRIBUTION_ID': 'your-distribution-id',
    },
}

```
 Configuration of credentials can done in multiple ways. You won’t need to store them in your Django settings file. You can read more about this here: [Boto 3 Docs](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html).

 In case you run multiple sites with Wagtail and each site has its CloudFront distribution, provide a mapping instead of a single distribution. Make sure the mapping matches with the hostnames provided in your site settings.

 
```
WAGTAILFRONTENDCACHE = {
    'cloudfront': {
        'BACKEND': 'wagtail.contrib.frontend_cache.backends.CloudfrontBackend',
        'DISTRIBUTION_ID': {
            'www.wagtail.org': 'your-distribution-id',
            'www.madewithwagtail.org': 'your-distribution-id',
        },
    },
}

```
  Note

 In most cases, absolute URLs with `www` prefixed domain names should be used in your mapping. Only drop the `www` prefix if you’re absolutely sure you’re not using it (for example a subdomain).

    ### Azure CDN

 With [Azure CDN](https://azure.microsoft.com/en-gb/products/cdn/) you will need a CDN profile with an endpoint configured.

 The third-party dependencies of this backend are:

 

| PyPI Package | Essential | Reason |
| --- | --- | --- |
| [`azure-mgmt-cdn`](https://pypi.org/project/azure-mgmt-cdn/) | Yes | Interacting with the CDN service. |
| [`azure-identity`](https://pypi.org/project/azure-identity/) | No | Obtaining credentials. It’s optional if you want to specify your own credential using a `CREDENTIALS` setting (more details below). |
| [`azure-mgmt-resource`](https://pypi.org/project/azure-mgmt-resource/) | No | For obtaining the subscription ID. Redundant if you want to explicitly specify a `SUBSCRIPTION_ID` setting (more details below). |

 Add an item into the `WAGTAILFRONTENDCACHE` and set the `BACKEND` parameter to `wagtail.contrib.frontend_cache.backends.AzureCdnBackend`. This backend requires the following settings to be set:

 * `RESOURCE_GROUP_NAME` - the resource group that your CDN profile is in.
* `CDN_PROFILE_NAME` - the profile name of the CDN service that you want to use.
* `CDN_ENDPOINT_NAME` - the name of the endpoint you want to be purged.

 
```
    WAGTAILFRONTENDCACHE = {
        'azure_cdn': {
            'BACKEND': 'wagtail.contrib.frontend_cache.backends.AzureCdnBackend',
            'RESOURCE_GROUP_NAME': 'MY-WAGTAIL-RESOURCE-GROUP',
            'CDN_PROFILE_NAME': 'wagtailio',
            'CDN_ENDPOINT_NAME': 'wagtailio-cdn-endpoint-123',
        },
    }

```
 By default the credentials will use `azure.identity.DefaultAzureCredential`. To modify the credential object used, please use `CREDENTIALS` setting. Read about your options on the [Azure documentation](https://learn.microsoft.com/en-us/azure/developer/python/sdk/authentication-overview).

 
```
from azure.common.credentials import ServicePrincipalCredentials

WAGTAILFRONTENDCACHE = {
    'azure_cdn': {
        'BACKEND': 'wagtail.contrib.frontend_cache.backends.AzureCdnBackend',
        'RESOURCE_GROUP_NAME': 'MY-WAGTAIL-RESOURCE-GROUP',
        'CDN_PROFILE_NAME': 'wagtailio',
        'CDN_ENDPOINT_NAME': 'wagtailio-cdn-endpoint-123',
        'CREDENTIALS': ServicePrincipalCredentials(
            client_id='your client id',
            secret='your client secret',
        )
    },
}

```
 Another option that can be set is `SUBSCRIPTION_ID`. By default the first encountered subscription will be used, but if your credential has access to more subscriptions, you should set this to an explicit value.

   ### Azure Front Door

 With [Azure Front Door](https://azure.microsoft.com/en-gb/products/frontdoor/) you will need a Front Door instance with caching enabled.

 The third-party dependencies of this backend are:

 

| PyPI Package | Essential | Reason |
| --- | --- | --- |
| [`azure-mgmt-frontdoor`](https://pypi.org/project/azure-mgmt-frontdoor/) | Yes | Interacting with the Front Door service. |
| [`azure-identity`](https://pypi.org/project/azure-identity/) | No | Obtaining credentials. It’s optional if you want to specify your own credential using a `CREDENTIALS` setting (more details below). |
| [`azure-mgmt-resource`](https://pypi.org/project/azure-mgmt-resource/) | No | For obtaining the subscription ID. Redundant if you want to explicitly specify a `SUBSCRIPTION_ID` setting (more details below). |

 Add an item into the `WAGTAILFRONTENDCACHE` and set the `BACKEND` parameter to `wagtail.contrib.frontend_cache.backends.AzureFrontDoorBackend`. This backend requires the following settings to be set:

 * `RESOURCE_GROUP_NAME` - the resource group that your Front Door instance is part of.
* `FRONT_DOOR_NAME` - your configured Front Door instance name.

 
```
WAGTAILFRONTENDCACHE = {
    'azure_front_door': {
        'BACKEND': 'wagtail.contrib.frontend_cache.backends.AzureFrontDoorBackend',
        'RESOURCE_GROUP_NAME': 'MY-WAGTAIL-RESOURCE-GROUP',
        'FRONT_DOOR_NAME': 'wagtail-io-front-door',
    },
}

```
 By default the credentials will use `azure.identity.DefaultAzureCredential`. To modify the credential object used, please use `CREDENTIALS` setting. Read about your options on the [Azure documentation](https://learn.microsoft.com/en-us/azure/developer/python/sdk/authentication-overview).

 
```
from azure.common.credentials import ServicePrincipalCredentials

WAGTAILFRONTENDCACHE = {
    'azure_front_door': {
        'BACKEND': 'wagtail.contrib.frontend_cache.backends.AzureFrontDoorBackend',
        'RESOURCE_GROUP_NAME': 'MY-WAGTAIL-RESOURCE-GROUP',
        'FRONT_DOOR_NAME': 'wagtail-io-front-door',
        'CREDENTIALS': ServicePrincipalCredentials(
            client_id='your client id',
            secret='your client secret',
        )
    },
}

```
 Another option that can be set is `SUBSCRIPTION_ID`. By default the first encountered subscription will be used, but if your credential has access to more subscriptions, you should set this to an explicit value.

    Advanced usage
--------------

  ### Invalidating more than one URL per page

 By default, Wagtail will only purge one URL per page. If your page has more than one URL to be purged, you will need to override the `get_cached_paths` method on your page type.

 
```
class BlogIndexPage(Page):
    def get_blog_items(self):
        # This returns a Django paginator of blog items in this section
        return Paginator(self.get_children().live().type(BlogPage), 10)

    def get_cached_paths(self):
        # Yield the main URL
        yield '/'

        # Yield one URL per page in the paginator to make sure all pages are purged
        for page_number in range(1, self.get_blog_items().num_pages + 1):
            yield '/?page=' + str(page_number)

```
   ### Invalidating index pages

 Pages that list other pages (such as a blog index) may need to be purged as well so any changes to a blog page are also reflected on the index (for example, a blog post was added, deleted or its title/thumbnail was changed).

 To purge these pages, we need to write a signal handler that listens for Wagtail’s `page_published` and `page_unpublished` signals for blog pages (note, `page_published` is called both when a page is created and updated). This signal handler would trigger the invalidation of the index page using the `PurgeBatch` class which is used to construct and dispatch invalidation requests.

 
```
# models.py
from django.dispatch import receiver
from django.db.models.signals import pre_delete

from wagtail.signals import page_published
from wagtail.contrib.frontend_cache.utils import PurgeBatch

...

def blog_page_changed(blog_page):
    # Find all the live BlogIndexPages that contain this blog_page
    batch = PurgeBatch()
    for blog_index in BlogIndexPage.objects.live():
        if blog_page in blog_index.get_blog_items().object_list:
            batch.add_page(blog_index)

    # Purge all the blog indexes we found in a single request
    batch.purge()


@receiver(page_published, sender=BlogPage)
def blog_published_handler(instance, **kwargs):
    blog_page_changed(instance)


@receiver(pre_delete, sender=BlogPage)
def blog_deleted_handler(instance, **kwargs):
    blog_page_changed(instance)

```
   ### Invalidating URLs

 The `PurgeBatch` class provides a `.add_url(url)` and a `.add_urls(urls)` for adding individual URLs to the purge batch.

 For example, this could be useful for purging a single page on a blog index:

 
```
from wagtail.contrib.frontend_cache.utils import PurgeBatch

# Purge the first page of the blog index
batch = PurgeBatch()
batch.add_url(blog_index.url + '?page=1')
batch.purge()

```
   ### The `PurgeBatch` class

 All of the methods available on `PurgeBatch` are listed below:

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/frontendcache.html>

RoutablePageMixin
=================

 The `RoutablePageMixin` mixin provides a convenient way for a page to respond on multiple sub-URLs with different views. For example, a blog section on a site might provide several different types of index page at URLs like `/blog/2013/06/`, `/blog/authors/bob/`, `/blog/tagged/python/`, all served by the same page instance.

 A `Page` using `RoutablePageMixin` exists within the page tree like any other page, but URL paths underneath it are checked against a list of patterns. If none of the patterns match, control is passed to subpages as usual (or failing that, a 404 error is thrown).

 By default a route for `r'^$'` exists, which serves the content exactly like a normal `Page` would. It can be overridden by using `@re_path(r'^$')` or `@path('')` on any other method of the inheriting class.

  Installation
------------

 Add `"wagtail.contrib.routable_page"` to your `INSTALLED_APPS`:

 
```
INSTALLED_APPS = [
    ...

    "wagtail.contrib.routable_page",
]

```
   The basics
----------

 To use `RoutablePageMixin`, you need to make your class inherit from both :class:`wagtail.contrib.routable_page.models.RoutablePageMixin` and [`wagtail.models.Page`](../pages/model_reference#wagtail.models.Page "wagtail.models.Page"), then define some view methods and decorate them with `path` or `re_path`.

 These view methods behave like ordinary Django view functions, and must return an `HttpResponse` object; typically this is done through a call to `django.shortcuts.render`.

 The `path` and `re_path` decorators from `wagtail.contrib.routable_page.models.path` are similar to [the Django `django.urls` `path` and `re_path` functions](https://docs.djangoproject.com/en/stable/topics/http/urls/ "(in Django v4.1)"). The former allows the use of plain paths and converters while the latter lets you specify your URL patterns as regular expressions.

 Here’s an example of an `EventIndexPage` with three views, assuming that an `EventPage` model with an `event_date` field has been defined elsewhere:

 
```
import datetime
from django.http import JsonResponse
from wagtail.fields import RichTextField
from wagtail.models import Page
from wagtail.contrib.routable_page.models import RoutablePageMixin, path


class EventIndexPage(RoutablePageMixin, Page):

    # Routable pages can have fields like any other - here we would
    # render the intro text on a template with {{ page.intro|richtext }}
    intro = RichTextField()

    @path('') # will override the default Page serving mechanism
    def current_events(self, request):
        """
        View function for the current events page
        """
        events = EventPage.objects.live().filter(event_date__gte=datetime.date.today())

        # NOTE: We can use the RoutablePageMixin.render() method to render
        # the page as normal, but with some of the context values overridden
        return self.render(request, context_overrides={
            'title': "Current events",
            'events': events,
        })

    @path('past/')
    def past_events(self, request):
        """
        View function for the past events page
        """
        events = EventPage.objects.live().filter(event_date__lt=datetime.date.today())

        # NOTE: We are overriding the template here, as well as few context values
        return self.render(
            request,
            context_overrides={
                'title': "Past events",
                'events': events,
            },
            template="events/event_index_historical.html",
        )

    # Multiple routes!
    @path('year/<int:year>/')
    @path('year/current/')
    def events_for_year(self, request, year=None):
        """
        View function for the events for year page
        """
        if year is None:
            year = datetime.date.today().year

        events = EventPage.objects.live().filter(event_date__year=year)

        return self.render(request, context_overrides={
            'title': "Events for %d" % year,
            'events': events,
        })

    @re_path(r'^year/(\d+)/count/$')
    def count_for_year(self, request, year=None):
        """
        View function that returns a simple JSON response that
        includes the number of events scheduled for a specific year
        """
        events = EventPage.objects.live().filter(event_date__year=year)

        # NOTE: The usual template/context rendering process is irrelevant
        # here, so we'll just return a HttpResponse directly
        return JsonResponse({'count': events.count()})

```
  ### Rendering other pages

 Another way of returning an `HttpResponse` is to call the `serve` method of another page. (Calling a page’s own `serve` method within a view method is not valid, as the view method is already being called within `serve`, and this would create a circular definition).

 For example, `EventIndexPage` could be extended with a `next/` route that displays the page for the next event:

 
```
@path('next/')
def next_event(self, request):
    """
    Display the page for the next event
    """
    future_events = EventPage.objects.live().filter(event_date__gt=datetime.date.today())
    next_event = future_events.order_by('event_date').first()

    return next_event.serve(request)

```
   ### Reversing URLs

 [`RoutablePageMixin`](#wagtail.contrib.routable_page.models.RoutablePageMixin "wagtail.contrib.routable_page.models.RoutablePageMixin") adds a [`reverse_subpage()`](#wagtail.contrib.routable_page.models.RoutablePageMixin.reverse_subpage "wagtail.contrib.routable_page.models.RoutablePageMixin.reverse_subpage") method to your page model which you can use for reversing URLs. For example:

 
```
    # The URL name defaults to the view method name.
    >>> event_page.reverse_subpage('events_for_year', args=(2015, ))
    'year/2015/'

```
 This method only returns the part of the URL within the page. To get the full URL, you must append it to the values of either the `url` or the [`full_url`](../pages/model_reference#wagtail.models.Page.full_url "wagtail.models.Page.full_url") attribute on your page:

 
```
>>> event_page.url + event_page.reverse_subpage('events_for_year', args=(2015, ))
'/events/year/2015/'

>>> event_page.full_url + event_page.reverse_subpage('events_for_year', args=(2015, ))
'http://example.com/events/year/2015/'

```
   ### Changing route names

 The route name defaults to the name of the view. You can override this name with the `name` keyword argument on `@path` or `re_path`:

 
```
from wagtail.models import Page
from wagtail.contrib.routable_page.models import RoutablePageMixin, route


class EventPage(RoutablePageMixin, Page):
    ...

    @re_path(r'^year/(\d+)/$', name='year')
    def events_for_year(self, request, year):
        """
        View function for the events for year page
        """
        ...

```
 
```
>>> event_page.url + event_page.reverse_subpage('year', args=(2015, ))
'/events/year/2015/'

```
    The `RoutablePageMixin` class
-----------------------------

 Example:

 
```
url = page.url + page.reverse_subpage('events_for_year', kwargs={'year': '2014'})

```
   The `routablepageurl` template tag
----------------------------------

 Example:

 
```
{% load wagtailroutablepage_tags %}

{% routablepageurl page "feed" %}
{% routablepageurl page "archive" 2014 08 14 %}
{% routablepageurl page "food" foo="bar" baz="quux" %}

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/routablepage.html>

modeladmin customisation primer
===============================

 The `modeladmin` app is designed to offer you as much flexibility as possible in how your model and its objects are represented in Wagtail’s CMS. This page aims to provide you with some background information to help you gain a better understanding of what the app can do, and to point you in the right direction, depending on the kind of customisations you’re looking to make.

 * [Wagtail’s `ModelAdmin` class isn’t the same as Django’s](#wagtail-s-modeladmin-class-isn-t-the-same-as-django-s)
* [Changing what appears in the listing](#changing-what-appears-in-the-listing)
* [Adding additional stylesheets and/or JavaScript](#adding-additional-stylesheets-and-or-javascript)
* [Overriding templates](#overriding-templates)
* [Overriding views](#overriding-views)
* [Overriding helper classes](#overriding-helper-classes)

  Wagtail’s `ModelAdmin` class isn’t the same as Django’s
-------------------------------------------------------

 Wagtail’s `ModelAdmin` class is designed to be used in a similar way to Django’s class of the same name, and it often uses the same attribute and method names to achieve similar things. However, there are a few key differences:

  ### Add & edit forms are still defined by `panels` and `edit_handlers`

 In Wagtail, controlling which fields appear in add/edit forms for your `Model`, and defining how they are grouped and ordered, is achieved by adding a `panels` attribute, or `edit_handler` to your `Model` class. This remains the same whether your model is a `Page` type, a snippet, or just a standard Django `Model`. Because of this, Wagtail’s `ModelAdmin` class is mostly concerned with ‘listing’ configuration. For example, `list_display`, `list_filter` and `search_fields` attributes are present and support largely the same values as Django’s ModelAdmin class, while `fields`, `fieldsets`, `exclude` and other attributes you may be used to using to configure Django’s add/edit views, simply aren’t supported by Wagtail’s version.

   ### ‘Page type’ models need to be treated differently from other models

 While `modeladmin`’s listing view and it’s supported customisation options work in exactly the same way for all types of `Model`, when it comes to the other management views, the treatment differs depending on whether your ModelAdmin class is representing a page type model (that extends `wagtailcore.models.Page`) or not.

 Pages in Wagtail have some unique properties, and require additional views, interface elements and general treatment in order to be managed effectively. For example, they have a tree structure that must be preserved properly as pages are added, deleted and moved around. They also have a revisions system, their own permission considerations, and the facility to preview changes before saving changes. Because of this added complexity, Wagtail provides its own specific views for managing any custom page types you might add to your project (whether you create a `ModelAdmin` class for them or not).

 In order to deliver a consistent user experience, `modeladmin` simply redirects users to Wagtail’s existing page management views wherever possible. You should bear this in mind if you ever find yourself wanting to change what happens when pages of a certain type are added, deleted, published, or have some other action applied to them. Customising the `CreateView` or `EditView` for your page type `Model` (even if just to add an additional stylesheet or JavaScript), simply won’t have any effect, as those views are not used.

 If you do find yourself needing to customise the add, edit or other behaviour for a page type model, you should take a look at the following part of the documentation: [Hooks](../../hooks#admin-hooks).

   ### Wagtail’s `ModelAdmin` class is ‘modular’

 Unlike Django’s class of the same name, wagtailadmin’s `ModelAdmin` acts primarily as a ‘controller’ class. While it does have a set of attributes and methods to enable you to configure how various components should treat your model, it has been deliberately designed to do as little work as possible by itself; it designates all of the real work to a set of separate, swappable components.

 The theory is: If you want to do something differently, or add some functionality that `modeladmin` doesn’t already have, you can create new classes (or extend the ones provided by `modeladmin`) and easily configure your `ModelAdmin` class to use them instead of the defaults.

 * Learn more about [Overriding views](#modeladmin-overriding-views)
* Learn more about [Overriding helper classes](#modeladmin-overriding-helper-classes)

    Changing what appears in the listing
------------------------------------

 You should familiarise yourself with the attributes and methods supported by the `ModelAdmin` class, that allow you to change what is displayed in the `IndexView`. The following page should give you everything you need to get going: [Customising IndexView - the listing view](indexview)

   Adding additional stylesheets and/or JavaScript
-----------------------------------------------

 The `ModelAdmin` class provides several attributes to enable you to easily add additional stylesheets and JavaScript to the admin interface for your model. Each attribute simply needs to be a list of paths to the files you want to include. If the path is for a file in your project’s static directory, then Wagtail will automatically prepend the path with `STATIC_URL` so that you don’t need to repeat it each time in your list of paths.

 If you’d like to add styles or scripts to the `IndexView`, you should set the following attributes:

 * `index_view_extra_css` - Where each item is the path name of a pre-compiled stylesheet that you’d like to include.
* `index_view_extra_js` - Where each item is the path name of a JavaScript file that you’d like to include.

 If you’d like to do the same for `CreateView` and `EditView`, you should set the following attributes:

 * `form_view_extra_css` - Where each item is the path name of a pre-compiled stylesheet that you’d like to include.
* `form_view_extra_js` - Where each item is the path name of a JavaScript file that you’d like to include.

 And if you’re using the `InspectView` for your model, and want to do the same for that view, you should set the following attributes:

 * `inspect_view_extra_css` - Where each item is the path name of a pre-compiled stylesheet that you’d like to include.
* `inspect_view_extra_js` - Where each item is the path name of a JavaScript file that you’d like to include.

   Overriding templates
--------------------

 For all modeladmin views, Wagtail looks for templates in the following folders within your project or app, before resorting to the defaults:

 1. `templates/modeladmin/app-name/model-name/`
2. `templates/modeladmin/app-name/`
3. `templates/modeladmin/`

 So, to override the template used by `IndexView` for example, you’d create a new `index.html` template and put it in one of those locations. For example, if you wanted to do this for an `ArticlePage` model in a `news` app, you’d add your custom template as `news/templates/modeladmin/news/articlepage/index.html`.

 For reference, `modeladmin` looks for templates with the following names for each view:

 * `'index.html'` for `IndexView`
* `'inspect.html'` for `InspectView`
* `'create.html'` for `CreateView`
* `'edit.html'` for `EditView`
* `'delete.html'` for `DeleteView`
* `'choose_parent.html'` for `ChooseParentView`

 To add extra information to a block within one of the above Wagtail templates, use Django’s `{{ block.super }}` within the `{% block ... %}` that you wish to extend. For example, if you wish to display an image in an edit form below the fields of the model that is being edited, you could do the following:

 
```
    {% extends "modeladmin/edit.html" %}
    {% load static %}

    {% block content %}
        {{ block.super }}
        <div>
            <img src="{% get_media_prefix %}{{ instance.image }}"/>
        </div>
    {% endblock %}

```
 If for any reason you’d rather bypass the above behaviour and explicitly specify a template for a specific view, you can set either of the following attributes on your `ModelAdmin` class:

 * `index_template_name` to specify a template for `IndexView`
* `inspect_template_name` to specify a template for `InspectView`
* `create_template_name` to specify a template for `CreateView`
* `edit_template_name` to specify a template for `EditView`
* `delete_template_name` to specify a template for `DeleteView`
* `choose_parent_template_name` to specify a template for `ChooseParentView`

   Overriding views
----------------

 For all of the views offered by `ModelAdmin`, the class provides an attribute that you can override in order to tell it which class you’d like to use:

 * `index_view_class`
* `inspect_view_class`
* `create_view_class` (not used for ‘page type’ models)
* `edit_view_class` (not used for ‘page type’ models)
* `delete_view_class` (not used for ‘page type’ models)
* `choose_parent_view_class` (only used for ‘page type’ models)

 For example, if you’d like to create your own view class and use it for the `IndexView`, you would do the following:

 
```
    from wagtail.contrib.modeladmin.views import IndexView
    from wagtail.contrib.modeladmin.options import ModelAdmin
    from .models import MyModel

    class MyCustomIndexView(IndexView):
        # New functionality and existing method overrides added here
        ...


    class MyModelAdmin(ModelAdmin):
        model = MyModel
        index_view_class = MyCustomIndexView

```
 Or, if you have no need for any of `IndexView`’s existing functionality in your view and would rather create your own view from scratch, `modeladmin` will support that too. However, it’s highly recommended that you use `modeladmin.views.WMABaseView` as a base for your view. It’ll make integrating with your `ModelAdmin` class much easier and will provide a bunch of useful attributes and methods to get you started.

 You can also use the url\_helper to easily reverse URLs for any ModelAdmin see [Reversing ModelAdmin URLs](tips_and_tricks/reversing_urls#modeladmin-reversing-urls).

   Overriding helper classes
-------------------------

 While ‘view classes’ are responsible for a lot of the work, there are also a number of other tasks that `modeladmin` must do regularly, that need to be handled in a consistent way, and in a number of different places. These tasks are designated to a set of simple classes (in `modeladmin`, these are termed ‘helper’ classes) and can be found in `wagtail.contrib.modeladmin.helpers`.

 If you ever intend to write and use your own custom views with `modeladmin`, you should familiarise yourself with these helpers, as they are made available to views via the `modeladmin.views.WMABaseView` view.

 There are three types of ‘helper class’:

 * **URL helpers** - That help with the consistent generation, naming and referencing of urls.
* **Permission helpers** - That help with ensuring only users with sufficient permissions can perform certain actions, or see options to perform those actions.
* **Button helpers** - That, with the help of the other two, helps with the generation of buttons for use in a number of places.

 The `ModelAdmin` class allows you to define and use your own helper classes by setting values on the following attributes:

  ### `ModelAdmin.url_helper_class`

 By default, the `modeladmin.helpers.url.PageAdminURLHelper` class is used when your model extends `wagtailcore.models.Page`, otherwise `modeladmin.helpers.url.AdminURLHelper` is used.

 If you find that the above helper classes don’t work for your needs, you can easily create your own helper class by sub-classing `AdminURLHelper` or `PageAdminURLHelper` (if your model extends Wagtail’s `Page` model), and making any necessary additions/overrides.

 Once your class is defined, set the `url_helper_class` attribute on your `ModelAdmin` class to use your custom URLHelper, like so:

 
```
    from wagtail.contrib.modeladmin.helpers import AdminURLHelper
    from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
    from .models import MyModel


    class MyURLHelper(AdminURLHelper):
        ...


    class MyModelAdmin(ModelAdmin):
        model = MyModel
        url_helper_class = MyURLHelper

    modeladmin_register(MyModelAdmin)

```
 Or, if you have a more complicated use case, where simply setting that attribute isn’t possible (due to circular imports, for example) or doesn’t meet your needs, you can override the `get_url_helper_class` method, like so:

 
```
    class MyModelAdmin(ModelAdmin):
        model = MyModel

        def get_url_helper_class(self):
            if self.some_attribute is True:
                return MyURLHelper
            return AdminURLHelper

```
   ### `ModelAdmin.permission_helper_class`

 By default, the `modeladmin.helpers.permission.PagePermissionHelper` class is used when your model extends `wagtailcore.models.Page`, otherwise `modeladmin.helpers.permission.PermissionHelper` is used.

 If you find that the above helper classes don’t work for your needs, you can easily create your own helper class, by sub-classing `PermissionHelper` (or `PagePermissionHelper` if your model extends Wagtail’s `Page` model), and making any necessary additions/overrides. Once defined, you set the `permission_helper_class` attribute on your `ModelAdmin` class to use your custom class instead of the default, like so:

 
```
    from wagtail.contrib.modeladmin.helpers import PermissionHelper
    from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
    from .models import MyModel


    class MyPermissionHelper(PermissionHelper):
        ...


    class MyModelAdmin(ModelAdmin):
        model = MyModel
        permission_helper_class = MyPermissionHelper

    modeladmin_register(MyModelAdmin)

```
 Or, if you have a more complicated use case, where simply setting an attribute isn’t possible or doesn’t meet your needs, you can override the `get_permission_helper_class` method, like so:

 
```
    class MyModelAdmin(ModelAdmin):
        model = MyModel

        def get_permission_helper_class(self):
            if self.some_attribute is True:
                return MyPermissionHelper
            return PermissionHelper

```
   ### `ModelAdmin.button_helper_class`

 By default, the `modeladmin.helpers.button.PageButtonHelper` class is used when your model extends `wagtailcore.models.Page`, otherwise `modeladmin.helpers.button.ButtonHelper` is used.

 If you wish to add or change buttons for your model’s IndexView, you’ll need to create your own button helper class by sub-classing `ButtonHelper` or `PageButtonHelper` (if your model extend’s Wagtail’s `Page` model), and make any necessary additions/overrides. Once defined, you set the `button_helper_class` attribute on your `ModelAdmin` class to use your custom class instead of the default, like so:

 
```
    from wagtail.contrib.modeladmin.helpers import ButtonHelper
    from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
    from .models import MyModel


    class MyButtonHelper(ButtonHelper):
        def add_button(self, classnames_add=None, classnames_exclude=None):
            if classnames_add is None:
                classnames_add = []
            if classnames_exclude is None:
                classnames_exclude = []
            classnames = self.add_button_classnames + classnames_add
            cn = self.finalise_classname(classnames, classnames_exclude)
            return {
                'url': self.url_helper.create_url,
                'label': _('Add %s') % self.verbose_name,
                'classname': cn,
                'title': _('Add a new %s') % self.verbose_name,
            }

        def inspect_button(self, pk, classnames_add=None, classnames_exclude=None):
            ...

        def edit_button(self, pk, classnames_add=None, classnames_exclude=None):
            ...

        def delete_button(self, pk, classnames_add=None, classnames_exclude=None):
            ...


    class MyModelAdmin(ModelAdmin):
        model = MyModel
        button_helper_class = MyButtonHelper

    modeladmin_register(MyModelAdmin)

```
 To customise the buttons found in the ModelAdmin List View you can change the returned dictionary in the `add_button`, `delete_button`, `edit_button` or `inspect_button` methods. For example if you wanted to change the `Delete` button you could modify the `delete_button` method in your `ButtonHelper` like so:

 
```
    class MyButtonHelper(ButtonHelper):
        ...
        def delete_button(self, pk, classnames_add=None, classnames_exclude=None):
            ...
            return {
                'url': reverse("your_custom_url"),
                'label': _('Delete'),
                'classname': "custom-css-class",
                'title': _('Delete this item')
            }

```
 Or, if you have a more complicated use case, where simply setting an attribute isn’t possible or doesn’t meet your needs, you can override the `get_button_helper_class` method, like so:

 
```
    class MyModelAdmin(ModelAdmin):
        model = MyModel

        def get_button_helper_class(self):
            if self.some_attribute is True:
                return MyButtonHelper
            return ButtonHelper

```
   ### Using helpers in your custom views

 As long as you sub-class `modeladmin.views.WMABaseView` (or one of the more ‘specific’ view classes) to create your custom view, instances of each helper should be available on instances of your class as:

 * `self.url_helper`
* `self.permission_helper`
* `self.button_helper`

 Unlike the other two, `self.button_helper` isn’t populated right away when the view is instantiated. In order to show the right buttons for the right users, ButtonHelper instances need to be ‘request aware’, so `self.button_helper` is only set once the view’s `dispatch()` method has run, which takes a `HttpRequest` object as an argument, from which the current user can be identified.

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/primer.html>

Customising the base URL path
=============================

 You can use the following attributes and methods on the `ModelAdmin` class to alter the base URL path used to represent your model in Wagtail’s admin area.

  `ModelAdmin.base_url_path`
--------------------------

 **Expected value**: A string.

 Set this attribute to a string value to override the default base URL path used for the model to `admin/{base_url_path}`. If not set, the base URL path will be `admin/{app_label}/{model_name}`.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/base_url.html>

Customising IndexView - the listing view
========================================

 For the sake of consistency, this section of the docs will refer to the listing view as `IndexView`, because that is the view class that does all the heavy lifting.

 You can use the following attributes and methods on the `ModelAdmin` class to alter how your model data is treated and represented by the `IndexView`.

 * [`ModelAdmin.list_display`](#modeladmin-list-display)
* [`ModelAdmin.list_export`](#modeladmin-list-export)
* [`ModelAdmin.list_filter`](#modeladmin-list-filter)
* [`ModelAdmin.export_filename`](#modeladmin-export-filename)
* [`ModelAdmin.search_fields`](#modeladmin-search-fields)
* [`ModelAdmin.search_handler_class`](#modeladmin-search-handler-class)
* [`ModelAdmin.extra_search_kwargs`](#modeladmin-extra-search-kwargs)
* [`ModelAdmin.ordering`](#modeladmin-ordering)
* [`ModelAdmin.list_per_page`](#modeladmin-list-per-page)
* [`ModelAdmin.get_queryset()`](#modeladmin-get-queryset)
* [`ModelAdmin.get_extra_attrs_for_row()`](#modeladmin-get-extra-attrs-for-row)
* [`ModelAdmin.get_extra_class_names_for_field_col()`](#modeladmin-get-extra-class-names-for-field-col)
* [`ModelAdmin.get_extra_attrs_for_field_col()`](#modeladmin-get-extra-attrs-for-field-col)
* [`wagtail.contrib.modeladmin.mixins.ThumbnailMixin`](#wagtail-contrib-modeladmin-mixins-thumbnailmixin)
* [`ModelAdmin.list_display_add_buttons`](#modeladmin-list-display-add-buttons)
* [`ModelAdmin.index_view_extra_css`](#modeladmin-index-view-extra-css)
* [`ModelAdmin.index_view_extra_js`](#modeladmin-index-view-extra-js)
* [`ModelAdmin.index_template_name`](#modeladmin-index-template-name)
* [`ModelAdmin.index_view_class`](#modeladmin-index-view-class)

  `ModelAdmin.list_display`
-------------------------

 **Expected value**: A list or tuple, where each item is the name of a field or single-argument callable on your model, or a similarly simple method defined on the `ModelAdmin` class itself.

 Default value: `('__str__',)`

 Set `list_display` to control which fields are displayed in the `IndexView` for your model.

 You have three possible values that can be used in `list_display`:

 * A field of the model. For example:

 
```
    from wagtail.contrib.modeladmin.options import ModelAdmin
    from .models import Person

    class PersonAdmin(ModelAdmin):
        model = Person
        list_display = ('first_name', 'last_name')

```
* The name of a custom method on your `ModelAdmin` class, that accepts a single parameter for the model instance. For example:

 
```
    from wagtail.contrib.modeladmin.options import ModelAdmin
    from .models import Person


    class PersonAdmin(ModelAdmin):
        model = Person
        list_display = ('upper_case_name',)

        def upper_case_name(self, obj):
            return ("%s %s" % (obj.first_name, obj.last_name)).upper()
        upper_case_name.short_description = 'Name'

```
* The name of a method on your `Model` class that accepts only `self` as an argument. For example:

 
```
    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class Person(models.Model):
        name = models.CharField(max_length=50)
        birthday = models.DateField()

        def decade_born_in(self):
            return self.birthday.strftime('%Y')[:3] + "0's"
        decade_born_in.short_description = 'Birth decade'


    class PersonAdmin(ModelAdmin):
        model = Person
        list_display = ('name', 'decade_born_in')

```

 A few special cases to note about `list_display`:

 * If the field is a `ForeignKey`, Django will display the output of `__str__()` of the related object.
* If the string provided is a method of the model or `ModelAdmin` class, Django will HTML-escape the output by default. To escape user input and allow your own unescaped tags, use `format_html()`. For example:

 
```
    from django.db import models
    from django.utils.html import format_html
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class Person(models.Model):
        first_name = models.CharField(max_length=50)
        last_name = models.CharField(max_length=50)
        color_code = models.CharField(max_length=6)

        def styled_name(self):
            return format_html(
                '<span style="color: #{};">{} {}</span>',
                self.color_code,
                self.first_name,
                self.last_name,
            )


    class PersonAdmin(ModelAdmin):
        model = Person
        list_display = ('first_name', 'last_name', 'styled_name')

```
* If the value of a field is `None`, an empty string, or an iterable without elements, Wagtail will display a dash (-) for that column. You can override this by setting `empty_value_display` on your `ModelAdmin` class. For example:

 
```
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class PersonAdmin(ModelAdmin):
        empty_value_display = 'N/A'
        ...

```
 Or, if you’d like to change the value used depending on the field, you can override `ModelAdmin`’s `get_empty_value_display()` method, like so:

 
```
    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin


    class Person(models.Model):
        name = models.CharField(max_length=100)
        nickname = models.CharField(blank=True, max_length=100)
        likes_cat_gifs = models.NullBooleanField()


    class PersonAdmin(ModelAdmin):
        model = Person
        list_display = ('name', 'nickname', 'likes_cat_gifs')

        def get_empty_value_display(self, field_name=None):
            if field_name == 'nickname':
                return 'None given'
            if field_name == 'likes_cat_gifs':
                return 'Unanswered'
            return super().get_empty_value_display(field_name)

```
 The `__str__()` method is just as valid in `list_display` as any other model method, so it’s perfectly OK to do this:

 
```
    list_display = ('__str__', 'some_other_field')

```
 By default, the ability to sort results by an item in `list_display` is only offered when it’s a field that has an actual database value (because sorting is done at the database level). However, if the output of the method is representative of a database field, you can indicate this fact by setting the `admin_order_field` attribute on that method, like so:

 
```
    from django.db import models
    from django.utils.html import format_html
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class Person(models.Model):
        first_name = models.CharField(max_length=50)
        last_name = models.CharField(max_length=50)
        color_code = models.CharField(max_length=6)

        def styled_first_name(self):
            return format_html(
                '<span style="color: #{};">{}</span>',
                self.color_code,
                self.first_name,
            )
        styled_first_name.admin_order_field = 'first_name'


    class PersonAdmin(ModelAdmin):
        model = Person
        list_display = ('styled_first_name', 'last_name')

```
 The above will tell Wagtail to order by the `first_name` field when trying to sort by `styled_first_name` in the index view.

 The above will tell Wagtail to order by the `first_name` field when trying to sort by `styled_first_name` in the index view.

 To indicate descending order with `admin_order_field` you can use a hyphen prefix on the field name. Using the above example, this would look like:

 .. code-block:: python

 
```
styled_first_name.admin_order_field = '-first_name'

```
 `admin_order_field` supports query lookups to sort by values on related models, too. This example includes an “author first name” column in the list display and allows sorting it by first name:

 
```
    from django.db import models


    class Blog(models.Model):
        title = models.CharField(max_length=255)
        author = models.ForeignKey(Person, on_delete=models.CASCADE)

        def author_first_name(self, obj):
            return obj.author.first_name

        author_first_name.admin_order_field = 'author__first_name'

```
* Elements of `list_display` can also be properties. Please note however, that due to the way properties work in Python, setting `short_description` on a property is only possible when using the `property()` function and **not** with the `@property` decorator.

 For example:

 
```
    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class Person(models.Model):
        first_name = models.CharField(max_length=50)
        last_name = models.CharField(max_length=50)

        def full_name_property(self):
            return self.first_name + ' ' + self.last_name
        full_name_property.short_description = "Full name of the person"

        full_name = property(full_name_property)


    class PersonAdmin(ModelAdmin):
        list_display = ('full_name',)

```

   `ModelAdmin.list_export`
------------------------

 **Expected value**: A list or tuple, where each item is the name of a field or single-argument callable on your model, or a similarly simple method defined on the `ModelAdmin` class itself.

 Set `list_export` to set the fields you wish to be exported as columns when downloading a spreadsheet version of your index\_view

 
```
    class PersonAdmin(ModelAdmin):
        list_export = ('is_staff', 'company')

```
   `ModelAdmin.list_filter`
------------------------

 **Expected value**: A list or tuple, where each item is the name of model field of type `BooleanField`, `CharField`, `DateField`, `DateTimeField`, `IntegerField` or `ForeignKey`.

 Set `list_filter` to activate filters in the right sidebar of the list page for your model. For example:

 
```
    class PersonAdmin(ModelAdmin):
        list_filter = ('is_staff', 'company')

```
   `ModelAdmin.export_filename`
----------------------------

 **Expected value**: A string specifying the filename of an exported spreadsheet, without file extensions.

 
```
    class PersonAdmin(ModelAdmin):
        export_filename = 'people_spreadsheet'

```
   `ModelAdmin.search_fields`
--------------------------

 **Expected value**: A list or tuple, where each item is the name of a model field of type `CharField`, `TextField`, `RichTextField` or `StreamField`.

 Set `search_fields` to enable a search box at the top of the index page for your model. You should add names of any fields on the model that should be searched whenever somebody submits a search query using the search box.

 Searching is handled via Django’s QuerySet API by default, see [ModelAdmin.search\_handler\_class](#modeladmin-search-handler-class) about changing this behaviour. This means by default it will work for all models, whatever search backend your project is using, and without any additional setup or configuration.

   `ModelAdmin.search_handler_class`
---------------------------------

 **Expected value**: A subclass of `wagtail.contrib.modeladmin.helpers.search.BaseSearchHandler`

 The default value is `DjangoORMSearchHandler`, which uses the Django ORM to perform lookups on the fields specified by `search_fields`.

 If you would prefer to use the built-in Wagtail search backend to search your models, you can use the `WagtailBackendSearchHandler` class instead. For example:

 
```
    from wagtail.contrib.modeladmin.helpers import WagtailBackendSearchHandler

    from .models import Person

    class PersonAdmin(ModelAdmin):
        model = Person
        search_handler_class = WagtailBackendSearchHandler

```
  ### Extra considerations when using `WagtailBackendSearchHandler`

  #### 
`ModelAdmin.search_fields` is used differently

 The value of `search_fields` is passed to the underlying search backend to limit the fields used when matching. Each item in the list must be indexed on your model using [index.SearchField](../../../topics/search/indexing#wagtailsearch-index-searchfield).

 To allow matching on **any** indexed field, set the `search_fields` attribute on your `ModelAdmin` class to `None`, or remove it completely.

   #### Indexing extra fields using `index.FilterField`

 The underlying search backend must be able to interpret all of the fields and relationships used in the queryset created by `IndexView`, including those used in `prefetch()` or `select_related()` queryset methods, or used in `list_display`, `list_filter` or `ordering`.

 Be sure to test things thoroughly in a development environment (ideally using the same search backend as you use in production). Wagtail will raise an `IndexError` if the backend encounters something it does not understand, and will tell you what you need to change.

     `ModelAdmin.extra_search_kwargs`
--------------------------------

 **Expected value**: A dictionary of keyword arguments that will be passed on to the `search()` method of `search_handler_class`.

 For example, to override the `WagtailBackendSearchHandler` default operator you could do the following:

 
```
    from wagtail.contrib.modeladmin.helpers import WagtailBackendSearchHandler
    from wagtail.search.utils import OR

    from .models import IndexedModel

    class DemoAdmin(ModelAdmin):
        model = IndexedModel
        search_handler_class = WagtailBackendSearchHandler
        extra_search_kwargs = {'operator': OR}

```
   `ModelAdmin.ordering`
---------------------

 **Expected value**: A list or tuple in the same format as a model’s [ordering](https://docs.djangoproject.com/en/stable/ref/models/options/#django.db.models.Options.ordering "(in Django v4.1)") parameter.

 Set `ordering` to specify the default ordering of objects when listed by IndexView. If not provided, the model’s default ordering will be respected.

 If you need to specify a dynamic order (for example, depending on user or language) you can override the `get_ordering()` method instead.

   `ModelAdmin.list_per_page`
--------------------------

 **Expected value**: A positive integer

 Set `list_per_page` to control how many items appear on each paginated page of the index view. By default, this is set to `100`.

   `ModelAdmin.get_queryset()`
---------------------------

 **Must return**: A QuerySet

 The `get_queryset` method returns the ‘base’ QuerySet for your model, to which any filters and search queries are applied. By default, the `all()` method of your model’s default manager is used. But, if for any reason you only want a certain sub-set of objects to appear in the IndexView listing, overriding the `get_queryset` method on your `ModelAdmin` class can help you with that. The method takes an `HttpRequest` object as a parameter, so limiting objects by the current logged-in user is possible.

 For example:

 
```
    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class Person(models.Model):
        first_name = models.CharField(max_length=50)
        last_name = models.CharField(max_length=50)
        managed_by = models.ForeignKey('auth.User', on_delete=models.CASCADE)


    class PersonAdmin(ModelAdmin):
        model = Person
        list_display = ('first_name', 'last_name')

        def get_queryset(self, request):
            qs = super().get_queryset(request)
            # Only show people managed by the current user
            return qs.filter(managed_by=request.user)

```
   `ModelAdmin.get_extra_attrs_for_row()`
--------------------------------------

 **Must return**: A dictionary

 The `get_extra_attrs_for_row` method allows you to add html attributes to the opening `<tr>` tag for each result, in addition to the `data-object_pk` and `class` attributes already added by the `result_row_display` template tag.

 If you want to add additional CSS classes, simply provide those class names as a string value using the `'class'` key, and the `odd`/`even` will be appended to your custom class names when rendering.

 For example, if you wanted to add some additional class names based on field values, you could do something like:

 
```
    from decimal import Decimal
    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class BankAccount(models.Model):
        name = models.CharField(max_length=50)
        account_number = models.CharField(max_length=50)
        balance = models.DecimalField(max_digits=5, num_places=2)


    class BankAccountAdmin(ModelAdmin):
        list_display = ('name', 'account_number', 'balance')

        def get_extra_attrs_for_row(self, obj, context):
            if obj.balance < Decimal('0.00'):
                classname = 'balance-negative'
            else:
                classname = 'balance-positive'
            return {
                'class': classname,
            }

```
   `ModelAdmin.get_extra_class_names_for_field_col()`
--------------------------------------------------

 **Must return**: A list

 The `get_extra_class_names_for_field_col` method allows you to add additional CSS class names to any of the columns defined by `list_display` for your model. The method takes two parameters:

 * `obj`: the object being represented by the current row
* `field_name`: the item from `list_display` being represented by the current column

 For example, if you’d like to apply some conditional formatting to a cell depending on the row’s value, you could do something like:

 
```
    from decimal import Decimal
    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class BankAccount(models.Model):
        name = models.CharField(max_length=50)
        account_number = models.CharField(max_length=50)
        balance = models.DecimalField(max_digits=5, num_places=2)


    class BankAccountAdmin(ModelAdmin):
        list_display = ('name', 'account_number', 'balance')

        def get_extra_class_names_for_field_col(self, obj, field_name):
            if field_name == 'balance':
                if obj.balance <= Decimal('-100.00'):
                    return ['brand-danger']
                elif obj.balance <= Decimal('-0.00'):
                    return ['brand-warning']
                elif obj.balance <= Decimal('50.00'):
                    return ['brand-info']
                else:
                    return ['brand-success']
            return []

```
   `ModelAdmin.get_extra_attrs_for_field_col()`
--------------------------------------------

 **Must return**: A dictionary

 The `get_extra_attrs_for_field_col` method allows you to add additional HTML attributes to any of the columns defined in `list_display`. Like the `get_extra_class_names_for_field_col` method above, this method takes two parameters:

 * `obj`: the object being represented by the current row
* `field_name`: the item from `list_display` being represented by the current column

 For example, you might like to add some tooltip text to a certain column, to help give the value more context:

 
```
    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin


    class Person(models.Model):
        name = models.CharField(max_length=100)
        likes_cat_gifs = models.NullBooleanField()


    class PersonAdmin(ModelAdmin):
        model = Person
        list_display = ('name', 'likes_cat_gifs')

        def get_extra_attrs_for_field_col(self, obj, field_name=None):
            attrs = super().get_extra_attrs_for_field_col(obj, field_name)
            if field_name == 'likes_cat_gifs' and obj.likes_cat_gifs is None:
                attrs.update({
                    'title': (
                        'The person was shown several cat gifs, but failed to '
                        'indicate a preference.'
                    ),
                })
            return attrs

```
 Or you might like to add one or more data attributes to help implement some kind of interactivity using JavaScript:

 
```
    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin


    class Event(models.Model):
        title = models.CharField(max_length=255)
        start_date = models.DateField()
        end_date = models.DateField()
        start_time = models.TimeField()
        end_time = models.TimeField()


    class EventAdmin(ModelAdmin):
        model = Event
        list_display = ('title', 'start_date', 'end_date')

        def get_extra_attrs_for_field_col(self, obj, field_name=None):
            attrs = super().get_extra_attrs_for_field_col(obj, field_name)
            if field_name == 'start_date':
                # Add the start time as data to the 'start_date' cell
                attrs.update({ 'data-time': obj.start_time.strftime('%H:%M') })
            elif field_name == 'end_date':
                # Add the end time as data to the 'end_date' cell
                attrs.update({ 'data-time': obj.end_time.strftime('%H:%M') })
            return attrs

```
   `wagtail.contrib.modeladmin.mixins.ThumbnailMixin`
--------------------------------------------------

 If you’re using `wagtailimages.Image` to define an image for each item in your model, `ThumbnailMixin` can help you add thumbnail versions of that image to each row in `IndexView`. To use it, simply extend `ThumbnailMixin` as well as `ModelAdmin` when defining your `ModelAdmin` class, and change a few attributes to change the thumbnail to your liking, like so:

 
```
    from django.db import models
    from wagtail.contrib.modeladmin.mixins import ThumbnailMixin
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class Person(models.Model):
        name = models.CharField(max_length=255)
        avatar = models.ForeignKey('wagtailimages.Image', on_delete=models.SET_NULL, null=True)
        likes_cat_gifs = models.NullBooleanField()

    class PersonAdmin(ThumbnailMixin, ModelAdmin):

        # Add 'admin_thumb' to list_display, where you want the thumbnail to appear
        list_display = ('admin_thumb', 'name', 'likes_cat_gifs')

        # Optionally tell IndexView to add buttons to a different column (if the
        # first column contains the thumbnail, the buttons are likely better off
        # displayed elsewhere)
        list_display_add_buttons = 'name'

        """
        Set 'thumb_image_field_name' to the name of the ForeignKey field that
        links to 'wagtailimages.Image'
        """
        thumb_image_field_name = 'avatar'

        # Optionally override the filter spec used to create each thumb
        thumb_image_filter_spec = 'fill-100x100' # this is the default

        # Optionally override the 'width' attribute value added to each `<img>` tag
        thumb_image_width = 50 # this is the default

        # Optionally override the class name added to each `<img>` tag
        thumb_classname = 'admin-thumb' # this is the default

        # Optionally override the text that appears in the column header
        thumb_col_header_text = 'image' # this is the default

        # Optionally specify a fallback image to be used when the object doesn't
        # have an image set, or the image has been deleted. It can an image from
        # your static files folder, or an external URL.
        thumb_default = 'https://lorempixel.com/100/100'

```
   `ModelAdmin.list_display_add_buttons`
-------------------------------------

 **Expected value**: A string matching one of the items in `list_display`.

 If for any reason you’d like to change which column the action buttons appear in for each row, you can specify a different column using `list_display_add_buttons` on your `ModelAdmin` class. The value must match one of the items your class’s `list_display` attribute. By default, buttons are added to the first column of each row.

 See the `ThumbnailMixin` example above to see how `list_display_add_buttons` can be used.

   `ModelAdmin.index_view_extra_css`
---------------------------------

 **Expected value**: A list of path names of additional stylesheets to be added to the `IndexView`

 See the following part of the docs to find out more: [Adding additional stylesheets and/or JavaScript](primer#modeladmin-adding-css-and-js)

   `ModelAdmin.index_view_extra_js`
--------------------------------

 **Expected value**: A list of path names of additional js files to be added to the `IndexView`

 See the following part of the docs to find out more: [Adding additional stylesheets and/or JavaScript](primer#modeladmin-adding-css-and-js)

   `ModelAdmin.index_template_name`
--------------------------------

 **Expected value**: The path to a custom template to use for `IndexView`

 See the following part of the docs to find out more: [Overriding templates](primer#modeladmin-overriding-templates)

   `ModelAdmin.index_view_class`
-----------------------------

 **Expected value**: A custom `view` class to replace `modeladmin.views.IndexView`

 See the following part of the docs to find out more: [Overriding views](primer#modeladmin-overriding-views)

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/indexview.html>

Enabling & customising InspectView
==================================

 The `InspectView` is disabled by default, as it’s not often useful for most models. However, if you need a view that enables users to view more detailed information about an instance without the option to edit it, you can easily enable the inspect view by setting `inspect_view_enabled=True` on your `ModelAdmin` class.

 When `InspectView` is enabled, an ‘Inspect’ button will automatically appear for each row in your index / listing view, linking to a new page that shows a list of field values for that particular object.

 By default, all ‘concrete’ fields (where the field value is stored as a column in the database table for your model) will be shown. You can customise what values are displayed by adding the following attributes to your `ModelAdmin` class:

 * [`ModelAdmin.inspect_view_fields`](#modeladmin-inspect-view-fields)
* [`ModelAdmin.inspect_view_fields_exclude`](#modeladmin-inspect-view-fields-exclude)
* [`ModelAdmin.inspect_view_extra_css`](#modeladmin-inspect-view-extra-css)
* [`ModelAdmin.inspect_view_extra_js`](#modeladmin-inspect-view-extra-js)
* [`ModelAdmin.inspect_template_name`](#modeladmin-inspect-template-name)
* [`ModelAdmin.inspect_view_class`](#modeladmin-inspect-view-class)

  `ModelAdmin.inspect_view_fields`
--------------------------------

 **Expected value:** A list or tuple, where each item is the name of a field or attribute on the instance that you’d like `InspectView` to render.

 A sensible value will be rendered for most field types.

 If you have `wagtail.images` installed, and the value happens to be an instance of `wagtailimages.models.Image` (or a custom model that subclasses `wagtailimages.models.AbstractImage`), a thumbnail of that image will be rendered.

 If you have `wagtail.documents` installed, and the value happens to be an instance of `wagtaildocs.models.Document` (or a custom model that subclasses `wagtaildocs.models.AbstractDocument`), a link to that document will be rendered, along with the document title, file extension and size.

   `ModelAdmin.inspect_view_fields_exclude`
----------------------------------------

 **Expected value:** A list or tuple, where each item is the name of a field that you’d like to exclude from `InspectView`

 **Note:** If both `inspect_view_fields` and `inspect_view_fields_exclude` are set, `inspect_view_fields_exclude` will be ignored.

   `ModelAdmin.inspect_view_extra_css`
-----------------------------------

 **Expected value**: A list of path names of additional stylesheets to be added to the `InspectView`

 See the following part of the docs to find out more: [Adding additional stylesheets and/or JavaScript](primer#modeladmin-adding-css-and-js)

   `ModelAdmin.inspect_view_extra_js`
----------------------------------

 **Expected value**: A list of path names of additional js files to be added to the `InspectView`

 See the following part of the docs to find out more: [Adding additional stylesheets and/or JavaScript](primer#modeladmin-adding-css-and-js)

   `ModelAdmin.inspect_template_name`
----------------------------------

 **Expected value**: The path to a custom template to use for `InspectView`

 See the following part of the docs to find out more: [Overriding templates](primer#modeladmin-overriding-templates)

   `ModelAdmin.inspect_view_class`
-------------------------------

 **Expected value**: A custom `view` class to replace `modeladmin.views.InspectView`

 See the following part of the docs to find out more: [Overriding views](primer#modeladmin-overriding-views)

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/inspectview.html>

Reversing ModelAdmin URLs
=========================

 It’s sometimes useful to be able to derive the `index` (listing) or `create` URLs for a model along with the `edit`, `delete` or `inspect` URL for a specific object in a model you have registered via the `modeladmin` app.

 Wagtail itself does this by instantiating each `ModelAdmin` class you have registered, and using the `url_helper` attribute of each instance to determine what these URLs are.

 You can take a similar approach in your own code too, by creating a `ModelAdmin` instance yourself, and using its `url_helper` to determine URLs.

 See below for some examples:

 * [Getting the `edit` or `delete` or `inspect` URL for an object](#getting-the-edit-or-delete-or-inspect-url-for-an-object)
* [Getting the `index` or `create` URL for a model](#getting-the-index-or-create-url-for-a-model)

  Getting the `edit` or `delete` or `inspect` URL for an object
-------------------------------------------------------------

 In this example, we will provide a quick way to `edit` the Author that is linked to a blog post from the admin page listing menu. We have defined an `AuthorModelAdmin` class and registered it with Wagtail to allow `Author` objects to be administered via the admin area. The `BlogPage` model has an `author` field (a `ForeignKey` to the `Author` model) to allow a single author to be specified for each post.

 
```

    # file: wagtail_hooks.py

    from wagtail.admin.widgets import PageListingButton
    from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
    from wagtail import hooks

    # Author & BlogPage model not shown in this example
    from models import Author

    # ensure our modeladmin is created
    class AuthorModelAdmin(ModelAdmin):
        model = Author
        menu_order = 200

    # Creating an instance of `AuthorModelAdmin`
    author_modeladmin = AuthorModelAdmin()

    @hooks.register('register_page_listing_buttons')
    def add_author_edit_buttons(page, page_perms, next_url=None):
        """
        For pages that have an author, add an additional button to the page listing,
        linking to the 'edit' page for that author.
        """
        author_id = getattr(page, 'author_id', None)
        if author_id:
            # the url helper will return something like: /admin/my-app/author/edit/2/
            author_edit_url = author_modeladmin.url_helper.get_action_url('edit', author_id)
            yield PageListingButton('Edit Author',  author_edit_url, priority=10)

    modeladmin_register(AuthorModelAdmin)

```
 As you can see from the example above, when using `get_action_url()` to generate object-specific URLs, the target object’s primary key value must be supplied so that it can be included in the resulting URL (for example `"/admin/my-app/author/edit/2/"`). The following object-specific action names are supported by `get_action_url()`:

 * `'edit'` Returns a URL for updating a specific object.
* `'delete'` Returns a URL for deleting a specific object.
* `'inspect'` Returns a URL for viewing details of a specific object.


	+ **NOTE:** This will only work if `inspect_view_enabled` is set to `True` on your `ModelAdmin` class.

  Note

 If you are using string values as primary keys for you model, you may need to handle cases where the key contains characters that are not URL safe. Only alphanumerics (`[0-9a-zA-Z]`), or the following special characters are safe: `$`, `-`, `_`, `.`, `+`, `!`, `*`, `'`, `(`, `)`.

 `django.contrib.admin.utils.quote()` can be used to safely encode these primary key values before passing them to `get_action_url()`. Failure to do this may result in Wagtail not being able to recognise the primary key when the URL is visited, resulting in 404 errors.

    Getting the `index` or `create` URL for a model
-----------------------------------------------

 There are URLs available for the model listing view (action is `'index'`) and the create model view (action is `'create'`). Each of these has an equivalent shortcut available; `url_helper.index_url` and `url_helper.create_url`.

 For example:

 
```

    from .wagtail_hooks import AuthorModelAdmin

    url_helper = AuthorModelAdmin().url_helper

    index_url = url_helper.get_action_url('index')
    # OR we can use the 'index_url' shortcut
    also_index_url = url_helper.index_url # note: do not call this property as a function
    # both will output /admin/my-app/author

    create_url = url_helper.get_action_url('create')
    # OR we can use the 'create_url' shortcut
    also_create_url = url_helper.create_url # note: do not call this property as a function
    # both will output /admin/my-app/author/create

```
  Note

 If you have registered a page type with `modeladmin` (for example `BlogPage`), and pages of that type can be added to more than one place in the page tree, when a user visits the `create` URL, they’ll be automatically redirected to another view to choose a parent for the new page. So, this isn’t something you need to check or cater for in your own code.

  To customise `url_helper` behaviour, see [ModelAdmin.url\_helper\_class](../primer#modeladmin-url-helper-class).

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/tips_and_tricks/reversing_urls.html>

Promoted search results
=======================

 The `searchpromotions` module provides the models and user interface for managing “Promoted search results” and displaying them in a search results page.

 “Promoted search results” allow editors to explicitly link relevant content to search terms, so results pages can contain curated content in addition to results from the search engine.

  Installation
------------

 The `searchpromotions` module is not enabled by default. To install it, add `wagtail.contrib.search_promotions` to `INSTALLED_APPS` in your project’s Django settings file.

 
```
INSTALLED_APPS = [
    ...

    'wagtail.contrib.search_promotions',
]

```
 This app contains migrations so make sure you run the `migrate` django-admin command after installing.

   Usage
-----

 Once installed, a new menu item called “Promoted search results” should appear in the “Settings” menu. This is where you can assign pages to popular search terms.

  ### Displaying on a search results page

 To retrieve a list of promoted search results for a particular search query, you can use the `{% get_search_promotions %}` template tag from the `wagtailsearchpromotions_tags` templatetag library:

 
```
{% load wagtailcore_tags wagtailsearchpromotions_tags %}

...

{% get_search_promotions search_query as search_promotions %}

<ul>
    {% for search_promotion in search_promotions %}
        <li>
            <a href="{% pageurl search_promotion.page %}">
                <h2>{{ search_promotion.page.title }}</h2>
                <p>{{ search_promotion.description }}</p>
            </a>
        </li>
    {% endfor %}
</ul>

```
  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/searchpromotions.html>

Customising ChooseParentView
============================

 When adding a new page via Wagtail’s explorer view, you essentially choose where you want to add a new page by navigating the relevant part of the page tree and choosing to ‘add a child page’ to your chosen parent page. Wagtail then asks you to select what type of page you’d like to add.

 When adding a page from a `ModelAdmin` list page, we know what type of page needs to be added, but we might not automatically know where in the page tree it should be added. If there’s only one possible choice of parent for a new page (as defined by setting `parent_page_types` and `subpage_types` attributes on your models), then we skip a step and use that as the parent. Otherwise, the user must specify a parent page using modeladmin’s `ChooseParentView`.

 It should be very rare that you need to customise this view, but in case you do, modeladmin offers the following attributes that you can override:

 * [`ModelAdmin.choose_parent_template_name`](#modeladmin-choose-parent-template-name)
* [`ModelAdmin.choose_parent_view_class`](#modeladmin-choose-parent-view-class)

  `ModelAdmin.choose_parent_template_name`
----------------------------------------

 **Expected value**: The path to a custom template to use for `ChooseParentView`

 See the following part of the docs to find out more: [Overriding templates](primer#modeladmin-overriding-templates)

   `ModelAdmin.choose_parent_view_class`
-------------------------------------

 **Expected value**: A custom `view` class to replace `modeladmin.views.ChooseParentView`

 See the following part of the docs to find out more: [Overriding views](primer#modeladmin-overriding-views)

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/chooseparentview.html>

Additional tips and tricks
==========================

 This section explores some of modeladmin’s lesser-known features, and provides examples to help with modeladmin customisation. More pages will be added in future.

 * [Adding a custom clean method to your ModelAdmin models](custom_clean)
* [Reversing ModelAdmin URLs](reversing_urls)



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/tips_and_tricks/index.html>

Adding a custom clean method to your ModelAdmin models
======================================================

 The simplest way is to extend your ModelAdmin model and add a clean() model to it. For example:

 
```
from django import forms
from django.db import models

class ModelAdminModel(models.Model):
    def clean(self):
        if self.image.width < 1920 or self.image.height < 1080:
            raise forms.ValidationError("The image must be at least 1920x1080 pixels in size.")

```
 This will run the clean and raise the `ValidationError` whenever you save the model and the check fails. The error will be displayed at the top of the wagtail admin.

 If you want more fine grained-control you can add a custom `clean()` method to the `WagtailAdminPageForm` of your model. You can override the form of your ModelAdmin in a similar matter as wagtail Pages.

 So, create a custom `WagtailAdminPageForm`:

 
```
from wagtail.admin.forms import WagtailAdminPageForm

class ModelAdminModelForm(WagtailAdminPageForm):
    def clean(self):
        cleaned_data = super().clean()
        image = cleaned_data.get("image")
        if image and image.width < 1920 or image.height < 1080:
            self.add_error("image", "The image must be at least 1920x1080px")

        return cleaned_data

```
 And then set the `base_form_class` of your model:

 
```
from django.db import models

class ModelAdminModel(models.Model):
    base_form_class = ModelAdminModelForm

```
 Using `self.add_error` will display the error to the particular field that has the error.



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/tips_and_tricks/custom_clean.html>

TableBlock
==========

 The TableBlock module provides an HTML table block type for StreamField. This module uses [handsontable 6.2.2](https://handsontable.com/) to provide users with the ability to create and edit HTML tables in Wagtail. Table blocks provides a caption field for accessibility.

 ![The TableBlock component in StreamField, with row header, column header, caption fields - and then the editable table]

  Installation
------------

 Add `"wagtail.contrib.table_block"` to your INSTALLED\_APPS:

 
```
INSTALLED_APPS = [
    ...

    "wagtail.contrib.table_block",
]

```
   Basic Usage
-----------

 After installation, the TableBlock module can be used in a similar fashion to other StreamField blocks in the Wagtail core.

 Import the TableBlock `from wagtail.contrib.table_block.blocks import TableBlock` and add it to your StreamField declaration.

 
```
class DemoStreamBlock(StreamBlock):
    ...
    table = TableBlock()

```
 Then, on your page template, the `{% include_block %}` tag (called on either the individual block, or the StreamField value as a whole) will render any table blocks it encounters as an HTML `<table>` element:

 
```
{% load wagtailcore_tags %}

{% include_block page.body %}

```
 Or:

 
```
{% load wagtailcore_tags %}

{% for block in page.body %}
    {% if block.block_type == 'table' %}
        {% include_block block %}
    {% else %}
        {# rendering for other block types #}
    {% endif %}
{% endfor %}

```
   Advanced Usage
--------------

  ### Default Configuration

 When defining a TableBlock, Wagtail provides the ability to pass an optional `table_options` dictionary. The default TableBlock dictionary looks like this:

 
```
default_table_options = {
    'minSpareRows': 0,
    'startRows': 3,
    'startCols': 3,
    'colHeaders': False,
    'rowHeaders': False,
    'contextMenu': [
        'row_above',
        'row_below',
        '---------',
        'col_left',
        'col_right',
        '---------',
        'remove_row',
        'remove_col',
        '---------',
        'undo',
        'redo'
    ],
    'editor': 'text',
    'stretchH': 'all',
    'height': 108,
    'language': language,
    'renderer': 'text',
    'autoColumnSize': False,
}

```
   ### Configuration Options

 Every key in the `table_options` dictionary maps to a [handsontable](https://handsontable.com/) option. These settings can be changed to alter the behaviour of tables in Wagtail. The following options are available:

 * [minSpareRows](https://handsontable.com/docs/6.2.2/Options.html#minSpareRows) - The number of rows to append to the end of an empty grid. The default setting is 0.
* [startRows](https://handsontable.com/docs/6.2.2/Options.html#startRows) - The default number of rows for a new table.
* [startCols](https://handsontable.com/docs/6.2.2/Options.html#startCols) - The default number of columns for new tables.
* [colHeaders](https://handsontable.com/docs/6.2.2/Options.html#colHeaders) - Can be set to `True` or `False`. This setting designates if new tables should be created with column headers. **Note:** this only sets the behaviour for newly created tables. Page editors can override this by checking the the “Column header” checkbox in the table editor in the Wagtail admin.
* [rowHeaders](https://handsontable.com/docs/6.2.2/Options.html#rowHeaders) - Operates the same as `colHeaders` to designate if new tables should be created with the first column as a row header. Just like `colHeaders` this option can be overridden by the page editor in the Wagtail admin.
* [contextMenu](https://handsontable.com/docs/6.2.2/Options.html#contextMenu) - Enables or disables the Handsontable right-click menu. By default this is set to `True`. Alternatively you can provide a list or a dictionary with [specific options](https://handsontable.com/docs/6.2.2/demo-context-menu.html#page-specific).
* [editor](https://handsontable.com/docs/6.2.2/Options.html#editor) - Defines the editor used for table cells. The default setting is text.
* [stretchH](https://handsontable.com/docs/6.2.2/Options.html#stretchH) - Sets the default horizontal resizing of tables. Options include, ‘none’, ‘last’, and ‘all’. By default TableBlock uses ‘all’ for the even resizing of columns.
* [height](https://handsontable.com/docs/6.2.2/Options.html#height) - The default height of the grid. By default TableBlock sets the height to `108` for the optimal appearance of new tables in the editor. This is optimized for tables with `startRows` set to `3`. If you change the number of `startRows` in the configuration, you might need to change the `height` setting to improve the default appearance in the editor.
* [language](https://handsontable.com/docs/6.2.2/Options.html#language) - The default language setting. By default TableBlock tries to get the language from `django.utils.translation.get_language`. If needed, this setting can be overridden here.
* [renderer](https://handsontable.com/docs/6.2.2/Options.html#renderer) - The default setting Handsontable uses to render the content of table cells.
* [autoColumnSize](https://handsontable.com/docs/6.2.2/Options.html#autoColumnSize) - Enables or disables the `autoColumnSize` plugin. The TableBlock default setting is `False`.

 A [complete list of handsontable options](https://handsontable.com/docs/6.2.2/Options.html) can be found on the Handsontable website.

   ### Changing the default table\_options

 To change the default table options just pass a new table\_options dictionary when a new TableBlock is declared.

 
```
new_table_options = {
    'minSpareRows': 0,
    'startRows': 6,
    'startCols': 4,
    'colHeaders': False,
    'rowHeaders': False,
    'contextMenu': True,
    'editor': 'text',
    'stretchH': 'all',
    'height': 216,
    'language': 'en',
    'renderer': 'text',
    'autoColumnSize': False,
}

class DemoStreamBlock(StreamBlock):
    ...
    table = TableBlock(table_options=new_table_options)

```
   ### Supporting cell alignment

 You can activate the `alignment` option by setting a custom `contextMenu` which allows you to set the alignment on a cell selection. HTML classes set by handsontable will be kept on the rendered block. You’ll then be able to apply your own custom CSS rules to preserve the style. Those class names are:

 * Horizontal: `htLeft`, `htCenter`, `htRight`, `htJustify`
* Vertical: `htTop`, `htMiddle`, `htBottom`

 
```
new_table_options = {
    'contextMenu': [
        'row_above',
        'row_below',
        '---------',
        'col_left',
        'col_right',
        '---------',
        'remove_row',
        'remove_col',
        '---------',
        'undo',
        'redo',
        '---------',
        'copy',
        'cut'
        '---------',
        'alignment',
    ],
}

class DemoStreamBlock(StreamBlock):
    ...
    table = TableBlock(table_options=new_table_options)

```
  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/table_block.html>

Management commands
===================

  publish\_scheduled
------------------

  Changed in version 4.1: This command has been renamed from `publish_scheduled_pages` to `publish_scheduled` and it now also handles non-page objects. The `publish_scheduled_pages` command is still available as an alias, but it is recommended to update your configuration to run the `publish_scheduled` command instead.

  
```
./manage.py publish_scheduled

```
 This command publishes, updates or unpublishes objects that have had these actions scheduled by an editor. We recommend running this command once an hour.

   fixtree
-------

 
```
./manage.py fixtree

```
 This command scans for errors in your database and attempts to fix any issues it finds.

   move\_pages
-----------

 
```
manage.py move_pages from to

```
 This command moves a selection of pages from one section of the tree to another.

 Options:

 * **from** This is the **id** of the page to move pages from. All descendants of this page will be moved to the destination. After the operation is complete, this page will have no children.
* **to** This is the **id** of the page to move pages to.

   purge\_revisions
----------------

 
```
manage.py purge_revisions [--days=<number of days>]

```
 This command deletes old page revisions which are not in moderation, live, approved to go live, or the latest revision for a page. If the `days` argument is supplied, only revisions older than the specified number of days will be deleted.

   update\_index
-------------

 
```
./manage.py update_index [--backend <backend name>]

```
 This command rebuilds the search index from scratch.

 It is recommended to run this command once a week and at the following times:

 * whenever any pages have been created through a script (after an import, for example)
* whenever any changes have been made to models or search configuration

 The search may not return any results while this command is running, so avoid running it at peak times.

  ### Specifying which backend to update

 By default, `update_index` will rebuild all the search indexes listed in `WAGTAILSEARCH_BACKENDS`.

 If you have multiple backends and would only like to update one of them, you can use the `--backend` option.

 For example, to update just the default backend:

 
```
python manage.py update_index --backend default

```
 The `--chunk_size` option can be used to set the size of chunks that are indexed at a time. This defaults to 1000 but may need to be reduced for larger document sizes.

   ### Indexing the schema only

 You can prevent the `update_index` command from indexing any data by using the `--schema-only` option:

 
```
python manage.py update_index --schema-only

```
   ### Silencing the command

 You can prevent logs to the console by providing `--verbosity 0` as an argument:

 
```
$ python manage.py update_index --verbosity 0

```
 If this is omitted or provided with any number above 0 it will produce the same logs.

    wagtail\_update\_index
----------------------

 An alias for the `update_index` command that can be used when another installed package (such as [Haystack](https://haystacksearch.org/)) provides a command named `update_index`. In this case, the other package’s entry in `INSTALLED_APPS` should appear above `wagtail.search` so that its `update_index` command takes precedence over Wagtail’s.

   rebuild\_references\_index
--------------------------

 
```
./manage.py rebuild_references_index

```
 This command populates the table that tracks cross-references between objects, used for the usage reports on images, documents and snippets. This table is updated automatically saving objects, but it is recommended to run this command periodically to ensure that the data remains consistent.

   search\_garbage\_collect
------------------------

 
```
./manage.py search_garbage_collect

```
 Wagtail keeps a log of search queries that are popular on your website. On high traffic websites, this log may get big and you may want to clean out old search queries. This command cleans out all search query logs that are more than one week old (or a number of days configurable through the [`WAGTAILSEARCH_HITS_MAX_AGE`](settings#wagtailsearch-hits-max-age) setting).

   wagtail\_update\_image\_renditions
----------------------------------

 
```
./manage.py wagtail_update_image_renditions

```
 This command provides the ability to regenerate image renditions. This is useful if you have deployed to a server where the image renditions have not yet been generated or you have changed the underlying image rendition behaviour and need to ensure all renditions are created again.

 This does not remove rendition images that are unused, this can be done by clearing the folder using `rm -rf` or similar, once this is done you can then use the management command to generate the renditions.

 Options:

 * **–purge-only** : This argument will purge all image renditions without regenerating them. They will be regenerated when next requested.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/management_commands.html>

Hooks
=====

 On loading, Wagtail will search for any app with the file `wagtail_hooks.py` and execute the contents. This provides a way to register your own functions to execute at certain points in Wagtail’s execution, such as when a page is saved or when the main menu is constructed.

  Note

 Hooks are typically used to customise the view-level behaviour of the Wagtail admin and front-end. For customisations that only deal with model-level behaviour - such as calling an external service when a page or document is added - it is often better to use [Django’s signal mechanism](https://docs.djangoproject.com/en/stable/topics/signals/ "(in Django v4.1)") (see also: [Wagtail signals](signals)), as these are not dependent on a user taking a particular path through the admin interface.

  Registering functions with a Wagtail hook is done through the `@hooks.register` decorator:

 
```
from wagtail import hooks

@hooks.register('name_of_hook')
def my_hook_function(arg1, arg2...)
    # your code here

```
 Alternatively, `hooks.register` can be called as an ordinary function, passing in the name of the hook and a handler function defined elsewhere:

 
```
hooks.register('name_of_hook', my_hook_function)

```
 If you need your hooks to run in a particular order, you can pass the `order` parameter. If order is not specified then the hooks proceed in the order given by `INSTALLED_APPS`. Wagtail uses hooks internally, too, so you need to be aware of order when overriding built-in Wagtail functionality (such as removing default summary items):

 
```
@hooks.register('name_of_hook', order=1)  # This will run after every hook in the wagtail core
def my_hook_function(arg1, arg2...)
    # your code here

@hooks.register('name_of_hook', order=-1)  # This will run before every hook in the wagtail core
def my_other_hook_function(arg1, arg2...)
    # your code here

@hooks.register('name_of_hook', order=2)  # This will run after `my_hook_function`
def yet_another_hook_function(arg1, arg2...)
    # your code here

```
  Unit testing hooks
------------------

 Hooks are usually registered on startup and can’t be changed at runtime. But when writing unit tests, you might want to register a hook function just for a single test or block of code and unregister it so that it doesn’t run when other tests are run.

 You can register hooks temporarily using the `hooks.register_temporarily` function, this can be used as both a decorator and a context manager. Here’s an example of how to register a hook function for just a single test:

 
```
def my_hook_function():
    pass

class MyHookTest(TestCase):

    @hooks.register_temporarily('name_of_hook', my_hook_function)
    def test_my_hook_function(self):
        # Test with the hook registered here
        pass

```
 And here’s an example of registering a hook function for a single block of code:

 
```
def my_hook_function():
    pass

with hooks.register_temporarily('name_of_hook', my_hook_function):
    # Hook is registered here
    ..

# Hook is unregistered here

```
 If you need to register multiple hooks in a `with` block, you can pass the hooks in as a list of tuples:

 
```
def my_hook(...):
    pass

def my_other_hook(...):
    pass

with hooks.register_temporarily([
    ('hook_name', my_hook),
    ('hook_name', my_other_hook),
]):
    # All hooks are registered here
    ..

# All hooks are unregistered here

```
 The available hooks are listed below.

   Admin modules
-------------

 Hooks for building new areas of the admin interface (alongside pages, images, documents and so on).

  ### `construct_homepage_panels`

 Add or remove panels from the Wagtail admin homepage. The callable passed into this hook should take a `request` object and a list of panel objects, and should modify this list in-place as required. Panel objects are [Template components](../extending/template_components#template-components) with an additional `order` property, an integer that determines the panel’s position in the final ordered list. The default panels use integers between `100` and `300`.

 
```
from django.utils.safestring import mark_safe

from wagtail.admin.ui.components import Component
from wagtail import hooks

class WelcomePanel(Component):
    order = 50

    def render_html(self, parent_context):
        return mark_safe("""
        <section class="panel summary nice-padding">
          <h3>No, but seriously -- welcome to the admin homepage.</h3>
        </section>
        """)

@hooks.register('construct_homepage_panels')
def add_another_welcome_panel(request, panels):
    panels.append(WelcomePanel())

```
   ### `construct_homepage_summary_items`

 Add or remove items from the ‘site summary’ bar on the admin homepage (which shows the number of pages and other object that exist on the site). The callable passed into this hook should take a `request` object and a list of summary item objects, and should modify this list in-place as required. Summary item objects are instances of `wagtail.admin.site_summary.SummaryItem`, which extends [the Component class](../extending/template_components#creating-template-components) with the following additional methods and properties:

   ### `construct_main_menu`

 Called just before the Wagtail admin menu is output, to allow the list of menu items to be modified. The callable passed to this hook will receive a `request` object and a list of `menu_items`, and should modify `menu_items` in-place as required. Adding menu items should generally be done through the `register_admin_menu_item` hook instead - items added through `construct_main_menu` will not have their `is_shown` check applied.

 
```
from wagtail import hooks

@hooks.register('construct_main_menu')
def hide_explorer_menu_item_from_frank(request, menu_items):
  if request.user.username == 'frank':
    menu_items[:] = [item for item in menu_items if item.name != 'explorer']

```
   ### `describe_collection_contents`

 Called when Wagtail needs to find out what objects exist in a collection, if any. Currently this happens on the confirmation before deleting a collection, to ensure that non-empty collections cannot be deleted. The callable passed to this hook will receive a `collection` object, and should return either `None` (to indicate no objects in this collection), or a dict containing the following keys:

 * `count` - A numeric count of items in this collection
* `count_text` - A human-readable string describing the number of items in this collection, such as “3 documents”. (Sites with multi-language support should return a translatable string here, most likely using the `django.utils.translation.ngettext` function.)
* `url` (optional) - A URL to an index page that lists the objects being described.

   ### `register_account_settings_panel`

 Registers a new settings panel class to add to the “Account” view in the admin.

 This hook can be added to a sub-class of `BaseSettingsPanel`. For example:

 
```
from wagtail.admin.views.account import BaseSettingsPanel
from wagtail import hooks

@hooks.register('register_account_settings_panel')
class CustomSettingsPanel(BaseSettingsPanel):
    name = 'custom'
    title = "My custom settings"
    order = 500
    form_class = CustomSettingsForm

```
 Alternatively, it can also be added to a function. For example, this function is equivalent to the above:

 
```
from wagtail.admin.views.account import BaseSettingsPanel
from wagtail import hooks

class CustomSettingsPanel(BaseSettingsPanel):
    name = 'custom'
    title = "My custom settings"
    order = 500
    form_class = CustomSettingsForm

@hooks.register('register_account_settings_panel')
def register_custom_settings_panel(request, user, profile):
    return CustomSettingsPanel(request, user, profile)

```
 More details about the options that are available can be found at [Customising the user account settings form](../extending/custom_account_settings#custom-account-settings).

   ### `register_account_menu_item`

 Add an item to the “More actions” tab on the “Account” page within the Wagtail admin. The callable for this hook should return a dict with the keys `url`, `label` and `help_text`. For example:

 
```
from django.urls import reverse
from wagtail import hooks

@hooks.register('register_account_menu_item')
def register_account_delete_account(request):
    return {
        'url': reverse('delete-account'),
        'label': 'Delete account',
        'help_text': 'This permanently deletes your account.'
    }

```
   ### `register_admin_menu_item`

 Add an item to the Wagtail admin menu. The callable passed to this hook must return an instance of `wagtail.admin.menu.MenuItem`. New items can be constructed from the `MenuItem` class by passing in a `label` which will be the text in the menu item, and the URL of the admin page you want the menu item to link to (usually by calling `reverse()` on the admin view you’ve set up). Additionally, the following keyword arguments are accepted:

 * `name` - an internal name used to identify the menu item; defaults to the slugified form of the label.
* `icon_name` - icon to display against the menu item; no defaults, optional, but should be set for top-level menu items so they can be identified when collapsed.
* `classnames` - additional classnames applied to the link
* `order` - an integer which determines the item’s position in the menu

 For menu items that are only available to superusers, the subclass `wagtail.admin.menu.AdminOnlyMenuItem` can be used in place of `MenuItem`.

 `MenuItem` can be further subclassed to customise its initialisation or conditionally show or hide the item for specific requests (for example, to apply permission checks); see the source code (`wagtail/admin/menu.py`) for details.

 
```
from django.urls import reverse

from wagtail import hooks
from wagtail.admin.menu import MenuItem

@hooks.register('register_admin_menu_item')
def register_frank_menu_item():
  return MenuItem('Frank', reverse('frank'), icon_name='folder-inverse', order=10000)

```
   ### `register_admin_urls`

 Register additional admin page URLs. The callable fed into this hook should return a list of Django URL patterns which define the structure of the pages and endpoints of your extension to the Wagtail admin. For more about vanilla Django URLconfs and views, see [url dispatcher](https://docs.djangoproject.com/en/stable/topics/http/urls/ "(in Django v4.1)").

 
```
from django.http import HttpResponse
from django.urls import path

from wagtail import hooks

def admin_view(request):
  return HttpResponse(
    "I have approximate knowledge of many things!",
    content_type="text/plain")

@hooks.register('register_admin_urls')
def urlconf_time():
  return [
    path('how_did_you_almost_know_my_name/', admin_view, name='frank'),
  ]

```
   ### `register_group_permission_panel`

 Add a new panel to the Groups form in the ‘settings’ area. The callable passed to this hook must return a ModelForm / ModelFormSet-like class, with a constructor that accepts a group object as its `instance` keyword argument, and which implements the methods `save`, `is_valid`, and `as_admin_panel` (which returns the HTML to be included on the group edit page).

   ### `register_settings_menu_item`

 As `register_admin_menu_item`, but registers menu items into the ‘Settings’ sub-menu rather than the top-level menu.

   ### `construct_settings_menu`

 As `construct_main_menu`, but modifies the ‘Settings’ sub-menu rather than the top-level menu.

   ### `register_reports_menu_item`

 As `register_admin_menu_item`, but registers menu items into the ‘Reports’ sub-menu rather than the top-level menu.

   ### `construct_reports_menu`

 As `construct_main_menu`, but modifies the ‘Reports’ sub-menu rather than the top-level menu.

   ### `register_help_menu_item`

 As `register_admin_menu_item`, but registers menu items into the ‘Help’ sub-menu rather than the top-level menu.

   ### `construct_help_menu`

 As `construct_main_menu`, but modifies the ‘Help’ sub-menu rather than the top-level menu.

   ### `register_admin_search_area`

 Add an item to the Wagtail admin search “Other Searches”. Behaviour of this hook is similar to `register_admin_menu_item`. The callable passed to this hook must return an instance of `wagtail.admin.search.SearchArea`. New items can be constructed from the `SearchArea` class by passing the following parameters:

 * `label` - text displayed in the “Other Searches” option box.
* `name` - an internal name used to identify the search option; defaults to the slugified form of the label.
* `url` - the URL of the target search page.
* `classnames` - arbitrary CSS classnames applied to the link
* `icon_name` - icon to display next to the label.
* `attrs` - additional HTML attributes to apply to the link.
* `order` - an integer which determines the item’s position in the list of options.

 Setting the URL can be achieved using reverse() on the target search page. The GET parameter ‘q’ will be appended to the given URL.

 A template tag, `search_other` is provided by the `wagtailadmin_tags` template module. This tag takes a single, optional parameter, `current`, which allows you to specify the `name` of the search option currently active. If the parameter is not given, the hook defaults to a reverse lookup of the page’s URL for comparison against the `url` parameter.

 `SearchArea` can be subclassed to customise the HTML output, specify JavaScript files required by the option, or conditionally show or hide the item for specific requests (for example, to apply permission checks); see the source code (`wagtail/admin/search.py`) for details.

 
```
from django.urls import reverse
from wagtail import hooks
from wagtail.admin.search import SearchArea

@hooks.register('register_admin_search_area')
def register_frank_search_area():
    return SearchArea('Frank', reverse('frank'), icon_name='folder-inverse', order=10000)

```
   ### `register_permissions`

 Return a QuerySet of `Permission` objects to be shown in the Groups administration area.

 
```
  from django.contrib.auth.models import Permission
  from wagtail import hooks


  @hooks.register('register_permissions')
  def register_permissions():
      app = 'blog'
      model = 'extramodelset'

      return Permission.objects.filter(content_type__app_label=app, codename__in=[
          f"view_{model}", f"add_{model}", f"change_{model}", f"delete_{model}"
      ])

```
   ### `filter_form_submissions_for_user`

 Allows access to form submissions to be customised on a per-user, per-form basis.

 This hook takes two parameters:

 * The user attempting to access form submissions
* A `QuerySet` of form pages

 The hook must return a `QuerySet` containing a subset of these form pages which the user is allowed to access the submissions for.

 For example, to prevent non-superusers from accessing form submissions:

 
```
from wagtail import hooks


@hooks.register('filter_form_submissions_for_user')
def construct_forms_for_user(user, queryset):
    if not user.is_superuser:
        queryset = queryset.none()

    return queryset

```
    Editor interface
----------------

 Hooks for customising the editing interface for pages and snippets.

  ### `register_rich_text_features`

 Rich text fields in Wagtail work with a list of ‘feature’ identifiers that determine which editing controls are available in the editor, and which elements are allowed in the output; for example, a rich text field defined as `RichTextField(features=['h2', 'h3', 'bold', 'italic', 'link'])` would allow headings, bold / italic formatting and links, but not (for example) bullet lists or images. The `register_rich_text_features` hook allows new feature identifiers to be defined - see [Limiting features in a rich text field](../advanced_topics/customisation/page_editing_interface#rich-text-features) for details.

   ### `insert_editor_css`

 Add additional CSS files or snippets to the page editor.

 
```
from django.templatetags.static import static
from django.utils.html import format_html

from wagtail import hooks

@hooks.register('insert_editor_css')
def editor_css():
    return format_html(
        '<link rel="stylesheet" href="{}">',
        static('demo/css/vendor/font-awesome/css/font-awesome.min.css')
    )

```
   ### `insert_global_admin_css`

 Add additional CSS files or snippets to all admin pages.

 
```
from django.utils.html import format_html
from django.templatetags.static import static

from wagtail import hooks

@hooks.register('insert_global_admin_css')
def global_admin_css():
    return format_html('<link rel="stylesheet" href="{}">', static('my/wagtail/theme.css'))

```
   ### `insert_editor_js`

 Add additional JavaScript files or code snippets to the page editor.

 
```
from django.utils.html import format_html_join
from django.utils.safestring import mark_safe
from django.templatetags.static import static

from wagtail import hooks

@hooks.register('insert_editor_js')
def editor_js():
    js_files = [
        'js/fireworks.js', # https://fireworks.js.org
    ]
    js_includes = format_html_join('\n', '<script src="{0}"></script>',
        ((static(filename),) for filename in js_files)
    )
    return js_includes + mark_safe(
        """
        <script>
            window.addEventListener('DOMContentLoaded', (event) => {
                var container = document.createElement('div');
                container.style.cssText = 'position: fixed; width: 100%; height: 100%; z-index: 100; top: 0; left: 0; pointer-events: none;';
                container.id = 'fireworks';
                document.getElementById('main').prepend(container);
                var options = { "acceleration": 1.2, "autoresize": true, "mouse": { "click": true, "max": 3 } };
                var fireworks = new Fireworks(document.getElementById('fireworks'), options);
                fireworks.start();
            });
        </script>
        """
    )

```
   ### `insert_global_admin_js`

 Add additional JavaScript files or code snippets to all admin pages.

 
```
from django.utils.safestring import mark_safe

from wagtail import hooks

@hooks.register('insert_global_admin_js')
def global_admin_js():
    return mark_safe(
        '<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r74/three.js"></script>',
    )

```
   ### `register_page_header_buttons`

 Add buttons to the secondary dropdown menu in the page header. This works similarly to the `register_page_listing_buttons` hook.

 This example will add a simple button to the secondary dropdown menu:

 
```
from wagtail.admin import widgets as wagtailadmin_widgets

@hooks.register('register_page_header_buttons')
def page_header_buttons(page, page_perms, next_url=None):
    yield wagtailadmin_widgets.Button(
        'A dropdown button',
        '/goes/to/a/url/',
        priority=60
    )

```
 The arguments passed to the hook are as follows:

 * `page` - the page object to generate the button for
* `page_perms` - a `PagePermissionTester` object that can be queried to determine the current user’s permissions on the given page
* `next_url` - the URL that the linked action should redirect back to on completion of the action, if the view supports it

 The `priority` argument controls the order the buttons are displayed in the dropdown. Buttons are ordered from low to high priority, so a button with `priority=10` will be displayed before a button with `priority=60`.

    Editor workflow
---------------

 Hooks for customising the way users are directed through the process of creating page content.

  ### `after_create_page`

 Do something with a `Page` object after it has been saved to the database (as a published page or a revision). The callable passed to this hook should take a `request` object and a `page` object. The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the Explorer page for the new page’s parent.

 
```
from django.http import HttpResponse

from wagtail import hooks

@hooks.register('after_create_page')
def do_after_page_create(request, page):
    return HttpResponse("Congrats on making content!", content_type="text/plain")

```
 If you set attributes on a `Page` object, you should also call `save_revision()`, since the edit and index view pick up their data from the revisions table rather than the actual saved page record.

 
```
  @hooks.register('after_create_page')
  def set_attribute_after_page_create(request, page):
      page.title = 'Persistent Title'
      new_revision = page.save_revision()
      if page.live:
          # page has been created and published at the same time,
          # so ensure that the updated title is on the published version too
          new_revision.publish()

```
   ### `before_create_page`

 Called at the beginning of the “create page” view passing in the request, the parent page and page model class.

 The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.

 Unlike, `after_create_page`, this is run both for both `GET` and `POST` requests.

 This can be used to completely override the editor on a per-view basis:

 
```
from wagtail import hooks

from .models import AwesomePage
from .admin_views import edit_awesome_page

@hooks.register('before_create_page')
def before_create_page(request, parent_page, page_class):
    # Use a custom create view for the AwesomePage model
    if page_class == AwesomePage:
        return create_awesome_page(request, parent_page)

```
   ### `after_delete_page`

 Do something after a `Page` object is deleted. Uses the same behaviour as `after_create_page`.

   ### `before_delete_page`

 Called at the beginning of the “delete page” view passing in the request and the page object.

 Uses the same behaviour as `before_create_page`, is is run both for both `GET` and `POST` requests.

 
```
from django.shortcuts import redirect
from django.utils.html import format_html

from wagtail.admin import messages
from wagtail import hooks

from .models import AwesomePage


@hooks.register('before_delete_page')
def before_delete_page(request, page):
    """Block awesome page deletion and show a message."""

    if request.method == 'POST' and page.specific_class in [AwesomePage]:
        messages.warning(request, "Awesome pages cannot be deleted, only unpublished")
        return redirect('wagtailadmin_pages:delete', page.pk)

```
   ### `after_edit_page`

 Do something with a `Page` object after it has been updated. Uses the same behaviour as `after_create_page`.

   ### `before_edit_page`

 Called at the beginning of the “edit page” view passing in the request and the page object.

 Uses the same behaviour as `before_create_page`.

   ### `after_publish_page`

 Do something with a `Page` object after it has been published via page create view or page edit view.

 The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.

   ### `before_publish_page`

 Do something with a `Page` object before it has been published via page create view or page edit view.

 The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.

   ### `after_unpublish_page`

 Called after unpublish action in “unpublish” view passing in the request and the page object.

 The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.

   ### `before_unpublish_page`

 Called before unpublish action in “unpublish” view passing in the request and the page object.

 The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.

   ### `after_copy_page`

 Do something with a `Page` object after it has been copied passing in the request, page object and the new copied page. Uses the same behaviour as `after_create_page`.

   ### `before_copy_page`

 Called at the beginning of the “copy page” view passing in the request and the page object.

 Uses the same behaviour as `before_create_page`.

   ### `after_move_page`

 Do something with a `Page` object after it has been moved passing in the request and page object. Uses the same behaviour as `after_create_page`.

   ### `before_move_page`

 Called at the beginning of the “move page” view passing in the request, the page object and the destination page object.

 Uses the same behaviour as `before_create_page`.

   ### `before_convert_alias_page`

 Called at the beginning of the `convert_alias` view, which is responsible for converting alias pages into normal Wagtail pages.

 The request and the page being converted are passed in as arguments to the hook.

 The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.

   ### `after_convert_alias_page`

 Do something with a `Page` object after it has been converted from an alias.

 The request and the page that was just converted are passed in as arguments to the hook.

 The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.

   ### `construct_translated_pages_to_cascade_actions`

 Return additional pages to process in a synced tree setup.

 This hook is only triggered on unpublishing a page when `WAGTAIL_I18N_ENABLED = True`.

 The list of pages and the action are passed in as arguments to the hook.

 The function should return a dictionary with the page from the pages list as key, and a list of additional pages to perform the action on. We recommend they are non-aliased, direct translations of the pages from the function argument.

   ### `register_page_action_menu_item`

 Add an item to the popup menu of actions on the page creation and edit views. The callable passed to this hook must return an instance of `wagtail.admin.action_menu.ActionMenuItem`. `ActionMenuItem` is a subclass of [Component](../extending/template_components#creating-template-components) and so the rendering of the menu item can be customised through `template_name`, `get_context_data`, `render_html` and `Media`. In addition, the following attributes and methods are available to be overridden:

 * `order` - an integer (default 100) which determines the item’s position in the menu. Can also be passed as a keyword argument to the object constructor. The lowest-numbered item in this sequence will be selected as the default menu item; as standard, this is “Save draft” (which has an `order` of 0).
* `label` - the displayed text of the menu item
* `get_url` - a method which returns a URL for the menu item to link to; by default, returns `None` which causes the menu item to behave as a form submit button instead
* `name` - value of the `name` attribute of the submit button, if no URL is specified
* `icon_name` - icon to display against the menu item
* `classname` - a `class` attribute value to add to the button element
* `is_shown` - a method which returns a boolean indicating whether the menu item should be shown; by default, true except when editing a locked page

 The `get_url`, `is_shown`, `get_context_data` and `render_html` methods all accept a context dictionary containing the following fields:

 * `view` - name of the current view: `'create'`, `'edit'` or `'revisions_revert'`
* `page` - for `view` = `'edit'` or `'revisions_revert'`, the page being edited
* `parent_page` - for `view` = `'create'`, the parent page of the page being created
* `request` - the current request object
* `user_page_permissions` - a `UserPagePermissionsProxy` object for the current user, to test permissions against

 
```
from wagtail import hooks
from wagtail.admin.action_menu import ActionMenuItem

class GuacamoleMenuItem(ActionMenuItem):
    name = 'action-guacamole'
    label = "Guacamole"

    def get_url(self, context):
        return "https://www.youtube.com/watch?v=dNJdJIwCF_Y"


@hooks.register('register_page_action_menu_item')
def register_guacamole_menu_item():
    return GuacamoleMenuItem(order=10)

```
   ### `construct_page_action_menu`

 Modify the final list of action menu items on the page creation and edit views. The callable passed to this hook receives a list of `ActionMenuItem` objects, a request object and a context dictionary as per `register_page_action_menu_item`, and should modify the list of menu items in-place.

 
```
@hooks.register('construct_page_action_menu')
def remove_submit_to_moderator_option(menu_items, request, context):
    menu_items[:] = [item for item in menu_items if item.name != 'action-submit']

```
 The `construct_page_action_menu` hook is called after the menu items have been sorted by their order attributes, and so setting a menu item’s order will have no effect at this point. Instead, items can be reordered by changing their position in the list, with the first item being selected as the default action. For example, to change the default action to Publish:

 
```
@hooks.register('construct_page_action_menu')
def make_publish_default_action(menu_items, request, context):
    for (index, item) in enumerate(menu_items):
        if item.name == 'action-publish':
            # move to top of list
            menu_items.pop(index)
            menu_items.insert(0, item)
            break

```
   ### `construct_page_listing_buttons`

 Modify the final list of page listing buttons in the page explorer. The callable passed to this hook receives a list of `PageListingButton` objects, a page, a page perms object, and a context dictionary as per `register_page_listing_buttons`, and should modify the list of listing items in-place.

 
```
@hooks.register('construct_page_listing_buttons')
def remove_page_listing_button_item(buttons, page, page_perms, context=None):
    if page.is_root:
        buttons.pop() # removes the last 'more' dropdown button on the root page listing buttons

```
   ### `construct_wagtail_userbar`

 Add or remove items from the wagtail userbar. Add, edit, and moderation tools are provided by default. The callable passed into the hook must take the `request` object and a list of menu objects, `items`. The menu item objects must have a `render` method which can take a `request` object and return the HTML string representing the menu item. See the userbar templates and menu item classes for more information.

 
```
from wagtail import hooks

class UserbarPuppyLinkItem:
    def render(self, request):
        return '<li><a href="http://cuteoverload.com/tag/puppehs/" ' \
            + 'target="_parent" role="menuitem" class="action icon icon-wagtail">Puppies!</a></li>'

@hooks.register('construct_wagtail_userbar')
def add_puppy_link_item(request, items):
    return items.append( UserbarPuppyLinkItem() )

```
    Admin workflow
--------------

 Hooks for customising the way admins are directed through the process of editing users.

  ### `after_create_user`

 Do something with a `User` object after it has been saved to the database. The callable passed to this hook should take a `request` object and a `user` object. The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the User index page.

 
```
from django.http import HttpResponse

from wagtail import hooks

@hooks.register('after_create_user')
def do_after_create_user(request, user):
    return HttpResponse("Congrats on creating a new user!", content_type="text/plain")

```
   ### `before_create_user`

 Called at the beginning of the “create user” view passing in the request.

 The function does not have to return anything, but if an object with a `status_code` property is returned, Wagtail will use it as a response object and skip the rest of the view.

 Unlike, `after_create_user`, this is run both for both `GET` and `POST` requests.

 This can be used to completely override the user editor on a per-view basis:

 
```
from django.http import HttpResponse

from wagtail import hooks

from .models import AwesomePage
from .admin_views import edit_awesome_page

@hooks.register('before_create_user')
def do_before_create_user(request):
    return HttpResponse("A user creation form", content_type="text/plain")

```
   ### `after_delete_user`

 Do something after a `User` object is deleted. Uses the same behaviour as `after_create_user`.

   ### `before_delete_user`

 Called at the beginning of the “delete user” view passing in the request and the user object.

 Uses the same behaviour as `before_create_user`.

   ### `after_edit_user`

 Do something with a `User` object after it has been updated. Uses the same behaviour as `after_create_user`.

   ### `before_edit_user`

 Called at the beginning of the “edit user” view passing in the request and the user object.

 Uses the same behaviour as `before_create_user`.

    Choosers
--------

  ### `construct_page_chooser_queryset`

 Called when rendering the page chooser view, to allow the page listing QuerySet to be customised. The callable passed into the hook will receive the current page QuerySet and the request object, and must return a Page QuerySet (either the original one, or a new one).

 
```
from wagtail import hooks

@hooks.register('construct_page_chooser_queryset')
def show_my_pages_only(pages, request):
    # Only show own pages
    pages = pages.filter(owner=request.user)

    return pages

```
   ### `construct_document_chooser_queryset`

 Called when rendering the document chooser view, to allow the document listing QuerySet to be customised. The callable passed into the hook will receive the current document QuerySet and the request object, and must return a Document QuerySet (either the original one, or a new one).

 
```
from wagtail import hooks

@hooks.register('construct_document_chooser_queryset')
def show_my_uploaded_documents_only(documents, request):
    # Only show uploaded documents
    documents = documents.filter(uploaded_by_user=request.user)

    return documents

```
   ### `construct_image_chooser_queryset`

 Called when rendering the image chooser view, to allow the image listing QuerySet to be customised. The callable passed into the hook will receive the current image QuerySet and the request object, and must return an Image QuerySet (either the original one, or a new one).

 
```
from wagtail import hooks

@hooks.register('construct_image_chooser_queryset')
def show_my_uploaded_images_only(images, request):
    # Only show uploaded images
    images = images.filter(uploaded_by_user=request.user)

    return images

```
    Page explorer
-------------

  ### `construct_explorer_page_queryset`

 Called when rendering the page explorer view, to allow the page listing QuerySet to be customised. The callable passed into the hook will receive the parent page object, the current page QuerySet, and the request object, and must return a Page QuerySet (either the original one, or a new one).

 
```
from wagtail import hooks

@hooks.register('construct_explorer_page_queryset')
def show_my_profile_only(parent_page, pages, request):
    # If we're in the 'user-profiles' section, only show the user's own profile
    if parent_page.slug == 'user-profiles':
        pages = pages.filter(owner=request.user)

    return pages

```
   ### `register_page_listing_buttons`

 Add buttons to the actions list for a page in the page explorer. This is useful when adding custom actions to the listing, such as translations or a complex workflow.

 This example will add a simple button to the listing:

 
```
from wagtail.admin import widgets as wagtailadmin_widgets

@hooks.register('register_page_listing_buttons')
def page_listing_buttons(page, page_perms, next_url=None):
    yield wagtailadmin_widgets.PageListingButton(
        'A page listing button',
        '/goes/to/a/url/',
        priority=10
    )

```
 The arguments passed to the hook are as follows:

 * `page` - the page object to generate the button for
* `page_perms` - a `PagePermissionTester` object that can be queried to determine the current user’s permissions on the given page
* `next_url` - the URL that the linked action should redirect back to on completion of the action, if the view supports it

 The `priority` argument controls the order the buttons are displayed in. Buttons are ordered from low to high priority, so a button with `priority=10` will be displayed before a button with `priority=20`.

   ### `register_page_listing_more_buttons`

 Add buttons to the “More” dropdown menu for a page in the page explorer. This works similarly to the `register_page_listing_buttons` hook but is useful for lesser-used custom actions that are better suited for the dropdown.

 This example will add a simple button to the dropdown menu:

 
```
from wagtail.admin import widgets as wagtailadmin_widgets

@hooks.register('register_page_listing_more_buttons')
def page_listing_more_buttons(page, page_perms, next_url=None):
    yield wagtailadmin_widgets.Button(
        'A dropdown button',
        '/goes/to/a/url/',
        priority=60
    )

```
 The arguments passed to the hook are as follows:

 * `page` - the page object to generate the button for
* `page_perms` - a `PagePermissionTester` object that can be queried to determine the current user’s permissions on the given page
* `next_url` - the URL that the linked action should redirect back to on completion of the action, if the view supports it

 The `priority` argument controls the order the buttons are displayed in the dropdown. Buttons are ordered from low to high priority, so a button with `priority=10` will be displayed before a button with `priority=60`.

  #### Buttons with dropdown lists

 The admin widgets also provide `ButtonWithDropdownFromHook`, which allows you to define a custom hook for generating a dropdown menu that gets attached to your button.

 Creating a button with a dropdown menu involves two steps. Firstly, you add your button to the `register_page_listing_buttons` hook, just like the example above. Secondly, you register a new hook that yields the contents of the dropdown menu.

 This example shows how Wagtail’s default admin dropdown is implemented. You can also see how to register buttons conditionally, in this case by evaluating the `page_perms`:

 
```
from wagtail.admin import widgets as wagtailadmin_widgets

@hooks.register('register_page_listing_buttons')
def page_custom_listing_buttons(page, page_perms, next_url=None):
    yield wagtailadmin_widgets.ButtonWithDropdownFromHook(
        'More actions',
        hook_name='my_button_dropdown_hook',
        page=page,
        page_perms=page_perms,
        next_url=next_url,
        priority=50
    )

@hooks.register('my_button_dropdown_hook')
def page_custom_listing_more_buttons(page, page_perms, next_url=None):
    if page_perms.can_move():
        yield wagtailadmin_widgets.Button('Move', reverse('wagtailadmin_pages:move', args=[page.id]), priority=10)
    if page_perms.can_delete():
        yield wagtailadmin_widgets.Button('Delete', reverse('wagtailadmin_pages:delete', args=[page.id]), priority=30)
    if page_perms.can_unpublish():
        yield wagtailadmin_widgets.Button('Unpublish', reverse('wagtailadmin_pages:unpublish', args=[page.id]), priority=40)

```
 The template for the dropdown button can be customised by overriding `wagtailadmin/pages/listing/_button_with_dropdown.html`. The JavaScript that runs the dropdowns makes use of custom data attributes, so you should leave `data-dropdown` and `data-dropdown-toggle` in the markup if you customise it.

     Page serving
------------

  ### `before_serve_page`

 Called when Wagtail is about to serve a page. The callable passed into the hook will receive the page object, the request object, and the `args` and `kwargs` that will be passed to the page’s `serve()` method. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, and Wagtail will not proceed to call `serve()` on the page.

 
```
from django.http import HttpResponse

from wagtail import hooks

@hooks.register('before_serve_page')
def block_googlebot(page, request, serve_args, serve_kwargs):
    if request.META.get('HTTP_USER_AGENT') == 'GoogleBot':
        return HttpResponse("<h1>bad googlebot no cookie</h1>")

```
    Document serving
----------------

  ### `before_serve_document`

 Called when Wagtail is about to serve a document. The callable passed into the hook will receive the document object and the request object. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, instead of serving the document. Note that this hook will be skipped if the [`WAGTAILDOCS_SERVE_METHOD`](settings#wagtaildocs-serve-method) setting is set to `direct`.

    Snippets
--------

 Hooks for working with registered Snippets.

  ### `after_edit_snippet`

 Called when a Snippet is edited. The callable passed into the hook will receive the model instance, the request object. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, and Wagtail will not proceed to call `redirect()` to the listing view.

 
```
from django.http import HttpResponse

from wagtail import hooks

@hooks.register('after_edit_snippet')
def after_snippet_update(request, instance):
    return HttpResponse(f"Congrats on editing a snippet with id {instance.pk}", content_type="text/plain")

```
   ### `before_edit_snippet`

 Called at the beginning of the edit snippet view. The callable passed into the hook will receive the model instance, the request object. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, and Wagtail will not proceed to call `redirect()` to the listing view.

 
```
from django.http import HttpResponse

from wagtail import hooks

@hooks.register('before_edit_snippet')
def block_snippet_edit(request, instance):
    if isinstance(instance, RestrictedSnippet) and instance.prevent_edit:
        return HttpResponse("Sorry, you can't edit this snippet", content_type="text/plain")

```
   ### `after_create_snippet`

 Called when a Snippet is created. `after_create_snippet` and `after_edit_snippet` work in identical ways. The only difference is where the hook is called.

   ### `before_create_snippet`

 Called at the beginning of the create snippet view. Works in a similar way to `before_edit_snippet` except the model is passed as an argument instead of an instance.

   ### `after_delete_snippet`

 Called when a Snippet is deleted. The callable passed into the hook will receive the model instance(s) as a queryset along with the request object. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, and Wagtail will not proceed to call `redirect()` to the listing view.

 
```
from django.http import HttpResponse

from wagtail import hooks

@hooks.register('after_delete_snippet')
def after_snippet_delete(request, instances):
    # "instances" is a QuerySet
    total = len(instances)
    return HttpResponse(f"{total} snippets have been deleted", content_type="text/plain")

```
   ### `before_delete_snippet`

 Called at the beginning of the delete snippet view. The callable passed into the hook will receive the model instance(s) as a queryset along with the request object. If the callable returns an `HttpResponse`, that response will be returned immediately to the user, and Wagtail will not proceed to call `redirect()` to the listing view.

 
```
from django.http import HttpResponse

from wagtail import hooks

@hooks.register('before_delete_snippet')
def before_snippet_delete(request, instances):
    # "instances" is a QuerySet
    total = len(instances)

    if request.method == 'POST':
      # Override the deletion behaviour
      instances.delete()

      return HttpResponse(f"{total} snippets have been deleted", content_type="text/plain")

```
   ### `register_snippet_action_menu_item`

 Add an item to the popup menu of actions on the snippet creation and edit views. The callable passed to this hook must return an instance of `wagtail.snippets.action_menu.ActionMenuItem`. `ActionMenuItem` is a subclass of [Component](../extending/template_components#creating-template-components) and so the rendering of the menu item can be customised through `template_name`, `get_context_data`, `render_html` and `Media`. In addition, the following attributes and methods are available to be overridden:

 * `order`- an integer (default 100) which determines the item’s position in the menu. Can also be passed as a keyword argument to the object constructor. The lowest-numbered item in this sequence will be selected as the default menu item; as standard, this is “Save draft” (which has an`order` of 0).
* `label` - the displayed text of the menu item
* `get_url` - a method which returns a URL for the menu item to link to; by default, returns `None` which causes the menu item to behave as a form submit button instead
* `name` - value of the `name` attribute of the submit button if no URL is specified
* `icon_name` - icon to display against the menu item
* `classname` - a `class` attribute value to add to the button element
* `is_shown` - a method which returns a boolean indicating whether the menu item should be shown; by default, true except when editing a locked page

 The `get_url`, `is_shown`, `get_context_data` and `render_html` methods all accept a context dictionary containing the following fields:

 * `view` - name of the current view: `'create'` or `'edit'`
* `model` - the snippet’s model class
* `instance` - for `view` = `'edit'`, the instance being edited
* `request` - the current request object

 
```
from wagtail import hooks
from wagtail.snippets.action_menu import ActionMenuItem

class GuacamoleMenuItem(ActionMenuItem):
    name = 'action-guacamole'
    label = "Guacamole"

    def get_url(self, context):
        return "https://www.youtube.com/watch?v=dNJdJIwCF_Y"


@hooks.register('register_snippet_action_menu_item')
def register_guacamole_menu_item():
    return GuacamoleMenuItem(order=10)

```
   ### `construct_snippet_action_menu`

 Modify the final list of action menu items on the snippet creation and edit views. The callable passed to this hook receives a list of `ActionMenuItem` objects, a request object and a context dictionary as per `register_snippet_action_menu_item`, and should modify the list of menu items in-place.

 
```
@hooks.register('construct_snippet_action_menu')
def remove_delete_option(menu_items, request, context):
    menu_items[:] = [item for item in menu_items if item.name != 'delete']

```
 The `construct_snippet_action_menu` hook is called after the menu items have been sorted by their order attributes, and so setting a menu item’s order will have no effect at this point. Instead, items can be reordered by changing their position in the list, with the first item being selected as the default action. For example, to change the default action to Delete:

 
```
@hooks.register('construct_snippet_action_menu')
def make_delete_default_action(menu_items, request, context):
    for (index, item) in enumerate(menu_items):
        if item.name == 'delete':
            # move to top of list
            menu_items.pop(index)
            menu_items.insert(0, item)
            break

```
   ### `register_snippet_listing_buttons`

 Add buttons to the actions list for a snippet in the snippets listing. This is useful when adding custom actions to the listing, such as translations or a complex workflow.

 This example will add a simple button to the listing:

 
```
from wagtail.snippets import widgets as wagtailsnippets_widgets

@hooks.register('register_snippet_listing_buttons')
def snippet_listing_buttons(snippet, user, next_url=None):
    yield wagtailsnippets_widgets.SnippetListingButton(
        'A page listing button',
        '/goes/to/a/url/',
        priority=10
    )

```
 The arguments passed to the hook are as follows:

 * `snippet` - the snippet object to generate the button for
* `user` - the user who is viewing the snippets listing
* `next_url` - the URL that the linked action should redirect back to on completion of the action, if the view supports it

 The `priority` argument controls the order the buttons are displayed in. Buttons are ordered from low to high priority, so a button with `priority=10` will be displayed before a button with `priority=20`.

   ### `construct_snippet_listing_buttons`

 Modify the final list of snippet listing buttons. The callable passed to this hook receives a list of `SnippetListingButton` objects, a user, and a context dictionary as per `register_snippet_listing_buttons`, and should modify the list of menu items in-place.

 
```
@hooks.register('construct_snippet_listing_buttons')
def remove_snippet_listing_button_item(buttons, snippet, user, context=None):
    buttons.pop()  # Removes the 'delete' button

```
    Bulk actions
------------

 Hooks for registering and customising bulk actions. See [Adding custom bulk actions](../extending/custom_bulk_actions#custom-bulk-actions) on how to write custom bulk actions.

  ### `register_bulk_action`

 Registers a new bulk action to add to the list of bulk actions in the explorer

 This hook must be registered with a sub-class of `BulkAction` . For example:

 
```
from wagtail.admin.views.bulk_action import BulkAction
from wagtail import hooks


@hooks.register("register_bulk_action")
class CustomBulkAction(BulkAction):
    display_name = _("Custom Action")
    action_type = "action"
    aria_label = _("Do custom action")
    template_name = "/path/to/template"
    models = [...]  # list of models the action should execute upon


    @classmethod
    def execute_action(cls, objects, **kwargs):
        for object in objects:
            do_something(object)
        return num_parent_objects, num_child_objects  # return the count of updated objects

```
   ### `before_bulk_action`

 Do something right before a bulk action is executed (before the `execute_action` method is called)

 This hook can be used to return an HTTP response. For example:

 
```
from wagtail import hooks

@hooks.register("before_bulk_action")
def hook_func(request, action_type, objects, action_class_instance):
  if action_type == 'delete':
    return HttpResponse(f"{len(objects)} objects would be deleted", content_type="text/plain")

```
   ### `after_bulk_action`

 Do something right after a bulk action is executed (after the `execute_action` method is called)

 This hook can be used to return an HTTP response. For example:

 
```
from wagtail import hooks

@hooks.register("after_bulk_action")
def hook_func(request, action_type, objects, action_class_instance):
  if action_type == 'delete':
    return HttpResponse(f"{len(objects)} objects have been deleted", content_type="text/plain")

```
    Audit log
---------

  ### `register_log_actions`

 See [Audit log](../extending/audit_log#audit-log)

 To add new actions to the registry, call the `register_action` method with the action type, its label and the message to be displayed in administrative listings.

 
```
from django.utils.translation import gettext_lazy as _

from wagtail import hooks

@hooks.register('register_log_actions')
def additional_log_actions(actions):
    actions.register_action('wagtail_package.echo', _('Echo'), _('Sent an echo'))

```
 Alternatively, for a log message that varies according to the log entry’s data, create a subclass of `wagtail.log_actions.LogFormatter` that overrides the `format_message` method, and use `register_action` as a decorator on that class:

 
```
from django.utils.translation import gettext_lazy as _

from wagtail import hooks
from wagtail.log_actions import LogFormatter

@hooks.register('register_log_actions')
def additional_log_actions(actions):
    @actions.register_action('wagtail_package.greet_audience')
    class GreetingActionFormatter(LogFormatter):
        label = _('Greet audience')

        def format_message(self, log_entry):
            return _('Hello %(audience)s') % {
                'audience': log_entry.data['audience'],
            }

```
  Changed in version 2.15: The `LogFormatter` class was introduced. Previously, dynamic messages were achieved by passing a callable as the `message` argument to `register_action`.

   

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/hooks.html>

Simple translation
==================

 The simple\_translation module provides a user interface that allows users to copy pages and translatable snippets into another language.

 * Copies are created in the source language (not translated)
* Copies of pages are in draft status

 Content editors need to translate the content and publish the pages.

  Note

 Simple Translation is optional. It can be switched out by third-party packages. Like the more advanced [`wagtail-localize`](https://github.com/wagtail/wagtail-localize).

   Basic configuration
-------------------

 Add `"wagtail.contrib.simple_translation"` to `INSTALLED_APPS` in your settings file:

 
```
INSTALLED_APPS = [
    ...
    "wagtail.contrib.simple_translation",
]

```
 Run `python manage.py migrate` to create the necessary permissions.

 In the Wagtail admin, go to settings and give some users or groups the “Can submit translations” permission.

   Page tree synchronisation
-------------------------

 Depending on your use case, it may be useful to keep the page trees in sync between different locales.

 You can enable this feature by setting `WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE` to `True`.

 
```
WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE = True

```
 When this feature is turned on, every time an editor creates a page, Wagtail creates an alias for that page under the page trees of all the other locales.

 For example, when an editor creates the page `"/en/blog/my-blog-post/"`, Wagtail creates an alias of that page at `"/fr/blog/my-blog-post/"` and `"/de/blog/my-blog-post/"`.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/simple_translation.html>

Typed table block
=================

 The `typed_table_block` module provides a StreamField block type for building tables consisting of mixed data types. Developers can specify a set of block types (such as `RichTextBlock` or `FloatBlock`) to be available as column types; page authors can then build up tables of any size by choosing column types from that list, in much the same way that they would insert blocks into a StreamField. Within each column, authors enter data using the standard editing control for that field (such as the Draftail editor for rich text cells).

  Installation
------------

 Add `"wagtail.contrib.typed_table_block"` to your `INSTALLED_APPS`:

 
```
INSTALLED_APPS = [
    ...
    "wagtail.contrib.typed_table_block",
]

```
   Usage
-----

 `TypedTableBlock` can be imported from the module `wagtail.contrib.typed_table_block.blocks` and used within a StreamField definition. Just like `StructBlock` and `StreamBlock`, it accepts a list of `(name, block_type)` tuples to use as child blocks:

 
```
from wagtail.contrib.typed_table_block.blocks import TypedTableBlock
from wagtail import blocks
from wagtail.images.blocks import ImageChooserBlock

class DemoStreamBlock(blocks.StreamBlock):
    title = blocks.CharBlock()
    paragraph = blocks.RichTextBlock()
    table = TypedTableBlock([
        ('text', blocks.CharBlock()),
        ('numeric', blocks.FloatBlock()),
        ('rich_text', blocks.RichTextBlock()),
        ('image', ImageChooserBlock())
    ])

```
 To keep the UI as simple as possible for authors, it’s generally recommended to use Wagtail’s basic built-in block types as column types, as above. However, all custom block types and parameters are supported. For example, to define a ‘country’ column type consisting of a dropdown of country choices:

 
```
table = TypedTableBlock([
    ('text', blocks.CharBlock()),
    ('numeric', blocks.FloatBlock()),
    ('rich_text', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
    ('country', ChoiceBlock(choices=[
        ('be', 'Belgium'),
        ('fr', 'France'),
        ('de', 'Germany'),
        ('nl', 'Netherlands'),
        ('pl', 'Poland'),
        ('uk', 'United Kingdom'),
    ])),
])

```
 On your page template, the `{% include_block %}` tag (called on either the individual block, or the StreamField value as a whole) will render any typed table blocks as an HTML `<table>` element.

 
```
{% load wagtailcore_tags %}

{% include_block page.body %}

```
 Or:

 
```
{% load wagtailcore_tags %}

{% for block in page.body %}
    {% if block.block_type == 'table' %}
        {% include_block block %}
    {% else %}
        {# rendering for other block types #}
    {% endif %}
{% endfor %}

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/typed_table_block.html>

Redirects
=========

 The `redirects` module provides the models and user interface for managing arbitrary redirection between urls and `Pages` or other urls.

  Installation
------------

 The `redirects` module is not enabled by default. To install it, add `wagtail.contrib.redirects` to `INSTALLED_APPS` and `wagtail.contrib.redirects.middleware.RedirectMiddleware` to `MIDDLEWARE` in your project’s Django settings file.

 
```
INSTALLED_APPS = [
    # ...

    'wagtail.contrib.redirects',
]

MIDDLEWARE = [
    # ...
    # all other django middlware first

    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
]

```
 This app contains migrations so make sure you run the `migrate` django-admin command after installing.

   Usage
-----

 Once installed, a new menu item called “Redirects” should appear in the “Settings” menu. This is where you can add arbitrary redirects to your site.

 For an editor’s guide to the interface, see .

   Automatic redirect creation
---------------------------

  New in version 2.16.

  Wagtail automatically creates permanent redirects for pages (and their descendants) when they are moved or their slug is changed. This helps to preserve SEO rankings of pages over time, and helps site visitors get to the right place when using bookmarks or using outdated links.

  ### Creating redirects for alternative page routes

 If your project uses `RoutablePageMixin` to create pages with alternative routes, you might want to consider overriding the `get_route_paths()` method for those page types. Adding popular route paths to this list will result in the creation of additional redirects; helping visitors to alternative routes to get to the right place also.

 For more information, please see :meth:`~wagtail.models.Page.get_route_paths`.

   ### Disabling automatic redirect creation

  New in version 4.0: When generating redirects, custom field values are now fetched as part of the initial database query, so using custom field values in overridden url methods will no longer trigger additional per-object queries.

  Wagtail’s default implementation works best for small-to-medium sized projects (5000 pages or fewer) that mostly use Wagtail’s built-in methods for URL generation.

 Overrides to the following `Page` methods are respected when generating redirects, but use of specific page fields in those overrides will trigger additional database queries.

 * [`get_url_parts()`](../pages/model_reference#wagtail.models.Page.get_url_parts "wagtail.models.Page.get_url_parts")
* [`get_route_paths()`](../pages/model_reference#wagtail.models.Page.get_route_paths "wagtail.models.Page.get_route_paths")

 If you find the feature is not a good fit for your project, you can disable it by adding the following to your project settings:

 
```
WAGTAILREDIRECTS_AUTO_CREATE = False

```
    Management commands
-------------------

  ### `import_redirects`

 
```
./manage.py import_redirects

```
 This command imports and creates redirects from a file supplied by the user.

 Options:

 

| Option | Description |
| --- | --- |
| **src** | This is the path to the file you wish to import redirects from. |
| **site** | This is the **site** for the site you wish to save redirects to. |
| **permanent** | If the redirects imported should be **permanent** (True) or not (False). It’s True by default. |
| **from** | The column index you want to use as redirect from value. |
| **to** | The column index you want to use as redirect to value. |
| **dry\_run** | Lets you run a import without doing any changes. |
| **ask** | Lets you inspect and approve each redirect before it is created. |

    The `Redirect` class
--------------------

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/redirects.html>

Legacy richtext
===============

 Provides the legacy richtext wrapper.

 Place `wagtail.contrib.legacy.richtext` before `wagtail` in `INSTALLED_APPS`.

 
```
INSTALLED_APPS = [
    ...
    "wagtail.contrib.legacy.richtext",
    "wagtail",
    ...
]

```
 The `{{ page.body|richtext }}` template filter will now render:

 
```
<div class="rich-text">...</div>

```


 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/contrib/legacy_richtext.html>

Signals
=======

 Wagtail’s [Revision](pages/model_reference#revision-model-ref) and [Page](pages/model_reference#page-model-ref) implement [Signals](https://docs.djangoproject.com/en/stable/topics/signals/ "(in Django v4.1)") from `django.dispatch`. Signals are useful for creating side-effects from page publish/unpublish events.

 For example, you could use signals to send publish notifications to a messaging service, or `POST` messages to another app that’s consuming the API, such as a static site generator.

  `page_published`
----------------

 This signal is emitted from a `Revision` when a page revision is set to `published`.

 * `sender` - The page `class`.
* `instance` - The specific `Page` instance.
* `revision` - The `Revision` that was published.
* `kwargs` - Any other arguments passed to `page_published.send()`.

 To listen to a signal, implement `page_published.connect(receiver, sender, **kwargs)`. Here’s a simple example showing how you might notify your team when something is published:

 
```
from wagtail.signals import page_published
import requests


# Let everyone know when a new page is published
def send_to_slack(sender, **kwargs):
    instance = kwargs['instance']
    url = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
    values = {
        "text" : "%s was published by %s " % (instance.title, instance.owner.username),
        "channel": "#publish-notifications",
        "username": "the squid of content",
        "icon_emoji": ":octopus:"
    }

    response = requests.post(url, values)

# Register a receiver
page_published.connect(send_to_slack)

```
  ### Receiving specific model events

 Sometimes you’re not interested in receiving signals for every model, or you want to handle signals for specific models in different ways. For instance, you may wish to do something when a new blog post is published:

 
```
from wagtail.signals import page_published
from mysite.models import BlogPostPage

# Do something clever for each model type
def receiver(sender, **kwargs):
    # Do something with blog posts
    pass

# Register listeners for each page model class
page_published.connect(receiver, sender=BlogPostPage)

```
 Wagtail provides access to a list of registered page types through the `get_page_models()` function in `wagtail.models`.

 Read the [Django documentation](https://docs.djangoproject.com/en/stable/topics/signals/ "(in Django v4.1)") for more information about specifying senders.

    `page_unpublished`
------------------

 This signal is emitted from a `Page` when the page is unpublished.

 * `sender` - The page `class`.
* `instance` - The specific `Page` instance.
* `kwargs` - Any other arguments passed to `page_unpublished.send()`

   
`pre_page_move` and `post_page_move`
-------------------------------------

 These signals are emitted from a `Page` immediately before and after it is moved.

 Subscribe to `pre_page_move` if you need to know values BEFORE any database changes are applied. For example: Getting the page’s previous URL, or that of its descendants.

 Subscribe to `post_page_move` if you need to know values AFTER database changes have been applied. For example: Getting the page’s new URL, or that of its descendants.

 The following arguments are emitted for both signals:

 * `sender` - The page `class`.
* `instance` - The specific `Page` instance.
* `parent_page_before` - The parent page of `instance` **before** moving.
* `parent_page_after` - The parent page of `instance` **after** moving.
* `url_path_before` - The value of `instance.url_path` **before** moving.
* `url_path_after` - The value of `instance.url_path` **after** moving.
* `kwargs` - Any other arguments passed to `pre_page_move.send()` or `post_page_move.send()`.

  ### Distinguishing between a ‘move’ and a ‘reorder’

 The signal can be emitted as a result of a page being moved to a different section (a ‘move’), or as a result of a page being moved to a different position within the same section (a ‘reorder’). Knowing the difference between the two can be particularly useful, because only a ‘move’ affects a page’s URL (and that of its descendants), whereas a ‘reorder’ only affects the natural page order; which is probably less impactful.

 The best way to distinguish between a ‘move’ and ‘reorder’ is to compare the `url_path_before` and `url_path_after` values. For example:

 
```
from wagtail.signals import pre_page_move
from wagtail.contrib.frontend_cache.utils import purge_page_from_cache

# Clear a page's old URLs from the cache when it moves to a different section
def clear_page_url_from_cache_on_move(sender, **kwargs):

    if kwargs['url_path_before'] == kwargs['url_path_after']:
        # No URLs are changing :) nothing to do here!
        return

    # The page is moving to a new section (possibly even a new site)
    # so clear old URL(s) from the cache
    purge_page_from_cache(kwargs['instance'])

# Register a receiver
pre_page_move.connect(clear_old_page_urls_from_cache)

```
    `page_slug_changed`
-------------------

 This signal is emitted from a `Page` when a change to its slug is published.

 The following arguments are emitted by this signal:

 * `sender` - The page `class`.
* `instance` - The updated (and saved), specific `Page` instance.
* `instance_before` - A copy of the specific `Page` instance from **before** the changes were saved.

   workflow\_submitted
-------------------

 This signal is emitted from a `WorkflowState` when a page is submitted to a workflow.

 * `sender` - `WorkflowState`
* `instance` - The specific `WorkflowState` instance.
* `user` - The user who submitted the workflow
* `kwargs` - Any other arguments passed to `workflow_submitted.send()`

   workflow\_rejected
------------------

 This signal is emitted from a `WorkflowState` when a page is rejected from a workflow.

 * `sender` - `WorkflowState`
* `instance` - The specific `WorkflowState` instance.
* `user` - The user who rejected the workflow
* `kwargs` - Any other arguments passed to `workflow_rejected.send()`

   workflow\_approved
------------------

 This signal is emitted from a `WorkflowState` when a page’s workflow completes successfully

 * `sender` - `WorkflowState`
* `instance` - The specific `WorkflowState` instance.
* `user` - The user who last approved the workflow
* `kwargs` - Any other arguments passed to `workflow_approved.send()`

   workflow\_cancelled
-------------------

 This signal is emitted from a `WorkflowState` when a page’s workflow is cancelled

 * `sender` - `WorkflowState`
* `instance` - The specific `WorkflowState` instance.
* `user` - The user who cancelled the workflow
* `kwargs` - Any other arguments passed to `workflow_cancelled.send()`

   task\_submitted
---------------

 This signal is emitted from a `TaskState` when a page is submitted to a task.

 * `sender` - `TaskState`
* `instance` - The specific `TaskState` instance.
* `user` - The user who submitted the page to the task
* `kwargs` - Any other arguments passed to `task_submitted.send()`

   task\_rejected
--------------

 This signal is emitted from a `TaskState` when a page is rejected from a task.

 * `sender` - `TaskState`
* `instance` - The specific `TaskState` instance.
* `user` - The user who rejected the task
* `kwargs` - Any other arguments passed to `task_rejected.send()`

   task\_approved
--------------

 This signal is emitted from a `TaskState` when a page’s task is approved

 * `sender` - `TaskState`
* `instance` - The specific `TaskState` instance.
* `user` - The user who approved the task
* `kwargs` - Any other arguments passed to `task_approved.send()`

   task\_cancelled
---------------

 This signal is emitted from a `TaskState` when a page’s task is cancelled.

 * `sender` - `TaskState`
* `instance` - The specific `TaskState` instance.
* `user` - The user who cancelled the task
* `kwargs` - Any other arguments passed to `task_cancelled.send()`

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/signals.html>

Settings
========

 Wagtail makes use of the following settings, in addition to [Django’s core settings](https://docs.djangoproject.com/en/stable/ref/settings/ "(in Django v4.1)")`:

  Sites
-----

  ### `WAGTAIL_SITE_NAME`

 
```
WAGTAIL_SITE_NAME = 'Stark Industries Skunkworks'

```
 This is the human-readable name of your Wagtail install which welcomes users upon login to the Wagtail admin.

   ### `WAGTAILADMIN_BASE_URL`

 
```
WAGTAILADMIN_BASE_URL = 'http://example.com'

```
 This is the base URL used by the Wagtail admin site. It is typically used for generating URLs to include in notification emails.

 If this setting is not present, Wagtail will try to fall back to `request.site.root_url` or to the request’s host name.

  Changed in version 3.0: This setting was previously named `BASE_URL` and was undocumented, using `BASE_URL` will be removed in a future release.

     Append Slash
------------

  ### `WAGTAIL_APPEND_SLASH`

 
```
# Don't add a trailing slash to Wagtail-served URLs
WAGTAIL_APPEND_SLASH = False

```
 Similar to Django’s `APPEND_SLASH`, this setting controls how Wagtail will handle requests that don’t end in a trailing slash.

 When `WAGTAIL_APPEND_SLASH` is `True` (default), requests to Wagtail pages which omit a trailing slash will be redirected by Django’s [`CommonMiddleware`](https://docs.djangoproject.com/en/stable/ref/middleware/#django.middleware.common.CommonMiddleware "(in Django v4.1)") to a URL with a trailing slash.

 When `WAGTAIL_APPEND_SLASH` is `False`, requests to Wagtail pages will be served both with and without trailing slashes. Page links generated by Wagtail, however, will not include trailing slashes.

  Note

 If you use the `False` setting, keep in mind that serving your pages both with and without slashes may affect search engines’ ability to index your site. See [this Google Search Central Blog post](https://developers.google.com/search/blog/2010/04/to-slash-or-not-to-slash) for more details.

     Search
------

  ### `WAGTAILSEARCH_BACKENDS`

 
```
WAGTAILSEARCH_BACKENDS = {
    'default': {
        'BACKEND': 'wagtail.search.backends.elasticsearch5',
        'INDEX': 'myapp'
    }
}

```
 Define a search backend. For a full explanation, see [Backends](../topics/search/backends#wagtailsearch-backends).

   ### `WAGTAILSEARCH_HITS_MAX_AGE`

 
```
WAGTAILSEARCH_HITS_MAX_AGE = 14

```
 Set the number of days (default 7) that search query logs are kept for; these are used to identify popular search terms for [promoted search results](contrib/searchpromotions#editors-picks). Queries older than this will be removed by the [rebuild\_references\_index](management_commands#search-garbage-collect) command.

    Internationalisation
--------------------

 Wagtail supports internationalisation of content by maintaining separate trees of pages for each language.

 For a guide on how to enable internationalisation on your site, see the [configuration guide](../advanced_topics/i18n#enabling-internationalisation).

  ### `WAGTAIL_I18N_ENABLED`

 (boolean, default `False`)

 When set to `True`, Wagtail’s internationalisation features will be enabled:

 
```
WAGTAIL_I18N_ENABLED = True

```
   ### `WAGTAIL_CONTENT_LANGUAGES`

 (list, default `[]`)

 A list of languages and/or locales that Wagtail content can be authored in.

 For example:

 
```
WAGTAIL_CONTENT_LANGUAGES = [
    ('en', _("English")),
    ('fr', _("French")),
]

```
 Each item in the list is a 2-tuple containing a language code and a display name. The language code can either be a language code on its own (such as `en`, `fr`), or it can include a region code (such as `en-gb`, `fr-fr`). You can mix the two formats if you only need to localize in some regions but not others.

 This setting follows the same structure of Django’s `LANGUAGES` setting, so they can both be set to the same value:

 
```
LANGUAGES = WAGTAIL_CONTENT_LANGUAGES = [
    ('en-gb', _("English (United Kingdom)")),
    ('en-us', _("English (United States)")),
    ('es-es', _("Spanish (Spain)")),
    ('es-mx', _("Spanish (Mexico)")),
]

```
 However having them separate allows you to configure many different regions on your site yet have them share Wagtail content (but defer on things like date formatting, currency, etc):

 
```
LANGUAGES = [
    ('en', _("English (United Kingdom)")),
    ('en-us', _("English (United States)")),
    ('es', _("Spanish (Spain)")),
    ('es-mx', _("Spanish (Mexico)")),
]


WAGTAIL_CONTENT_LANGUAGES = [
    ('en', _("English")),
    ('es', _("Spanish")),
]

```
 This would mean that your site will respond on the `https://www.mysite.com/es/` and `https://www.mysite.com/es-MX/` URLs, but both of them will serve content from the same “Spanish” tree in Wagtail.

  Note

 `WAGTAIL_CONTENT_LANGUAGES` must be a subset of `LANGUAGES`

 Note that all languages that exist in `WAGTAIL_CONTENT_LANGUAGES` must also exist in your `LANGUAGES` setting. This is so that Wagtail can generate a live URL to these pages from an untranslated context (such as the admin interface).

     Embeds
------

 Wagtail supports generating embed code from URLs to content on an external providers such as Youtube or Twitter. By default, Wagtail will fetch the embed code directly from the relevant provider’s site using the oEmbed protocol. Wagtail has a builtin list of the most common providers.

 The embeds fetching can be fully configured using the `WAGTAILEMBEDS_FINDERS` setting. This is fully documented in [Configuring embed “finders”](../advanced_topics/embeds#configuring-embed-finders).

  ### `WAGTAILEMBEDS_RESPONSIVE_HTML`

 
```
WAGTAILEMBEDS_RESPONSIVE_HTML = True

```
 Adds `class="responsive-object"` and an inline `padding-bottom` style to embeds, to assist in making them responsive. See [Responsive Embeds](../topics/writing_templates#responsive-embeds) for details.

    Dashboard
---------

  ### `WAGTAILADMIN_RECENT_EDITS_LIMIT`

 
```
WAGTAILADMIN_RECENT_EDITS_LIMIT = 5

```
 This setting lets you change the number of items shown at ‘Your most recent edits’ on the dashboard.

    General editing
---------------

  ### `WAGTAILADMIN_RICH_TEXT_EDITORS`

 
```
WAGTAILADMIN_RICH_TEXT_EDITORS = {
    'default': {
        'WIDGET': 'wagtail.admin.rich_text.DraftailRichTextArea',
        'OPTIONS': {
            'features': ['h2', 'bold', 'italic', 'link', 'document-link']
        }
    },
    'secondary': {
        'WIDGET': 'some.external.RichTextEditor',
    }
}

```
 Customise the behaviour of rich text fields. By default, `RichTextField` and `RichTextBlock` use the configuration given under the `'default'` key, but this can be overridden on a per-field basis through the `editor` keyword argument, for example `body = RichTextField(editor='secondary')`. Within each configuration block, the following fields are recognised:

 * `WIDGET`: The rich text widget implementation to use. Wagtail provides `wagtail.admin.rich_text.DraftailRichTextArea` (a modern extensible editor which enforces well-structured markup). Other widgets may be provided by third-party packages.
* `OPTIONS`: Configuration options to pass to the widget. Recognised options are widget-specific, but `DraftailRichTextArea` accept a `features` list indicating the active rich text features (see [Limiting features in a rich text field](../advanced_topics/customisation/page_editing_interface#rich-text-features)).

 If a `'default'` editor is not specified, rich text fields that do not specify an `editor` argument will use the Draftail editor with the default feature set enabled.

   ### `WAGTAILADMIN_EXTERNAL_LINK_CONVERSION`

 
```
WAGTAILADMIN_EXTERNAL_LINK_CONVERSION = 'exact'

```
 Customise Wagtail’s behaviour when an internal page url is entered in the external link chooser. Possible values for this setting are `'all'`, `'exact'`, `'confirm`, or `''`. The default, `'all'`, means that Wagtail will automatically convert submitted urls that exactly match page urls to the corresponding internal links. If the url is an inexact match - for example, the submitted url has query parameters - then Wagtail will confirm the conversion with the user. `'exact'` means that any inexact matches will be left as external urls, and the confirmation step will be skipped. `'confirm'` means that every link conversion will be confirmed with the user, even if the match is exact. `''` means that Wagtail will not attempt to convert any urls entered to internal page links.

   ### 
`WAGTAIL_DATE_FORMAT`, `WAGTAIL_DATETIME_FORMAT`, `WAGTAIL_TIME_FORMAT`

 
```
WAGTAIL_DATE_FORMAT = '%d.%m.%Y.'
WAGTAIL_DATETIME_FORMAT = '%d.%m.%Y. %H:%M'
WAGTAIL_TIME_FORMAT = '%H:%M'

```
 Specifies the date, time and datetime format to be used in input fields in the Wagtail admin. The format is specified in [Python datetime module syntax](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior), and must be one of the recognised formats listed in the `DATE_INPUT_FORMATS`, `TIME_INPUT_FORMATS`, or `DATETIME_INPUT_FORMATS` setting respectively (see [DATE\_INPUT\_FORMATS](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DATE_INPUT_FORMATS)).

    Page editing
------------

  ### `WAGTAILADMIN_COMMENTS_ENABLED`

 
```
# Disable commenting
WAGTAILADMIN_COMMENTS_ENABLED = False

```
 Sets whether commenting is enabled for pages (`True` by default).

   ### `WAGTAIL_ALLOW_UNICODE_SLUGS`

 
```
WAGTAIL_ALLOW_UNICODE_SLUGS = True

```
 By default, page slugs can contain any alphanumeric characters, including non-Latin alphabets. Set this to False to limit slugs to ASCII characters.

   ### `WAGTAIL_AUTO_UPDATE_PREVIEW`

 
```
WAGTAIL_AUTO_UPDATE_PREVIEW = True

```
 When enabled, the preview panel in the page editor is automatically updated on each change. If set to `False`, a refresh button will be shown and the preview is only updated when the button is clicked. This behaviour is enabled by default.

 To completely disable the preview panel, set [preview modes](pages/model_reference#wagtail.models.Page.preview_modes "wagtail.models.Page.preview_modes") to be empty on your model `preview_modes = []`.

   ### `WAGTAIL_AUTO_UPDATE_PREVIEW_INTERVAL`

 
```
WAGTAIL_AUTO_UPDATE_PREVIEW_INTERVAL = 500

```
 The interval (in milliseconds) to check for changes made in the page editor before updating the preview. The default value is `500`.

   ### `WAGTAILADMIN_GLOBAL_PAGE_EDIT_LOCK`

 `WAGTAILADMIN_GLOBAL_PAGE_EDIT_LOCK` can be set to `True` to prevent users from editing pages that they have locked.

   ### `WAGTAILADMIN_UNSAFE_PAGE_DELETION_LIMIT`

 
```
WAGTAILADMIN_UNSAFE_PAGE_DELETION_LIMIT = 20

```
 This setting enables an additional confirmation step when deleting a page with a large number of child pages. If the number of pages is greater than or equal to this limit (10 by default), the user must enter the site name (as defined by `WAGTAIL_SITE_NAME`) to proceed.

    Images
------

  ### `WAGTAILIMAGES_IMAGE_MODEL`

 
```
WAGTAILIMAGES_IMAGE_MODEL = 'myapp.MyImage'

```
 This setting lets you provide your own image model for use in Wagtail, which should extend the built-in `AbstractImage` class.

   ### `WAGTAILIMAGES_IMAGE_FORM_BASE`

 
```
WAGTAILIMAGES_IMAGE_FORM_BASE = 'myapp.forms.MyImageBaseForm'

```
 This setting lets you provide your own image base form for use in Wagtail, which should extend the built-in `BaseImageForm` class. You can use it to specify or override the widgets to use in the admin form.

   ### `WAGTAILIMAGES_MAX_UPLOAD_SIZE`

 
```
WAGTAILIMAGES_MAX_UPLOAD_SIZE = 20 * 1024 * 1024  # 20MB

```
 This setting lets you override the maximum upload size for images (in bytes). If omitted, Wagtail will fall back to using its 10MB default value.

   ### `WAGTAILIMAGES_MAX_IMAGE_PIXELS`

 
```
WAGTAILIMAGES_MAX_IMAGE_PIXELS = 128000000  # 128 megapixels

```
 This setting lets you override the maximum number of pixels an image can have. If omitted, Wagtail will fall back to using its 128 megapixels default value. The pixel count takes animation frames into account - for example, a 25-frame animation of size 100x100 is considered to have 100 \_ 100 \_ 25 = 250000 pixels.

   ### `WAGTAILIMAGES_FEATURE_DETECTION_ENABLED`

 
```
WAGTAILIMAGES_FEATURE_DETECTION_ENABLED = True

```
 This setting enables feature detection once OpenCV is installed, see all details on the [Feature Detection](../advanced_topics/images/feature_detection#image-feature-detection) documentation.

   ### `WAGTAILIMAGES_INDEX_PAGE_SIZE`

 
```
WAGTAILIMAGES_INDEX_PAGE_SIZE = 20

```
 Specifies the number of images per page shown on the main Images listing in the Wagtail admin.

   ### `WAGTAILIMAGES_USAGE_PAGE_SIZE`

 
```
WAGTAILIMAGES_USAGE_PAGE_SIZE = 20

```
 Specifies the number of items per page shown when viewing an image’s usage.

   ### `WAGTAILIMAGES_CHOOSER_PAGE_SIZE`

 
```
WAGTAILIMAGES_CHOOSER_PAGE_SIZE = 12

```
 Specifies the number of images shown per page in the image chooser modal.

   ### `WAGTAILIMAGES_RENDITION_STORAGE`

 
```
WAGTAILIMAGES_RENDITION_STORAGE = 'myapp.backends.MyCustomStorage'

```
 This setting allows image renditions to be stored using an alternative storage backend. The default is `None`, which will use Django’s default `FileSystemStorage`.

 Custom storage classes should subclass `django.core.files.storage.Storage`. See the [Django file storage API](https://docs.djangoproject.com/en/stable/ref/files/storage/ "(in Django v4.1)").

    Documents
---------

  ### `WAGTAILDOCS_DOCUMENT_MODEL`

 
```
WAGTAILDOCS_DOCUMENT_MODEL = 'myapp.MyDocument'

```
 This setting lets you provide your own document model for use in Wagtail, which should extend the built-in `AbstractDocument` class.

   ### `WAGTAILDOCS_DOCUMENT_FORM_BASE`

 
```
WAGTAILDOCS_DOCUMENT_FORM_BASE = 'myapp.forms.MyDocumentBaseForm'

```
 This setting lets you provide your own Document base form for use in Wagtail, which should extend the built-in `BaseDocumentForm` class. You can use it to specify or override the widgets to use in the admin form.

   ### `WAGTAILDOCS_SERVE_METHOD`

 
```
WAGTAILDOCS_SERVE_METHOD = 'redirect'

```
 Determines how document downloads will be linked to and served. Normally, requests for documents are sent through a Django view, to perform privacy checks (see ) and potentially other housekeeping tasks such as hit counting. To fully protect against users bypassing this check, it needs to happen in the same request where the document is served; however, this incurs a performance hit as the document then needs to be served by the Django server. In particular, this cancels out much of the benefit of hosting documents on external storage, such as S3 or a CDN.

 For this reason, Wagtail provides a number of serving methods which trade some of the strictness of the permission check for performance:

 * `'direct'` - links to documents point directly to the URL provided by the underlying storage, bypassing the Django view that provides the permission check. This is most useful when deploying sites as fully static HTML (for example using [wagtail-bakery](https://github.com/wagtail/wagtail-bakery) or [Gatsby](https://www.gatsbyjs.org/)).
* `'redirect'` - links to documents point to a Django view which will check the user’s permission; if successful, it will redirect to the URL provided by the underlying storage to allow the document to be downloaded. This is most suitable for remote storage backends such as S3, as it allows the document to be served independently of the Django server. Note that if a user is able to guess the latter URL, they will be able to bypass the permission check; some storage backends may provide configuration options to generate a random or short-lived URL to mitigate this.
* `'serve_view'` - links to documents point to a Django view which both checks the user’s permission, and serves the document. Serving will be handled by [django-sendfile](https://github.com/johnsensible/django-sendfile), if this is installed and supported by your server configuration, or as a streaming response from Django if not. When using this method, it is recommended that you configure your webserver to *disallow* serving documents directly from their location under `MEDIA_ROOT`, as this would provide a way to bypass the permission check.

 If `WAGTAILDOCS_SERVE_METHOD` is unspecified or set to `None`, the default method is `'redirect'` when a remote storage backend is in use (one that exposes a URL but not a local filesystem path), and `'serve_view'` otherwise. Finally, some storage backends may not expose a URL at all; in this case, serving will proceed as for `'serve_view'`.

   ### `WAGTAILDOCS_CONTENT_TYPES`

 
```
WAGTAILDOCS_CONTENT_TYPES = {
    'pdf': 'application/pdf',
    'txt': 'text/plain',
}

```
 Specifies the MIME content type that will be returned for the given file extension, when using the `serve_view` method. Content types not listed here will be guessed using the Python `mimetypes.guess_type` function, or `application/octet-stream` if unsuccessful.

   ### `WAGTAILDOCS_INLINE_CONTENT_TYPES`

 
```
WAGTAILDOCS_INLINE_CONTENT_TYPES = ['application/pdf', 'text/plain']

```
 A list of MIME content types that will be shown inline in the browser (by serving the HTTP header `Content-Disposition: inline`) rather than served as a download, when using the `serve_view` method. Defaults to `application/pdf`.

   ### `WAGTAILDOCS_EXTENSIONS`

 
```
WAGTAILDOCS_EXTENSIONS = ['pdf', 'docx']

```
 A list of allowed document extensions that will be validated during document uploading. If this isn’t supplied all document extensions are allowed. Warning: this doesn’t always ensure that the uploaded file is valid as files can be renamed to have an extension no matter what data they contain.

    User Management
---------------

  ### `WAGTAIL_PASSWORD_MANAGEMENT_ENABLED`

 
```
WAGTAIL_PASSWORD_MANAGEMENT_ENABLED = True

```
 This specifies whether users are allowed to change their passwords (enabled by default).

   ### `WAGTAIL_PASSWORD_RESET_ENABLED`

 
```
WAGTAIL_PASSWORD_RESET_ENABLED = True

```
 This specifies whether users are allowed to reset their passwords. Defaults to the same as `WAGTAIL_PASSWORD_MANAGEMENT_ENABLED`. Password reset emails will be sent from the address specified in Django’s `DEFAULT_FROM_EMAIL` setting.

   ### `WAGTAILUSERS_PASSWORD_ENABLED`

 
```
WAGTAILUSERS_PASSWORD_ENABLED = True

```
 This specifies whether password fields are shown when creating or editing users through Settings -> Users (enabled by default). Set this to False (along with `WAGTAIL_PASSWORD_MANAGEMENT_ENABLED` and `WAGTAIL_PASSWORD_RESET_ENABLED`) if your users are authenticated through an external system such as LDAP.

   ### `WAGTAILUSERS_PASSWORD_REQUIRED`

 
```
WAGTAILUSERS_PASSWORD_REQUIRED = True

```
 This specifies whether password is a required field when creating a new user. True by default; ignored if `WAGTAILUSERS_PASSWORD_ENABLED` is false. If this is set to False, and the password field is left blank when creating a user, then that user will have no usable password; in order to log in, they will have to reset their password (if `WAGTAIL_PASSWORD_RESET_ENABLED` is True) or use an alternative authentication system such as LDAP (if one is set up).

   ### `WAGTAIL_EMAIL_MANAGEMENT_ENABLED`

 
```
WAGTAIL_EMAIL_MANAGEMENT_ENABLED = True

```
 This specifies whether users are allowed to change their email (enabled by default).

   ### `WAGTAILADMIN_USER_PASSWORD_RESET_FORM`

 
```
WAGTAILADMIN_USER_PASSWORD_RESET_FORM = 'users.forms.PasswordResetForm'

```
 Allows the default `PasswordResetForm` to be extended with extra fields.

   ### `WAGTAIL_USER_EDIT_FORM`

 
```
WAGTAIL_USER_EDIT_FORM = 'users.forms.CustomUserEditForm'

```
 Allows the default `UserEditForm` class to be overridden with a custom form when a custom user model is being used and extra fields are required in the user edit form.

 For further information See [Custom user models](../advanced_topics/customisation/custom_user_models).

   ### `WAGTAIL_USER_CREATION_FORM`

 
```
WAGTAIL_USER_CREATION_FORM = 'users.forms.CustomUserCreationForm'

```
 Allows the default `UserCreationForm` class to be overridden with a custom form when a custom user model is being used and extra fields are required in the user creation form.

 For further information See [Custom user models](../advanced_topics/customisation/custom_user_models).

   ### `WAGTAIL_USER_CUSTOM_FIELDS`

 
```
WAGTAIL_USER_CUSTOM_FIELDS = ['country']

```
 A list of the extra custom fields to be appended to the default list.

 For further information See [Custom user models](../advanced_topics/customisation/custom_user_models).

   ### `WAGTAILADMIN_USER_LOGIN_FORM`

 
```
WAGTAILADMIN_USER_LOGIN_FORM = 'users.forms.LoginForm'

```
 Allows the default `LoginForm` to be extended with extra fields.

    User preferences
----------------

  ### `WAGTAIL_GRAVATAR_PROVIDER_URL`

 
```
WAGTAIL_GRAVATAR_PROVIDER_URL = '//www.gravatar.com/avatar'

```
 If a user has not uploaded a profile picture, Wagtail will look for an avatar linked to their email address on gravatar.com. This setting allows you to specify an alternative provider such as like robohash.org, or can be set to `None` to disable the use of remote avatars completely.

   ### `WAGTAIL_USER_TIME_ZONES`

 Logged-in users can choose their current time zone for the admin interface in the account settings. If is no time zone selected by the user, then `TIME_ZONE` will be used. (Note that time zones are only applied to datetime fields, not to plain time or date fields. This is a Django design decision.)

 The list of time zones is by default the common\_timezones list from pytz. It is possible to override this list via the `WAGTAIL_USER_TIME_ZONES` setting. If there is zero or one time zone permitted, the account settings form will be hidden.

 
```
WAGTAIL_USER_TIME_ZONES = ['America/Chicago', 'Australia/Sydney', 'Europe/Rome']

```
   ### `WAGTAILADMIN_PERMITTED_LANGUAGES`

 Users can choose between several languages for the admin interface in the account settings. The list of languages is by default all the available languages in Wagtail with at least 90% coverage. To change it, set `WAGTAILADMIN_PERMITTED_LANGUAGES`:

 
```
WAGTAILADMIN_PERMITTED_LANGUAGES = [('en', 'English'),
                                    ('pt', 'Portuguese')]

```
 Since the syntax is the same as Django `LANGUAGES`, you can do this so users can only choose between front office languages:

 
```
LANGUAGES = WAGTAILADMIN_PERMITTED_LANGUAGES = [('en', 'English'), ('pt', 'Portuguese')]

```
    Email notifications
-------------------

  ### `WAGTAILADMIN_NOTIFICATION_FROM_EMAIL`

 
```
WAGTAILADMIN_NOTIFICATION_FROM_EMAIL = 'wagtail@myhost.io'

```
 Wagtail sends email notifications when content is submitted for moderation, and when the content is accepted or rejected. This setting lets you pick which email address these automatic notifications will come from. If omitted, Wagtail will fall back to using Django’s `DEFAULT_FROM_EMAIL` setting.

   ### `WAGTAILADMIN_NOTIFICATION_USE_HTML`

 
```
WAGTAILADMIN_NOTIFICATION_USE_HTML = True

```
 Notification emails are sent in `text/plain` by default, change this to use HTML formatting.

   ### `WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS`

 
```
WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS = False

```
 Notification emails are sent to moderators and superusers by default. You can change this to exclude superusers and only notify moderators.

    Wagtail update notifications
----------------------------

  ### `WAGTAIL_ENABLE_UPDATE_CHECK`

 
```
WAGTAIL_ENABLE_UPDATE_CHECK = True

```
 For admins only, Wagtail performs a check on the dashboard to see if newer releases are available. This also provides the Wagtail team with the hostname of your Wagtail site. If you’d rather not receive update notifications, or if you’d like your site to remain unknown, you can disable it with this setting.

 If admins should only be informed of new long term support (LTS) versions, then set this setting to `"lts"` (the setting is case-insensitive).

   ### `WAGTAIL_ENABLE_WHATS_NEW_BANNER`

 
```
WAGTAIL_ENABLE_WHATS_NEW_BANNER = True

```
 For new releases, Wagtail may show a notification banner on the dashboard that helps users learn more about the UI changes and new features in the release. Users are able to dismiss this banner, which will hide it until the next release. If you’d rather not show these banners, you can disable it with this setting.

    Frontend authentication
-----------------------

  ### `PASSWORD_REQUIRED_TEMPLATE`

 
```
PASSWORD_REQUIRED_TEMPLATE = 'myapp/password_required.html'

```
 This is the path to the Django template which will be used to display the “password required” form when a user accesses a private page. For more details, see the [Private pages](../advanced_topics/privacy#private-pages) documentation.

   ### `DOCUMENT_PASSWORD_REQUIRED_TEMPLATE`

 
```
DOCUMENT_PASSWORD_REQUIRED_TEMPLATE = 'myapp/document_password_required.html'

```
 As above, but for password restrictions on documents. For more details, see the [Private pages](../advanced_topics/privacy#private-pages) documentation.

   ### `WAGTAIL_FRONTEND_LOGIN_TEMPLATE`

 The basic login page can be customised with a custom template.

 
```
WAGTAIL_FRONTEND_LOGIN_TEMPLATE = 'myapp/login.html'

```
   ### `WAGTAIL_FRONTEND_LOGIN_URL`

 Or the login page can be a redirect to an external or internal URL.

 
```
WAGTAIL_FRONTEND_LOGIN_URL = '/accounts/login/'

```
 For more details, see the [Setting up a login page](../advanced_topics/privacy#login-page) documentation.

    Tags
----

  ### `TAGGIT_CASE_INSENSITIVE`

 
```
TAGGIT_CASE_INSENSITIVE = True

```
 Tags are case-sensitive by default (‘music’ and ‘Music’ are treated as distinct tags). In many cases the reverse behaviour is preferable.

   ### `TAG_SPACES_ALLOWED`

 
```
TAG_SPACES_ALLOWED = False

```
 Tags can only consist of a single word, no spaces allowed. The default setting is `True` (spaces in tags are allowed).

   ### `TAG_LIMIT`

 
```
TAG_LIMIT = 5

```
 Limit the number of tags that can be added to (django-taggit) Tag model. Default setting is `None`, meaning no limit on tags.

    Static files
------------

  ### `WAGTAILADMIN_STATIC_FILE_VERSION_STRINGS`

 
```
WAGTAILADMIN_STATIC_FILE_VERSION_STRINGS = False

```
 Static file URLs within the Wagtail admin are given a version-specific query string of the form `?v=1a2b3c4d`, to prevent outdated cached copies of JavaScript and CSS files from persisting after a Wagtail upgrade. To disable these, set `WAGTAILADMIN_STATIC_FILE_VERSION_STRINGS` to `False`.

    API
---

 For full documentation on API configuration, including these settings, see [Wagtail API v2 Configuration Guide](../advanced_topics/api/v2/configuration#api-v2-configuration) documentation.

  ### `WAGTAILAPI_BASE_URL`

 
```
WAGTAILAPI_BASE_URL = 'http://api.example.com/'

```
 Required when using frontend cache invalidation, used to generate absolute URLs to document files and invalidating the cache.

   ### `WAGTAILAPI_LIMIT_MAX`

 
```
WAGTAILAPI_LIMIT_MAX = 500

```
 Default is 20, used to change the maximum number of results a user can request at a time, set to `None` for no limit.

   ### `WAGTAILAPI_SEARCH_ENABLED`

 
```
WAGTAILAPI_SEARCH_ENABLED = False

```
 Default is true, setting this to false will disable full text search on all endpoints.

   ### `WAGTAILAPI_USE_FRONTENDCACHE`

 
```
WAGTAILAPI_USE_FRONTENDCACHE = True

```
 Requires `wagtailfrontendcache` app to be installed, indicates the API should use the frontend cache.

    Frontend cache
--------------

 For full documentation on frontend cache invalidation, including these settings, see [Frontend cache invalidator](contrib/frontendcache#frontend-cache-purging).

  ### `WAGTAILFRONTENDCACHE`

 
```
  WAGTAILFRONTENDCACHE = {
      'varnish': {
          'BACKEND': 'wagtail.contrib.frontend_cache.backends.HTTPBackend',
          'LOCATION': 'http://localhost:8000',
      },
  }

```
 See documentation linked above for full options available.

  Note

 `WAGTAILFRONTENDCACHE_LOCATION` is no longer the preferred way to set the cache location, instead set the `LOCATION` within the `WAGTAILFRONTENDCACHE` item.

    ### `WAGTAILFRONTENDCACHE_LANGUAGES`

 
```
WAGTAILFRONTENDCACHE_LANGUAGES = [l[0] for l in settings.LANGUAGES]

```
 Default is an empty list, must be a list of languages to also purge the urls for each language of a purging url. This setting needs `settings.USE_I18N` to be `True` to work.

    Redirects
---------

  ### `WAGTAIL_REDIRECTS_FILE_STORAGE`

 
```
WAGTAIL_REDIRECTS_FILE_STORAGE = 'tmp_file'

```
 By default the redirect importer keeps track of the uploaded file as a temp file, but on certain environments (load balanced/cloud environments), you cannot keep a shared file between environments. For those cases you can use the built-in cache to store the file instead.

 
```
WAGTAIL_REDIRECTS_FILE_STORAGE = 'cache'

```
    Form builder
------------

  ### `WAGTAILFORMS_HELP_TEXT_ALLOW_HTML`

 
```
WAGTAILFORMS_HELP_TEXT_ALLOW_HTML = True

```
 When true, HTML tags in form field help text will be rendered unescaped (default: False).

  Warning

 Enabling this option will allow editors to insert arbitrary HTML into the page, such as scripts that could allow the editor to acquire administrator privileges when another administrator views the page. Do not enable this setting unless your editors are fully trusted.

     Workflow
--------

  ### `WAGTAIL_MODERATION_ENABLED`

 
```
WAGTAIL_MODERATION_ENABLED = True

```
 Changes whether the Submit for Moderation button is displayed in the action menu.

   ### `WAGTAIL_WORKFLOW_ENABLED`

 
```
WAGTAIL_WORKFLOW_ENABLED = False

```
 Specifies whether moderation workflows are enabled (default: True). When disabled, editors will no longer be given the option to submit pages to a workflow, and the settings areas for admins to configure workflows and tasks will be unavailable.

   ### `WAGTAIL_WORKFLOW_REQUIRE_REAPPROVAL_ON_EDIT`

 
```
WAGTAIL_WORKFLOW_REQUIRE_REAPPROVAL_ON_EDIT = True

```
 Moderation workflows can be used in two modes. The first is to require that all tasks must approve a specific page revision for the workflow to complete. As a result, if edits are made to a page while it is in moderation, any approved tasks will need to be re-approved for the new revision before the workflow finishes. This is the default, `WAGTAIL_WORKFLOW_REQUIRE_REAPPROVAL_ON_EDIT = True` . The second mode does not require reapproval: if edits are made when tasks have already been approved, those tasks do not need to be reapproved. This is more suited to a hierarchical workflow system. To use workflows in this mode, set `WAGTAIL_WORKFLOW_REQUIRE_REAPPROVAL_ON_EDIT = False`.

   ### `WAGTAIL_FINISH_WORKFLOW_ACTION`

 
```
WAGTAIL_FINISH_WORKFLOW_ACTION = 'wagtail.workflows.publish_workflow_state'

```
 This sets the function to be called when a workflow completes successfully - by default, `wagtail.workflows.publish_workflow_state`,which publishes the page. The function must accept a `WorkflowState` object as its only positional argument.

   ### `WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH`

 
```
WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH = True

```
 This determines whether publishing a page with an ongoing workflow will cancel the workflow (if true) or leave the workflow unaffected (false). Disabling this could be useful if your site has long, multi-step workflows, and you want to be able to publish urgent page updates while the workflow continues to provide less urgent feedback.

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/settings.html>

The project template
====================

 
```
mysite/
    home/
        migrations/
            __init__.py
            0001_initial.py
            0002_create_homepage.py
        templates/
            home/
                home_page.html
        __init__.py
        models.py
    search/
        templates/
            search/
                search.html
        __init__.py
        views.py
    mysite/
        settings/
            __init__.py
            base.py
            dev.py
            production.py
        static/
            css/
                mysite.css
            js/
                mysite.js
        templates/
            404.html
            500.html
            base.html
        __init__.py
        urls.py
        wsgi.py
    Dockerfile
    manage.py
    requirements.txt

```
  The “home” app
--------------

 Location: `/mysite/home/`

 This app is here to help get you started quicker by providing a `HomePage` model with migrations to create one when you first set up your app.

   Default templates and static files
----------------------------------

 Location: `/mysite/mysite/templates/` and `/mysite/mysite/static/`

 The templates directory contains `base.html`, `404.html` and `500.html`. These files are very commonly needed on Wagtail sites to they have been added into the template.

 The static directory contains an empty JavaScript and CSS file.

   Django settings
---------------

 Location: `/mysite/mysite/settings/`

 The Django settings files are split up into `base.py`, `dev.py`, `production.py` and `local.py`.

 * `base.py` This file is for global settings that will be used in both development and production. Aim to keep most of your configuration in this file.
* `dev.py` This file is for settings that will only be used by developers. For example: `DEBUG = True`
* `production.py` This file is for settings that will only run on a production server. For example: `DEBUG = False`
* `local.py` This file is used for settings local to a particular machine. This file should never be tracked by a version control system.

  Note

 On production servers, we recommend that you only store secrets in `local.py` (such as API keys and passwords). This can save you headaches in the future if you are ever trying to debug why a server is behaving badly. If you are using multiple servers which need different settings then we recommend that you create a different `production.py` file for each one.

    Dockerfile
----------

 Location: `/mysite/Dockerfile`

 Contains configuration for building and deploying the site as a [Docker](https://docs.docker.com/) container. To build and use the Docker image for your project, run:

 
```
docker build -t mysite .
docker run -p 8000:8000 mysite

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/project_template.html>

Jinja2 template support
=======================

 Wagtail supports Jinja2 templating for all front end features. More information on each of the template tags below can be found in the [Writing templates](../topics/writing_templates#writing-templates) documentation.

  Configuring Django
------------------

 Django needs to be configured to support Jinja2 templates. As the Wagtail admin is written using standard Django templates, Django has to be configured to use **both** templating engines. Add the Jinja2 template backend configuration to the `TEMPLATES` setting for your app as shown here:

 
```
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        # ... the rest of the existing Django template configuration ...
    },
    {
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        'APP_DIRS': True,
        'OPTIONS': {
            'extensions': [
                'wagtail.jinja2tags.core',
                'wagtail.admin.jinja2tags.userbar',
                'wagtail.images.jinja2tags.images',
            ],
        },
    }
]

```
 Jinja templates must be placed in a `jinja2/` directory in your app. For example, the standard template location for an `EventPage` model in an `events` app would be `events/jinja2/events/event_page.html`.

 By default, the Jinja environment does not have any Django functions or filters. The Django documentation has more information on [`django.template.backends.jinja2.Jinja2`](https://docs.djangoproject.com/en/stable/topics/templates/#django.template.backends.jinja2.Jinja2 "(in Django v4.1)") (configuring Jinja for Django).

   
`self` in templates
--------------------

 In Django templates, `self` can be used to refer to the current page, stream block, or field panel. In Jinja, `self` is reserved for internal use. When writing Jinja templates, use `page` to refer to pages, `value` for stream blocks, and `field_panel` for field panels.

   Template tags, functions & filters
----------------------------------

  ### `pageurl()`

 Generate a URL for a Page instance:

 
```
<a href="{{ pageurl(page.more_information) }}">More information</a>

```
 See [pageurl](../topics/writing_templates#pageurl-tag) for more information

   ### `slugurl()`

 Generate a URL for a Page with a slug:

 
```
<a href="{{ slugurl("about") }}">About us</a>

```
 See [slugurl](../topics/writing_templates#slugurl-tag) for more information

   ### `image()`

 Resize an image, and print an `<img>` tag:

 
```
{# Print an image tag #}
{{ image(page.header_image, "fill-1024x200", class="header-image") }}

{# Resize an image #}
{% set background=image(page.background_image, "max-1024x1024") %}
<div class="wrapper" style="background-image: url({{ background.url }});">

```
 See [How to use images in templates](../topics/images#image-tag) for more information

   ### `|richtext`

 Transform Wagtail’s internal HTML representation, expanding internal references to pages and images.

 
```
{{ page.body|richtext }}

```
 See [Rich text (filter)](../topics/writing_templates#rich-text-filter) for more information

   ### `wagtail_site`

 Returns the Site object corresponding to the current request.

 
```
{{ wagtail_site().site_name }}

```
 See [wagtail\_site](../topics/writing_templates#wagtail-site-tag) for more information

   ### `wagtailuserbar()`

 Output the Wagtail contextual flyout menu for editing pages from the front end

 
```
{{ wagtailuserbar() }}

```
 See [Wagtail User Bar](../topics/writing_templates#wagtailuserbar-tag) for more information

   ### `{% include_block %}`

 Output the HTML representation for the stream content as a whole, as well as for each individual block.

 Allows to pass template context (by default) to the StreamField template.

 
```
{% include_block page.body %}
{% include_block page.body with context %} {# The same as the previous #}
{% include_block page.body without context %}

```
 See [StreamField template rendering](../topics/streamfield#streamfield-template-rendering) for more information.

  Note

 The `{% include_block %}` tag is designed to closely follow the syntax and behaviour of Jinja’s `{% include %}`, so it does not implement the Django version’s feature of only passing specified variables into the context.

   

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/jinja2.html>

Panel API
=========

  `Panel`
-------

   `BoundPanel`
------------

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/panel_api.html>

Viewsets
========

 Viewsets are Wagtail’s mechanism for defining a group of related admin views with shared properties, as a single unit. See [Generic views](../extending/generic_views).

  ViewSet
-------

   ModelViewSet
------------

   ChooserViewSet
--------------

   SnippetViewSet
--------------

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/reference/viewsets.html>

Support
=======

 If you have any problems or questions about working with Wagtail, you are invited to visit any of the following support channels, where volunteer members of the Wagtail community will be happy to assist.

 **Please respect the time and effort of volunteers, by not asking the same question in multiple places.** At best, you’ll be spamming the same set of people each time; at worst, you’ll waste the effort of volunteers who spend time answering a question unaware that it has already been answered elsewhere. If you absolutely must ask a question on multiple forums, post it on Stack Overflow first and include the Stack Overflow link in your subsequent posts.

  Stack Overflow
--------------

 [Stack Overflow](https://stackoverflow.com/questions/tagged/wagtail) is the best place to find answers to your questions about working with Wagtail - there is an active community of Wagtail users and developers responding to questions there. When posting questions, please read Stack Overflow’s advice on [how to ask questions](https://stackoverflow.com/help/how-to-ask) and remember to tag your question with “wagtail”.

   Mailing list
------------

 For topics and discussions that do not fit Stack Overflow’s question-and-answer format, there is a Wagtail Support mailing list at [groups.google.com/d/forum/wagtail](https://groups.google.com/g/wagtail).

   Slack
-----

 The Wagtail Slack workspace is open to all users and developers of Wagtail. To join, head to: <https://wagtail.org/slack/>

 Please use the **#support** channel for support questions. Support is provided by members of the Wagtail community on a voluntary basis, and we cannot guarantee that questions will be answered quickly (or at all). If you want to see this resource succeed, please consider sticking around to help out! Also, please keep in mind that many of Wagtail’s core and expert developers prefer to handle support queries on a non-realtime basis through Stack Overflow, and questions asked there may well get a better response.

   GitHub discussions
------------------

 Our [Github discussion boards](https://github.com/wagtail/wagtail/discussions) are open for sharing ideas and plans for the Wagtail project.

   Issues
------

 If you think you’ve found a bug in Wagtail, or you’d like to suggest a new feature, please check the current list at [github.com/wagtail/wagtail/issues](https://github.com/wagtail/wagtail/issues). If your bug or suggestion isn’t there, raise a new issue, providing as much relevant context as possible.

 If your bug report is a security issue, **do not** report it with an issue. Please read our guide to [reporting security issues](contributing/security).

   Torchbox
--------

 Finally, if you have a query which isn’t relevant for any of the above forums, feel free to contact the Wagtail team at Torchbox directly, on [hello@wagtail.org](mailto:hello%40wagtail.org) or [@wagtailcms](https://twitter.com/wagtailcms).

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/support.html>

Using Wagtail: an Editor’s guide
================================

 Wagtail’s Editor Guide now has its own website: [guide.wagtail.org](https://guide.wagtail.org/). This guide is written for the users of a Wagtail-powered site. That is, the content editors, moderators and administrators who will be running things on a day-to-day basis.



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/editor_manual/index.html>

Contributing to Wagtail
=======================

  Issues
------

 The easiest way to contribute to Wagtail is to tell us how to improve it! First, check to see if your bug or feature request has already been submitted at [github.com/wagtail/wagtail/issues](https://github.com/wagtail/wagtail/issues). If it has, and you have some supporting information which may help us deal with it, comment on the existing issue. If not, please [create a new one](https://github.com/wagtail/wagtail/issues/new), providing as much relevant context as possible. For example, if you’re experiencing problems with installation, detail your environment and the steps you’ve already taken. If something isn’t displaying correctly, tell us what browser you’re using, and include a screenshot if possible.

 If your bug report is a security issue, **do not** report it with an issue. Please read our guide to [reporting security issues](security).

 * [Issue tracking](issue_tracking)
	+ [Issues](issue_tracking#issues)
	+ [Pull requests](issue_tracking#pull-requests)
	+ [Release schedule](issue_tracking#release-schedule)

   Pull requests
-------------

 If you’re a Python or Django developer, [fork it](https://github.com/wagtail/wagtail/) and read the [developing docs](developing#developing-for-wagtail) to get stuck in! We welcome all contributions, whether they solve problems which are specific to you or they address existing issues. If you’re stuck for ideas, pick something from the [issue list](https://github.com/wagtail/wagtail/issues?q=is%3Aopen), or email us directly on [hello@wagtail.org](mailto:hello%40wagtail.org) if you’d like us to suggest something!

 For large-scale changes, we’d generally recommend breaking them down into smaller pull requests that achieve a single well-defined task and can be reviewed individually. If this isn’t possible, we recommend opening a pull request on the [Wagtail RFCs](https://github.com/wagtail/rfcs/) repository, so that there’s a chance for the community to discuss the change before it gets implemented.

 * [Development](developing)
	+ [Setting up the Wagtail codebase](developing#setting-up-the-wagtail-codebase)
	+ [Testing](developing#testing)
	+ [Compiling static assets](developing#compiling-static-assets)
	+ [Using the pattern library](developing#using-the-pattern-library)
	+ [Compiling the documentation](developing#compiling-the-documentation)
	+ [Automatically lint and code format on commits](developing#automatically-lint-and-code-format-on-commits)
* [Committing code](committing)
	+ [Check out the code locally](committing#check-out-the-code-locally)
	+ [Rebase on to `main`](committing#rebase-on-to-main)
	+ [Update `CHANGELOG.txt` and release notes](committing#update-changelog-txt-and-release-notes)
	+ [Push to `main`](committing#push-to-main)
	+ [When you have made a mistake](committing#when-you-have-made-a-mistake)
	+ [Add commits to someone else’s pull request](committing#add-commits-to-someone-else-s-pull-request)

   Translations
------------

 Wagtail has internationalisation support so if you are fluent in a non-English language you can contribute by localising the interface.

 Translation work should be submitted through [Transifex](https://explore.transifex.com/torchbox/wagtail/).

   Other contributions
-------------------

 We welcome contributions to all aspects of Wagtail. If you would like to improve the design of the user interface, or extend the documentation, please submit a pull request as above. If you’re not familiar with Github or pull requests, [contact us directly](mailto:hello%40wagtail.org) and we’ll work something out.

   Developing packages for Wagtail
-------------------------------

 If you are developing packages for Wagtail, you can add the following [PyPI](https://pypi.org/) classifiers:

 * [`Framework :: Wagtail`](https://pypi.org/search/?c=Framework+%3A%3A+Wagtail)
* [`Framework :: Wagtail :: 1`](https://pypi.org/search/?c=Framework+%3A%3A+Wagtail+%3A%3A+1)
* [`Framework :: Wagtail :: 2`](https://pypi.org/search/?c=Framework+%3A%3A+Wagtail+%3A%3A+2)
* [`Framework :: Wagtail :: 3`](https://pypi.org/search/?c=Framework+%3A%3A+Wagtail+%3A%3A+3)
* [`Framework :: Wagtail :: 4`](https://pypi.org/search/?c=Framework+%3A%3A+Wagtail+%3A%3A+4)

 You can also find a curated list of awesome packages, articles, and other cool resources from the Wagtail community at [Awesome Wagtail](https://github.com/springload/awesome-wagtail).

   More information
----------------

 * [UI Styleguide](styleguide)
* [General coding guidelines](general_guidelines)
	+ [Language](general_guidelines#language)
	+ [File names](general_guidelines#file-names)
* [Python coding guidelines](python_guidelines)
	+ [PEP8](python_guidelines#pep8)
	+ [Django compatibility](python_guidelines#django-compatibility)
	+ [Tests](python_guidelines#tests)
* [UI Guidelines](ui_guidelines)
	+ [Linting and formatting](ui_guidelines#linting-and-formatting)
	+ [HTML guidelines](ui_guidelines#html-guidelines)
	+ [CSS guidelines](ui_guidelines#css-guidelines)
	+ [JavaScript guidelines](ui_guidelines#javascript-guidelines)
	+ [Multilingual support](ui_guidelines#multilingual-support)
* [Documentation guidelines](documentation_guidelines)
	+ [Formatting recommendations](documentation_guidelines#formatting-recommendations)
	+ [Formatting to avoid](documentation_guidelines#formatting-to-avoid)
* [Writing documentation](documentation_modes)
	+ [Choose a writing mode](documentation_modes#choose-a-writing-mode)
	+ [Tutorial](documentation_modes#tutorial)
	+ [How-to guide](documentation_modes#how-to-guide)
	+ [Reference](documentation_modes#reference)
	+ [Explanation](documentation_modes#explanation)
* [Reporting security issues](security)
	+ [Supported versions](security#supported-versions)
	+ [How Wagtail discloses security issues](security#how-wagtail-discloses-security-issues)
	+ [CSV export security considerations](security#csv-export-security-considerations)
* [Wagtail’s release process](release_process)
	+ [Official releases](release_process#official-releases)
	+ [Release cadence](release_process#release-cadence)
	+ [Deprecation policy](release_process#deprecation-policy)
	+ [Supported versions](release_process#supported-versions)
	+ [Supported versions of Django](release_process#supported-versions-of-django)
	+ [Release schedule](release_process#release-schedule)

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/index.html>

Issue tracking
==============

 We welcome bug reports, feature requests and pull requests through Wagtail’s [Github issue tracker](https://github.com/wagtail/wagtail/issues).

  Issues
------

 An issue must always correspond to a specific action with a well-defined completion state: fixing a bug, adding a new feature, updating documentation, or cleaning up code. Open-ended issues where the end result is not immediately clear (“come up with a way of doing translations” or “Add more features to rich text fields.”) are better suited to [Github discussions](https://github.com/wagtail/wagtail/discussions), so that there can be feedback on clear way to progress the issue and identify when it has been completed through separate issues created from the discussion.

 Do not use issues for support queries or other questions (“How do I do X?” - although “Implement a way of doing X” or “Document how to do X” could well be valid issues). These should be asked on [Stack Overflow](https://stackoverflow.com/questions/tagged/wagtail) instead. For discussions that do not fit Stack Overflow’s question-and-answer format, see the other (Wagtail community support options)[https://github.com/wagtail/wagtail#community-support].

 As soon as a ticket is opened - ideally within one day - a member of the core team will give it an initial classification, by either closing it due to it being invalid or updating it with the relevant labels. When a bug is opened, it will automatically be assigned the [`type:Bug`](https://github.com/wagtail/wagtail/labels/type%3ABug) and [`status:Unconfirmed`](https://github.com/wagtail/wagtail/labels/status%3AUnconfirmed) labels, once confirmed the bug can have the unconfirmed status removed. A member of the team will potentially also add a release milestone to help guide the priority of this issue. Anyone is invited to help Wagtail with reproducing `status:Unconfirmed` bugs and commenting if it is a valid bug or not with additional steps to reproduce if needed.

 Don’t be discouraged if you feel that your ticket has been given a lower priority than it deserves - this decision isn’t permanent. We will consider all feedback, and reassign or reopen tickets where appropriate. (From the other side, this means that the core team member doing the classification should feel free to make bold unilateral decisions - there’s no need to seek consensus first. If they make the wrong judgement call, that can always be reversed later.)

 The possible milestones that it might be assigned to are as follows:

 * **invalid** (closed): this issue doesn’t identify a specific action to be taken, or the action is not one that we want to take. For example - a bug report for something that’s working as designed, or a feature request for something that’s actively harmful.
* **real-soon-now**: no-one on the core team has resources allocated to work on this right now, but we know it’s a pain point, and it will be prioritised whenever we next get a chance to choose something new to work on. In practice, that kind of free choice doesn’t happen very often - there are lots of pressures determining what we work on from day to day - so if this is a feature or fix you need, we encourage you to work on it and contribute a pull request, rather than waiting for the core team to get round to it!
* A specific version number (for example **1.6**): the issue is important enough that it needs to be fixed in this version. There are resources allocated and/or plans to work on the issue in the given version.
* No milestone: the issue is accepted as valid once the `status:Unconfirmed` label is removed (when it’s confirmed as a report for a legitimate bug, or a useful feature request) but is not deemed a priority to work on (in the opinion of the core team). For example - a bug that’s only cosmetic, or a feature that would be kind of neat but not really essential. There are no resources allocated to it - feel free to take it on!

 On some occasions it may take longer for the core team to classify an issue into a milestone. For example:

 * It may require a non-trivial amount of work to confirm the presence of a bug. In this case, feedback and further details from other contributors, whether or not they can replicate the bug, would be particularly welcomed.
* It may require further discussion to decide whether the proposal is a good idea or not - if so, it will be tagged [“design decision needed”](https://github.com/wagtail/wagtail/labels/status%3ANeeds%20Design%20Decision).

 We will endeavour to make sure that issues don’t remain in this state for prolonged periods. Issues and PRs tagged “design decision needed” will be revisited regularly and discussed with at least two core contributors - we aim to review each ticket at least once per release cycle (= 6 weeks) as part of weekly core team meetings.

   Pull requests
-------------

 As with issues, the core team will classify pull requests as soon as they are opened, usually within one day. Unless the change is invalid or particularly contentious (in which case it will be closed or marked as “design decision needed”). It will generally be classified under the next applicable version - the next minor release for new features, or the next patch release for bugfixes - and marked as ‘Needs review’.

 * All contributors, core and non-core, are invited to offer feedback on the pull request.
* Core team members are invited to assign themselves to the pull request for review.
* More specific details on how to triage Pull Requests can be found on the [PR triage wiki page](https://github.com/wagtail/wagtail/wiki/PR-triage).

 Subsequently (ideally within a week or two, but possibly longer for larger submissions) a core team member will merge it if it is ready to be merged, or tag it as requiring further work (‘needs work’ / ‘needs tests’ / ‘needs docs’). Pull requests that require further work are handled and prioritised in the same way as issues - anyone is welcome to pick one up from the backlog, whether or not they were the original committer.

 Rebasing / squashing of pull requests is welcome, but not essential. When doing so, do not squash commits that need reviewing into previous ones and make sure to preserve the sequence of changes. To fix mistakes in earlier commits, use `git commit --fixup` so that the final merge can be done with `git rebase -i --autosquash`.

 Core team members working on Wagtail are expected to go through the same process with their own fork of the project.

   Release schedule
----------------

 We aim to release a new version every 2 months. To keep to this schedule, we will tend to ‘bump’ issues and PRs to a future release where necessary, rather than let them delay the present one. For this reason, an issue being tagged under a particular release milestone should not be taken as any kind of guarantee that the feature will actually be shipped in that release.

 * See the [Release Schedule wiki page](https://github.com/wagtail/wagtail/wiki/Release-schedule) for a full list of dates.
* See the [Roadmap wiki page](https://github.com/wagtail/wagtail/wiki/Roadmap) for a general guide of project planning.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/issue_tracking.html>

Development
===========

 Setting up a local copy of [the Wagtail git repository](https://github.com/wagtail/wagtail) is slightly more involved than running a release package of Wagtail, as it requires [Node.js](https://nodejs.org/) and npm for building JavaScript and CSS assets. (This is not required when running a release version, as the compiled assets are included in the release package.)

 If you’re happy to develop on a local virtual machine, the [docker-wagtail-develop](https://github.com/wagtail/docker-wagtail-develop) and [vagrant-wagtail-develop](https://github.com/wagtail/vagrant-wagtail-develop) setup scripts are the fastest way to get up and running. They will provide you with a running instance of the [Wagtail Bakery demo site](https://github.com/wagtail/bakerydemo/), with the Wagtail and bakerydemo codebases available as shared folders for editing on your host machine.

 You can also set up a cloud development environment that you can work with in a browser-based IDE using the [gitpod-wagtail-develop](https://github.com/wagtail/gitpod-wagtail-develop) project.

 (Build scripts for other platforms would be very much welcomed - if you create one, please let us know via the [Slack workspace](https://github.com/wagtail/wagtail/wiki/Slack)!)

 If you’d prefer to set up all the components manually, read on. These instructions assume that you’re familiar with using pip and [virtual environments](https://docs.python.org/3/tutorial/venv.html) to manage Python packages.

  Setting up the Wagtail codebase
-------------------------------

 The preferred way to install the correct version of Node is to use [Node Version Manager (nvm)](https://github.com/nvm-sh/nvm) or [Fast Node Manager (fnm)](https://github.com/Schniz/fnm), which will always align the version with the supplied `.nvmrc` file in the root of the project. To ensure you are running the correct version of Node, run `nvm install` or `fnm install` from the project root. Alternatively, you can install [Node.js](https://nodejs.org/) directly, ensure you install the version as declared in the project’s root `.nvmrc` file.

 You will also need to install the **libjpeg** and **zlib** libraries, if you haven’t done so already - see Pillow’s [platform-specific installation instructions](https://pillow.readthedocs.io/en/stable/installation.html#external-libraries).

 Clone a copy of [the Wagtail codebase](https://github.com/wagtail/wagtail):

 
```
git clone https://github.com/wagtail/wagtail.git
cd wagtail

```
 **With your preferred virtualenv activated,** install the Wagtail package in development mode with the included testing and documentation dependencies:

 
```
pip install -e '.[testing,docs]' -U

```
 Install the tool chain for building static assets:

 
```
npm ci

```
 Compile the assets:

 
```
npm run build

```
 Any Wagtail sites you start up in this virtualenv will now run against this development instance of Wagtail. We recommend using the [Wagtail Bakery demo site](https://github.com/wagtail/bakerydemo/) as a basis for developing Wagtail. Keep in mind that the setup steps for a Wagtail site may include installing a release version of Wagtail, which will override the development version you’ve just set up. In this case, you should install the site before running the `pip install -e` step, or re-run that step after the site is installed.

   Testing
-------

 From the root of the Wagtail codebase, run the following command to run all the Python tests:

 
```
python runtests.py

```
  ### Running only some of the tests

 At the time of writing, Wagtail has well over 2500 tests, which takes a while to run. You can run tests for only one part of Wagtail by passing in the path as an argument to `runtests.py` or `tox`:

 
```
# Running in the current environment
python runtests.py wagtail

# Running in a specified Tox environment
tox -e py39-dj32-sqlite-noelasticsearch wagtail

# See a list of available Tox environments
tox -l

```
 You can also run tests for individual TestCases by passing in the path as an argument to `runtests.py`

 
```
# Running in the current environment
python runtests.py wagtail.tests.test_blocks.TestIntegerBlock

# Running in a specified Tox environment
tox -e py39-dj32-sqlite-noelasticsearch wagtail.tests.test_blocks.TestIntegerBlock

```
   ### Running migrations for the test app models

 You can create migrations for the test app by running the following from the Wagtail root.

 
```
django-admin makemigrations --settings=wagtail.test.settings

```
   ### Testing against PostgreSQL

  Note

 In order to run these tests, you must install the required modules for PostgreSQL as described in Django’s [Databases documentation](https://docs.djangoproject.com/en/stable/ref/databases/).

  By default, Wagtail tests against SQLite. You can switch to using PostgreSQL by using the `--postgres` argument:

 
```
python runtests.py --postgres

```
 If you need to use a different user, password, host or port, use the `PGUSER`, `PGPASSWORD`, `PGHOST` and `PGPORT` environment variables respectively.

   ### Testing against a different database

  Note

 In order to run these tests, you must install the required client libraries and modules for the given database as described in Django’s [Databases documentation](https://docs.djangoproject.com/en/stable/ref/databases/) or 3rd-party database backend’s documentation.

  If you need to test against a different database, set the `DATABASE_ENGINE` environment variable to the name of the Django database backend to test against:

 
```
DATABASE_ENGINE=django.db.backends.mysql python runtests.py

```
 This will create a new database called `test_wagtail` in MySQL and run the tests against it.

 If you need to use different connection settings, use the following environment variables which correspond to the respective keys within Django’s [DATABASES](https://docs.djangoproject.com/en/stable/ref/settings/#databases) settings dictionary:

 * `DATABASE_ENGINE`
* `DATABASE_NAME`
* `DATABASE_PASSWORD`
* `DATABASE_HOST`


	+ Note that for MySQL, this must be `127.0.0.1` rather than `localhost` if you need to connect using a TCP socket
* `DATABASE_PORT`

 It is also possible to set `DATABASE_DRIVER`, which corresponds to the `driver` value within `OPTIONS` if an SQL Server engine is used.

   ### Testing Elasticsearch

 You can test Wagtail against Elasticsearch by passing the `--elasticsearch` argument to `runtests.py`:

 
```
python runtests.py --elasticsearch

```
 Wagtail will attempt to connect to a local instance of Elasticsearch (`http://localhost:9200`) and use the index `test_wagtail`.

 If your Elasticsearch instance is located somewhere else, you can set the `ELASTICSEARCH_URL` environment variable to point to its location:

 
```
ELASTICSEARCH_URL=http://my-elasticsearch-instance:9200 python runtests.py --elasticsearch

```
   ### Unit tests for JavaScript

 We use [Jest](https://jestjs.io/) for unit tests of client-side business logic or UI components. From the root of the Wagtail codebase, run the following command to run all the front-end unit tests:

 
```
npm run test:unit

```
   ### Integration tests

 Our end-to-end browser testing suite also uses [Jest](https://jestjs.io/), combined with [Puppeteer](https://pptr.dev/). We set this up to be installed separately so as not to increase the installation size of the existing Node tooling. To run the tests, you will need to install the dependencies and, in a separate terminal, run the test suite’s Django development server:

 
```
export DJANGO_SETTINGS_MODULE=wagtail.test.settings_ui
# Assumes the current environment contains a valid installation of Wagtail for local development.
./wagtail/test/manage.py migrate
./wagtail/test/manage.py createcachetable
DJANGO_SUPERUSER_EMAIL=admin@example.com DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_PASSWORD=changeme ./wagtail/test/manage.py createsuperuser --noinput
./wagtail/test/manage.py runserver 0:8000
# In a separate terminal:
npm --prefix client/tests/integration install
npm run test:integration

```
 Integration tests target `http://localhost:8000` by default. Use the `TEST_ORIGIN` environment variable to use a different port, or test a remote Wagtail instance: `TEST_ORIGIN=http://localhost:9000 npm run test:integration`.

   ### Browser and device support

 Wagtail is meant to be used on a wide variety of devices and browsers. Supported browser / device versions include:

 

| Browser | Device/OS | Version(s) |
| --- | --- | --- |
| Mobile Safari | iOS Phone | Last 2 |
| Mobile Safari | iOS Tablet | Last 2 |
| Chrome | Android | Last 2 |
| Chrome | Desktop | Last 2 |
| MS Edge | Windows | Last 2 |
| Firefox | Desktop | Latest |
| Firefox ESR | Desktop | Latest |
| Safari | macOS | Last 3 |

 We aim for Wagtail to work in those environments, there are known support gaps for Safari 13 introduced in Wagtail 4.0 to provide better support for RTL languages. Our development standards ensure that the site is usable on other browsers **and will work on future browsers**.

 IE 11 support has been officially dropped in 2.15 as it is gradually falling out of use. Features already known not to work include:

 * Rich text copy-paste in the rich text editor.
* Sticky toolbar in the rich text editor.
* Focus outline styles in the main menu & explorer menu.
* Keyboard access to the actions in page listing tables.

 **Unsupported browsers / devices include:**

 

| Browser | Device/OS | Version(s) |
| --- | --- | --- |
| Stock browser | Android | All |
| IE | Desktop | All |
| Safari | Windows | All |

   ### Accessibility targets

 We want to make Wagtail accessible for users of a wide variety of assistive technologies. The specific standard we aim for is [WCAG2.1](https://www.w3.org/TR/WCAG21/), AA level. Here are specific assistive technologies we aim to test for, and ultimately support:

 * [NVDA](https://www.nvaccess.org/download/) on Windows with Firefox ESR
* [VoiceOver](https://support.apple.com/en-gb/guide/voiceover-guide/welcome/web) on macOS with Safari
* [Windows Magnifier](https://support.microsoft.com/en-gb/help/11542/windows-use-magnifier) and macOS Zoom
* Windows Speech Recognition and macOS Dictation
* Mobile [VoiceOver](https://support.apple.com/en-gb/guide/voiceover-guide/welcome/web) on iOS, or [TalkBack](https://support.google.com/accessibility/android/answer/6283677?hl=en-GB) on Android
* Windows [High-contrast mode](https://support.microsoft.com/en-us/windows/use-high-contrast-mode-in-windows-10-fedc744c-90ac-69df-aed5-c8a90125e696)

 We aim for Wagtail to work in those environments. Our development standards ensure that the site is usable with other assistive technologies. In practice, testing with assistive technology can be a daunting task that requires specialised training – here are tools we rely on to help identify accessibility issues, to use during development and code reviews:

 * [react-axe](https://github.com/dequelabs/react-axe) integrated directly in our build tools, to identify actionable issues. Logs its results in the browser console.
* [@wordpress/jest-puppeteer-axe](https://github.com/WordPress/gutenberg/tree/trunk/packages/jest-puppeteer-axe) running Axe checks as part of integration tests.
* [Axe](https://chrome.google.com/webstore/detail/axe/lhdoppojpmngadmnindnejefpokejbdd) Chrome extension for more comprehensive automated tests of a given page.
* [Accessibility Insights for Web](https://accessibilityinsights.io/docs/en/web/overview) Chrome extension for semi-automated tests, and manual audits.

   ### Known accessibility issues

 Wagtail’s administration interface isn’t fully accessible at the moment. We actively work on fixing issues both as part of ongoing maintenance and bigger overhauls. To learn about known issues, check out:

 * The [WCAG2.1 AA for CMS admin](https://github.com/wagtail/wagtail/projects/5) issues backlog.
* Our [2021 accessibility audit](https://docs.google.com/spreadsheets/d/1l7tnpEyJiC5BWE_JX0XCkknyrjxYA5T2aee5JgPnmi4/edit).

 The audit also states which parts of Wagtail have and haven’t been tested, how issues affect WCAG 2.1 compliance, and the likely impact on users.

    Compiling static assets
-----------------------

 All static assets such as JavaScript, CSS, images, and fonts for the Wagtail admin are compiled from their respective sources by Webpack. The compiled assets are not committed to the repository, and are compiled before packaging each new release. Compiled assets should not be submitted as part of a pull request.

 To compile the assets, run:

 
```
npm run build

```
 This must be done after every change to the source files. To watch the source files for changes and then automatically recompile the assets, run:

 
```
npm start

```
   Using the pattern library
-------------------------

 Wagtail’s UI component library is built with [Storybook](https://storybook.js.org/) and [django-pattern-library](https://github.com/torchbox/django-pattern-library). To run it locally,

 
```
export DJANGO_SETTINGS_MODULE=wagtail.test.settings_ui
# Assumes the current environment contains a valid installation of Wagtail for local development.
./wagtail/test/manage.py migrate
./wagtail/test/manage.py createcachetable
./wagtail/test/manage.py runserver 0:8000
# In a separate terminal:
npm run storybook

```
 The last command will start Storybook at `http://localhost:6006/`. It will proxy specific requests to Django at `http://localhost:8000` by default. Use the `TEST_ORIGIN` environment variable to use a different port for Django: `TEST_ORIGIN=http://localhost:9000 npm run storybook`.

   Compiling the documentation
---------------------------

 The Wagtail documentation is built by Sphinx. To install Sphinx and compile the documentation, run:

 
```
cd /path/to/wagtail
# Install the documentation dependencies
pip install -e .[docs]
# or if using zsh as your shell:
#    pip install -e '.[docs]' -U
# Compile the docs
cd docs/
make html

```
 The compiled documentation will now be in `docs/_build/html`. Open this directory in a web browser to see it. Python comes with a module that makes it very easy to preview static files in a web browser. To start this simple server, run the following commands:

 
```
cd docs/_build/html/
python -m http.server 8080

```
 Now you can open <http://localhost:8080/> in your web browser to see the compiled documentation.

 Sphinx caches the built documentation to speed up subsequent compilations. Unfortunately, this cache also hides any warnings thrown by unmodified documentation source files. To clear the built HTML and start fresh, so you can see all warnings thrown when building the documentation, run:

 
```
cd docs/
make clean
make html

```
 Wagtail also provides a way for documentation to be compiled automatically on each change. To do this, you can run the following command to see the changes automatically at `localhost:4000`:

 
```
cd docs/
make livehtml

```
   Automatically lint and code format on commits
---------------------------------------------

 [pre-commit](https://pre-commit.com/) is configured to automatically run code linting and formatting checks with every commit. To install pre-commit into your git hooks run:

 
```
pre-commit install

```
 pre-commit should now run on every commit you make.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/developing.html>

Committing code
===============

 **This section is for the core team of Wagtail, or for anyone interested in the process of getting code committed to Wagtail.**

 Code should only be committed after it has been reviewed by at least one other reviewer or committer, unless the change is a small documentation change or fixing a typo. If additional code changes are made after the review, it is OK to commit them without further review if they are uncontroversial and small enough that there is minimal chance of introducing new bugs.

 Most code contributions will be in the form of pull requests from Github. Pull requests should not be merged from Github, apart from small documentation fixes, which can be merged with the ‘Squash and merge’ option. Instead, the code should be checked out by a committer locally, the changes examined and rebased, the `CHANGELOG.txt` and release notes updated, and finally the code should be pushed to the `main` branch. This process is covered in more detail below.

  Check out the code locally
--------------------------

 If the code has been submitted as a pull request, you should fetch the changes and check them out in your Wagtail repository. A simple way to do this is by adding the following `git` alias to your `~/.gitconfig` (assuming `upstream` is `wagtail/wagtail`):

 
```
[alias]
    pr = !sh -c \"git fetch upstream pull/${1}/head:pr/${1} && git checkout pr/${1}\"

```
 Now you can check out pull request number `xxxx` by running `git pr xxxx`.

   Rebase on to `main`
-------------------

 Now that you have the code, you should rebase the commits on to the `main` branch. Rebasing is preferred over merging, as merge commits make the commit history harder to read for small changes.

 You can fix up any small mistakes in the commits, such as typos and formatting, as part of the rebase. `git rebase --interactive` is an excellent tool for this job.

 Ideally, use this as an opportunity to squash the changes to a few commits, so each commit is making a single meaningful change (and not breaking anything). If this is not possible because of the nature of the changes, it’s acceptable to either squash into a commit or leave all commits unsquashed, depending on which will be more readable in the commit history.

 
```
# Get the latest commits from Wagtail
git fetch upstream
git checkout main
git merge --ff-only upstream/main
# Rebase this pull request on to main
git checkout pr/xxxx
git rebase main
# Update main to this commit
git checkout main
git merge --ff-only pr/xxxx

```
   Update `CHANGELOG.txt` and release notes
----------------------------------------

  Note

 This should only be done by core committers, once the changes have been reviewed and accepted.

  Every significant change to Wagtail should get an entry in the `CHANGELOG.txt`, and the release notes for the current version.

 The `CHANGELOG.txt` contains a short summary of each new feature, refactoring, or bug fix in each release. Each summary should be a single line. Bug fixes should be grouped together at the end of the list for each release, and be prefixed with “Fix:”. The name of the contributor should be added at the end of the summary, in brackets. For example:

 
```
* Fix: Tags added on the multiple image uploader are now saved correctly (Alex Smith)

```
 The release notes for each version contain a more detailed description of each change. Backwards compatibility notes should also be included. Large new features or changes should get their own section, while smaller changes and bug fixes should be grouped together in their own section. See previous release notes for examples. The release notes for each version are found in `docs/releases/x.x.x.md`.

 If the contributor is a new person, and this is their first contribution to Wagtail, they should be added to the `CONTRIBUTORS.rst` list. Contributors are added in chronological order, with new contributors added to the bottom of the list. Use their preferred name. You can usually find the name of a contributor on their Github profile. If in doubt, or if their name is not on their profile, ask them how they want to be named.

 If the changes to be merged are small enough to be a single commit, amend this single commit with the additions to the `CHANGELOG.txt`, release notes, and contributors:

 
```
git add CHANGELOG.txt docs/releases/x.x.x.md CONTRIBUTORS.md
git commit --amend --no-edit

```
 If the changes do not fit in a single commit, make a new commit with the updates to the `CHANGELOG.txt`, release notes, and contributors. The commit message should say `Release notes for #xxxx`:

 
```
git add CHANGELOG.txt docs/releases/x.x.x.md CONTRIBUTORS.md
git commit -m 'Release notes for #xxxx'

```
   Push to `main`
--------------

 The changes are ready to be pushed to `main` now.

 
```
# Check that everything looks OK
git log upstream/main..main --oneline
git push --dry-run upstream main
# Push the commits!
git push upstream main
git branch -d pr/xxxx

```
   When you have made a mistake
----------------------------

 It’s ok! Everyone makes mistakes. If you realise that recent merged changes have a negative impact, create a new pull request with a revert of the changes and merge it without waiting for a review. The PR will serve as additional documentation for the changes, and will run through the CI tests.

   Add commits to someone else’s pull request
------------------------------------------

 Github users with write access to wagtail/wagtail (core members) can add commits to the pull request branch of the contributor.

 Given that the contributor username is johndoe and his pull request branch is called foo:

 
```
git clone git@github.com:wagtail/wagtail.git
cd wagtail
git remote add johndoe git@github.com:johndoe/wagtail.git
git fetch johndoe foo
git checkout johndoe/foo
# Make changes
# Commit changes
git push johndoe HEAD:foo

```
 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/committing.html>

UI Styleguide
=============

 Developers working on the Wagtail UI or creating new UI components may wish to test their work against our Styleguide, which is provided as the contrib module “wagtailstyleguide”.

 To install the styleguide module on your site, add it to the list of `INSTALLED_APPS` in your settings:

 
```
INSTALLED_APPS = (
    # ...
    'wagtail.contrib.styleguide',
    # ...
)

```
 This will add a ‘Styleguide’ item to the Settings menu in the admin.

 At present the styleguide is static: new UI components must be added to it manually, and there are no hooks into it for other modules to use. We hope to support hooks in the future.

 The styleguide doesn’t currently provide examples of all the core interface components; notably the Page, Document, Image and Snippet chooser interfaces are not currently represented.



 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/styleguide.html>

General coding guidelines
=========================

  Language
--------

 British English is preferred for user-facing text; this text should also be marked for translation (using the `django.utils.translation.gettext` function and `{% trans %}` template tag, for example). However, identifiers within code should use American English if the British or international spelling would conflict with built-in language keywords; for example, CSS code should consistently use the spelling `color` to avoid inconsistencies like `background-color: $colour-red`.

  ### Latin phrases and abbreviations

 Try to avoid Latin phrases (such as `ergo` or `de facto`) and abbreviations (such as `i.e.` or `e.g.`), and use common English phrases instead. Alternatively find a simpler way to communicate the concept or idea to the reader. The exception is `etc.` which can be used when space is limited.

 Examples:

 

| Don’t use this | Use this instead |
| --- | --- |
| e.g. | for example, such as |
| i.e. | that is |
| viz. | namely |
| ergo | therefore |

    File names
----------

 Where practical, try to adhere to the existing convention of file names within the folder where added.

 Examples:

 * Django templates - `lower_snake_case.html`
* Documentation - `lower_snake_case.md`

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/general_guidelines.html>

Python coding guidelines
========================

  PEP8
----

 We ask that all Python contributions adhere to the [PEP8](https://peps.python.org/pep-0008/) style guide. All files should be formatted using the [black](https://github.com/psf/black) auto-formatter. This will be run by `pre-commit` if that is configured.

 * The project repository includes an `.editorconfig` file. We recommend using a text editor with [EditorConfig](https://editorconfig.org/) support to avoid indentation and whitespace issues. Python and HTML files use 4 spaces for indentation.

 In addition, import lines should be sorted according to [isort](https://pycqa.github.io/isort/) 5.6.4 rules. If you have installed Wagtail’s testing dependencies (`pip install -e '.[testing]'`), you can check your code by running `make lint`. You can also just check python related linting by running `make lint-server`.

 You can run all Python formatting with `make format`. Similar to linting you can format python/template only files by running `make format-server`.

   Django compatibility
--------------------

 Wagtail is written to be compatible with multiple versions of Django. Sometimes, this requires running one piece of code for recent version of Django, and another piece of code for older versions of Django. In these cases, always check which version of Django is being used by inspecting `django.VERSION`:

 
```
import django

if django.VERSION >= (1, 9):
    # Use new attribute
    related_field = field.rel
else:
    # Use old, deprecated attribute
    related_field = field.related

```
 Always compare against the version using greater-or-equals (`>=`), so that code for newer versions of Django is first.

 Do not use a `try ... except` when seeing if an object has an attribute or method introduced in a newer versions of Django, as it does not clearly express why the `try ... except` is used. An explicit check against the Django version makes the intention of the code very clear.

 
```
# Do not do this
try:
    related_field = field.rel
except AttributeError:
    related_field = field.related

```
 If the code needs to use something that changed in a version of Django many times, consider making a function that encapsulates the check:

 
```
import django

def related_field(field):
    if django.VERSION >= (1, 9):
        return field.rel
    else:
        return field.related

```
 If a new function has been introduced by Django that you think would be very useful for Wagtail, but is not available in older versions of Django that Wagtail supports, that function can be copied over in to Wagtail. If the user is running a new version of Django that has the function, the function should be imported from Django. Otherwise, the version bundled with Wagtail should be used. A link to the Django source code where this function was taken from should be included:

 
```
import django

if django.VERSION >= (1, 9):
    from django.core.validators import validate_unicode_slug
else:
    # Taken from https://github.com/django/django/blob/1.9/django/core/validators.py#L230
    def validate_unicode_slug(value):
            # Code left as an exercise to the reader
            pass

```
   Tests
-----

 Wagtail has a suite of tests, which we are committed to improving and expanding. See [Testing](developing#testing).

 We run continuous integration to ensure that no commits or pull requests introduce test failures. If your contributions add functionality to Wagtail, please include the additional tests to cover it; if your contributions alter existing functionality, please update the relevant tests accordingly.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/python_guidelines.html>

UI Guidelines
=============

 Wagtail’s user interface is built with:

 * **HTML** using [Django templates](https://docs.djangoproject.com/en/stable/ref/templates/language/)
* **CSS** using [Sass](https://sass-lang.com/) and [Tailwind](https://tailwindcss.com/)
* **JavaScript** with [TypeScript](https://www.typescriptlang.org/)
* **SVG** for our icons, minified with [SVGO](https://jakearchibald.github.io/svgomg/)

  Linting and formatting
----------------------

 Here are the available commands:

 * `make lint` will run all linting, `make lint-server` lints templates, `make lint-client` lints JS/CSS.
* `make format` will run all formatting and fixing of linting issues. There is also `make format-server` and `make format-client`.

 Have a look at our `Makefile` tasks and `package.json` scripts if you prefer more granular options.

   HTML guidelines
---------------

 We use [djhtml](https://github.com/rtts/djhtml) for formatting and [Curlylint](https://www.curlylint.org/) for linting.

 * Write [valid](https://validator.w3.org/nu/), [semantic](https://html5doctor.com/element-index/) HTML.
* Follow [ARIA authoring practices](https://w3c.github.io/aria-practices/), in particular [No ARIA is better than Bad ARIA](https://w3c.github.io/aria-practices/#no_aria_better_bad_aria).
* Use classes for styling, `data-` attributes for JavaScript behaviour, IDs for semantics only.
* For comments, use [Django template syntax](https://docs.djangoproject.com/en/stable/ref/templates/language/#comments) instead of HTML.

   CSS guidelines
--------------

 We use [Prettier](https://prettier.io/) for formatting and [Stylelint](https://stylelint.io/) for linting.

 * We follow [BEM](https://getbem.com/) and [ITCSS](https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/), with a large amount of utilities created with [Tailwind](https://tailwindcss.com/).
* Familiarise yourself with our [stylelint-config-wagtail](https://github.com/wagtail/stylelint-config-wagtail) configuration, which details our preferred code style.
* Use `rems` for `font-size`, because they offer absolute control over text. Additionally, unit-less `line-height` is preferred because it does not inherit a percentage value of its parent element, but instead is based on a multiplier of the `font-size`.
* Always use variables for design tokens such as colours or font sizes, rather than hard-coding specific values.
* We use the `w-` prefix for all styles intended to be reusable by Wagtail site implementers.

   JavaScript guidelines
---------------------

 We use [Prettier](https://prettier.io/) for formatting and [ESLint](https://eslint.org/) for linting.

 * We follow a somewhat relaxed version of the [Airbnb styleguide](https://github.com/airbnb/javascript).
* Familiarise yourself with our [eslint-config-wagtail](https://github.com/wagtail/eslint-config-wagtail) configuration, which details our preferred code style.

   Multilingual support
--------------------

 This is an area of active improvement for Wagtail, with [ongoing discussions](https://github.com/wagtail/wagtail/discussions/8017).

 * Always use the `trimmed` attribute on `blocktrans` tags to prevent unnecessary whitespace from being added to the translation strings.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/ui_guidelines.html>

Documentation guidelines
========================

 * [Formatting recommendations](#formatting-recommendations)
* [Formatting to avoid](#formatting-to-avoid)

  Formatting recommendations
--------------------------

 Wagtail’s documentation uses a mixture of [Markdown](https://myst-parser.readthedocs.io/en/stable/syntax/syntax.html) and [reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html). We encourage writing documentation in Markdown first, and only reaching for more advanced reStructuredText formatting if there is a compelling reason.

 Here are formats we encourage using when writing documentation for Wagtail.

  ### Paragraphs

 It all starts here. Keep your sentences short, varied in length.

 Separate text with an empty line to create a new paragraph.

   ### Heading levels

 Use heading levels to create sections, and allow users to link straight to a specific section. Start documents with an `# h1`, and proceed with `## h2` and further sub-sections without skipping levels.

 
```
# Heading level 1

## Heading level 2

### Heading level 3

```
   ### Lists

 Use bullets for unordered lists, numbers when ordered. Prefer dashes `-` for bullets. Nest by indenting with 4 spaces.

 
```
-   Bullet 1
-   Bullet 2
    -   Nested bullet 2
-   Bullet 3

1. Numbered list 1
2. Numbered list 2
3. Numbered list 3

```
  Rendered output * Bullet 1
* Bullet 2


	+ Nested bullet 2
* Bullet 3

 1. Numbered list 1
2. Numbered list 2
3. Numbered list 3

    ### Inline styles

 Use **bold** and *italic* sparingly, inline `code` when relevant.

 
```
Use **bold** and _italic_ sparingly, inline `code` when relevant.

```
   ### Code blocks

 Make sure to include the correct language code for syntax highlighting, and to format your code according to our coding guidelines. Frequently used: `python`, `css`, `html`, `html+django`, `javascript`, `sh`.

 
```
```python
INSTALLED_APPS = [
    ...
    "wagtail",
    ...
]
```

```
  Rendered output 
```
INSTALLED_APPS = [
    ...
    "wagtail",
    ...
]

```
   #### When using console (terminal) code blocks

  Note

 `$` or `>` prompts are not needed, this makes it harder to copy and paste the lines and can be difficult to consistently add in every single code snippet.

  Use `sh` as it has better support for comment and code syntax highlighting in MyST’s parser, plus is more compatible with GitHub and VSCode.

 
```
```sh
# some comment
some command
```

```
  Rendered output 
```
# some comment
some command

```
  Use `doscon` (DOS Console) only if explicitly calling out Windows commands alongside their bash equivalent.

 
```
```doscon
# some comment
some command
```

```
  Rendered output 
```
# some comment
some command

```
     ### Links

 Links are fundamental in documentation. Use internal links to tie your content to other docs, and external links as needed. Pick relevant text for links, so readers know where they will land.

 Don’t rely on [`links over code`](https://www.example.com/), as they are impossible to spot.

 
```
An [external link](https://wwww.example.com).
An [internal link to another document](/reference/contrib/legacy_richtext).
An auto generated link label to a page [](/getting_started/tutorial).
A [link to a reference](register_reports_menu_item).

```
  Rendered output An [external link](https://wwww.example.com). An [internal link to another document](../reference/contrib/legacy_richtext). An auto generated link label to a page [Your first Wagtail site](../getting_started/tutorial). A [link to a reference](../reference/hooks#register-reports-menu-item).

   #### Reference links

 Reference links (links to a target within a page) rely on the page having a reference created. Each reference must have a unique name and should use the `lower_snake_case` format. A reference can be added as follows:

 
```
(my_awesome_section)=

##### Some awesome section title

...

```
 The reference can be linked to, with an optional label, using the Markdown link syntax as follows:

 
```
-   Auto generated label (preferred) [](my_awesome_section)
-   [label for section](my_awesome_section)

```
  Rendered output  ##### Some awesome section title

 …

 * Auto generated label (preferred) [Some awesome section title](#my-awesome-section)
* [label for section](#my-awesome-section)

  You can read more about other methods of linking to, and creating references in the MyST parser docs section on [Targets and cross-referencing](https://myst-parser.readthedocs.io/en/stable/syntax/syntax.html#targets-and-cross-referencing).

     ### Note and warning call-outs

 Use notes and warnings sparingly, as they rely on reStructuredText syntax which is more complicated for future editors.

 
```
```{note}
Notes can provide complementary information.
```

```{warning}
Warnings can be scary.
```

```
  Rendered output  Note

 Notes can provide complementary information.

   Warning

 Warnings can be scary.

   These call-outs do not support titles, so be careful not to include them, titles will just be moved to the body of the call-out.

 
```
```{note} Title's here will not work correctly
Notes can provide complementary information.
```

```
   ### Images

 Images are hard to keep up-to-date as documentation evolves, but can be worthwhile nonetheless. Here are guidelines when adding images:

 * All images should have meaningful [alt text](https://axesslab.com/alt-texts/) unless they are decorative.
* Images are served as-is – pick the correct format, and losslessly compress all images.
* Use absolute paths for image files so they are more portable.

 
```
![The TableBlock component in StreamField, with row header, column header, caption fields - and then the editable table](/_static/images/screen40_table_block.png)

```
  Rendered output ![The TableBlock component in StreamField, with row header, column header, caption fields - and then the editable table]

    ### Autodoc

 With its [autodoc](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html) feature, Sphinx supports writing documentation in Python docstrings for subsequent integration in the project’s documentation pages. This is a very powerful feature which we highly recommend using to document Wagtail’s APIs.

 
```
```{eval-rst}
.. module:: wagtail.coreutils

.. autofunction:: cautious_slugify
```

```
  Rendered output    ### Tables

 Only use tables when needed, using the [GitHub Flavored Markdown table syntax](https://github.github.com/gfm/#tables-extension-).

 
```
| Browser       | Device/OS |
| ------------- | --------- |
| Stock browser | Android   |
| IE            | Desktop   |
| Safari        | Windows   |

```
  Rendered output 

| Browser | Device/OS |
| --- | --- |
| Stock browser | Android |
| IE | Desktop |
| Safari | Windows |

    ### Tables of contents

 `toctree` and `contents` can be used as reStructuredText directives.

 
```
```{toctree}
---
maxdepth: 2
titlesonly:
---
getting_started/index
topics/index
```

```{contents}
---
local:
depth: 1
---
```

```
   ### Version added, changed, deprecations

 Sphinx offers release-metadata directives to generate this information consistently. Use as appropriate.

 
```
```{versionadded} 2.15
```

```{versionchanged} 2.15
```

```
  Rendered output  New in version 2.15.

   Changed in version 2.15.

     ### Progressive disclosure

 We can add supplementary information in documentation with the HTML `<details>` element. This relies on HTML syntax, which can be hard to author consistently, so keep this type of formatting to a minimum.

 
```
<details>
    <summary>Supplementary information</summary>

    This will be visible when expanding the content.
</details>

```
 Example:

  Supplementary information This will be visible when expanding the content.

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/documentation_guidelines.html>

Writing documentation
=====================

 Wagtail documentation is written in **four modes** of information delivery. Each type of information delivery has a purpose and targets a specific audience.

 * [Tutorial](#doc-mode-tutorial), learning-oriented
* [How-to guide](#doc-mode-how-to-guide), goal-oriented
* [Reference](#doc-mode-reference), information-oriented
* [Explanation](#doc-mode-explanation), understanding-oriented

 We are following Daniele Procida’s [Diátaxis documentation framework](https://diataxis.fr/).

  Choose a writing mode
---------------------

 Each page of the Wagtail documentation should be written in single mode of information delivery. Single pages with mixed modes are harder to understand. If you have documents that mix the types of information delivery, it’s best to split them up. Add links to the first section of each document to cross reference other documents on the same topic.

 Writing documentation in a specific mode will help our users to understand and quickly find what they are looking for.

   Tutorial
--------

 Tutorials are designed to be **learning-oriented** resources which guide newcomers through a specific topic. To help effective learning, tutorials should provide examples to illustrate the subjects they cover.

 Tutorials may not necessarily follow best practices. They are designed to make it easier to get started. A tutorial is concrete and particular. It must be repeatable, instil confidence, and should result in success, every time, for every learner.

  ### Do

 * Use conversational language
* Use contractions, speak in the first person plural, be reassuring. For example: “We’re going to do this.”
* Use pictures or concrete outputs of code to reassure people that they’re on the right track. For example: “Your new login page should look like this:” or “Your directory should now have three files”.

   ### Don’t

 * Tell people what they’re going to learn. Instead, tell them what tasks they’re going to complete.
* Use optionality in a tutorial. The word ‘if’ is a sign of danger! For example: “If you want to do this…” The expected actions and outcomes should be unambiguous.
* Assume that learners have a prior understanding of the subject.

 [More about tutorials](https://diataxis.fr/tutorials/)

    How-to guide
------------

 A guide offers advice on how best to achieve a given task. How-to guides are **task-oriented** with a clear **goal or objective**.

  ### Do

 * Name the guide well - ensure that the learner understands what exactly the guide does.
* Focus on actions and outcomes. For example: “If you do X, Y should happen.”
* Assume that the learner has a basic understanding of the general concepts
* Point the reader to additional resources

   ### Don’t

 * Use an unnecessarily strict tone of voice. For example: “You must absolutely NOT do X.”

 [More about how-to guides](https://diataxis.fr/how-to-guides/)

    Reference
---------

 Reference material is **information-oriented**. A reference is well-structured and allows the reader to find information about a specific topic. They should be short and to the point. Boring is fine! Use an imperative voice. For example: “Inherit from the Page model”.

 Most references will be auto-generated based on doc-strings in the Python code.

 [More about reference](https://diataxis.fr/reference/)

   Explanation
-----------

 Explanations are **understanding-oriented**. They are high-level and offer context to concepts and design decisions. There is little or no code involved in explanations, which are used to deepen the theoretical understanding of a practical draft. Explanations are used to establish connections and may require some prior knowledge of the principles being explored.

 [More about explanation](https://diataxis.fr/explanation/)

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/documentation_modes.html>

Reporting security issues
=========================

  Note

 Please report security issues **only** to [security@wagtail.org](mailto:security%40wagtail.org).

  Most normal bugs in Wagtail are reported as [GitHub issues](https://github.com/wagtail/wagtail/issues), but due to the sensitive nature of security issues, we ask that they not be publicly reported in this fashion.

 Instead, if you believe you’ve found something in Wagtail which has security implications, please send a description of the issue via email to [security@wagtail.org](mailto:security%40wagtail.org). Mail sent to that address reaches a subset of the core team, who can forward security issues to other core team members for broader discussion if needed.

 Once you’ve submitted an issue via email, you should receive an acknowledgement from a member of the security team within 48 hours, and depending on the action to be taken, you may receive further followup emails.

 If you want to send an encrypted email (optional), the public key ID for [security@wagtail.org](mailto:security%40wagtail.org) is `0xbed227b4daf93ff9`, and this public key is available from most commonly-used keyservers.

 Django security issues should be reported directly to the Django Project, following [Django’s security policies](https://docs.djangoproject.com/en/dev/internals/security/) (upon which Wagtail’s own policies are based).

  Supported versions
------------------

 At any given time, the Wagtail team provides official security support for several versions of Wagtail:

 * The `main` development branch, hosted on GitHub, which will become the next release of Wagtail, receives security support.
* The two most recent Wagtail release series receive security support. For example, during the development cycle leading to the release of Wagtail 2.6, support will be provided for Wagtail 2.5 and Wagtail 2.4. Upon the release of Wagtail 2.6, Wagtail 2.4’s security support will end.
* The latest long-term support release will receive security updates.

 When new releases are issued for security reasons, the accompanying notice will include a list of affected versions. This list is comprised solely of supported versions of Wagtail: older versions may also be affected, but we do not investigate to determine that, and will not issue patches or new releases for those versions.

   How Wagtail discloses security issues
-------------------------------------

 Our process for taking a security issue from private discussion to public disclosure involves multiple steps.

 There is no fixed period of time by which a confirmed security issue will be resolved as this is dependent on the issue, however it will be a priority of the Wagtail team to issue a security release as soon as possible.

 The reporter of the issue will receive notification of the date on which we plan to take the issue public. On the day of disclosure, we will take the following steps:

 1. Apply the relevant patch(es) to Wagtail’s codebase. The commit messages for these patches will indicate that they are for security issues, but will not describe the issue in any detail; instead, they will warn of upcoming disclosure.
2. Issue the relevant release(s), by placing new packages on [the Python Package Index](https://pypi.org/project/wagtail/), tagging the new release(s) in Wagtail’s GitHub repository and updating Wagtail’s [release notes](https://docs.wagtail.org/en/stable/releases/index.html).
3. Post a public entry on [Wagtail’s blog](https://wagtail.org/blog/), describing the issue and its resolution in detail, pointing to the relevant patches and new releases, and crediting the reporter of the issue (if the reporter wishes to be publicly identified).
4. Post a notice to the [Wagtail support forum](https://groups.google.com/g/wagtail) and Twitter feed ([@WagtailCMS](https://twitter.com/wagtailcms)) that links to the blog post.

 If a reported issue is believed to be particularly time-sensitive – due to a known exploit in the wild, for example – the time between advance notification and public disclosure may be shortened considerably.

   CSV export security considerations
----------------------------------

 In various places Wagtail provides the option to export data in CSV format, and several reporters have raised the possibility of a malicious user inserting data that will be interpreted as a formula when loaded into a spreadsheet package such as Microsoft Excel. We do not consider this to be a security vulnerability in Wagtail. CSV as defined by [RFC 4180](https://datatracker.ietf.org/doc/html/rfc4180) is purely a data format, and makes no assertions about how that data is to be interpreted; the decision made by certain software to treat some strings as executable code has no basis in the specification. As such, Wagtail cannot be responsible for the data it generates being loaded into a software package that interprets it insecurely, any more than it would be responsible for its data being loaded into a missile control system. This is consistent with [the Google security team’s position](https://sites.google.com/site/bughunteruniversity/nonvuln/csv-excel-formula-injection).

 Since the CSV format has no concept of formulae or macros, there is also no agreed-upon convention for escaping data to prevent it from being interpreted in that way; commonly-suggested approaches such as prefixing the field with a quote character would corrupt legitimate data (such as phone numbers beginning with ‘+’) when interpreted by software correctly following the CSV specification.

 Wagtail’s data exports default to XLSX, which can be loaded into spreadsheet software without any such issues. This minimises the risk of a user handling CSV files insecurely, as they would have to explicitly choose CSV over the more familiar XLSX format.

 

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/security.html>

Wagtail’s release process
=========================

  Official releases
-----------------

 Release numbering works as follows:

 * Versions are numbered in the form `A.B` or `A.B.C`.
* `A.B` is the *feature release* version number. Each version will be mostly backwards compatible with the previous release. Exceptions to this rule will be listed in the release notes.
* `C` is the *patch release* version number, which is incremented for bugfix and security releases. These releases will be 100% backwards-compatible with the previous patch release. The only exception is when a security or data loss issue can’t be fixed without breaking backwards-compatibility. If this happens, the release notes will provide detailed upgrade instructions.
* Before a new feature release, we’ll make at least one release candidate release. These are of the form `A.BrcN`, which means the `Nth` release candidate of version `A.B`.

 In git, each Wagtail release will have a tag indicating its version number. Additionally, each release series has its own branch, called `stable/A.B.x`, and bugfix/security releases will be issued from those branches.

 **Feature release**

 Feature releases (A.B, A.B+1, etc.) happen every three months – see [release schedule](#release-schedule) for details. These releases will contain new features and improvements to existing features.

 **Patch release**

 Patch releases (A.B.C, A.B.C+1, etc.) will be issued as needed, to fix bugs and/or security issues.

 These releases will be 100% compatible with the associated feature release, unless this is impossible for security reasons or to prevent data loss. So the answer to “should I upgrade to the latest patch release?” will always be “yes.”

 **Long-term support release**

 Certain feature releases will be designated as long-term support (LTS) releases. These releases will get security and data loss fixes applied for a guaranteed period of time, typically six months.

   Release cadence
---------------

 Wagtail uses a loose form of [semantic versioning](https://semver.org/). SemVer makes it easier to see at a glance how compatible releases are with each other. It also helps to anticipate when compatibility shims will be removed. It’s not a pure form of SemVer as each feature release will continue to have a few documented backwards incompatibilities where a deprecation path isn’t possible or not worth the cost.

   Deprecation policy
------------------

 A feature release may deprecate certain features from previous releases. If a feature is deprecated in feature release A.B, it will continue to work in the following version but raise warnings. Features deprecated in release A.B will be removed in the A.B+2 release to ensure deprecations are done over at least 2 feature releases.

 So, for example, if we decided to start the deprecation of a function in Wagtail 1.4:

 * Wagtail 1.4 will contain a backwards-compatible replica of the function which will raise a `RemovedInWagtail16Warning`.
* Wagtail 1.5 will still contain the backwards-compatible replica.
* Wagtail 1.6 will remove the feature outright.

 The warnings are silent by default. You can turn on display of these warnings with the `python -Wd` option.

   Supported versions
------------------

 At any moment in time, Wagtail’s developer team will support a set of releases to varying levels.

 * The current development `main` will get new features and bug fixes requiring non-trivial refactoring.
* Patches applied to the `main` branch must also be applied to the last feature release branch, to be released in the next patch release of that feature series, when they fix critical problems:


	+ Security issues.
	+ Data loss bugs.
	+ Crashing bugs.
	+ Major functionality bugs in newly-introduced features.
	+ Regressions from older versions of Wagtail.The rule of thumb is that fixes will be backported to the last feature release for bugs that would have prevented a release in the first place (release blockers).
* Security fixes and data loss bugs will be applied to the current `main`, the last feature release branch, and any other supported long-term support release branches.
* Documentation fixes generally will be more freely backported to the last release branch. That’s because it’s highly advantageous to have the docs for the last release be up-to-date and correct, and the risk of introducing regressions is much less of a concern.

 As a concrete example, consider a moment in time halfway between the release of Wagtail 1.6 and 1.7. At this point in time:

 * Features will be added to `main`, to be released as Wagtail 1.7.
* Critical bug fixes will be applied to the `stable/1.6.x` branch, and released as 1.6.1, 1.6.2, etc.
* Security fixes and bug fixes for data loss issues will be applied to `main` and to the `stable/1.6.x` and `stable/1.4.x` (LTS) branches. They will trigger the release of `1.6.1`, `1.4.8`, etc.
* Documentation fixes will be applied to `main`, and, if easily backported, to the latest stable branch, `1.6.x`.

   Supported versions of Django
----------------------------

 Each release of Wagtail declares which versions of Django it supports.

 Typically, a new Wagtail feature release supports the last long-term support version and all following versions of Django.

 For example, consider a moment in time before release of Wagtail 1.5 and after the following releases:

 * Django 1.8 (LTS)
* Django 1.9
* Wagtail 1.4 (LTS) - Released before Django 1.10 and supports Django 1.8 and 1.9
* Django 1.10

 Wagtail 1.5 will support Django 1.8 (LTS), 1.9, 1.10. Wagtail 1.4 will still support only Django 1.8 (LTS) and 1.9.

   Release schedule
----------------

 Wagtail uses a [time-based release schedule](https://github.com/wagtail/wagtail/wiki/Release-schedule), with feature releases every three months.

 After each feature release, the release manager will announce a timeline for the next feature release.

  ### Release cycle

 Each release cycle consists of three parts:

  #### Phase one: feature proposal

 The first phase of the release process will include figuring out what major features to include in the next version. This should include a good deal of preliminary work on those features – working code trumps grand design.

   #### Phase two: development

 The second part of the release schedule is the “heads-down” working period. Using the roadmap produced at the end of phase one, we’ll all work very hard to get everything on it done.

 At the end of phase two, any unfinished features will be postponed until the next release.

 At this point, the `stable/A.B.x` branch will be forked from `main`.

   #### Phase three: bugfixes

 The last part of a release cycle is spent fixing bugs – no new features will be accepted during this time.

 Once all known blocking bugs have been addressed, a release candidate will be made available for testing. The final release will usually follow two weeks later, although this period may be extended if the further release blockers are found.

 During this phase, committers will be more and more conservative with backports, to avoid introducing regressions. After the release candidate, only release blockers and documentation fixes should be backported.

 Developers should avoid adding any new translatable strings after the release candidate - this ensures that translators have the full period between the release candidate and the final release to bring translations up to date. Translations will be re-imported immediately before the final release.

 In parallel to this phase, `main` can receive new features, to be released in the `A.B+1` cycle.

    ### Bug-fix releases

 After a feature release `A.B`, the previous release will go into bugfix mode.

 The branch for the previous feature release `stable/A.B-1.x` will include bugfixes. Critical bugs fixed on `main` must *also* be fixed on the bugfix branch; this means that commits need to cleanly separate bug fixes from feature additions. The developer who commits a fix to `main` will be responsible for also applying the fix to the current bugfix branch.

  

 © 2014-present Torchbox Ltd and individual contributors.  
All rights are reserved.  
Licensed under the BSD License.  

<https://docs.wagtail.org/en/stable/contributing/release_process.html>

wagtail