34

Which is the best way to implement my own django.contrib.admin.sites.AdminSite?

Actually I get a problem with the registration of INSTALLED_APPS in django.contrib.admin.autodiscover. If I use my custom AdminSite class in urls.py, there were no apps displayed on the admin page.

I fixed this with a litte hack. I wrote this class:

from django.contrib.admin.sites import site as default_site

class AdminSiteRegistryFix( object ):
    '''
    This fix links the '_registry' property to the orginal AdminSites
    '_registry' property. This is necessary, because of the character of
    the admins 'autodiscover' function. Otherwise the admin site will say,
    that you havn't permission to edit anything.
    '''

    def _registry_getter(self):
        return default_site._registry

    def _registry_setter(self,value):
        default_site._registry = value

    _registry = property(_registry_getter, _registry_setter)

And implement my custom AdminSite like this:

from wltrweb.hacks.django.admin import AdminSiteRegistryFix
from django.contrib.admin import AdminSite

class MyAdminSite( AdminSite, AdminSiteRegistryFix ):
    # do some magic
    pass        


site = MyAdminSite()

So I can use this site for urls.py.

Anyone knows a better way? Since I access a var starting with a underscore it is no more than a hack. I don't like hacks.

Edit: Another way would be to rewrite the django.contrib.admin.autodiscover function, but in this case I would have redundant code.

Louis
  • 146,715
  • 28
  • 274
  • 320
svenwltr
  • 17,002
  • 12
  • 56
  • 68
  • 1
    Not sure that this resolve the problem, because didn't try it. Django has admin customization ability by default since 2.1 https://docs.djangoproject.com/en/dev/ref/contrib/admin/#overriding-default-admin-site . If someone tried it, please add comment/answer about the results. – alexche8 May 24 '18 at 06:43

4 Answers4

30

The Problem

Using a custom class derived from django.contrib.admin.AdminSite for the admin site of a project, without having to write custom registration code to register models with the new class. When I use 3rd party apps with their own models, I'd rather not have to edit custom registration code only because models were added or removed from these apps.

The Solution

You have to switch the instance created with the default class used for the admin site to your own instance, created with your own class before django.contrib.admin's autodiscover function is called. I do this by:

  1. Having an app that will perform the switch. (I use my project-specific app named core for my own purposes.)

  2. Two choices:

    1. Django 1.6 to 1.9: use __init__ of the app to perform the switch. In Django 1.8, you will get a deprecation warning due to the change in Django 1.9 quoted below. Note that this method will work with 1.9 too because the Django modules loaded by the code shown below have been changed in 1.9 so that they no longer load models. When I use this method my core/__init__.py file contains:

      from django.contrib import admin
      from django.contrib.admin import sites
      
      class MyAdminSite(admin.AdminSite):
          pass
      
      mysite = MyAdminSite()
      admin.site = mysite
      sites.site = mysite
      
    2. Django 1.9 and over: use the app configuration of the app to perform the switch. As of Django 1.9, as the release notes state:

      All models need to be defined inside an installed application or declare an explicit app_label. Furthermore, it isn’t possible to import them before their application is loaded. In particular, it isn’t possible to import models inside the root package of an application.

      I prefer to limit the imports I do at the root level to avoid the risk of loading models. While as of version 1.9 using the __init__ method above will work, there's no telling if 1.10 or a later version will introduce a change that will cause problems.

      When I use this method the core/__init__.py sets default_app_config = "core.apps.DefaultAppConfig" and I have a core/apps.py like this:

      from django.apps import AppConfig
      
      class DefaultAppConfig(AppConfig):
          name = 'core'
      
          def ready(self):
              from django.contrib import admin
              from django.contrib.admin import sites
      
              class MyAdminSite(admin.AdminSite):
                  pass
      
              mysite = MyAdminSite()
              admin.site = mysite
              sites.site = mysite
      

      While it is possible to use this method with versions 1.7 and 1.8, it is a bit risky to use it with those versions. See the notes below.

  3. Placing this app earlier than django.contrib.admin in the INSTALLED_APPS list. (This is absolutely necessary for 1.7 and later. In earlier versions of Django, it might work okay even if the app is later than django.contrib.admin. However, see the notes below.)

Notes and Caveats

  • The app that performs the switch should really be the first app in the INSTALLED_APPS list so as to minimize the chance that something else will grab the value of site from django.contrib.admin before the switch is made. If another app manages to get that value before the switch is done, then that other app will have a reference to the old site. Hilarity will surely ensue.

  • The method above won't work nicely if two apps are trying to install their own new default admin site class. This would have to be handled on a case-by-case basis.

  • It is possible that a future release of Django could break this method.

  • For version prior to 1.9, I preferred using __init__ to do site switch over using the app configuration because the documentation on initialization indicates that the ready() method of the app configurations is called relatively late. Between the time an app's module is loaded, and the time ready() is called, models have been loaded, and in some case, it could mean that a module has grabbed the value of site from django.contrib.admin before ready is called. So as to minimize the risk, I have the app's __init__ code do the switch.

    I believe the risk that existed in version 1.7 and 1.8 and that I avoided by using __init__ to perform the site switch as early as possible does not exist in 1.9. Everybody is prohibited from loading modules before all the applications are loaded. So doing the switch in the ready callback of the first application listed in INSTALLED_APPS should be safe. I've upgraded a large project to 1.9 and used the app configuration method, without any problem.

Louis
  • 146,715
  • 28
  • 274
  • 320
  • 4
    Note about compatibility, I'm using your method with Django 1.9.4 and works like a charm. – Paolo Mar 14 '16 at 20:59
  • A downside of this approach is that for Django 1.7, it appears to lose all URLs inside unittests. e.g. If I try to access `/admin/auth/user` from inside a unittest, I get a 404 error. – Cerin Jul 28 '17 at 21:11
  • @Cerin I did not experience issues inside unit tests when I was on 1.7. I'd say though that if you are using 1.7 you have a bigger problem on your hands: 1.7 stopped receiving *any* form of support in December 1st 2015. That's 1.5 years ago, at the time of writing. You should move to a version that's supported. – Louis Jul 31 '17 at 11:52
  • 1
    Does not work for me partialy. I use django 1.10.7. I customized django admin index page and it's OK. But I want to use the same settings file for my celery workers and every time when I init celery start that throws error: "django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet. " – alexche8 Dec 06 '17 at 10:39
  • @alexche8 What I describe in the answer has been in use on a public-facing site from Django 1.6 to Django 1.11, using Celery all along. So you're saying if you just remove the special app from INSTALLED_APPS, then `celery start` works fine, but when you add it back, it fails? – Louis Dec 06 '17 at 11:35
  • Yes, that's right. I run `celery -A config.celery worker --loglevel=info` with app in INSTALLED_APPS ''app.custom_admin.apps.CustomAdminConfig' and it failed. – alexche8 Dec 06 '17 at 17:40
  • Without application celery works fine. Can I add you somewhere instead of stackoverflow if you have time? Don't want share logs on public , then you can update your answer if I had some specific case. – alexche8 Dec 06 '17 at 17:48
  • @alexche8 I can provide dedicated services for people who hire me as a consultant. If that's not an option for you, my recommendation is to reduce your problem to a [mcve] and post a new question on SO. – Louis Dec 06 '17 at 18:38
  • If someone has read util there my problem was confusing the name property of new AppConfig. I use custom names like 'projectname.app' over project but new generated app config use just 'app'. I didn't mentioned that in time because I got a bunch of "django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet. " errors that captivated all my console and hide main error. After I fixed this, Louis solution works just great with any error. Django version 1.10.7 – alexche8 Dec 12 '17 at 09:40
15

From Django 2.1, there is an 'out-of-the-box' solution: https://docs.djangoproject.com/en/2.1/ref/contrib/admin/#overriding-the-default-admin-site

from django.contrib import admin

class MyAdminSite(admin.AdminSite):
...

Swapping the custom admin site is now done by adding your own AdminConfig to installed apps.

from django.contrib.admin.apps import AdminConfig

class MyAdminConfig(AdminConfig):
    default_site = 'myproject.admin.MyAdminSite'


INSTALLED_APPS = [
    ...
    'myproject.apps.MyAdminConfig',  # replaces 'django.contrib.admin'
    ...
]

Note the difference between AdminConfig and SimpleAdminConfig, where the latter doesn't trigger admin.autodiscover(). I'm currently using this solution in a project.

Elwin
  • 810
  • 1
  • 8
  • 15
  • 4
    With this configuration I have an error: ImportError: Module "myproject.admin" does not define a "MyAdminSite" attribute/class. Any idea? – 4ndt3s Aug 19 '18 at 09:25
  • @jahuuar Can you show me what your app.py looks like? – Elwin Aug 19 '18 at 11:19
  • It's same to yours, but I'm trying to register the models in admin.py: from django.contrib.auth.models import Group from django.contrib.auth.admin import GroupAdmin .... my_admin_site = MyAdminSite(name='my_admin') my_admin_site.register(Group, GroupAdmin) – 4ndt3s Aug 19 '18 at 15:05
  • 3
    When you swap the admin site like this, its not advisable to import your own admin site to register models. Just use `from django.contrib.admin import site` and `site.register(MyModel, MyModelAdmin)` – Elwin Aug 19 '18 at 16:07
  • Thanks @Elwin. I was trying to have two custom admin sites. – 4ndt3s Aug 20 '18 at 13:26
  • You're welcome @jahuuar In that case you have to follow these instructions: https://docs.djangoproject.com/en/2.1/ref/contrib/admin/#customizing-the-adminsite-class – Elwin Aug 21 '18 at 14:08
  • @Elwin Where is that site.register method documented? I was not able to get it to work properly. – bparker Mar 10 '20 at 18:18
  • @bparker Only it's usage is documented over here: https://docs.djangoproject.com/en/2.1/ref/contrib/admin/#modeladmin-objects – Elwin Mar 11 '20 at 19:13
  • Does anyone have a working app snippet of this? I couldn't get to register my models. – suayip uzulmez May 06 '20 at 10:08
  • what to change in urls.py? – Noortheen Raja Dec 23 '20 at 10:35
  • @NoortheenRaja Depends; When overriding the default admin there is no need the change urls.py; When customisation is needed follow instruction in the docs https://docs.djangoproject.com/en/3.1/ref/contrib/admin/#customizing-the-adminsite-class – Elwin Dec 24 '20 at 11:06
  • @Elwin The models of the app that holds the custom AdminSite class don't appear in the admin panel, do you have a fix for this? – Oussama He Sep 07 '21 at 17:46
  • @Oussama Please post a new question, so we can address it properly. – Elwin Sep 09 '21 at 07:57
10

Having encountered the same kind of issue while implemeting site-wide custom AdminSite in Django 3.2, I found a workaround to make it work.

It seems the documentation should be updated accordingly.

from django.contrib.admin.apps import AdminConfig

class MyAdminConfig(AdminConfig):
    default_site = 'myproject.admin.MyAdminSite'

This raises an exception:

RuntimeError: 'myproject.apps' declares more than one default AppConfig: 'AdminConfig', 'MyAdminConfig'.

It is solved by importing django.contrib.admin.apps instead of django.contrib.admin.apps.AdminConfig:

from django.contrib.admin import apps

class MyAdminConfig(apps.AdminConfig):
    default_site = 'myproject.admin.MyAdminSite'

Then the exception moves on:

django.core.exceptions.ImproperlyConfigured: Application labels aren't unique, duplicates: admin

This is caused by the settings.py configuration:

INSTALLED_APPS = [
    'myproject.apps.MyAdminConfig',  #replaces django.contrib.admin
   ...

It is solved by removing the 'MyAdminConfig' from the settings:

INSTALLED_APPS = [
    'myproject.apps', #replaces django.contrib.admin
   ...
JCMRS
  • 101
  • 1
  • 3
7

Quoting from https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#customizing-the-adminsite-class

If you'd like to set up your own administrative site with custom behavior, however, you're free to subclass AdminSite and override or add anything you like. Then, simply create an instance of your AdminSite subclass (the same way you'd instantiate any other Python class), and register your models and ModelAdmin subclasses with it instead of using the default.

I guess that is the most explicit approach, but it also means that you need to change the register code in your apps admin.py files.

There is really no need to use autodiscover when using your own AdminSite instance since you will likely be importing all the per-app admin.py modules in your myproject.admin module.

The assumption seems to be, that once you start writing your custom admin site, it becomes pretty much project specific and you know beforehand which apps you want to include.

So if you don't want to work with the hack above, I only really see these two options. Replace all register calls to your custom admin site or register the models explicitly in your adminsite module.

ruddra
  • 50,746
  • 7
  • 78
  • 101
Reiner Gerecke
  • 11,936
  • 1
  • 49
  • 41
  • Thanks, I read over this part of the docs and it's really much projet specific. I will explicit register all the models I need. – svenwltr Feb 04 '11 at 15:28
  • I did something similar by: 1. creating a module `admin_site.py` for my subclass of `AdminSite` and an instance of it, like `custom_site = MyAdminSite()`; 2. replacing `admin.site = custom_site`; 3. adding `admin_site` as the first item of `INSTALLED_APPS`. – boechat107 Jul 14 '16 at 18:31