kopia lustrzana https://github.com/wagtail/wagtail
documentation - migrate advanced_topics/customisation/** to md
rodzic
4424d23fa4
commit
e6865e7b0f
|
|
@ -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 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:
|
||||
|
||||
```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;
|
||||
|
||||
// Wagtail’s 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;
|
||||
```
|
||||
|
|
@ -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 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:
|
||||
|
||||
.. 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;
|
||||
|
||||
// Wagtail’s 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;
|
||||
|
|
@ -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']
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Customising Wagtail
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
---
|
||||
page_editing_interface
|
||||
admin_templates
|
||||
custom_user_models
|
||||
streamfield_blocks
|
||||
```
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
Customising Wagtail
|
||||
===================
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
page_editing_interface
|
||||
admin_templates
|
||||
custom_user_models
|
||||
streamfield_blocks
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
Ładowanie…
Reference in New Issue