Custom User (AbstractUser) unable to save permissions/groups

Hi all,

I’ve reached about double figures on Django projects but still feel like a beginner and need a little guidance on debugging an issue with AbstractUser not saving permissions and Groups in admin.

This project required that I combine several existing apps and decided to use a Custom User primarily so that I could differentiate sales staff (is_sales) in one of the apps. I created a test project single app and it worked fine, the key being not only to have the “is_sales” field but also define Groups to differentiate sales and control permissions especially in admin rights.

This was the first project using Django v4 and I thought I followed the django guidance (https://docs.djangoproject.com/) on creating Custom User from scratch but clearly something went wrong. My code now contains some additional attempts to resolve the isssue like PermissionMixin, adding Manager and Forms but I’m lost how to debug further.

So when I login to admin as superuser or user and try to edit the permissions or apply a Group I see the confirmation “the user ‘username’ was changed successfully” however if I then view that user in admin or in the database I don’t see those changes.

Here’s my code:-

class User(AbstractUser, PermissionsMixin):

    is_sales = models.BooleanField(default=False, help_text='select to ...... ')

    def __str__(self):
        return self.username
class UserAdmin(UserAdmin):
        model = User
        list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_sales')
        filter_horizontal = ('groups', 'user_permissions')
        add_form = UserCreationForm
        form = UserChangeForm

        fieldsets = UserAdmin.fieldsets + (
            (None, {'fields': ('is_sales',)}),
        )

        add_fieldsets = UserAdmin.add_fieldsets + (
            (None, {'fields': ('is_sales',)}),
        )
admin.site.register(User, UserAdmin)
class UserCreationForm(UserCreationForm):

    class Meta(UserCreationForm.Meta):
       model = User
       fields = UserCreationForm.Meta.fields + ('is_sales',)

class UserChangeForm(UserChangeForm):

    class Meta(UserChangeForm.Meta):
       model = User

When I use the shell to try to debug I can see my main User (AB) has permissions and is part of a Group I made test-admin’

 from django.contrib.auth import get_user_model
>>> from django.contrib.auth.models import Group
>>> UserModel=get_user_model()
>>> u = UserModel.objects.get(username='AB')
>>> u.get_group_permissions()
{'itasc.change_pairings', 'itasc.change_measurements', 'trials.add_ctgov1', 'mdaily.view_morg', 'itasc.view_devices', 'mdaily.add_mdaily', 'auth.change_user', 'devices.delete_mdevown', 'mdaily.delete_minv', 'devices.delete_msub', 'mdaily.view_mdaily', 'devices.view_morg', 'auth.add_group', 'devices.add_msub', 'trials.delete_contacts', 'admin.view_logentry', 'itasc.delete_measurements', .........................,
 'mdaily.view_minv', 'auth.add_user', 'devices.add_morgs', 'trials.change_contacts', 'devices.delete_minv', 'auth.delete_permission', 'itasc.delete_devices', 'auth.view_permission', 'mdaily.change_mdevown', 'auth.delete_user', 'auth.view_group', 'contenttypes.add_contenttype', 'devices.add_morg', 'contenttypes.delete_contenttype', 'sessions.change_session'}
>>> 

If I then runserver, enter Admin and try to change AB or another user, say A2, I get success message but I can see in shell and database nothing actually changes :_

>>> u = UserModel.objects.get(username='A2')
>>> u.get_group_permissions()
set()

So, first I’ve lost the plot how I created AB successfully, I guess I fiddled with something that now impacts my ability to change any user’s permissions.

I would appreciate some guidance on what to look for to resolve this, many thanks.

For one I would change to:

from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin

To make this line more explicit:

class UserAdmin(UserAdmin):
# Can then become -> class UserAdmin(DjangoUserAdmin):

I am not sure of the solution, and I have only made 2 django projects, but you don’t need to specify the model in a UserAdmin instance (only for inlines like TabularInline or StackedInline), admin.site.register creates the relationship, or even better keep it DRY and use the decorator:

@admin.register(models.User)
class UserAdmin(DjangoUserAdmin):
    ...

Somethings to check:

  1. Have you specified settings.py → `AUTH_USER_MODEL’ ?
  2. Are you access the right database? (Do you have to user tables, and Django is not using yours, see 1. above?)
  3. The right table?

Thanks run_the_race, really appreciated to get some ideas.

I went through your 1,2,3 list, which all made sense but good to get a fresh look.

However the explicit import of DjangoUserAdmin did make a difference - I lost the Users section in the admin!

But I noticed that when I now edit the Groups those changes are being saved and that does at least seem to work as expected.

It seems with custom Users there will be no Users link in the Auth section, because the custom class takes over – including permissions, but I thought I’d captured these with the fieldsets as below?

@admin.register(User)
class UserAdmin(DjangoUserAdmin):
        list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_sales')
        filter_horizontal = ('groups', 'user_permissions')
        add_form = UserCreationForm
        form = UserChangeForm

        fieldsets = DjangoUserAdmin.fieldsets + (
            (None, {'fields': ('is_sales',)}),
        )
        add_fieldsets = DjangoUserAdmin.add_fieldsets + (
            (None, {'fields': ('is_sales',)}),
        )

Glad its now using your custom user table instead of the auth table. Yes the User model does not appear under auth section because you now using a custom model from a different app which is not registered.

Having gone down this route before, the best solution I could find is unregistering groups from the auth app, and adding it to your new app that has the User models, like this:

# file: models.py that has the custom User model
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import Group as DjangoGroup

class User(AbstractUser):
   ...

class Group(DjangoGroup):
    """Instead of trying to get new user under existing `Aunthentication and Authorization`
    banner, create a proxy group model under our Accounts app label.
    Refer to: https://github.com/tmm/django-username-email/blob/master/cuser/admin.py
    """

    class Meta:
        verbose_name = 'group'
        verbose_name_plural = 'groups'
        proxy = True

# file: admin.py for your new app with the custom User model
from django.contrib.auth.models import Group as DjangoGroup
from . import models

admin.site.unregister(DjangoGroup)

@admin.register(models.Group)
class GroupAdmin(BaseGroupAdmin):
    fields = ('name', 'permissions')
    list_display = ('name', )
    list_display_links = list_display
    order = ('name',)

Also checkout this very helpful link: How to use email as username for Django authentication (removing the username) :: Fomfus - For my future self

wow, that’s a lot of detail, thanks again.

I had seen the technique of unregistering before, my first run using the code snippets didn’t seem to bring up the Users section alongside Groups. I’ll study this a bit more along with the links, obviously I’m missing something.

I got a partial resolution in that both the Groups and Users now appear under the Account app admin section.

However I still cannot get a Users Permissions nor Groups to save when I attempt to edit them. I can delete a User and edit a Group’s permissions.

I can also change a User’s Is_supeuser and is_sales which is probably enough for this project but still I don’t understand where I lost the ability to edit a User permissons and group.

I noticed that having edited a Users permission of group I still get the success message popup but in the browser I see error “GET /admin/accounts/user/ HTTP/1.1” 302 0

Could it be that I need to create another managers module in addition to Def create_user() and create_superuser()?

This is my custom user that users email instead of username:

"""
    Based on: https://www.fomfus.com/articles/how-to-use-email-as-username-for-django-authentication-removing-the-username/
"""
from django.contrib.auth.models import AbstractUser, BaseUserManager
from django.contrib.auth.models import Group as DjangoGroup
from django.db import models
from django.utils.translation import gettext_lazy as _

from utils.time import ttb


class UserManager(BaseUserManager):
    """Define a model manager for User model with no username field."""

    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        """Create and save a User with the given email and password."""
        if not email:
            raise ValueError('The given email must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        """Create and save a regular User with the given email and password."""
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        """Create and save a SuperUser with the given email and password."""
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(email, password, **extra_fields)


class User(AbstractUser):
    """
    Requirements:
    - Must be defined in models.py, due to the way settings.AUTH_USER_MODEL is defined
    """

    objects = UserManager()

    username = None
    email = models.EmailField('email address', unique=True)
    is_staff = models.BooleanField(
        'admin access',
        default=False,
        help_text='Designates whether the user can log into this admin site.',
    )

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ('first_name',)

    def __str__(self):
        return self.get_full_name()

    class Meta:
        db_table = 'auth_user'

    @property
    def cached_groups(self):
        """Warning 'groups' is a field name (M2M) so can't called this property 'groups'"""
        if not hasattr(self, '_cached_groups'):
            self._cached_groups = DjangoGroup.objects.filter(user=self).values_list(
                'name', flat=True
            )
        return self._cached_groups

    def in_groups(self, *names):
        for name in names:
            if name in self.cached_groups:
                return True
        return False


class Group(DjangoGroup):
    """Instead of trying to get new user under existing `Aunthentication and Authorization`
    banner, create a proxy group model under our Accounts app label.
    Refer to: https://github.com/tmm/django-username-email/blob/master/cuser/admin.py
    """

    class Meta:
        verbose_name = 'group'
        verbose_name_plural = 'groups'
        proxy = True

admin.py:

from django.contrib import admin
from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from django.contrib.auth.models import Group as DjangoGroup
from django.utils.translation import gettext_lazy as _

from dist.plug.user import UserAdminMixin
from extend.admin import DevEditOnlyAdminMixin, ReadonlyAdminMixin

from . import models
from .utils import get_usermixin_field_names


@admin.register(models.User)
class UserAdmin(UserAdminMixin, DjangoUserAdmin):
    """Define admin model for custom User model with no email field."""

    list_display = (
        'first_name',
        'last_name',
        'email',
        'is_staff',
        'is_active',
        'in_groups',
    )
    list_display_links = list_display
    # list_filter = DjangoUserAdmin.list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups')
    list_filter = (
        'groups',
        'is_staff',
        'is_active',
    )
    search_fields = ('email', 'first_name', 'last_name')
    ordering = ('first_name', 'last_name', 'email')

    # add_form_template = 'admin/cuser/cuser/add_form.html'
    # add_form = UserCreationForm
    # form = UserChangeForm

    # fieldsets = UserAdmin.fieldsets  # Inherited
    section_basic = (None, {'fields': ('email', 'password')})
    section_personal = (_('Personal info'), {'fields': ('first_name', 'last_name')})
    section_permission = (
        _('Permissions'),
        {
            'fields': (
                'is_active',
                'is_staff',
                #'is_superuser',
                'groups',
                #'user_permissions',
            )
        },
    )
    # section_dates = (_('Important dates'), {'fields': ('last_login', 'date_joined')})
    section_dates = None
    if usermixin_field_names := get_usermixin_field_names():
        section_custom = ('User config', {'fields': usermixin_field_names})
    else:
        section_custom = None
    fieldsets = [
        x
        for x in (
            section_basic,
            section_personal,
            section_custom,
            section_permission,
            section_dates,
        )
        if x is not None
    ]

    # fieldset for when one clicks the add button (required)
    add_fieldsets = (
        (
            None,
            {
                'classes': ('wide',),
                'fields': ('email', 'password1', 'password2'),
            },
        ),
    )

    def in_groups(self, obj):
        """
        get group, separate by comma, and display empty string if user has no group
        """
        return ', '.join([x.name for x in obj.groups.all()]) if obj.groups.count() else ''


admin.site.unregister(DjangoGroup)


@admin.register(models.Group)
class GroupAdmin(BaseGroupAdmin):
    fields = ('name', 'permissions')
    list_display = ('name', )
    list_display_links = list_display
    order = ('name',)