django tenant: tenant specific template loader

I am trying to get tenants to have custom templates to be rendered. I am struggling to achieve this using django tenants. I have been following the documentations here: Tenant-aware file handling — django_tenants dev documentation. I have noticed that django-tenant does not come with a template loader like django-tenant-schemas.

Here is what my project tree roughly looks like:

myapp
├── customers
│   ├── __pycache__
│   ├── migrations
│   │   └── __pycache__
│   └── templates
│       ├── landpage
│       │   ├── contactform
│       │   │   └── img
│       │   ├── css
│       │   ├── fonts
│       │   └── js
│       └── registration
├── dashboard2
│   ├── __pycache__
│   ├── migrations
│   │   └── __pycache__
│   ├── migrations2
│   │   └── __pycache__
│   ├── templates
│   │   └── dashboard2
│   │       └── pdf
│   └── tenants
│       ├── itoiz
│       │   └── templates
│       └── test
│           └── templates
├── inventory4
│   └── __pycache__
├── locale
│   └── fr
│       └── LC_MESSAGES
├── logs
├── mediafiles
│   └── tenants
│       └── t5
│           └── api
│               ├── historical
│               └── preprocessing
├── static
│   ├── admin
│   │   ├── css
│   │   │   └── vendor
│   │   │       └── select2
│   │   ├── fonts
│   │   ├── img
│   │   │   └── gis
│   │   └── js
│   │       ├── admin
│   │       └── vendor
│   │           ├── jquery
│   │           ├── select2
│   │           │   └── i18n
│   │           └── xregexp
│   ├── css
│   ├── css2
│   ├── css3
│   ├── django_tables2
│   │   └── themes
│   │       └── paleblue
│   │           ├── css
│   │           └── img
│   ├── fonts
│   ├── fonts2
│   ├── icons-reference
│   │   └── fonts
│   ├── img
│   │   ├── flags
│   │   │   └── 16
│   │   └── photos
│   ├── js
│   ├── rest_framework
│   │   ├── css
│   │   ├── docs
│   │   │   ├── css
│   │   │   ├── img
│   │   │   └── js
│   │   ├── fonts
│   │   ├── img
│   │   └── js
│   └── vendor
│       ├── bootstrap
│       │   ├── css
│       │   └── js
│       ├── chart.js
│       ├── font-awesome
│       │   ├── css
│       │   └── fonts
│       ├── jquery
│       ├── jquery-validation
│       │   └── localization
│       ├── jquery.cookie
│       └── popper.js
│           ├── esm
│           └── umd
├── staticfiles
│   ├── admin
│   │   ├── css
│   │   │   └── vendor
│   │   │       └── select2
│   │   ├── fonts
│   │   ├── img
│   │   │   └── gis
│   │   └── js
│   │       ├── admin
│   │       └── vendor
│   │           ├── jquery
│   │           ├── select2
│   │           │   └── i18n
│   │           └── xregexp
│   ├── css
│   ├── css2
│   ├── css3
│   ├── django_tables2
│   │   └── themes
│   │       └── paleblue
│   │           ├── css
│   │           └── img
│   ├── fonts
│   ├── fonts2
│   ├── icons-reference
│   │   └── fonts
│   ├── img
│   │   └── flags
│   │       └── 16
│   ├── js
│   ├── rest_framework
│   │   ├── css
│   │   ├── docs
│   │   │   ├── css
│   │   │   ├── img
│   │   │   └── js
│   │   ├── fonts
│   │   ├── img
│   │   └── js
│   └── vendor
│       ├── bootstrap
│       │   ├── css
│       │   └── js
│       ├── chart.js
│       ├── font-awesome
│       │   ├── css
│       │   └── fonts
│       ├── jquery
│       ├── jquery-validation
│       │   └── localization
│       ├── jquery.cookie
│       └── popper.js
│           ├── esm
│           └── umd
├── tenantlogin
│   ├── __pycache__
│   ├── migrations
│   │   └── __pycache__
│   └── templates
│       └── registration
└── uploadfiles
    ├── __pycache__
    ├── migrations
    │   └── __pycache__
    └── templates

for each tenant, I want to override the templates in dashboard2/templates with the templates located in the tenant folder in dashboard2.

I have set up my settings.py file as such to handle the tenant file handling system:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR,"dashboard2/templates/dashboard2")],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'django.template.context_processors.i18n'
            ],
        },
    },
]


STATIC_URL = '/static/'


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

MULTITENANT_STATICFILES_DIRS = [
    os.path.join( "myapp/dashboard2/", "tenants/%s/static" ),
]

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'staticfiles')
]

STATIC_ROOT = os.path.join(BASE_DIR, 'static')
MULTITENANT_RELATIVE_STATIC_ROOT = "tenants/%s


MEDIA_URL = '/mediafiles/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')

TENANT_MODEL = "customers.Client"
TENANT_DOMAIN_MODEL = "customers.Domain"

DEFAULT_FILE_STORAGE = "django_tenants.files.storage.TenantFileSystemStorage"
MULTITENANT_RELATIVE_MEDIA_ROOT = "tenants/%s"

Unfortunately, this does not work, and I am not sure what to do to make it work. Any help would be appreciated.

File handling is different than template loading - they serve two different functions within a Django environment.

Fortunately, django_tenants does handle this - see: Configuring the template loaders

Thank you for your response Ken, I had skimmed over this part of the doc too quickly, that’s my bad!
So I modified my settings.py to reflect the doc:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        #'DIRS': [os.path.join(BASE_DIR, "dashboard/templates/dashboard", "dashboard2/templates/dashboard2")],
        'DIRS': [os.path.join(BASE_DIR,"dashboard2/templates/dashboard2")],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'django.template.context_processors.i18n'
            ],
            "loaders": [
                "django_tenants.template.loaders.filesystem.Loader",  # Must be first
                "django.template.loaders.filesystem.Loader",
                "django.template.loaders.app_directories.Loader",
            ],
        },
    },
]

MULTITENANT_TEMPLATE_DIRS = [
    os.path.join(BASE_DIR,"dashboard2/tenants/%s/templates"
]

It still giving the same output. In django log I found this:

Traceback (most recent call last):
  File "/home/ubuntu/exo/lib/python3.6/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/home/ubuntu/exo/lib/python3.6/site-packages/django/core/handlers/base.py", line 100, in _get_response
    resolver_match = resolver.resolve(request.path_info)
  File "/home/ubuntu/exo/lib/python3.6/site-packages/django/urls/resolvers.py", line 575, in resolve
    raise Resolver404({'tried': tried, 'path': new_path})
django.urls.exceptions.Resolver404: {'tried': [[<URLResolver <module 'django.conf.urls.i18n' from '/home/ubuntu/exo/lib/python3.6/site-packages/django/conf/urls/i18n.py'> (None:None) 'i18n/'>], [<URLPattern 'registerUser'>], [<URLPattern$

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/ubuntu/exo/lib/python3.6/site-packages/django/template/base.py", line 828, in _resolve_lookup
    current = current[bit]
TypeError: 'URLResolver' object is not subscriptable

To be honest with you, I am not too sure I understand what is being faulty there

I’m assuming it’s a copy/paste error, but you don’t have the close-parens in the os.path.join function call.

Also, is this the complete stackdump from this error? I would have expected there to be a bit above this.

Making some guesses based upon the methods being shown in this traceback, I’d start my search with the template being rendered - particularly any url references. But without more details, I’m not sure I have any more useful information to provide.

Hey Kevin, thanks for your help, I was not able to add more log due to memory shortage on my machine! Thanks for providing the link to the doc, it has been helpful, at this point, I no longer have an error message thrown off, however it does not seem that the templates are being searched in the tenant specific folder, they are still rendered from the templates folder of the app. I am a bit confused about I could possibly try at this point!

here is what my template related settings look like:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        #'DIRS': [os.path.join(BASE_DIR, "dashboard/templates/dashboard", "dashboard2/templates/dashboard2")],
        #'DIRS': [os.path.join(BASE_DIR, "dashboard/templates/dashboard"), os.path.join(BASE_DIR,"dashboard2/templates/dashboard2")],
        'DIRS': os.path.join(BASE_DIR,"dashboard2/templates/dashboard2"),
        #'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'django.template.context_processors.i18n'
            ],
            "loaders": [
              "django_tenants.template.loaders.filesystem.Loader",  # Must be first
               "django.template.loaders.filesystem.Loader",
               "django.template.loaders.app_directories.Loader",
            ],
        },
    },
]

MULTITENANT_TEMPLATE_DIRS = [
    os.path.join(BASE_DIR,"dashboard2/tenants/%s/templates")
]

What could be possibly wrong with the way templates are rendered? I am not sure how to define the urls in the views in order to be tenant specific. should it be a unique name?

Please provide one specific example of a template you’re trying to load, and how it’s specified in your code. Also, please provide the full path for that specific template for both the generic version and the tenant version. It’ll be helpful at this point to talk specifics rather than generalities.

What I’m noticing is the difference in the pattern between these two samples. My immediate inclination is that there’s some issue with the pathing between the two relative to the tenant version vs the generic version.

Sorry Ken, I struggled with the general concept so it was difficult to dive into the specifics of why the issue was occurring and what could have been specific causes of the issues.

Regardless your intuition was correct, modifying ‘DIRS’ path to match the ‘MULTITENANT_TEMPLATE_DIRS’ path was the key.
In addition, the templates names that are inside of the tenants templates folders must differ from the ones located in the regular template folder (this could be helpful to someone stumbling upon this point) otherwise the general one will be rendered.

Sorry again and thank you so much!

This isn’t how this should work - if you’re finding yourself needing to do this, then there’s still something not right.

Quoting from the docs page:

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.