Search code examples
djangodjango-admindjango-authentication

How can I add an action to the Django User admin page?


I'm using Django 3.0. I want to add an action to the User ChangeList in the admin.

The documentation for admin actions indicates that to add an action to an admin page, I need to add either the method or a reference to it to the Model's admin.ModelAdmin subclass in admin.py:

# admin.py
from django.contrib import admin

def make_published(modeladmin, request, queryset):
    queryset.update(status='p')
make_published.short_description = "Mark selected stories as published"

class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'status']
    ordering = ['title']
    actions = [make_published]

or

# admin.py
from django.contrib import admin

class ArticleAdmin(admin.ModelAdmin):
    ...

    actions = ['make_published']

    def make_published(self, request, queryset):
        queryset.update(status='p')
    make_published.short_description = "Mark selected stories as published"

Since the User Model's admin.ModelAdmin subclass is in the auth system and not in my admin.py, though, I don't know where to put the code for this case.

I tried following user Davor Lucic's answer to a much older but similar question

from django.contrib.auth.models import User

class UserAdmin(admin.ModelAdmin):
    actions = ['activate_user','deactivate_user']

    def activate_user(self, request, queryset):
        queryset.update(is_active=True)

    def deactivate_user(self, request, queryset):
        queryset.update(is_active=False)

    activate_user.short_description = "Activate user(s)"
    deactivate_user.short_description = "Deactivate user(s)"

admin.site.unregister(User)
admin.site.register(User, UserAdmin)

When I try this, the server halts with the error (full stack trace requested by responder):

Project/project/app/admin.py changed, reloading.
Watching for file changes with StatReloader
Exception in thread django-main-thread:
Traceback (most recent call last):
  File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "Project/env/lib/python3.6/site-packages/django/utils/autoreload.py", line 53, in wrapper
    fn(*args, **kwargs)
  File "Project/env/lib/python3.6/site-packages/django/core/management/commands/runserver.py", line 109, in inner_run
    autoreload.raise_last_exception()
  File "Project/env/lib/python3.6/site-packages/django/utils/autoreload.py", line 76, in raise_last_exception
    raise _exception[1]
  File "Project/env/lib/python3.6/site-packages/django/core/management/__init__.py", line 357, in execute
    autoreload.check_errors(django.setup)()
  File "Project/env/lib/python3.6/site-packages/django/utils/autoreload.py", line 53, in wrapper
    fn(*args, **kwargs)
  File "Project/env/lib/python3.6/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "Project/env/lib/python3.6/site-packages/django/apps/registry.py", line 122, in populate
    app_config.ready()
  File "Project/env/lib/python3.6/site-packages/django/contrib/admin/apps.py", line 24, in ready
    self.module.autodiscover()
  File "Project/env/lib/python3.6/site-packages/django/contrib/admin/__init__.py", line 26, in autodiscover
    autodiscover_modules('admin', register_to=site)
  File "Project/env/lib/python3.6/site-packages/django/utils/module_loading.py", line 47, in autodiscover_modules
    import_module('%s.%s' % (app_config.name, module_to_search))
  File "Project/env/lib/python3.6/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 994, in _gcd_import
  File "<frozen importlib._bootstrap>", line 971, in _find_and_load
  File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 678, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "Project/project/app/admin.py", line 153, in <module>
    admin.site.unregister(User)
  File "Project/env/lib/python3.6/site-packages/django/contrib/admin/sites.py", line 144, in unregister
    raise NotRegistered('The model %s is not registered' % model.__name__)
django.contrib.admin.sites.NotRegistered: The model User is not registered

I think the error might be with the register() and unregister() functions, but these have no documentation so I have no way of knowing.

Is there a simple, functioning way to add an admin action to the auth.models.User Model?

EDIT: Here's the final result, putting everything together thanks to 'tim-mccurrach':

from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin as AuthUserAdmin

class UserAdmin(AuthUserAdmin):
    actions = ['activate_user','deactivate_user']

    def activate_user(self, request, queryset):
        queryset.update(is_active=True)

    def deactivate_user(self, request, queryset):
        queryset.update(is_active=False)

    activate_user.short_description = "Activate selected users"
    deactivate_user.short_description = "Deactivate selected users"

admin.site.unregister(User)
admin.site.register(User, UserAdmin)

I checked, and using this scheme it isn't necessary to re-order INSTALLED_APPS in settings.py (it's fine if your app comes before contrib.admin and contrib.auth).


Solution

  • The problem is the order of your INSTALLED_APPS.

    Because your App app comes before contrib.auth, the app.admin.py file is imported before the conrib.auth.admin.py file. This means you are trying to unregister the User admin, before it's even been registered.

    change the INSTALLED_APPS order to put your own app after the contrib.auth app, and everything should work :)

    Note also: Rather than inheriting from admin.ModelAdmin, if you inherit from auths UserAdmin you'll get all of the existing functionality e.g:

    from django.contrib.auth.admin import UserAdmin as OriginalUserAdmin
    
    class UserAdmin(OriginalUserAdmin):
        actions = ['activate_user','deactivate_user']
        ... 
    
    
    admin.site.unregister(User)
    admin.site.register(User, UserAdmin)
    

    As it happens, I think importing the UserAdmin here would also fix your problem without having to change the INSTALLED_APPS order, since whilst importing UserAdmin the code will have run to register the old UserAdmin. Changing the order of the apps seems like the more robust way to fix your issue though.