docs/releases/5.1.md
August 1, 2023
---
local:
depth: 1
---
FieldPanels can now be marked as read-only with the read_only=True keyword argument, so that they are displayed in the admin but cannot be edited. This feature was developed by Andy Babic.
As part of Google Season of Docs 2023, we worked with technical writer Damilola Oladele to make improvements to Wagtail’s "Getting started" tutorial. Here are the specific changes made as part of this project:
Thank you to Damilola for his work, and to Google for sponsoring this project.
wagtail startThe wagtail start command now supports an optional --template argument that allows you to specify a custom project template to use. This is useful if you want to use a custom template that includes additional features or customizations. For more details, see the project template reference. This feature was developed by Thibaud Colas.
The boost option on SearchField, to increase the ranking of search results that match on the specified field, is now respected by Elasticsearch 6 and above. This was previously only supported up to Elasticsearch 5, due to a change in Elasticsearch's API. This feature was developed by Shohan Dutta Roy.
This release adds support for Elasticsearch 8. This can be set up by installing a version 8.x release of the elasticsearch Python package, and setting wagtail.search.backends.elasticsearch8 as the search backend. Compatibility updates were contributed by Matt Westcott and Wesley van Lee.
As part of tackling Wagtail’s technical debt and improving CSP compatibility, we have continued extending our usage of Stimulus, based on the plans laid out in RFC 78: Adopt Stimulus.
attrs on FieldPanel and other panels to aid in custom Stimulus usage (Aman Pandey, Antoni Martyniuk, LB (Ben) Johnston)w-swap, a Stimulus controller (LB (Ben) Johnston)w-tooltip Stimulus controller (LB (Ben) Johnston)w-dialog Stimulus controller (Loveth Omokaro, LB (Ben) Johnston)w-teleport Stimulus controller (Loveth Omokaro, LB (Ben) Johnston)Wagtail now supports AVIF, a modern image format. We encourage all site implementers to consider using it to improve the performance of the sites and reduce their carbon footprint. For further details, see image file format, output image format and image quality.
This feature was developed by Aman Pandey as part of the Google Summer of Code program and a partnership with the Green Web Foundation and Green Coding Berlin, with support from Dan Braghis, Thibaud Colas, Sage Abdullah, Arne Tarara (Green Coding Berlin), and Chris Adams (Green Web Foundation).
This release includes several changes to permissions, to make them easier to use and maintain, as well as to improve performance.
PagePermissionPolicy (Sage Abdullah)UserPagePermissionsProxy and PagePermissionTester to use PagePermissionPolicy (Sage Abdullah, Tidiane Dia)UserPagePermissionsProxy (Sage Abdullah)We have made several improvements to snippets as part of RFC 85: Snippets parity with ModelAdmin, ahead of the deprecation of ModelAdmin contrib app.
SnippetViewSet.list_export (Sage Abdullah)purge_revisions command (Sage Abdullah)md5 as not being used for secure purposes, to avoid flagging on FIPS-mode systems (Sean Kelly)parse_query_string as a QueryDict to support multiple values (Aman Pandey)MenuItem.name for all admin menu and submenu items (Justin Koestinger)AbstractImage.get_renditions() for efficient generation of multiple renditions (Andy Babic)href="tel:..." attribute (Sahil Jangra)StreamField block when only one block type is declared (Sébastien Corbin)attrs on FieldPanel, FieldRowPanel, MultiFieldPanel, and others (Aman Pandey, Antoni Martyniuk, LB (Ben) Johnston)parent_page_types would disallow it (Dan Braghis)UsageView from EditView for snippets (Christer Jensen)RichText objects with the same values compare as equal (NikilTn)gettext_lazy on generic model views so that language settings are correctly used (Matt Westcott)MultipleChooserPanel (Matt Westcott)gettext_lazy works correctly when using verbose_name on a generic Settings models (Sébastien Corbin)innerHTML when modifying DOM content (LB (Ben) Johnston)ValueError when extending PagesAPIViewSet and setting meta_fields to an empty list (Henry Harutyunyan, Alex Morega)PagePermissionHelper.user_can_unpublish_obj() in ModelAdmin (Sébastien Corbin)search_promotions 0004_copy_queries migration for long-lived Wagtail instances (Sage Abdullah)TypeError in 0088_fix_log_entry_json_timestamps migration (Sage Abdullah)page_header_buttons template tag when accessing the context's request object (Robert Rollins)ModelAdminGroup (Onno Timmerman)log_action parameter on RevisionMixin.save_revision (Christer Jensen)searchpromotions (Scott Foster)insert_editor_css in favour of insert_global_admin_css (Ester Beltrami)specific on Task and TaskState (Matt Westcott)parent_context is mutable (Andreas Nüßlein)CONTRIBUTORS file to Markdown (Dan Braghis)django-filter version upper bound to v23 (Yuekui)jest-environment-jsdom and new snapshot format (LB (Ben) Johnston)"wagtailadmin/shared/field_as_li.html" template include (Storm Heg)sphinx_wagtail_theme to v6.1.1 which includes multiple styling fixes and always visible code copy buttons (LB (Ben) Johnston)AutocompleteField for full functionalityIn Wagtail 4.2, the search bar within snippet chooser interfaces (and custom choosers created via ChooserViewSet) returned results for partial word matches - for example, a search for "wagt" would return results containing "Wagtail" - if this was supported by the search backend in use, and at least one AutocompleteField was present in the model's search_fields definition. Otherwise, it would fall back to only matching on complete words. In Wagtail 5.0, this fallback behavior was removed, and consequently a model with no AutocompleteFields in place would return no results.
As of Wagtail 5.1.2, the fallback behavior has been restored. Nevertheless, it is strongly recommended that you add AutocompleteField to your models' search_fields definitions, to ensure that users can receive search results continuously as they type. For example:
from wagtail.search import index
# ... other imports
@register_snippet
class MySnippet(index.Indexed, models.Model):
search_fields = [
index.SearchField("name"),
index.AutocompleteField("name"),
]
GroupPagePermission now uses Django's Permission modelThe GroupPagePermission model that is responsible for assigning page permissions to groups now uses Django's Permission model instead of a custom string. This means that the permission_type CharField has been deprecated and replaced with a permission ForeignKey to the Permission model.
In addition to this, "edit" permissions now use the term change within the code. As a result, GroupPagePermissions that were previously recorded with permission_type="edit" are now recorded with a Permission object that has the codename="change_page" and a content_type that points to the Page model. Any permission checks that are done using PagePermissionPolicy should also use change instead of edit.
If you have any fixtures for the GroupPagePermission model, you will need to update them to use the new Permission model. For example, if you have a fixture that looks like this:
{
"pk": 11,
"model": "wagtailcore.grouppagepermission",
"fields": {
"group": ["Event moderators"],
"page": 12,
"permission_type": "edit"
}
}
Update it to use a natural key for the permission field instead of the permission_type field:
{
"pk": 11,
"model": "wagtailcore.grouppagepermission",
"fields": {
"group": ["Event moderators"],
"page": 12,
"permission": ["change_page", "wagtailcore", "page"]
}
}
If you have any code that creates GroupPagePermission objects, you will need to update it to use the Permission model instead of the permission_type string. For example, if you have code that looks like this:
from wagtail.models import GroupPagePermission
permission = GroupPagePermission(group=group, page=page, permission_type="edit")
permission.save()
Update it to use the Permission model instead:
from django.contrib.auth.models import Permission
from wagtail.models import GroupPagePermission
permission = GroupPagePermission(
group=group,
page=page,
permission=Permission.objects.get(content_type__app_label="wagtailcore", codename="change_page"),
)
permission.save()
During the deprecation period, the permission_type field will still be available on the GroupPagePermission model and is used to automatically populate empty permission field as part of a system check. The permission_type field will be removed in Wagtail 6.0.
The ordering for "Object permissions" and "Other permissions" now follows a predictable order equivalent to Django's default Model ordering.
This will be different to the previous indeterminate ordering.
The default ordering is now ["content_type__app_label", "content_type__model"]. See for details on how to customize this order.
ModelLogEntry and PageLogEntry are now ISO-formatted and UTCPreviously, timestamps stored in the "data"-JSONField of ModelLogEntry and PageLogEntry have used the custom python format %d %b %Y %H:%M. Additionally, the "go_live_at" timestamp had been stored with the configured local timezone, instead of UTC.
This has now been fixed, all timestamps are now stored as UTC, and because the "data"-JSONField now uses Django's DjangoJSONEncoder, those datetime objects are now automatically converted to the ISO format. This release contains a new migration 0088_fix_log_entry_json_timestamps which converts all existing timestamps used by Wagtail to the new format.
If you've developed your own subclasses of ModelLogEntry, PageLogEntry or BaseLogEntry, or used those existing models to create custom log entries, and you've stored timestamps similarly to Wagtail's old implementation (using strftime("%d %b %Y %H:%M")). You may want to adapt the storage of those timestamps to a consistent format too.
There are probably three places in your code, which have to be changed:
strftime("%d %b %Y %H:%M"), you can now store the datetime directly in the "data" field. We've implemented a new helper wagtail.utils.timestamps.ensure_utc(), which ensures the correct timezone (UTC).LogFormatter, we've created utils to parse (wagtail.utils.timestamps.parse_datetime_localized()) and render (wagtail.utils.timestamps.render_timestamp()) those timestamps. Look at the existing formatters here.Wagtail will try to use the cache called "renditions". If no such cache exists, it will fall back to using the default cache. You can configure the "renditions" cache to use a different cache backend or to provide additional configuration parameters.
Python 3.7 is no longer supported as of this release; please upgrade to Python 3.8 or above before upgrading Wagtail.
Wagtail no longer supports Pillow versions below 9.1.0.
The Elasticsearch 5 and 6 search backends are deprecated and will be removed in a future release; please upgrade to Elasticsearch 7 or above.
insert_editor_css hook is deprecatedThe insert_editor_css hook has been deprecated. The insert_global_admin_css hook has the same functionality, and all uses of insert_editor_css should be changed to insert_global_admin_css.
wagtail.contrib.modeladmin is deprecatedAs part of the RFC 85: Snippets parity with ModelAdmin implementation, the wagtail.contrib.modeladmin app is deprecated. To manage non-page models in Wagtail, use wagtail.snippets instead.
If you still rely on ModelAdmin, use the separate wagtail-modeladmin package. The wagtail.contrib.modeladmin module will be removed in a future release.
UserPagePermissionsProxy is deprecatedThe undocumented wagtail.models.UserPagePermissionsProxy class is deprecated.
If you use the .for_page(page) method of the class to get a PagePermissionTester instance, you can replace it with page.permissions_for_user(user).
If you use the other methods, they can be replaced via the wagtail.permission_policies.pages.PagePermissionPolicy class. The following is a list of the PagePermissionPolicy equivalent of each method:
from wagtail.models import UserPagePermissionsProxy
from wagtail.permission_policies.pages import PagePermissionPolicy
# proxy = UserPagePermissionsProxy(user)
permission_policy = PagePermissionPolicy()
# proxy.revisions_for_moderation()
permission_policy.revisions_for_moderation(user)
# proxy.explorable_pages()
permission_policy.explorable_instances(user)
# proxy.editable_pages()
permission_policy.instances_user_has_permission_for(user, "change")
# proxy.can_edit_pages()
permission_policy.instances_user_has_permission_for(user, "change").exists()
# proxy.publishable_pages()
permission_policy.instances_user_has_permission_for(user, "publish")
# proxy.can_publish_pages()
permission_policy.instances_user_has_permission_for(user, "publish").exists()
# proxy.can_remove_locks()
permission_policy.user_has_permission(user, "unlock")
The UserPagePermissionsProxy object that is available in page's ActionMenuItem context as user_page_permissions (which might be used as part of a register_page_action_menu_item hook) has been deprecated. In cases where the page object is available (e.g. the page edit view), the PagePermissionTester object stored as the user_page_permissions_tester context variable can still be used.
The UserPagePermissionsProxy object that is available in the template context as user_page_permissions as a side-effect of the page_permissions template tag has also been deprecated.
If you use the user_page_permissions context variable or use the UserPagePermissionsProxy class directly, make sure to replace it either with the PagePermissionTester or the PagePermissionPolicy equivalent.
get_pages_with_direct_explore_permission, get_explorable_root_page, and users_with_page_permission are deprecatedThe undocumented get_pages_with_direct_explore_permission and get_explorable_root_page functions in wagtail.admin.navigation are deprecated. They can be replaced with PagePermissionPolicy().instances_with_direct_explore_permission(user) and PagePermissionPolicy().explorable_root_instance(user), respectively.
The undocumented users_with_page_permission function in wagtail.admin.auth is also deprecated. It can be replaced with PagePermissionPolicy().users_with_permission_for_instance(action, page, include_superusers).
wagtailadmin/shared/last_updated.html is no longer availableThe undocumented shared include wagtailadmin/shared/last_updated.html is no longer available as it used the legacy Bootstrap tooltips and was not accessible. If you need to achieve a similar output, an element that shows a simple date with a tooltip for the full date, use the human_readable_date template tag instead.
{% include "wagtailadmin/shared/last_updated.html" with last_updated=my_model.timestamp %}
{% load wagtailadmin_tags %}
<!-- ... -->
{% human_readable_date my_model.timestamp %}
field_as_li.html will be removedThe documented include "wagtailadmin/shared/field_as_li.html" will be removed in a future release, if being used it will need to be replaced with "wagtailadmin/shared/field.html" wrapped within li tags.
{% include "wagtailadmin/shared/field_as_li.html" %}
<li>
{% include "wagtailadmin/shared/field.html" %}
</li>
The AdminTagWidget widget has now been migrated to a Stimulus controller, if using this widget in Python, no changes are needed to adopt the new approach.
If the widget is being instantiated in JavaScript or HTML with the global util window.initTagField, this undocumented util should be replaced with the new data-* attributes approach. Additionally, any direct usage of the jQuery widget in JavaScript (e.g. $('#my-element).tagit()) should be removed.
The global util will be removed in a future release. It is recommended that the documented AdminTagWidget be used. However, if you need to use the JavaScript approach you can do this with the following example.
<input id="id_tags" type="text" value="popular, technology" hidden />
<script>
window.initTagField('id_tags', 'path/to/url', { autocompleteOnly: true });
</script>
<input
id="id_tags"
type="text"
value="popular, technology"
hidden
data-controller="w-tag"
data-w-tag-options-value='{"autocompleteOnly": true}'
data-w-tag-url-value="/path/to/url"
/>
Note: The data-w-tag-options-value is a JSON object serialized into string. Django's HTML escaping will handle it automatically when you use the AdminTagWidget, but if you are manually writing the attributes, be sure to use quotation marks correctly.
Previously the header search relied on inline scripts and a window.headerSearch global to activate the behavior. This has now changed to a data attributes approach and the window global usage will be removed in a future major release.
If you are using the documented Wagtail viewsets, Snippets or ModelAdmin approaches to building custom admin views, there should be no change required.
If you are using the shared header template include for a custom search integration, here's how to adopt the new approach.
{% extends "wagtailadmin/base.html" %}
{% load wagtailadmin_tags %}
{% block extra_js %}
{{ block.super }}
<script>
window.headerSearch = {
url: "{% url 'myapp:search_results' %}",
termInput: '#id_q',
targetOutput: '#my-results',
};
</script>
{% endblock %}
{% block content %}
{% include "wagtailadmin/shared/header.html" with title="my title" search_url="myapp:index" %}
... other content
{% endblock %}
Note: No need for extra_js usage at all.
{% extends "wagtailadmin/base.html" %}
{% load wagtailadmin_tags %}
{% block content %}
{% url 'myapp:search_results' as search_results_url %}
{% include "wagtailadmin/shared/header.html" with title="my title" search_url="myapp:index" search_results_url=search_results_url search_target="#my-results" %}
... other content
{% endblock %}
Alternatively, if you have customizations that manually declare or override window.headerSearch, here's how to adopt the new approach.
<script>
window.headerSearch = {
url: '{{ my_async_results_url }}',
termInput: '#id_q',
targetOutput: '#some-results',
};
</script>
<form role="search">
<input type="text" name="q" id="id_q" />
</form>
<div id="some-results"></div>
<form
role="search"
data-controller="w-swap"
data-action="change->w-swap#searchLazy input->w-swap#searchLazy"
data-w-swap-src-value="{{ my_async_results_url }}"
data-w-swap-target-value="#some-results"
>
<input type="text" name="q" id="id_q" data-w-swap-target="input" />
</form>
<div id="some-results"></div>
The undocumented Bootstrap jQuery tooltip widget is no longer in use, you will need to update any HTML that is using these attributes to the new syntax.
<!-- Old attributes: -->
<span data-wagtail-tooltip="Tooltip content here">Label</span>
<!-- New attributes: -->
<span data-controller="w-tooltip" data-w-tooltip-content-value="Tooltip content here">Label</span>
The undocumented client-side Custom Event handling for dialog showing & hiding will change in a future release.
| Action | Old event | New event |
|---|---|---|
| Show | wagtail:show | w-dialog:show |
| Hide | wagtail:hide | w-dialog:hide |
Additionally, two new events will be dispatched when the dialog visibility changes.
| Action | Event name |
|---|---|
| Show | w-dialog:shown |
| Hide | w-dialog:hidden |
.../tables/attrs.html has been renamed to .../shared/attrs.htmlThe undocumented shared template for rendering a dict of attrs to HTML, similar to Django form widgets, has been renamed.
| Template location | Usage with include | |
|---|---|---|
| Old | wagtail/admin/templates/wagtailadmin/tables/attrs.html | {% include "wagtailadmin/tables/attrs.html" with attrs=link_attrs %} |
| New | wagtail/admin/templates/wagtailadmin/shared/attrs.html | {% include "wagtailadmin/shared/attrs.html" with attrs=link_attrs %} |