2

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).

Darien Marks
  • 446
  • 4
  • 18
  • Actions are ok to be located in `admin.py` file and also you do not need to call unregister - simply register your `UserAdmin` class – Charnel Jan 25 '21 at 14:05
  • If I remove the `unregister` function and register the class using either the `register()` function or the decorator, the server fails with an "AlreadyRegistered" error: `django.contrib.admin.sites.AlreadyRegistered: The model User is already registered with 'auth.UserAdmin'` – Darien Marks Jan 25 '21 at 14:35
  • 1
    Have you by chance set the `AUTH_USER_MODEL` setting? – tim-mccurrach Jan 25 '21 at 14:45
  • Also can you post your `INSTALLED_APPS` (in particular the order of 'contrib.auth`, `contrib.admin` and your app). The full traceback would also be useful as well – tim-mccurrach Jan 25 '21 at 14:53
  • @tim-mccurrach I haven't set `AUTH_USER_MODEL` in `settings.py`. `INSTALLED_APPS` order is [App, `contrib.admin`, `contrib.auth`]. I'll edit in the stack trace now. – Darien Marks Jan 25 '21 at 19:36
  • @DarienMarks ah, that will be it. See my answer below :) – tim-mccurrach Jan 25 '21 at 19:52

1 Answers1

2

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.

tim-mccurrach
  • 6,395
  • 4
  • 23
  • 41
  • Thank you! This worked to put functioning actions in the "Actions" drop-down of the ChangeList page. However, it also deleted the search box and filters options from that page. Unless you know of an easy fix for that, I'll post it as a separate question tonight. – Darien Marks Jan 25 '21 at 20:41
  • @DarienMarks There actually is an easy fix, so I'll add it to my answer. – tim-mccurrach Jan 25 '21 at 21:06
  • Thanks. I'm trying your suggestion but still getting errors (new ones, though). Just to check, what Model goes in the argument to the `@admin.register()` decorator under this scheme? – Darien Marks Jan 26 '21 at 00:50
  • Same as before. I've updated my answer to clarify – tim-mccurrach Jan 26 '21 at 01:07
  • Don't know why that didn't work the first time, but it does now. Everything tests out perfectly. Thanks again for your help! – Darien Marks Jan 26 '21 at 01:28