Migrating existing Django admin to Unfold

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.

  • calendar_month 11.5.2024
  • pace 20 minutes

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.

Quick overview of steps

  1. Registering new Unfold admin site in INSTALLED_APPS and urls.py
  2. Changing TEMPLATES by new template loader to distinguish between Unfold and default admin
  3. Adding a new record to MIDDLEWARE to send additional info to the template loader

Installing Unfold

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

New template loader for Unfold

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
            ],
        },
    }
]

Additional "request" variable for template loader

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.

Let's work together

Did you decide to start using Unfold in your application but you need help with integration? Feel free to get in touch for consulting or development services.

  • verified Save time and focus on what matters: building product
  • verified Get instant access to the know-how pool and experience
  • verified Supercharge your development processes and quality of application
  • verified Incorporate custom Django applications into Unfold

* The cost of service starts from €375 EUR.

© 2024 Created by unfoldadmin.com. All rights reserved.