Wagtail Customising User Account Settings Form With One-to-One Model

Question:

I have a model in my app called "portal", in portal/models.py:

from django.db import models
from django.contrib.auth.models import User
from django.dispatch import receiver
from django.db.models.signals import post_save

from wagtail.snippets.models import register_snippet
from wagtail.admin.edit_handlers import FieldPanel

@register_snippet
class StaffRoles(models.Model):
    role = models.CharField(max_length=154, unique=True, help_text="Create new staff roles here, these roles an be assigned to 'staff' users.")
    panels = [
        FieldPanel('role'),
    ]

    def __str__(self):
        return self.role

    class Meta:
        verbose_name = "Staff Role"
        verbose_name_plural = "Staff Roles"

@register_snippet
class Staff(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=1024, blank=True)
    roles = models.ManyToManyField(StaffRoles, blank=True)
    
    def __str__(self):
        return str(self.user.first_name) + " " + str(self.user.last_name)

    class Meta:
        verbose_name = "Staff"
        verbose_name_plural = "Staff"


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created and instance.is_superuser:
        Staff.objects.create(user=instance)

Any new superuser is automatically given the Staff model. Any existing (non superuser) user can also be added as Staff.

I want Staff members to be able to set a bio and a role. Roles can be added through the Snippets page in Wagtail admin.

Right now, Staff is registered as a snippet, this means any Staff member can edit another Staff member’s Staff attributes.

I want to customise the Wagtail User Account Settings by adding a form to each user’s respective Staff attributes. This way, I can lock out the Staff snippet so each user who has a Staff object linked to their User model can change only their own Staff fields/attributes.

Following the Wagtail documentation from here, I first created the file portal/forms.py with the content:

from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User

from wagtail.users.models import UserProfile

from .models import Staff

class CustomStaffSettingsForm(forms.ModelForm):

    class Meta:
        model = Staff
        exclude = []

And another file portal/wagtail_hooks.py with the contents:

from wagtail.admin.views.account import BaseSettingsPanel
from wagtail.core import hooks

from .forms import CustomStaffSettingsForm

@hooks.register('register_account_settings_panel')
class CustomStaffSettingsPanel(BaseSettingsPanel):
    name = 'custom_staff'
    title = "Staff settings"
    order = 500
    form_class = CustomStaffSettingsForm
    form_object = 'profile'

This doesn’t work as intended. When accessing "Account settings" and scrolling down to the "Staff settings" I can see the CustomStaffSettingsForm as intended. Although the correct user is pre-selected in the User field, I can still select other users. Also, the stored values for bio and roles aren’t being retrieved and new values aren’t being saved.

Below are the Staff Roles registered in the snippets page:
enter image description here

Below is the Staff model for the user "sid" in the snippets page:
enter image description here

Below is what the Staff settings section in the "Account settings" looks like.
enter image description here

As you can see, although the User dropdown is selected to the right user, I can still click and select another user. The bio and roles fields aren’t filled with the correct values from the snippets page and not shown in the picture, filling and saving values doesn’t work.

I hope my question is as detailed as possible, how do I fix this?

Asked By: SidS

||

Answers:

After a few hours of digging, I managed to answer my own question.

  1. A related_name needs to be added with the value of profile to Staff.user. This prevents the 'User' object has no attribute 'profile' error message
class Staff(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    ...
  1. We need to exclude the user field from the CustomStaffSettingsForm. This is to prevent users to select a different user and change another user’s Staff fields.
class CustomStaffSettingsForm(forms.ModelForm):
    class Meta:
        model = Staff
        exclude = ["user"]
  1. The tricky part (at least for me) was to render the correct instance of the form, in other words, if a user named "sid" is accessing this form, they need to be able to see their own Staff fields which are stored and also be able to change the stored values. To achieve this, I had to override the get_form method which CustomStaffSettingsPanel inherited form Django’s BaseSettingsPanel. It’s easier for me to paste the entire wagtail_hooks.py contents here below.
from wagtail.admin.views.account import BaseSettingsPanel
from wagtail.core import hooks

from .forms import CustomStaffSettingsForm

@hooks.register('register_account_settings_panel')
class CustomStaffSettingsPanel(BaseSettingsPanel):
    name = 'custom_staff'
    title = "Staff settings"
    order = 300
    form_class = CustomStaffSettingsForm
    form_object = 'profile'

    def get_form(self):
        """
        Returns an initialised form.
        """
        kwargs = {
            'instance': self.request.user.profile,
            'prefix': self.name
        }

        if self.request.method == 'POST':
            return self.form_class(self.request.POST, **kwargs)
        else:
            return self.form_class(**kwargs)

The key part of the code above is 'instance': self.request.user.profile. Passing this argument into self.form_class results in the correct user’s stored Staff fields and values to be retrieved and stored correctly.

This solution allows me to make sure superusers who are also registered as Staff can edit their own Staff fields and have access to others, this is a functionality which cannot be achieved easily with Snippets.

As Staff is no longer registered as a Snippet, we lose access to change another user’s Staff fields. This can be fixed by adding the following in portal/admin.py:

admin.site.register(Staff)

Only superusers who had been created via python manage.py createsuperuser or has had staff_status set to true can access the Django Admin interface at http://localhost:8000/django-admin. Unlike Snippets, this way can be used to allow only the Django superuser staff to access and modify Staff fields.

What about non-superusers who are registered as Staff? Non-superusers cannot access the Wagtail admin, therefore they cannot access the Account Settings page. In most cases, this shouldn’t be a problem as the ideal use-case is where only the superusers can be set as Staff. If you want to set non-superusers as Staff you might have to create your own views and forms so the user can change their Staff fields outside of Wagtail admin.

Answered By: SidS