Updating user groups in custom Form

I’ve run into a weird case where the the user groups won’t update if I initialize a user update form conditionally where the groups field is only shown if user has change_user permission.
This works fine:

user_form = UpdateUserForm(request.POST, instance=user_instance)
user_form.save()

#forms.py:
class UpdateUserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['username', 'email', 'first_name', 'last_name', 'birth_date', 'groups']
        
    birth_date = forms.DateField(
        widget = forms.DateInput(attrs={'type': 'date', 'max': date.today().strftime('%Y-%m-%d')})
    )

However if I initialize the form with a request.user to check for permissions, the groups are never updated:

user_form = UpdateUserForm(request.POST, instance=user_instance, user=request.user)
user_form.save()

#forms.py:
class UpdateUserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['username', 'email', 'first_name', 'last_name', 'birth_date', 'groups']
        
    birth_date = forms.DateField(
        widget = forms.DateInput(attrs={'type': 'date', 'max': date.today().strftime('%Y-%m-%d')})
    )

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user', None)
        super().__init__(*args, **kwargs)
        if not (self.user and self.user.has_perm('AppUser.change_user')):
            del self.fields['groups']

Is there a better way to do this? Any help is appreciated

You wrote:

Please clarify: Are you seeing the Groups field when the form is being prepared for a GET but it’s not processing on a POST? Or are you not seeing that field at all?

If you’re not seeing it at all, then the issue may be:

The permission is “app_label.permission_name”, using the app label and not the module name, which is almost always lower case.

So depending upon how you have your app defined, it might be appuser, or it could be something else if you’ve got a different name defined in your apps.py file.

(If in doubt, examine the app_label in the ContentType model.)

If this is correct, then the other thing to check would involve looking at the complete view. Keep in mind that the relationship between User and Group is a many-to-many relationship, and if you’re doing something like using commit=False in your first save, then it’s up to you to save the many-to-many relationship after saving the base model. (See the docs for The save method)

If neither of these help, then you might want to add some print statements (or use the debugger) in both your form and view to see what exactly the status of these objects are at key points in your processing.

Hi Ken, I am seeing the Group field prepared correctly. However, when I try to change it, it doesn’t seem to save the changes. I’m not using commit=False.
my views.py:

@login_required
def userView(request, user_pk):
    user_instance = User.objects.get(pk=user_pk)
    user_form = UpdateUserForm(instance=user_instance, user=request.user)
    return render(request, 'user.html',{
        'user_instance': user_instance,
        'user_form': user_form,
    })

    
@login_required
def apiUpdateUser(request):
    user_instance_id = int(request.POST.get('user_instance_id'))
    # check permissions first
    if not (request.user.id == user_instance_id or request.user.has_perm('AppUser.change_user')):
        return JsonResponse({"status": "error", "errors": "Permission Denied"}, status=403)

    if request.method == 'POST':
        user_instance = User.objects.get(pk=user_instance_id)
        user_form = UpdateUserForm(request.POST, instance=user_instance)
        if user_form.is_valid():
            user_form.save()
            messages.success(request, f'{user_instance.username}\'s information successfully updated.')
            return JsonResponse({
                "status": "success",
            })
        else:
            errors = user_form.errors
            return JsonResponse({"status": "error", "errors": errors}, status=400)
    else:
        return HttpResponse(status=405)

The view is rendered in userView, and users are updated in a call to apiUpdateUser. It seems adding user=request.user to the form declaration in apiUpdateUser fixes the issue. But I’m still not sure if this is the best way to do this.

The default Groups field is a multiselect, which isn’t very user friendly, but changing it to a multicheckbox seems to not work again. I’d appreciate it if you can give any advice on how these types of situations are generally handled in Django.

I added ‘groups’ to both the Meta fields list as well as a custom groups definition as shown below. This is working as I’d want.

class UpdateUserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['username', 'email', 'first_name', 'last_name', 'birth_date', 'groups']
        
    birth_date = forms.DateField(
        widget = forms.DateInput(attrs={'type': 'date', 'max': date.today().strftime('%Y-%m-%d')})
    )
    groups = forms.ModelMultipleChoiceField(
        queryset=Group.objects.all(),
        widget=forms.CheckboxSelectMultiple,
        required=False,
    )

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user', None)
        super().__init__(*args, **kwargs)
        if not (self.user and self.user.has_perm('AppUser.change_user')):
            self.fields['groups'].disabled = True

However, I guess my remaining question is on the ‘correct’ way to use django’s permissions. I want users to change their own information such as emails, first and last names, and birthdate. However, I don’t want a user to change the permissions they have (i.e the groups they belong to) unless they have the change_user permission (which I give to users who are in a user admin group). So to clarify what I’m asking:

  1. Is what I have so far the ‘correct’ set up for views? It doesn’t seem very dry that I have to update the form declaration by adding user=request.user in both the userView page render and the apiUpdateUser function.
  2. I’m unsure if I’m using permissions correctly since it seems a user doesn’t actually need AppUser.change_user to “change” a User model instance in the AppUser app, and seems to rely on manually added check conditions.

Any advice from @KenWhitesell or anyone else would be greatly appreciated.

There is no “right” or “wrong” here. There are common patterns, but as with every architectural pattern, there are always cases where you’re better off doing something different.

The most common Django pattern is a view that handles both the GET and POST requests. Since you’ve separated this into two separate views, there will be some duplicated code. (This does not make it “wrong” by any definition, merely necessary.)

That’s how Django permission work. The Permission model is just a “tag” or a “label”. It has no intrinsic functionality. There are no Django-level semantics applied to them. All behavior associated with a Permission is for the developer to define.

(From that perspective, they’re really no different than Groups. There is no system-specific meaning associated with any group name, it’s all an issue of how you use them.)