Tenant-aware file handling

The default Django behaviour is for all tenants to share one set of templates and static files between them. This can be changed so that each tenant will have its own:

  • Static files (like cascading stylesheets and JavaScript)

  • Location for files uploaded by users (usually stored in a /media directory)

  • Django templates

The process for making Django’s file handling tenant-aware generally consists of the following steps:

  1. Using a custom tenant-aware finder for locating files

  2. Specifying where finders should look for files

  3. Using a custom tenant-aware file storage handler for collecting and managing those files

  4. Using a custom tenant-aware loader for finding and loading Django templates

We’ll cover the configuration steps for each in turn.

Project layout

This configuration guide assumes the following Django project layout (loosely based on django-cookiecutter):

absolute/path/to/your_project_dir
    ...
    static              # System-wide static files
    templates           # System-wide templates
    # Tenant-specific files below will override pre-existing system-wide files with same name.
    tenants
        tenant_1        # Static files / templates for tenant_1
            templates
            static
        tenant_2        # Static files / templates for tenant_2
            templates
            static
    media               # Created automatically when users upload files
        tenant_1
        tenant_2
staticfiles             # Created automatically when collectstatic_schemas is run
    tenant_1
    tenant_2
    ...

The configuration details may differ depending on your specific requirements for your chosen layout. Fortunately, django-tenants makes it easy to cater for a wide range of project layouts as illustrated below.

Configuring the static file finders

Start by inserting django-tenants’ django_tenants.staticfiles.finders.TenantFileSystemFinder at the top of the list of available STATICFILES_FINDERS in your Django configuration file:

# in settings.py

STATICFILES_FINDERS = [
    "django_tenants.staticfiles.finders.TenantFileSystemFinder",  # Must be first
    "django.contrib.staticfiles.finders.FileSystemFinder",
    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
    "compressor.finders.CompressorFinder",
]

# or this way

STATICFILES_FINDERS.insert(0, "django_tenants.staticfiles.finders.TenantFileSystemFinder")

By adding TenantFileSystemFinder at the top, we ensure that Django will look for the tenant-specific files first, before reverting to the standard search path. This makes it possible for tenants to override any static files (e.g. stylesheets or javascript files) that are specific to that tenant, and use the standard static files for the rest.

Next, add MULTITENANT_STATICFILES_DIRS to the configuration file in order to let TenantFileSystemFinder know where to look for tenant-specific static files:

# in settings.py

MULTITENANT_STATICFILES_DIRS = [
    os.path.join( "absolute/path/to/your_project_dir", "tenants/%s/static" ),
]

For the path provided above, %s will be replaced with the current tenant’s schema_name during runtime (see Specifying a different target directory for details).

Configuring the static files storage

By default, Django uses StaticFilesStorage for collecting static files into a dedicated folder on the server when the collectstatic management command is run. The location that the files are written to is specified in the STATIC_ROOT setting (usually configured to point to ‘staticfiles’).

django-tenants provides a replacement tenant-aware TenantStaticFilesStorage than can be configured by setting:

# in settings.py

STATICFILES_STORAGE = "django_tenants.staticfiles.storage.TenantStaticFilesStorage"

MULTITENANT_RELATIVE_STATIC_ROOT = ""  # (default: create sub-directory for each tenant)

The path specified in MULTITENANT_RELATIVE_STATIC_ROOT tells TenantStaticFilesStorage where in STATIC_ROOT the tenant’s files should be saved. The default behaviour is to just create a sub-directory for each tenant in STATIC_ROOT.

The command to collect the static files for all tenants is collectstatic_schemas. The optional --schema argument can be used to only collect files for a single tenant.

./manage.py collectstatic_schemas --schema=your_tenant_schema_name

Note

If you have configured an HTTP server, like nginx, to serve static files instead of the Django built-in server, then you also need to set REWRITE_STATIC_URLS = True. This tells django-tenants to rewrite STATIC_URL to include MULTITENANT_RELATIVE_STATIC_ROOT when static files are requested so that these files can be found and served directly by the external HTTP server.

Configuring media file storage

The default Django behavior is to store all files that are uploaded by users in one folder. The path for this upload folder can be configured via the standard MEDIA_ROOT setting.

The above behaviour can be changed for multi-tenant setups so that each tenant will have a dedicated sub-directory for storing user-uploaded files. To do this simply change DEFAULT_FILE_STORAGE so that TenantFileSystemStorage replaces the standard FileSystemStorage handler:

# in settings.py

DEFAULT_FILE_STORAGE = "django_tenants.files.storage.TenantFileSystemStorage"

MULTITENANT_RELATIVE_MEDIA_ROOT = ""  # (default: create sub-directory for each tenant)

The path specified in MULTITENANT_RELATIVE_MEDIA_ROOT tells TenantFileSystemStorage where in MEDIA_ROOT the tenant’s files should be saved. The default behaviour is to just create a sub-directory for each tenant in MEDIA_ROOT.

Configuring the template loaders

django-tenants provides a tenant-aware template loader that uses the current tenant’s schema_name when looking for templates.

It can be configured by inserting the custom Loader at the top of the list in the TEMPLATES setting, and specifying the template search path to be used in the MULTITENANT_TEMPLATE_DIRS setting, as illustrated below:

TEMPLATES = [
    {
        ...
        "DIRS": ["absolute/path/to/your_project_dir/templates"],  # -> Dirs used by the standard template loader
        "OPTIONS": {
            ...
            "loaders": [
                "django_tenants.template.loaders.filesystem.Loader",  # Must be first
                "django.template.loaders.filesystem.Loader",
                "django.template.loaders.app_directories.Loader",
            ],
            ...
        ...
    }
]

MULTITENANT_TEMPLATE_DIRS = [
    "absolute/path/to/your_project_dir/tenants/%s/templates"
]

Notice that django_tenants.template.loaders.filesystem.Loader is added at the top of the list. This will cause Django to look for the tenant-specific templates first, before reverting to the standard search path. This makes it possible for tenants to override individual templates as required.

Just like with standard Django, the first template found will be returned.

Attention

If the template contains any include tags, then all of the included templates need to be located in the tenant’s template folder as well. It is not currently possible to include templates from sources outside of the tenant’s template folder.

Specifying a different target directory

django-tenants supports simple Python string formatting for configuring the various path strings used throughout the configuration steps. any occurances of %s in the path string will be replaced with the current tenant’s schema_name during runtime.

This makes it possible to cater for more elaborate folder structures. Some examples are provided below:

# in settings.py

STATIC_ROOT = "absolute/path/to/your_project_dir/staticfiles"

MULTITENANT_RELATIVE_STATIC_ROOT = "tenants/%s

Static files will be collected into -> absolute/path/to/your_project_dir/staticfiles/tenants/schema_name.

…and for media files:

# in settings.py

MEDIA_ROOT = "absolute/path/to/your_project_dir/apps_dir/media/"

MULTITENANT_RELATIVE_MEDIA_ROOT = "%s/other_dir"

Media files will be uploaded at -> absolute/path/to/your_project_dir/apps_dir/media/schema_name/other_dir