documentation - migrate advanced_topics/customisation/** to md

pull/8604/head
Thiago Costa de Souza 2022-05-25 20:35:32 -03:00 zatwierdzone przez LB (Ben Johnston)
rodzic 4424d23fa4
commit e6865e7b0f
10 zmienionych plików z 828 dodań i 873 usunięć

Wyświetl plik

@ -0,0 +1,272 @@
# 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`:
```python
INSTALLED_APPS = (
# ...
'dashboard',
'wagtail',
'wagtail.admin',
# ...
)
```
(custom_branding)=
## 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`:
```html+django
{% 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 on the admin 404 error page; to replace it there too, create a template file `dashboard/templates/wagtailadmin/404.html` that overrides the `branding_logo` block.
The logo also appears on the wagtail userbar; to replace it there too, 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`:
```html+django
{% 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`:
```html+django
{% 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`:
```html+django
{% 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`:
```html+django
{% extends "wagtailadmin/home.html" %}
{% block branding_welcome %}Welcome to Frank's Site{% endblock %}
```
(custom_user_interface_fonts)=
## 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) and override the variables within the `:root` selector:
```css
:root {
--w-font-sans: Papyrus;
--w-font-mono: Courier;
}
```
(custom_user_interface_colours)=
## 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 primary colour used in the admin user interface, inject a CSS file using the hook [](insert_global_admin_css) and override the variables within the `:root` selector:
```css
:root {
--color-primary-hue: 25;
}
```
`color-primary` is an [hsl colour](https://en.wikipedia.org/wiki/HSL_and_HSV) composed of 3 CSS variables - `--color-primary-hue` (0-360 with no unit), `--color-primary-saturation` (a percentage), and `--color-primary-lightness` (also a percentage). Separating the colour into 3 allows us to calculate variations on the colour to use alongside the primary colour. If needed, you can also control those variations manually by setting `hue`, `saturation`, and `lightness` variables for the following colours: `color-primary-darker`, `color-primary-dark`, `color-primary-lighter`, `color-primary-light`, `color-input-focus`, and `color-input-focus-border`:
```css
:root {
--color-primary-hue: 25;
--color-primary-saturation: 100%;
--color-primary-lightness: 25%;
--color-primary-darker-hue: 24;
--color-primary-darker-saturation: 100%;
--color-primary-darker-lightness: 20%;
--color-primary-dark-hue: 23;
--color-primary-dark-saturation: 100%;
--color-primary-dark-lightness: 15%;
}
```
If instead you intend to set all available colours, you can use any valid css colours:
```css
:root {
--color-primary: mediumaquamarine;
--color-primary-darker: rebeccapurple;
--color-primary-dark: hsl(330, 100%, 70%);
--color-primary-lighter: azure;
--color-primary-light: aliceblue;
--color-input-focus: rgb(204, 0, 102);
--color-input-focus-border: #4d0026;
}
```
## 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:
```html+django
{% 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:
```html+django
{% 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:
```html+django
{% extends "wagtailadmin/login.html" %}
{% block fields %}
{{ block.super }}
<li class="full">
<div class="field iconfield">
Two factor auth token
<div class="input icon-key">
<input type="text" name="two-factor-auth">
</div>
</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:
```html+django
{% 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:
```html+django
{% extends "wagtailadmin/login.html" %}
{% block login_form %}
<p>Some extra form content</p>
{{ block.super }}
{% endblock %}
```
(extending_clientside_components)=
## Extending client-side components
Some of Wagtails 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:
```javascript
// '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:
```javascript
window.wagtail.components.Icon;
window.wagtail.components.Portal;
```
Pages containing rich text editors also have access to:
```javascript
// 'draft-js'
window.DraftJS;
// 'draftail'
window.Draftail;
// Wagtails Draftail-related APIs and components.
window.draftail;
window.draftail.ModalWorkflowSource;
window.draftail.ImageModalWorkflowSource;
window.draftail.EmbedModalWorkflowSource;
window.draftail.LinkModalWorkflowSource;
window.draftail.DocumentModalWorkflowSource;
window.draftail.Tooltip;
window.draftail.TooltipEntity;
```

Wyświetl plik

@ -1,291 +0,0 @@
===========================
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``:
.. code-block:: python
INSTALLED_APPS = (
# ...
'dashboard',
'wagtail',
'wagtail.admin',
# ...
)
.. _custom_branding:
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``:
.. code-block:: html+django
{% 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 on the admin 404 error page; to replace it there too, create a template file ``dashboard/templates/wagtailadmin/404.html`` that overrides the ``branding_logo`` block.
The logo also appears on the wagtail userbar; to replace it there too, 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``:
.. code-block:: html+django
{% 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``:
.. code-block:: html+django
{% 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``:
.. code-block:: html+django
{% 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``:
.. code-block:: html+django
{% extends "wagtailadmin/home.html" %}
{% block branding_welcome %}Welcome to Frank's Site{% endblock %}
.. _custom_user_interface_fonts:
Custom user interface fonts
===========================
To customise the font families used in the admin user interface, inject a CSS file using the hook :ref:`insert_global_admin_css` and override the variables within the ``:root`` selector:
.. code-block:: css
:root {
--w-font-sans: Papyrus;
--w-font-mono: Courier;
}
.. _custom_user_interface_colours:
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 primary colour used in the admin user interface, inject a CSS file using the hook :ref:`insert_global_admin_css` and override the variables within the ``:root`` selector:
.. code-block:: text
:root {
--color-primary-hue: 25;
}
``color-primary`` is an `hsl colour <https://en.wikipedia.org/wiki/HSL_and_HSV>`_ composed of 3 CSS variables - ``--color-primary-hue`` (0-360 with no unit), ``--color-primary-saturation`` (a percentage), and ``--color-primary-lightness`` (also a percentage). Separating the colour into 3 allows us to calculate variations on the colour to use alongside the primary colour. If needed, you can also control those variations manually by setting ``hue``, ``saturation``, and ``lightness`` variables for the following colours: ``color-primary-darker``, ``color-primary-dark``, ``color-primary-lighter``, ``color-primary-light``, ``color-input-focus``, and ``color-input-focus-border``:
.. code-block:: text
:root {
--color-primary-hue: 25;
--color-primary-saturation: 100%;
--color-primary-lightness: 25%;
--color-primary-darker-hue: 24;
--color-primary-darker-saturation: 100%;
--color-primary-darker-lightness: 20%;
--color-primary-dark-hue: 23;
--color-primary-dark-saturation: 100%;
--color-primary-dark-lightness: 15%;
}
If instead you intend to set all available colours, you can use any valid css colours:
.. code-block:: text
:root {
--color-primary: mediumaquamarine;
--color-primary-darker: rebeccapurple;
--color-primary-dark: hsl(330, 100%, 70%);
--color-primary-lighter: azure;
--color-primary-light: aliceblue;
--color-input-focus: rgb(204, 0, 102);
--color-input-focus-border: #4d0026;
}
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:
.. code-block:: html+django
{% 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:
.. code-block:: html+django
{% 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:
.. code-block:: html+django
{% extends "wagtailadmin/login.html" %}
{% block fields %}
{{ block.super }}
<li class="full">
<div class="field iconfield">
Two factor auth token
<div class="input icon-key">
<input type="text" name="two-factor-auth">
</div>
</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:
.. code-block:: html+django
{% 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:
.. code-block:: html+django
{% extends "wagtailadmin/login.html" %}
{% block login_form %}
<p>Some extra form content</p>
{{ block.super }}
{% endblock %}
.. _extending_clientside_components:
Extending client-side components
================================
Some of Wagtails 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:
.. code-block:: javascript
// '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:
.. code-block:: javascript
window.wagtail.components.Icon;
window.wagtail.components.Portal;
Pages containing rich text editors also have access to:
.. code-block:: javascript
// 'draft-js'
window.DraftJS;
// 'draftail'
window.Draftail;
// Wagtails Draftail-related APIs and components.
window.draftail;
window.draftail.ModalWorkflowSource;
window.draftail.ImageModalWorkflowSource;
window.draftail.EmbedModalWorkflowSource;
window.draftail.LinkModalWorkflowSource;
window.draftail.DocumentModalWorkflowSource;
window.draftail.Tooltip;
window.draftail.TooltipEntity;

Wyświetl plik

@ -0,0 +1,83 @@
# 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).
```python
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`
```python
AUTH_USER_MODEL = 'users.User'
```
Create your custom user 'create' and 'edit' forms in your app:
```python
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:
```html+django
{% extends "wagtailusers/users/create.html" %}
{% block extra_fields %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.country %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.status %}
{% endblock extra_fields %}
```
Template edit.html:
```html+django
{% extends "wagtailusers/users/edit.html" %}
{% block extra_fields %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.country %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.status %}
{% 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:
```python
WAGTAIL_USER_EDIT_FORM = 'users.forms.CustomUserEditForm'
WAGTAIL_USER_CREATION_FORM = 'users.forms.CustomUserCreationForm'
WAGTAIL_USER_CUSTOM_FIELDS = ['country', 'status']
```

Wyświetl plik

@ -1,89 +0,0 @@
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).
.. code-block:: python
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_ to reference
your model. In this example the app is called ``users`` and the model is ``User``
.. code-block:: python
AUTH_USER_MODEL = 'users.User'
Create your custom user 'create' and 'edit' forms in your app:
.. code-block:: python
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:
.. code-block:: html+django
{% extends "wagtailusers/users/create.html" %}
{% block extra_fields %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.country %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.status %}
{% endblock extra_fields %}
Template edit.html:
.. code-block:: html+django
{% extends "wagtailusers/users/edit.html" %}
{% block extra_fields %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.country %}
{% include "wagtailadmin/shared/field_as_li.html" with field=form.status %}
{% 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:
.. code-block:: python
WAGTAIL_USER_EDIT_FORM = 'users.forms.CustomUserEditForm'
WAGTAIL_USER_CREATION_FORM = 'users.forms.CustomUserCreationForm'
WAGTAIL_USER_CUSTOM_FIELDS = ['country', 'status']
.. _AUTH_USER_MODEL: https://docs.djangoproject.com/en/stable/topics/auth/customizing/#substituting-a-custom-user-model

Wyświetl plik

@ -0,0 +1,11 @@
# Customising Wagtail
```{toctree}
---
maxdepth: 2
---
page_editing_interface
admin_templates
custom_user_models
streamfield_blocks
```

Wyświetl plik

@ -1,11 +0,0 @@
Customising Wagtail
===================
.. toctree::
:maxdepth: 2
page_editing_interface
admin_templates
custom_user_models
streamfield_blocks

Wyświetl plik

@ -0,0 +1,206 @@
# Customising the editing interface
(customising_the_tabbed_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:
```python
from wagtail.admin.panels import TabbedInterface, ObjectList
class BlogPage(Page):
# field definitions omitted
content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('date'),
FieldPanel('body', classname="full"),
]
sidebar_content_panels = [
FieldPanel('advert'),
InlinePanel('related_links', label="Related links"),
]
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', classname="settings"),
])
```
(rich-text)=
## 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:
```python
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel
class BookPage(Page):
body = RichTextField()
content_panels = Page.content_panels + [
FieldPanel('body', classname="full"),
]
```
`RichTextField` inherits from Django's basic `TextField` field, so you can pass any field parameters into `RichTextField` as if using a normal Django field. 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).
(rich_text_features)=
### 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:
```python
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))
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:
- [](../../extending/rich_text_internals)
- [](../../extending/extending_draftail)
(rich_text_image_formats)=
### 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:
```python
# 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 [](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.
```
(custom_edit_handler_forms)=
## Customising generated forms
```{eval-rst}
.. class:: wagtail.admin.forms.WagtailAdminModelForm
.. class:: wagtail.admin.forms.WagtailAdminPageForm
```
Wagtail automatically generates forms using the panels configured on the model.
By default, this form subclasses [WagtailAdminModelForm](wagtail.admin.forms.WagtailAdminModelForm),
or [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),
and custom forms for pages must subclass [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:
```python
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.

Wyświetl plik

@ -1,213 +0,0 @@
Customising the editing interface
=================================
.. _customising_the_tabbed_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:
.. code-block:: python
from wagtail.admin.panels import TabbedInterface, ObjectList
class BlogPage(Page):
# field definitions omitted
content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('date'),
FieldPanel('body', classname="full"),
]
sidebar_content_panels = [
FieldPanel('advert'),
InlinePanel('related_links', label="Related links"),
]
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', classname="settings"),
])
.. _rich-text:
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 :class:`~wagtail.fields.RichTextField` function when defining a model field:
.. code-block:: python
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel
class BookPage(Page):
body = RichTextField()
content_panels = Page.content_panels + [
FieldPanel('body', classname="full"),
]
:class:`~wagtail.fields.RichTextField` inherits from Django's basic ``TextField`` field, so you can pass any field parameters into :class:`~wagtail.fields.RichTextField` as if using a normal Django field. This field does not need a special panel and can be defined with ``FieldPanel``.
However, template output from :class:`~wagtail.fields.RichTextField` is special and needs to be filtered in order to preserve embedded content. See :ref:`rich-text-filter`.
.. _rich_text_features:
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 :ref:`StreamField <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:
.. code-block:: python
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 :ref:`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:
* :doc:`../../extending/rich_text_internals`
* :doc:`../../extending/extending_draftail`
.. _rich_text_image_formats:
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 :class:`~wagtail.fields.RichTextField` editor.
As an example, add a "thumbnail" format:
.. code-block:: python
# 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 :class:`~wagtail.fields.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 the :ref:`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.
.. _custom_edit_handler_forms:
Customising generated forms
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. class:: wagtail.admin.forms.WagtailAdminModelForm
.. class:: wagtail.admin.forms.WagtailAdminPageForm
Wagtail automatically generates forms using the panels configured on the model.
By default, this form subclasses :class:`~wagtail.admin.forms.WagtailAdminModelForm`,
or :class:`~wagtail.admin.forms.WagtailAdminPageForm` for pages.
A custom base form class can be configured by setting the :attr:`base_form_class` attribute on any model.
Custom forms for snippets must subclass :class:`~wagtail.admin.forms.WagtailAdminModelForm`,
and custom forms for pages must subclass :class:`~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:
.. code-block:: python
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.

Wyświetl plik

@ -0,0 +1,256 @@
(custom_streamfield_blocks)=
# How to build custom StreamField blocks
(custom_editing_interfaces_for_structblock)=
## 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`:
```python
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) 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:
```python
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:
```html+django
{% 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="field {% if child.block.required %}required{% endif %}" data-contentpath="{{ child.block.name }}">
{% if child.block.label %}
<label class="field__label" {% if child.id_for_label %}for="{{ child.id_for_label }}"{% endif %}>{{ child.block.label }}</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:
```python
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:
```python
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:
```javascript
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);
```
(custom_value_class_for_structblock)=
## 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:
```python
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:
```python
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.
```python
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:
```python
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:
```html+django
{% 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:
```python
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 [](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](django:custom-deconstruct-method) 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.

Wyświetl plik

@ -1,269 +0,0 @@
.. _custom_streamfield_blocks:
How to build custom StreamField blocks
======================================
.. _custom_editing_interfaces_for_structblock:
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``:
.. code-block:: python
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 :ref:`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:
.. code-block:: python
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:
.. code-block:: html+django
{% 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="field {% if child.block.required %}required{% endif %}" data-contentpath="{{ child.block.name }}">
{% if child.block.label %}
<label class="field__label" {% if child.id_for_label %}for="{{ child.id_for_label }}"{% endif %}>{{ child.block.label }}</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:
.. code-block:: python
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:
.. code-block:: python
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:
.. code-block:: javascript
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);
.. _custom_value_class_for_structblock:
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:
.. code-block:: python
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:
.. code-block:: python
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.
.. code-block:: python
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:
.. code-block:: python
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:
.. code-block:: html+django
{% 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:
.. code-block:: python
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 :ref:`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 :ref:`custom deconstruct method <django:custom-deconstruct-method>` 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.