The [built-in accessibility checker](authoring_accessible_content) 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.
* Support a [`HOSTNAMES` parameter on `WAGTAILFRONTENDCACHE`](frontendcache_multiple_backends) to define which hostnames a backend should respond to (Jake Howard, sponsored by Oxfam America)
### Specifying a dict of distribution IDs for CloudFront cache invalidation is deprecated
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](frontendcache_multiple_backends), with a `HOSTNAMES` parameter to define the hostnames associated with each one. For example, a configuration such as:
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`](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:
```py
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:
```py
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
### Deprecation of `WAGTAIL_USER_EDIT_FORM`, `WAGTAIL_USER_CREATION_FORM`, and `WAGTAIL_USER_CUSTOM_FIELDS` settings
This 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](user_form_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.
#### Before
Given the following custom user model:
```py
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:
```py
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"))
status = forms.ModelChoiceField(queryset=MembershipStatus.objects, required=True, label=_("Status"))
# Use ModelForm's automatic form fields generation for the model's `country` field.
# Alternatively, you can also define a `country` form field directly on
# `CustomUserEditForm` along with any desired options for the field, and skip
# the following custom Meta subclass.
class Meta(UserEditForm.Meta):
fields = UserEditForm.Meta.fields | {"country"}
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.
# Alternatively, you can also define a `country` form field directly on
# `CustomUserCreationForm` along with any desired options for the field, and skip
# the following custom Meta subclass.
class Meta(UserCreationForm.Meta):
fields = UserEditForm.Meta.fields | {"country"}
```
Create a custom `UserViewSet` subclass in e.g. `myapp/viewsets.py`:
```py
# myapp/viewsets.py
from wagtail.users.views.users import UserViewSet as WagtailUserViewSet
from .forms import CustomUserCreationForm, CustomUserEditForm
class UserViewSet(WagtailUserViewSet):
def get_form_class(self, for_update=False):
if for_update:
return CustomUserEditForm
return CustomUserCreationForm
```
Create a custom `WagtailUsersAppConfig` subclass in e.g. `myapp/apps.py` (or reuse the same class if you have a custom `GroupViewSet` as described in [](customizing_group_views)). Then, set a `user_viewset` attribute pointing to the custom `UserViewSet` subclass:
```py
# myapp/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`:
### Changes to report views with the new Universal Listings UI
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.
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:
- Change `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`).
- The template should extend from `wagtailadmin/reports/base_page_report_results.html`.
- The `listing` and `no_results` blocks should be renamed to `results` and `no_results_message`, respectively.
For a non-page report, the following changes are needed:
- Change `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`).
- The template should extend from `wagtailadmin/reports/base_report_results.html`.
- Existing templates will typically define a `results` block containing both the results listing and the "no results" message; these should now become separate blocks named `results` and `no_results_message`.
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.
### Deprecation of `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](custom_tasks_behavior), such as by overriding `Task.get_actions()`.