ModelAdmin class from django.contrib.admin.options has a bad design from the beginning: every registered
ModelAdmin in your project is a singleton.
This fact creates a big barrier for every developer who uses Django Admin in his project. This slows down the project, creates problems when the same
ModelAdmin is used by several users, and encourages programmers to use hacks to get around the limitations of this (anti) pattern. You can learn how to solve this problem in 5 lines in this article.
Why the developers of django.contrib.admin continue to rely on this architecture is a mystery to me.
Luckily there has been a quick and easy fix for this issue that works from the first version of Django up to the current one (4.0.3 in april 2022).
- The problems. I will describe in detail the problems encountered when using the singleton architecture for ModelAdmin.
- The solution. I will walk you through the fix and explain how and why it works.
- Django devs. I'm asking Django developers reading this article to consider changing the django.contrib.admin structure to make life easier for everyone.
- Talk from PyCon 2022 about this problem. In addition to this article I have a video about this topic from PyCon DE 2022 in Berlin.
When multiple users are running the admin panel at the same time, the singleton design pattern can allow one user to modify another user's data.
Let me demonstrate it with the code from my previous article about Django admin dynamic inline positioning. Right now it is not really important what the code does. We just modify the functions in the admin panel like this:
Now let's open the two tabs of the admin panel to simulate two users. Now let's open the change form for the product with pk=1 on the first tab (/admin/products/product/1/change). After this line, we are stopped by the
10 self.hello = 'hello this is the first obj'
If we check the
self.hello attribute, we see the following:
Now let's open a change form for a product with pk=2 in another tab ('/admin/products/product/2/change') and see what happens. We get a response from the change form right away because we skipped the
breakpoint. But let's see what happened to our
hello attribute that we set for our
ModelAdmin in the first tab.
ModelAdmin.hello attribute for GET request pk=1 has been changed to "this is the second obj", even though we did nothing in our first thread.
We proved that
ModelAdmin is a singleton, which means that we always get the same instance of
This is a simple demonstration of the problem. Since there are no real consequences in this example, some might even consider it trivial, but the consequences of this behavior for our project could be dangerous.
With this behavior, every ModelAdmin's do not guarantee the integrity of the data, because it is impossible to trace who has edited the data. Did this current thread edit our data? Did another request change something without our knowledge? Who knows.
You might argue that this isn't a problem because the requests have to happen at the same time for this problem to take effect. However, in a project where multiple people are running the admin panel at the same time, the issue is not if someone opens
ModelAdmin at the same time as someone else, but when.
This causes errors, for which there is no obvious explanation without an in-depth understanding of the admin panel architecture.
So the easiest way to avoid the problem is not to use the
ModelAdmin instance as a container.
But not every developer is aware of this, and
ModelAdminis often used by inexperienced developers to store data.
In this example the highest voted answer does exactly that.
If we can't use
ModelAdmin, then where can we store our data? Django does not give us a clear answer by default.
Since you can't use an instance of ModelAdmin as a container, developers are looking for other options.
Most of the workarounds I've found are to store the information in a global dictionary and map it to the locals of current thread. Yes, that works, but shouldn't there be another way than that?
If it were possible to use
ModelAdmin as a container for the data, Django itself could do it. Currently Django repeatedly calls certain
ModelAdmin methods to get the same result during the same request. All of these results should be cached. If
ModelAdmin behaved like Django-GCBV, we could save valuable computation time.
The table shows how often the following
ModelAdmin methods are called several times per request:
|changelist GET||change GET||change POST||add GET||add POST||delete|
The number of calls to some methods depends on the number of
Let's compare the response time of the "vanilla" Django admin panel by default and the Django admin panel with cached method of ModelAdmin. As a code example I took the project from my last article:
I have created a middleware that outputs the time elapsed between the request and the response:
If the middleware code is executed, we will get the result:
|default django admin panel||modified admin panel|
|average time: 0.031673s||average time: 0.026001s|
As you can see, even in this simple project the speed increase, due to caching the results of calculating functions, is **almost 20%!
This speed can be greatly increased by refactoring the entire
ModelAdmin class, since it is currently a bloated piece of spagetty-code.
Let's now take a look at where
ModelAdmin singletons come from in Django and try to fix it.
Each instance of
ModelAdmin is created when it is registered by instance of
AdminSite from django.contrib.admin.sites.
In the docstring of the
AdminSite class, we learn that the
get_urls method is used to retrieve views from each registered instance of ModelAdmin. Thanks to the author of this docstring.
AdminSite.get_urls we can find this code snipped:
Here we see that the
ModelAdmin.urls property is called.
This property returns the result of the
ModelAdmin.get_urls method. So, let's explore what this method
At first glance it seems like a lot of code, but let's unpack it piece by piece. Let's start exploring this code from the bottom.
The returned list is created for url-dispatcher. It contains tuples from the url, the view to be called, and the name of the view.
What matters to us is, what is given as a view. This is a wrapped bounded method of a ModelAdmin instance. So, let's take a look at what this wrapper
Fortunately, this wrapper
wrap is defined right above.
update_wrapper at the end of
wrap is not very important, it just makes the final view behave like a ModelAdmin instance.
The body of the
wrapper function is crucial for our mission. There we see a call of
admin_site.admin_view with bounded method of ModelAdmin instance as an argument.
admin_site is the instance of
AdminSite on which the ModelAdmin instance is registered.
This method we need to override to rid Django ModelAdmin of the curse of the singleton pattern.
Pffew, all these explanations are for one simple function that needs to be overridden. Yes, because without knowing how this whole system works, we wouldn't understand how to properly override this function to solve our problem and not break anything.
Let's create a child class of
AdminSite and override the
admin_view method like this:
Let's ignore all the wrappers and focus on these lines of code:
new_instance = type(instance)(instance.model, instance.admin_site) return func.__func__(new_instance, *args, **kwargs)
func is a bounded method of our singleton instance of ModelAdmin, which is always the same for every query.
__func__ attribute of func is a method of the
ModelAdmin class. It is not more associated with any instance of
Here I am using python terminology bound and unbound method.
func.__func__is not bound to an instance of
func.__self__, but it is also not a static function, it is a class method. An instance of the same class as
func.__self__is still required as the first argument to call this method.
To create a new instance of
ModelAdmin we need model and admin_site as arguments, which can easily be taken from the old singleton-instance.
Now we can return 'func' call with new instance as self, and arguments *args and **kwargs, that are passed at the time of the call.
Now the Singleton architecture is successfully bypassed.
This means that every time a request is sent to the view a new ModelAdmin instance is created and it's corresponding view function is called
As mentioned earlier, it was possible to avoid all this agony from the beginning of Django.
All that remains is to make our child AdminSite as default in our project.
We can create a child of the
AdminConfig class like this:
default_site attribute must match your
AdminConfig in the settings.py in INSTALLED_APPS to complete the changes. Remember to remove the default admin panel.
This new admin panel in its current form is not faster than the old one. We still need to cache
ModelAdmin methods. To do this, you can create a Mixin for ModelAdmin, and wrap the above methods as follows:
I haven't had any problems with this caching implementation, but if you're worried about multiple calls to the same method with different arguments, you can wrap the methods with memoize wrapper. This can also reduce the speedboost of our solution.
I love the Django Framework. It's a powerful tool that helps me create great things. I can fix any problem of this framework with just a small piece of code in the right place. But finding that place is not always easy. And Django's documentation can't help. This lack of documentation - This is Django's biggest problem.
I hope that after the points I have laid out in this article, you will agree that there are serious problems with Django's implementation of the singleton architecture in the admin panel.
This pattern makes the admin panel experience cumbersome and slow, and something needs to be done to fix it. And I'm writing here how that can be done.
The fix I've proposed is an important first step to improving the Django admin panel, but unfortunately the next work begins after its implementation. The ModelAdmin class code needs to be greatly improved, i ask about it, for example, in this issue, but if it can be done, I'm sure the Django admin panel will be a better tool to work with.
I have spoken about this problem at many events. You can see my video from PyCon DE in Berlin in April 2022, I wrote here that I will be speaking at that conference.