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:
Using a custom tenant-aware
finder
for locating filesSpecifying where
finders
should look for filesUsing a custom tenant-aware file
storage
handler for collecting and managing those filesUsing 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