user42488
user42488

Reputation: 1455

Permission for field in Wagtail Page

How can permissions be applied to individual fields of a Wagtail page? Let's say we have a page like this one:

class HomePage(Page):
   body = RichTextField(blank=True)

   content_panels = Page.content_panels + [
       FieldPanel('body', classname="full"),
   ]

Everyone should be allowed to edit the title - but only users with a certain permission should be able to alter the body.

Upvotes: 3

Views: 1112

Answers (2)

Ramesh-X
Ramesh-X

Reputation: 5085

Wagtail do not support field based permission control. But you can achieve this by enabling/disabling fields of the page's form. If you add a field to a page, you can pass whether that field should be enabled or disabled in the constructor. But how can you do that runtime?

Every HTML page with a form in wagtail is associated with a Django Form. You can change the default form to a your own one like this.

from django.db import models
from wagtail.admin.forms.pages import WagtailAdminPageForm
from wagtail.core.models import Page
from wagtail.admin.edit_handlers import FieldPanel

class HomeForm(WagtailAdminPageForm):
    pass
        

class HomePage(Page):
    body = models.CharField(max_length=500, default='', blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('body'),
    ]
    base_form_class = HomeForm    # Tell wagtail to user our form

This way, every time you load the create view or edit view of HomePage, wagtail will create an instance from HomeForm class. Now what you have to do is, to check the user status and enable/disable the required field when creating an instance of the HomeForm.

For this example, I will enable the body field only when the user is a superuser.

class HomeForm(WagtailAdminPageForm):

    # Override the constructor to do the things at object creation.
    def __init__(self, data=None, files=None, parent_page=None, subscription=None, *args, **kwargs):
        super().__init__(data, files, parent_page, subscription, *args, **kwargs)
        
        user = kwargs['for_user']    # Get the user accessing the form
        is_superuser = user.is_superuser
        body = self.fields.get('body')    # Get the body field
        body.disabled = not is_superuser    # Disable the body field if user is not a superuser

This way, every time a non superuser loads the create or edit page, the body field will be disabled.

But if you want to remove access only from edit page, you need to use self.initial variable. This variable is a dictionary with initial values to be used when showing the form.
You can check the value of a required field (like title) from self.initial and if the field's value is empty, that means the create page is loaded and if there is a value, that means the edit page is loaded.

Upvotes: 2

coredumperror
coredumperror

Reputation: 9131

I realize this is a really old question now, but just in case people come across it in the future, here's how my code shop does this in Wagtail 2.3. It may or may not work in later versions.

Add the following to the Page subclass you've written:

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

class Meta:
    verbose_name = 'Event'
    verbose_name_plural = 'Events'
    ordering = ['start_date', 'title']
    permissions = (
        ('can_access_admin_fields', 'Can access Event Admin fields'),
    )

@classmethod
def get_edit_handler(cls):
    """
    We override this method (which is added to the Page class in wagtail.admin.edit_handlers) in order to enforce
    our custom field-level permissions.
    """
    # Do the same thing that wagtail.admin.edit_handlers.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('calendar_app.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

This is, obviously, quite funky and fragile. But it's the only solution I could find.

Upvotes: 4

Related Questions