Short introduction into migrating default Django admin to Unfold in already production running applications. The post will describe how to get the default Django admin running side by side with the Unfold admin.
When you are deciding how to migrate an existing Django project that runs on a production instance to Unfold, this is an article for you. The main goal that we want to achieve is to have the default Django admin running side-by-side with the new Unfold admin. Users are going to be still able to use the default admin where at the same time access to the new Unfold admin will be available.
The first thing that is required is to add the Unfold app to INSTALLED_APPS in settings.py. Here are a few notes about it.
The Unfold app has to be before the default admin coming from django.contrib.admin. This is because Unfold templates are going to be preferred before templates from the default admin.
In the official installation guide, just "unfold" is added but now we have to add unfold.apps.BasicAppConfig because we are loading Unfold with a special application config saying that we are not overriding default admin.
# settings.py
INSTALLED_APPS = [
"unfold.apps.BasicAppConfig", # <- Custom app config, not overriding default admin
# some other apps
"django.contrib.admin",
"your_app",
]
When the Unfold is installed we need to create a new admin site inheriting from unfold.sites.UnfoldAdminSite which is going to be used in urlpatterns in urls.py and for registering models for the models in Unfold.
# sites.py
from unfold.sites import UnfoldAdminSite
class NewUnfoldAdminSite(UnfoldAdminSite):
pass
# You can route to new admin by "original-name-here-not-admin:index"
new_admin_site = NewUnfoldAdminSite(name="original-name-here-not-admin")
# urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from your_app.sites import new_admin_site
urlpatterns = [
path("admin/", admin.site.urls), # <-- Your default admin
path("unfold-admin/", new_admin_site.urls), # <-- Unfold admin
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
After this change, two URLs are available, one for default admin, and the other one for Unfold admin. You can visit both URLs, to see if they are loading but are not going to work properly. Default admin will load templates from the Unfold application which we have to adjust in the next steps.
Below you can see a short example of how to register your custom model to the new Unfold admin. The important part is site attribute when using @admin.register decorator.
# admin.py
from django.contrib import admin
from unfold.admin import ModelAdmin
from your_app.models import YourModel
from your_app.sites import new_admin_site
@admin.register(YourModel, site=new_admin_site)
class UnfoldYourModelAdmin(ModelAdmin):
pass
Both admins can work side-by-side because the system is going conditionally to load a different template file based on which admin is used. In general, when non Unfold theme is used, the template loader will remove all templates containing the "unfold" directory in the path.
# loaders.py
from django.core.validators import EMPTY_VALUES
from django.template.loaders.filesystem import Loader as FilesystemLoader
from django.template.utils import get_app_template_dirs
from django.urls import Resolver404, resolve
from your_app.middleware import _thread_data
class UnfoldAdminLoader(FilesystemLoader):
def _has_unfold_dir(self, template_dir):
request = getattr(_thread_data, "request", None)
if not request or request.path in EMPTY_VALUES:
return False
try:
if "admin" in resolve(request.path).namespaces:
for dir in template_dir.iterdir():
if dir.name == "unfold":
return True
except Resolver404:
pass
return False
def get_dirs(self):
template_dirs = []
for template_dir in get_app_template_dirs("templates"):
if not self._has_unfold_dir(template_dir):
template_dirs.append(template_dir)
return template_dirs
Once the template loader is implemented, we need to adjust TEMPLATES in settings.py to use this new template loader. In the code example below you can see a loaders section with the new UnfoldAdminLoader at the beginning. When the loaders are defined APP_DIRS will not work so we can comment that line of code out.
# settings.py
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
# Your paths
],
# "APP_DIRS": True, # <- This will not work with custom template loader
"OPTIONS": {
"loaders": [
"your_app.loaders.UnfoldAdminLoader", # <- New template loader
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
],
"context_processors": [
# Your context processors
],
},
}
]
After adding a new template loader, we can visit both admins. They are still going to load Unfold templates because the template loader is missing an important request object. By default, Django does not have a clean way of accessing request object in template loader so the threads are going to be used to store this variable to be later accessed in template loaders.
# middleware.py
from threading import local
_thread_data = local()
class CurrentRequestMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
_thread_data.request = request
response = self.get_response(request)
return response
Now it is time to update the MIDDLEWARE config with the new middleware. You can add a new middleware at the very end as the order should not have any impact on the other functionality.
# settings.py
MIDDLEWARE = [
# Rest of your middlewares
"formula.middleware.CurrentRequestMiddleware",
]
With the new middleware, the Unfold admin and the default admin should work as expected. By visiting admin URLs specified in urls.py, you will be able to see a different theme for each admin.
NOTE: After successful migration it is highly recommended to get rid of the new template loader and middleware because they can lead to unexpected side-effects.
NOTE: Template caching could have an impact on the custom template loader. In case of problems, feel free to create a new detailed issue on GitHub where it would be described how to replicate such an issue.