diff --git a/tenant_schemas/template/__init__.py b/tenant_schemas/template/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tenant_schemas/template/loaders/__init__.py b/tenant_schemas/template/loaders/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tenant_schemas/template/loaders/cached.py b/tenant_schemas/template/loaders/cached.py new file mode 100644 index 00000000..4c1e299b --- /dev/null +++ b/tenant_schemas/template/loaders/cached.py @@ -0,0 +1,37 @@ +""" +Wrapper class that takes a list of template loaders as an argument and attempts +to load templates from them in order, caching the result, in a multi-tenant setting. +""" + +from django.db import connection + +from django.template.loaders.cached import Loader as BaseLoader + + +class Loader(BaseLoader): + + def cache_key(self, template_name, skip=None): + """ + Generate a cache key for the template name, dirs, and skip. + If skip is provided, only origins that match template_name are included + in the cache key. This ensures each template is only parsed and cached + once if contained in different extend chains like: + x -> a -> a + y -> a -> a + z -> a -> a + """ + dirs_prefix = '' + skip_prefix = '' + tenant_prefix = '' + + if skip: + matching = [ + origin.name for origin in skip if origin.template_name == template_name] + + if matching: + skip_prefix = self.generate_hash(matching) + + if connection.tenant: + tenant_prefix = str(connection.tenant.pk) + + return '-'.join(s for s in (str(template_name), tenant_prefix, skip_prefix, dirs_prefix) if s) diff --git a/tenant_schemas/template/loaders/filesystem.py b/tenant_schemas/template/loaders/filesystem.py new file mode 100644 index 00000000..a5ce5a84 --- /dev/null +++ b/tenant_schemas/template/loaders/filesystem.py @@ -0,0 +1,48 @@ +""" +Wrapper for loading templates from the filesystem in a multi-tenant setting. +""" + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.db import connection +from django.template.loaders.filesystem import Loader as BaseLoader + +from tenant_schemas import utils + + +class Loader(BaseLoader): + def __init__(self, engine, dirs=None): + self._dirs = {} + + super().__init__(engine) + + if dirs is not None: + self.dirs = dirs + + @property + def dirs(self): + """ + Lazy retrieval of list of template directories based on current tenant schema. + :return: The list of template file dirs that have been configured for this tenant. + """ + if self._dirs.get(connection.schema_name, None) is None: + try: + # Use directories configured via MULTITENANT_TEMPLATE_DIRS + dirs = [ + utils.parse_tenant_config_path(dir_) + for dir_ in settings.MULTITENANT_TEMPLATE_DIRS + ] + except AttributeError: + raise ImproperlyConfigured( + "To use {}.{} you must define the MULTITENANT_TEMPLATE_DIRS setting.".format( + __name__, Loader.__name__ + ) + ) + + self.dirs = dirs + + return self._dirs[connection.schema_name] + + @dirs.setter + def dirs(self, value): + self._dirs[connection.schema_name] = value diff --git a/tenant_schemas/utils.py b/tenant_schemas/utils.py index 338ebc6a..71611486 100644 --- a/tenant_schemas/utils.py +++ b/tenant_schemas/utils.py @@ -116,3 +116,21 @@ def app_labels(apps_list): if AppConfig is None: return [app.split('.')[-1] for app in apps_list] return [AppConfig.create(app).label for app in apps_list] + + +def parse_tenant_config_path(config_path): + """ + Convenience function for parsing django-tenants' path configuration strings. + If the string contains '%s', then the current tenant's schema name will be inserted at that location. Otherwise + the schema name will be appended to the end of the string. + :param config_path: A configuration path string that optionally contains '%s' to indicate where the tenant + schema name should be inserted. + :return: The formatted string containing the schema name + """ + try: + # Insert schema name + print("schema -> %s" % connection.schema_name) + return config_path % connection.schema_name + except (TypeError, ValueError): + # No %s in string; append schema name at the end + return os.path.join(config_path, connection.schema_name)