August 1, 2024
---
local:
depth: 1
---
The page editor’s Checks panel now displays two content metrics: word count, and reading time. They are calculated based on the contents of the page preview, with a new mechanism to extract content from the previewed page for processing within the page editor. The Checks panel has also been redesigned to accommodate a wider breadth of types of checks, and interactive checks, in future releases.
This feature was developed by Albina Starykova and sponsored by The Motley Fool.
When multiple users concurrently work on the same content, Wagtail now displays notifications to inform them of potential editing conflicts. When a user saves their work, other users are informed and presented with options: they can refresh the page to view the latest changes, or proceed with their own changes, overwriting the other user's work.
Concurrent editing notifications are available for pages, and snippets. Specific messaging about conflicting versions is only available for pages and snippets with support for saving revisions. To configure how often notifications are updated, use WAGTAIL_EDITING_SESSION_PING_INTERVAL
.
This feature was implemented by Matt Westcott and Sage Abdullah.
The built-in accessibility checker now enforces a new alt-text-quality
rule, which tests alt text for the presence of known bad patterns such as file extensions and underscores. This rule is enabled by default, but can be disabled if necessary.
This feature was implemented by Albina Starykova, with support from the Wagtail accessibility team.
All built-in and custom report views now use the Universal Listings visual design and filtering features introduced in all other listings in the admin interface over past releases. Thank you to Sage Abdullah for implementing this feature and continuing the rollout of the new designs.
StreamField definitions within migrations are now represented in a more compact form, where blocks that appear in multiple places within a StreamField structure are only defined once. For complex and deeply-nested StreamFields, this considerably reduces the size of migration files, and the memory consumption when loading them. This feature was developed by Matt Westcott.
HOSTNAMES
parameter on WAGTAILFRONTENDCACHE
to define which hostnames a backend should respond to (Jake Howard, sponsored by Oxfam America)EditView
and breadcrumbs (Rohit Sharma)ChooseParentView
if only one possible valid parent page is available (Matthias Brück)copy_for_translation_done
signal when a page is copied for translation (Arnar Tumi Þorsteinsson)deactivate()
method to ProgressController
(Alex Morega)ModelViewSet
(Sage Abdullah)routable_resolver_match
attribute available on RoutablePageMixin responses (Andy Chosak)UserViewSet
via the app config (Sage Abdullah)StreamBlock
/ ListBlock
min_num
/ max_num
(Matt Westcott)WAGTAILIMAGES_CHOOSER_PAGE_SIZE
setting functional again (Rohit Sharma)richtext
template tag to convert lazy translation values (Benjamin Bach).ico
images (Julie Rymer)verbose_name
on TranslatableMixin.locale
so that it is translated when used as a label (Romein van Buren)wagtail_serve
view is on a non-root path (Sage Abdullah)for_instance
method to PageLogEntryManager
(Matt Westcott)WAGTAIL_DATE_FORMAT
, WAGTAIL_DATETIME_FORMAT
and WAGTAIL_TIME_FORMAT
take FORMAT_MODULE_PATH
into account (Sébastien Corbin)restriction_type
field on PageViewRestriction (Shlomo Markowitz)Orderable
is not required for inline panels (Bojan Mihelac)prefers-reduced-motion
to the accessibility documentation (Roel Koper)vary_fields
property for custom image filters (Daniel Kirkham)DjangoJSONEncoder
instead of custom LazyStringEncoder
to serialize Draftail config (Sage Abdullah)WAGTAILIMAGES_CHOOSER_PAGE_SIZE
at runtime (Matt Westcott)client/scss
directory in Tailwind content config to speed up CSS compilation (Sage Abdullah)contrib.frontend_cache.backends
into dedicated sub-modules (Andy Babic)docs/autobuild.sh
script (Sævar Öfjörð Magnússon)urlparse
with urlsplit
to improve performance (Jake Howard)'BlockWidget' object has no attribute '_block_json'
from masking errors during StreamField serialization (Matt Westcott)Previous versions allowed passing a dict for DISTRIBUTION_ID
within the WAGTAILFRONTENDCACHE
configuration for a CloudFront backend, to allow specifying different distribution IDs for different hostnames. This is now deprecated; instead, multiple distribution IDs should be defined as multiple backends, with a HOSTNAMES
parameter to define the hostnames associated with each one. For example, a configuration such as:
WAGTAILFRONTENDCACHE = {
'cloudfront': {
'BACKEND': 'wagtail.contrib.frontend_cache.backends.CloudfrontBackend',
'DISTRIBUTION_ID': {
'www.wagtail.org': 'your-distribution-id',
'www.madewithwagtail.org': 'other-distribution-id',
},
},
}
should now be rewritten as:
WAGTAILFRONTENDCACHE = {
'mainsite': {
'BACKEND': 'wagtail.contrib.frontend_cache.backends.CloudfrontBackend',
'DISTRIBUTION_ID': 'your-distribution-id',
'HOSTNAMES': ['www.wagtail.org'],
},
'madewithwagtail': {
'BACKEND': 'wagtail.contrib.frontend_cache.backends.CloudfrontBackend',
'DISTRIBUTION_ID': 'other-distribution-id',
'HOSTNAMES': ['www.madewithwagtail.org'],
},
}
Models registered with a ModelViewSet
will now automatically have their {class}~django.contrib.auth.models.Permission
objects registered in the Groups administration area. Previously, you need to use the register_permissions
hook to register them.
If you have a model registered with a ModelViewSet
and you registered the model's permissions using the register_permissions
hook, you can now safely remove the hook.
If the viewset has {attr}~wagtail.admin.viewsets.model.ModelViewSet.inspect_view_enabled
set to True
, all permissions for the model are registered. Otherwise, the "view" permission is excluded from the registration.
To customize which permissions get registered for the model, you can override the {meth}~wagtail.admin.viewsets.model.ModelViewSet.get_permissions_to_register
method.
This behavior now applies to snippets as well. Previously, the "view" permission for snippets is always registered regardless of inspect_view_enabled
. If you wish to register the "view" permission, you can enable the inspect view:
class FooViewSet(SnippetViewSet):
...
inspect_view_enabled = True
Alternatively, if you wish to register the "view" permission without enabling the inspect view (i.e. the previous behavior), you can override get_permissions_to_register
like the following:
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
class FooViewSet(SnippetViewSet):
def get_permissions_to_register(self):
content_type = ContentType.objects.get_for_model(self.model)
return Permission.objects.filter(content_type=content_type)
WAGTAIL_USER_EDIT_FORM
, WAGTAIL_USER_CREATION_FORM
, and WAGTAIL_USER_CUSTOM_FIELDS
settingsThis release introduces a customizable UserViewSet
class, which can be used to customize various aspects of Wagtail's admin views for managing users, including the form classes for creating and editing users. As a result, the WAGTAIL_USER_EDIT_FORM
, WAGTAIL_USER_CREATION_FORM
, and WAGTAIL_USER_CUSTOM_FIELDS
settings have been deprecated in favor of customizing the form classes via UserViewSet.get_form_class()
.
If you use the aforementioned settings, you can migrate your code by making the following changes.
Given the following custom user model:
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)
The following custom forms:
class CustomUserEditForm(UserEditForm):
status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status"))
class CustomUserCreationForm(UserCreationForm):
status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status"))
And the following settings:
WAGTAIL_USER_EDIT_FORM = "myapp.forms.CustomUserEditForm"
WAGTAIL_USER_CREATION_FORM = "myapp.forms.CustomUserCreationForm"
WAGTAIL_USER_CUSTOM_FIELDS = ["country", "status"]
Change the custom forms to the following:
class CustomUserEditForm(UserEditForm):
status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status"))
# Use ModelForm's automatic form fields generation for the model's `country` field,
# but use an explicit custom form field for `status`.
# This replaces the `WAGTAIL_USER_CUSTOM_FIELDS` setting.
class Meta(UserEditForm.Meta):
fields = UserEditForm.Meta.fields | {"country", "status"}
class CustomUserCreationForm(UserCreationForm):
status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status"))
# Use ModelForm's automatic form fields generation for the model's `country` field,
# but use an explicit custom form field for `status`.
# This replaces the `WAGTAIL_USER_CUSTOM_FIELDS` setting.
class Meta(UserCreationForm.Meta):
fields = UserEditForm.Meta.fields | {"country", "status"}
Create a custom UserViewSet
subclass in e.g. myapp/viewsets.py
:
# myapp/viewsets.py
from wagtail.users.views.users import UserViewSet as WagtailUserViewSet
from .forms import CustomUserCreationForm, CustomUserEditForm
class UserViewSet(WagtailUserViewSet):
# This replaces the WAGTAIL_USER_EDIT_FORM and WAGTAIL_USER_CREATION_FORM settings
def get_form_class(self, for_update=False):
if for_update:
return CustomUserEditForm
return CustomUserCreationForm
If you already have a custom GroupViewSet
as described in [](customizing_group_views), you can reuse the custom WagtailUsersAppConfig
subclass. Otherwise, create an apps.py
file within your project folder (the one containing the top-level settings and urls modules) e.g. myproject/apps.py
. Then, create a custom WagtailUsersAppConfig
subclass in that file, with a user_viewset
attribute pointing to the custom UserViewSet
subclass:
# myproject/apps.py
from wagtail.users.apps import WagtailUsersAppConfig
class CustomUsersAppConfig(WagtailUsersAppConfig):
user_viewset = "myapp.viewsets.UserViewSet"
# If you have customized the GroupViewSet before
group_viewset = "myapp.viewsets.GroupViewSet"
Replace wagtail.users
in settings.INSTALLED_APPS
with the path to CustomUsersAppConfig
:
INSTALLED_APPS = [
...,
# Make sure you have two separate entries for the custom user model's app
# and the custom app config for the wagtail.users app
"myapp", # an app that contains the custom user model
"myproject.apps.CustomUsersAppConfig", # a custom app config for the wagtail.users app
# "wagtail.users", # this should be removed in favour of the custom app config
...,
]
You can also place the `WagtailUsersAppConfig` subclass inside the same `apps.py` file of your custom user model's app (instead of in a `myproject/apps.py` file), but you need to be careful. Make sure to use two separate config classes instead of turning your existing `AppConfig` subclass into a `WagtailUsersAppConfig` subclass, as that would cause Django to pick up your custom user model as being part of `wagtail.users`. You may also need to set {attr}`~django.apps.AppConfig.default` to `True` in your own app's `AppConfig`, unless you already use a dotted path to the app's `AppConfig` subclass in `INSTALLED_APPS`.
For more details, see [](custom_userviewset).
The report views have been reimplemented to use the new Universal Listings UI, which introduces AJAX-based filtering and support for the wagtail.admin.ui.tables
framework.
As a result, a number of changes have been made to the ReportView
and PageReportView
classes, as well as their templates.
If you have custom report views as documented in [](adding_reports), you will need to make the following changes.
The title
attribute on the view class should be renamed to page_title
:
class UnpublishedChangesReportView(PageReportView):
- title = "Pages with unpublished changes"
+ page_title = "Pages with unpublished changes"
Set the index_url_name
and index_results_url_name
attributes on the view class:
class UnpublishedChangesReportView(PageReportView):
+ index_url_name = "unpublished_changes_report"
+ index_results_url_name = "unpublished_changes_report_results"
and register the results-only view:
@hooks.register("register_admin_urls")
def register_unpublished_changes_report_url():
return [
path("reports/unpublished-changes/", UnpublishedChangesReportView.as_view(), name="unpublished_changes_report"),
+ # Add a results-only view to add support for AJAX-based filtering
+ path("reports/unpublished-changes/results/", UnpublishedChangesReportView.as_view(results_only=True), name="unpublished_changes_report_results"),
]
If you are only extending the templates to add your own markup for the listing table (and not other parts of the view template), you need to change the template_name
into results_template_name
on the view class.
For a page report, the following changes are needed:
template_name
to results_template_name
, and optionally rename the template (e.g. reports/unpublished_changes_report.html
to reports/unpublished_changes_report_results.html
).wagtailadmin/reports/base_page_report_results.html
.The listing
and no_results
blocks should be renamed to results
and no_results_message
, respectively.
class UnpublishedChangesReportView(PageReportView):
- template_name = "reports/unpublished_changes_report.html"
+ results_template_name = "reports/unpublished_changes_report_results.html"
{# <project>/templates/reports/unpublished_changes_report_results.html #}
-{% extends "wagtailadmin/reports/base_page_report.html" %}
+{% extends "wagtailadmin/reports/base_page_report_results.html" %}
-{% block listing %}
+{% block results %}
{% include "reports/include/_list_unpublished_changes.html" %}
{% endblock %}
-{% block no_results %}
+{% block no_results_message %}
<p>No pages with unpublished changes.</p>
{% endblock %}
For a non-page report, the following changes are needed:
template_name
to results_template_name
, and optionally rename the template (e.g. reports/custom_non_page_report.html
to reports/custom_non_page_report_results.html
).wagtailadmin/reports/base_report_results.html
.results
block containing both the results listing and the "no results" message; these should now become separate blocks named results
and no_results_message
.Before:
class CustomNonPageReportView(ReportView):
template_name = "reports/custom_non_page_report.html"
{# <project>/templates/reports/custom_non_page_report.html #}
{% extends "wagtailadmin/reports/base_report.html" %}
{% block results %}
{% if object_list %}
<table class="listing">
<!-- Table markup goes here -->
</table>
{% else %}
<p>No results found.</p>
{% endif %}
{% endblock %}
After:
class CustomNonPageReportView(ReportView):
results_template_name = "reports/custom_non_page_report_results.html"
{# <project>/templates/reports/custom_non_page_report_results.html #}
{% extends "wagtailadmin/reports/base_report_results.html" %}
{% block results %}
<table class="listing">
<!-- Table markup goes here -->
</table>
{% endblock %}
{% block no_results_message %}
<p>No results found.</p>
{% endblock %}
If you need to completely customize the view's template, you can still override the template_name
attribute on the view class. Note that both ReportView
and PageReportView
now use the wagtailadmin/reports/base_report.html
template, which now extends the wagtailadmin/generic/listing.html
template. The wagtailadmin/reports/base_page_report.html
template is now unused and should be replaced with wagtailadmin/reports/base_report.html
.
If you override template_name
, it is still necessary to set results_template_name
to a template that extends wagtailadmin/reports/base_report_results.html
(or wagtailadmin/reports/base_page_report_results.html
for page reports), so the view can correctly update the listing and show the active filters as you apply or remove any filters.
window.ActivateWorkflowActionsForDashboard
and window.ActivateWorkflowActionsForEditView
The undocumented usage of the JavaScript window.ActivateWorkflowActionsForDashboard
and window.ActivateWorkflowActionsForEditView
functions will be removed in a future release.
These functions are only used by Wagtail to initialize event listeners to workflow action buttons via inline scripts and are never intended to be used in custom code. The inline scripts have been removed in favour of initializing the event listeners directly in the included workflow-action.js
script. As a result, the functions no longer need to be globally-accessible.
Any custom workflow actions should be done using the documented approach for customising the behavior of custom task types, such as by overriding Task.get_actions()
.