CNK's Blog

Wagtail 3 Upgrade: Per User FieldPanel Permissions

I work on a long running Wagtail project and over the years we have made a lot of customizations (aka monkey patches) to the Wagtail framework. So each upgrade takes a bit of work - though often this leads to cleaner code. In my previous post about custom menu items I described needing to change one of our customizations and finding Wagtail already had a better way to do what I wanted. In other cases, things we had been doing via ugly hacks become part of Wagtail itself. For Wagtail 3, one of those is the new Permission-dependent FieldPanels.

We have event pages that are mainly used for announcing academic seminars but are also used by our Public Programming office to advertise concerts. The concert pages need to have images but we don’t really want to allow other groups to add images to their pages. Similarly, we want the editors of our main site to be able to tag events for display on the home page or on an internally facing page. We had been using a kind of ugly hack to hide the image upload and display_location fields from everyone who did not have the calendar.can_access_admin_fields permission. First we created a MultiFieldPanel to contain the items we only want certain people to see and gave it a CSS class we can use to find it:

    MultiFieldPanel(
        heading='Admin-only Fields',
        # NOTE: The 'admin-only' class is how EventPage.get_edit_handler() identifies this MultiFieldPanel.
       classname='collapsible admin-only',
        children=[
           FieldPanel('display_locations', widget=forms.CheckboxSelectMultiple),
           StreamFieldPanel('assets'),
        ]
    ),

Then we created a get_edit_handler method that uses the field’s position in the DOM and that CSS class to find and remove that field:

    @classmethod
    def get_edit_handler(cls):
        """
        We override this method (which is added to the Page class in wagtail.admin.panels) in order to enforce
        our custom field-level permissions.
        """
        # Do the same thing that wagtail.admin.panels.get_edit_handler() would do...
        bound_handler = cls.edit_handler.bind_to(model=cls)
        # ... then enforce admin-only field permissions on the result.
        current_request = get_current_request()
        # This method gets called during certain manage.py commands, so we need to be able to gracefully fail if there
        # is no current request. Thus, if there is no current request, the admin-only fields are removed.
        if current_request is None or not current_request.user.has_perm('master_calendar.can_access_admin_fields'):
            # We know for sure that bound_handler.children[0].children is the list of Panels in the Content tab.
            # We must search through that list to find the admin-only MultiFieldPanel, and remove it.
            # The [:] gets us a copy of the list, so altering the original doesn't change what we're looping over.
            for child in bound_handler.children[0].children[:]:
                if 'admin-only' in child.classname:
                    bound_handler.children[0].children.remove(child)
                    break
        return bound_handler

As of Wagtail 3, I can remove the get_edit_handler override and enforce our per user permissions in the panel definition:

    MultiFieldPanel(
        heading='Admin-only Fields',
        classname='collapsible',
        children=[
            FieldPanel('display_locations', permission='master_calendar.can_access_admin_fields',
                       widget=forms.CheckboxSelectMultiple),
            FieldPanel('assets', permission='master_calendar.can_access_admin_fields'),
        ]
    ),

If the user does not have the can_access_admin_fields permission, the two FieldPanels get removed which causes the heading for the MultiFieldPanel to disappear. How beautiful is that?