Wagtail 3.0 release notes

May 16, 2022

What’s new

Page editor redesign

This release contains significant UI changes that affect all of Wagtail’s admin, largely driven by the implementation of the new Page Editor. These include:

  • Fully remove the legacy sidebar, with slim sidebar replacing it for all users (Thibaud Colas)

  • Add support for adding custom attributes for link menu items in the slim sidebar (Thibaud Colas)

  • Convert all UI code to CSS logical properties for Right-to-Left (RTL) language support (Thibaud Colas)

  • Switch the Wagtail branding font and monospace font to a system font stack (Steven Steinwand, Paarth Agarwal, Rishank Kanaparti)

  • Remove most uppercased text styles from admin UI (Paarth Agarwal)

  • Implement new tabs design across the admin interface (Steven Steinwand)

Other changes that are specific to the Page Editor include:

  • Implement new slim page editor header with breadcrumb and secondary page menu (Steven Steinwand, Karl Hobley)

  • Move page meta information from the header to a new status side panel component inside of the page editing UI (Steven Steinwand, Karl Hobley)

Further updates to the page editor are expected in the next release. Development on this feature was sponsored by Google.

Rich text block splitting

Rich text blocks within StreamField now provide the ability to split a block at the cursor position, allowing new blocks to be inserted in between. This feature was developed by Jacob Topp-Mugglestone and sponsored by The Motley Fool.

Removal of special-purpose field panel types

The panel types StreamFieldPanel, RichTextFieldPanel, ImageChooserPanel, DocumentChooserPanel and SnippetChooserPanel have been phased out, and can now be replaced with FieldPanel. Additionally, PageChooserPanel is only required when passing a page_type or can_choose_root, and can otherwise be replaced with FieldPanel. In all cases, FieldPanel will now automatically select the most appropriate form element. This feature was developed by Matt Westcott.

Permission-dependent FieldPanels

FieldPanel now accepts a permission keyword argument to specify that the field should only be available to users with a given permission level. This feature was developed by Matt Westcott and sponsored by Google as part of Wagtail’s page editor redevelopment.

Page descriptions

With every Wagtail Page you are able to add a helpful description text, similar to a help_text model attribute. By adding page_description to your Page model you’ll be adding a short description that can be seen in different places within Wagtail:

class LandingPage(Page):

    page_description = "Use this page for converting users"

Image duplicate detection

Trying to upload an image that’s a duplicate of one already in the image library will now lead to a confirmation step. This feature was developed by Tidiane Dia and sponsored by The Motley Fool.

Image renditions can now be prefetched

When using a queryset to render a list of items with images, you can now make use of Django’s built-in prefetch_related() queryset method to prefetch the renditions needed for rendering with a single extra query. For long lists of items, or where multiple renditions are used for each item, this can provide a significant boost to performance. This feature was developed by Andy Babic.

Other features

  • Upgrade ESLint and Stylelint configurations to latest shared Wagtail configs (Thibaud Colas, Paarth Agarwal)

  • Major updates to frontend tooling; move Node tooling from Gulp to Webpack, upgrade to Node v16 and npm v8, eslint v8, stylelint v14 and others (Thibaud Colas)

  • Change comment headers’ date formatting to use browser APIs instead of requiring a library (LB (Ben Johnston))

  • Lint with flake8-comprehensions and flake8-assertive, including adding a pre-commit hook for these (Mads Jensen, Dan Braghis)

  • Add black configuration and reformat code using it (Dan Braghis)

  • Remove UI code for legacy browser support: polyfills, IE11 workarounds, Modernizr (Thibaud Colas)

  • Remove redirect auto-creation recipe from documentation as this feature is now supported in Wagtail core (Andy Babic)

  • Remove IE11 warnings (Gianluca De Cola)

  • Replace content_json TextField with content JSONField in PageRevision (Sage Abdullah)

  • Remove replace_text management command (Sage Abdullah)

  • Replace data_json TextField with data JSONField in BaseLogEntry (Sage Abdullah)

  • Remove the legacy Hallo rich text editor as it has moved to an external package (LB (Ben Johnston))

  • Increase the size of checkboxes throughout the UI, and simplify their alignment (Steven Steinwand)

  • Adopt MyST for parsing documentation written in Markdown, replaces recommonmark (LB (Ben Johnston), Thibaud Colas)

  • Installing docs extras requirements in CircleCI so issues with the docs requirements are picked up earlier (Thibaud Colas)

  • Remove core usage of jinjalint and migrate to curlylint to resolve dependency incompatibility issues (Thibaud Colas)

  • Switch focus outlines implementation to :focus-visible for cross-browser consistency (Paarth Agarwal)

  • Migrate multiple documentation pages from RST to MD - including the editor’s guide (Vibhakar Solanki, LB (Ben Johnston), Shwet Khatri)

  • Add documentation for defining custom form validation on models used in Wagtail’s modelAdmin (Serafeim Papastefanos)

  • Update README.md logo to work for GitHub dark mode (Paarth Agarwal)

  • Avoid an unnecessary page reload when pressing enter within the header search bar (Images, Pages, Documents) (Riley de Mestre)

  • Removed unofficial length parameter on If-Modified-Since header in sendfile_streaming_backend which was only used by IE (Mariusz Felisiak)

  • Add Pinterest support to the list of default oEmbed providers (Dharmik Gangani)

  • Update Jinja2 template support for Jinja2 3.1 (Seb Brown)

  • Add ability for StreamField to use JSONField to store data, rather than TextField (Sage Abdullah)

  • Split up linting / formatting tasks in Makefile into client and server components (Hitansh Shah)

  • Add support for embedding Instagram reels (Luis Nell)

  • Use Django’s JavaScript catalog feature to manage translatable strings in JavaScript (Karl Hobley)

  • Add trimmed attribute to all blocktrans tags, so spacing is more reliable in translated strings (Harris Lapiroff)

  • Add documentation that describes how to use ModelAdmin to manage Tags (Abdulmajeed Isa)

  • Rename the setting BASE_URL (undocumented) to WAGTAILADMIN_BASE_URL and add to documentation, BASE_URL will be removed in a future release (Sandil Ranasinghe)

  • Validate to and from email addresses within form builder pages when using AbstractEmailForm (Jake Howard)

  • Add WAGTAILIMAGES_RENDITION_STORAGE setting to allow an alternative image rendition storage (Heather White)

  • Add wagtail_update_image_renditions management command to regenerate image renditions or purge all existing renditions (Hitansh Shah, Onno Timmerman, Damian Moore)

  • Add the ability for choices to be separated by new lines instead of just commas within the form builder, commas will still be supported if used (Abdulmajeed Isa)

  • Add internationalisation UI to modeladmin (Andrés Martano)

  • Support chunking in PageQuerySet.specific() to reduce memory consumption (Andy Babic)

  • Add useful help text to Tag fields to advise what content is allowed inside tags, including when TAG_SPACES_ALLOWED is True or False (Abdulmajeed Isa)

  • Change AbstractFormSubmission’s form_data to use JSONField to store form submissions (Jake Howard)

Bug fixes

  • Update django-treebeard dependency to 4.5.1 or above (Serafeim Papastefanos)

  • When using simple_translations ensure that the user is redirected to the page edit view when submitting for a single locale (Mitchel Cabuloy)

  • When previewing unsaved changes to Form pages, ensure that all added fields are correctly shown in the preview (Joshua Munn)

  • When Documents (e.g. PDFs) have been configured to be served inline via WAGTAILDOCS_CONTENT_TYPES & WAGTAILDOCS_INLINE_CONTENT_TYPES ensure that the filename is correctly set in the Content-Disposition header so that saving the files will use the correct filename (John-Scott Atlakson)

  • Improve the contrast of the “Remember me” checkbox against the login page’s background (Steven Steinwand)

  • Group permission rows with custom permissions no longer have extra padding (Steven Steinwand)

  • Make sure the focus outline of checkboxes is fully around the outer border (Steven Steinwand)

  • Consistently set aria-haspopup="menu" for all sidebar menu items that have sub-menus (LB (Ben Johnston))

  • Make sure aria-expanded is always explicitly set as a string in sidebar (LB (Ben Johnston))

  • Use a button element instead of a link for page explorer menu item, for the correct semantics and behaviour (LB (Ben Johnston))

  • Make sure the “Title” column label can be translated in the page chooser and page move UI (Stephanie Cheng Smith)

  • Remove redundant role="main" attributes on <main> elements causing HTML validation issues (Luis Espinoza)

  • Allow bulk publishing of pages without revisions (Andy Chosak)

  • Stop skipping heading levels in Wagtail welcome page (Jesse Menn)

  • Add missing lang attributes to <html> elements (James Ray)

  • Add missing translation usage in Workflow templates (Anuja Verma, Saurabh Kumar)

  • Avoid 503 server error when entering tags over 100chars and instead show a user facing validation error (Vu Pham, Khanh Hoang)

  • Ensure thumb_col_header_text is correctly used by ThumbnailMixin within ModelAdmin as the column header label (Kyle J. Roux)

  • Ensure page copy in Wagtail admin doesn’t ignore exclude_fields_in_copy (John-Scott Atlakson)

  • Generate new translation keys for translatable Orderables when page is copied without being published (Kalob Taulien, Dan Braghis)

  • Ignore GenericRelation when copying pages (John-Scott Atlakson)

  • Implement ARIA tabs markup and keyboards interactions for admin tabs (Steven Steinwand)

  • Ensure wagtail updatemodulepaths works when system locale is not UTF-8 (Matt Westcott)

  • Re-establish focus trap for Pages explorer in slim sidebar (Thibaud Colas)

  • Ensure the icon font loads correctly when STATIC_URL is not "/static/" (Jacob Topp-Mugglestone)

Upgrade considerations - changes affecting all projects

Changes to module paths

Various modules of Wagtail have been reorganised, and imports should be updated as follows:

  • The wagtail.core.utils module is renamed to wagtail.coreutils

  • All other modules under wagtail.core can now be found under wagtail - for example, from wagtail.core.models import Page should be changed to from wagtail.models import Page

  • The wagtail.tests module is renamed to wagtail.test

  • wagtail.admin.edit_handlers is renamed to wagtail.admin.panels

  • wagtail.contrib.forms.edit_handlers is renamed to wagtail.contrib.forms.panels

These changes can be applied automatically to your project codebase by running the following commands from the project root:

wagtail updatemodulepaths --list  # list the files to be changed without updating them
wagtail updatemodulepaths --diff  # show the changes to be made, without updating files
wagtail updatemodulepaths  # actually update the files

Removal of special-purpose field panel types

Within panel definitions on models, StreamFieldPanel, RichTextFieldPanel, ImageChooserPanel, DocumentChooserPanel and SnippetChooserPanel should now be replaced with FieldPanel. Additionally, PageChooserPanel can be replaced with FieldPanel if it does not use the page_type or can_choose_root arguments.

BASE_URL setting renamed to WAGTAILADMIN_BASE_URL

References to BASE_URL in your settings should be updated to WAGTAILADMIN_BASE_URL. This setting was not previously documented, but was part of the default project template when starting a project with the wagtail start command, and specifies the full base URL for the Wagtail admin, for use primarily in email notifications.

use_json_field argument added to StreamField

All uses of StreamField should be updated to include the argument use_json_field=True. After adding this, make sure to generate and run migrations. This converts the field to use JSONField as its internal type instead of TextField, which will allow you to use JSONField lookups and transforms on the field. This change is necessary to ensure that the database migration is applied; a future release will drop support for TextField-based StreamFields.

SQLite now requires the JSON1 extension enabled

Due to JSONField requirements, SQLite will only be supported with the JSON1 extension enabled. See Enabling JSON1 extension on SQLite and JSON1 extension for details.

Upgrade considerations - deprecation of old functionality

Removed support for Internet Explorer (IE11)

IE11 support was officially dropped in Wagtail 2.15, and as of this release there will no longer be a warning shown to users of this browser. Wagtail is fully compatible with Microsoft Edge, Microsoft’s replacement for Internet Explorer. You may consider using its IE mode to keep access to IE11-only sites, while other sites and apps like Wagtail can leverage modern browser capabilities.

Hallo legacy rich text editor has moved to an external package

Hallo was deprecated in Wagtail v2.0 (February 2018) and has had only a minimal level of support since then. If you still require Hallo for your Wagtail installation, you will need to install the Wagtail Hallo editor legacy package. We encourage all users of the Hallo editor to take steps to migrate to the new Draftail editor as this external package is unlikely to have ongoing maintenance. window.registerHalloPlugin will no longer be created on the page editor load, unless the legacy package is installed.

Removal of legacy clean_name on AbstractFormField

If you are upgrading a pre-2.10 project that uses the Wagtail form builder, and has existing form submission data that needs to be preserved, you must first upgrade to a version between 2.10 and 2.16, and run migrations and start the application server, before upgrading to 3.0. This ensures that the clean_name field introduced in Wagtail 2.10 is populated. The mechanism for doing this (which had a dependency on the Unidecode package) has been dropped in Wagtail 3.0. Any new form fields created under Wagtail 2.10 or above use the AnyAscii library instead.

Removed support for Jinja2 2.x

Jinja2 2.x is no longer supported as of this release; if you are using Jinja2 templating on your project, please upgrade to Jinja2 3.0 or above.

Upgrade considerations - changes affecting Wagtail customisations

API changes to panels (EditHandlers)

Various changes have been made to the internal API for defining panel types, previously known as edit handlers. As noted above, the module wagtail.admin.edit_handlers has been renamed to wagtail.admin.panels, and wagtail.contrib.forms.edit_handlers is renamed to wagtail.contrib.forms.panels.

Additionally, the base wagtail.admin.edit_handlers.EditHandler class has been renamed to wagtail.admin.panels.Panel, and wagtail.admin.edit_handlers.BaseCompositeEditHandler has been renamed to wagtail.admin.panels.PanelGroup.

Template paths have also been renamed accordingly - templates previously within wagtailadmin/edit_handlers/ are now located under wagtailadmin/panels/, and wagtailforms/edit_handlers/form_responses_panel.html is now at wagtailforms/panels/form_responses_panel.html.

Where possible, third-party packages that implement their own field panel types should be updated to allow using a plain FieldPanel instead, in line with Wagtail dropping its own special-purpose field panel types such as StreamFieldPanel and ImageChooserPanel. The steps for doing this will depend on the package’s functionality, but in general:

  • If the panel sets a custom template, your code should instead define a Widget class that produces your desired HTML rendering.

  • If the panel provides a widget_overrides method, your code should instead call register_form_field_override so that the desired widget is always selected for the relevant model field type.

  • If the panel provides a get_comparison_class method, your code should instead call wagtail.admin.compare.register_comparison_class to register the comparison class against the relevant model field type.

Within the Panel class, the methods widget_overrides, required_fields and required_formsets have been deprecated in favour of a new get_form_options method that returns a dict of configuration options to be passed on to the generated form class:

  • Panels that define required_fields should instead return this value as a fields item in the dict returned from get_form_options

  • Panels that define required_formsets should instead return this value as a formsets item in the dict returned from get_form_options

  • Panels that define widget_overrides should instead return this value as a widgets item in the dict returned from get_form_options

The methods on_request_bound, on_instance_bound and on_form_bound are no longer used. In previous versions, over the course of serving a request an edit handler would have the attributes request, model, instance and form attached to it, with the corresponding on_*_bound method being called at that point. In the new implementation, only the model attribute and on_model_bound method are still available. This means it is no longer possible to vary or patch the form class in response to per-request information such as the user object. For permission checks, you should use the new permission option on FieldPanel; for other per-request customisations to the form object, use a custom form class with an overridden __init__ method. (The current user object is available from the form as self.for_user.)

Binding to a request, instance and form object is now handled by a new class Panel.BoundPanel. Any initialisation logic previously performed in on_request_bound, on_instance_bound or on_form_bound can instead be moved to the constructor method of a subclass of BoundPanel:

class CustomPanel(Panel):
    class BoundPanel(Panel.BoundPanel):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
            # The attributes self.panel, self.request, self.instance and self.form
            # are available here

The template context for panels derived from BaseChooserPanel has changed. BaseChooserPanel is deprecated and now functionally identical to FieldPanel; as a result, the context variable is_chosen, and the variable name given by the panel’s object_type_name property, are no longer available on the template. The only available variables are now field and show_add_comment_button. If your template depends on these additional variables, you will need to pass them explicitly by overriding the BoundPanel.get_context_data method.

API changes to ModelAdmin

Some changes of behaviour have been made to ModelAdmin as a result of the panel API changes:

  • When overriding the get_form_class method of a ModelAdmin CreateView or EditView to pass a custom form class, that form class must now inherit from wagtail.admin.forms.models.WagtailAdminModelForm. Passing a plain Django ModelForm subclass is no longer valid.

  • The ModelAdmin.get_form_fields_exclude method is no longer passed a request argument. Subclasses that override this method should remove this from the method signature. If the request object is being used to vary the set of fields based on the user’s permission, this can be replaced with the new permission option on FieldPanel.

  • The ModelAdmin.get_edit_handler method is no longer passed a request or instance argument. Subclasses that override this method should remove this from the method signature.

Replaced content_json TextField with content JSONField in PageRevision

The content_json field in the PageRevision model has been renamed to content, and this field now internally uses JSONField instead of TextField. If you have a large number of PageRevision objects, running the migrations might take a while.

Replaced data_json TextField with data JSONField in BaseLogEntry

The data_json field in the BaseLogEntry model (and its subclasses PageLogEntry and ModelLogEntry) has been renamed to data, and this field now internally uses JSONField instead of TextField. If you have a large number of objects for these models, running the migrations might take a while.

If you have models that are subclasses of BaseLogEntry in your project, be careful when generating new migrations for these models. As the field is changed and renamed at the same time, Django’s makemigrations command will generate RemoveField and AddField operations instead of AlterField and RenameField. To avoid data loss, make sure to adjust the migrations accordingly. For example with a model named MyCustomLogEntry, change the following operations:

operations = [
    migrations.RemoveField(
        model_name='mycustomlogentry',
        name='data_json',
    ),
    migrations.AddField(
        model_name='mycustomlogentry',
        name='data',
        field=models.JSONField(blank=True, default=dict),
    ),
]

to the following operations:

operations = [
    migrations.AlterField(
        model_name="mycustomlogentry",
        name="data_json",
        field=models.JSONField(blank=True, default=dict),
    ),
    migrations.RenameField(
        model_name="mycustomlogentry",
        old_name="data_json",
        new_name="data",
    ),
]

Replaced form_data TextField with JSONField in AbstractFormSubmission

The form_data field in the AbstractFormSubmission model (and its subclasses FormSubmission) has been converted to JSONField instead of TextField. If you have customisations that programmatically add form submissions you will need to ensure that the form_data that is output is no longer a JSON string but instead a serialisable Python object. When interacting with the form_data you will now receive a Python object and not a string.

Example change

def process_form_submission(self, form):
    self.get_submission_class().objects.create(
        # form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
        form_data=form.cleaned_data, # new
        page=self, user=form.user
    )

Removed size argument from wagtail.utils.sendfile_streaming_backend.was_modified_since

The size argument of the undocumented wagtail.utils.sendfile_streaming_backend.was_modified_since function has been removed. This argument was used to add a length parameter to the HTTP header; however, this was never part of the HTTP/1.0 and HTTP/1.1 specifications see RFC7232 and existed only as a an unofficial implementation in IE browsers.