""" Set up lookup paths for mako templates. """ import contextlib import hashlib import os import pkg_resources from django.conf import settings from mako.exceptions import TopLevelLookupException from mako.lookup import TemplateLookup from request_cache.middleware import request_cached from openedx.core.djangoapps.theming.helpers import get_template as themed_template from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme, strip_site_theme_templates_path from . import LOOKUP class DynamicTemplateLookup(TemplateLookup): """ A specialization of the standard mako `TemplateLookup` class which allows for adding directories progressively. """ def __init__(self, *args, **kwargs): super(DynamicTemplateLookup, self).__init__(*args, **kwargs) self.__original_module_directory = self.template_args['module_directory'] def __repr__(self): return "<{0.__class__.__name__} {0.directories}>".format(self) def add_directory(self, directory, prepend=False): """ Add a new directory to the template lookup path. """ if prepend: self.directories.insert(0, os.path.normpath(directory)) else: self.directories.append(os.path.normpath(directory)) # Since the lookup path has changed, the compiled modules might be # wrong because now "foo.html" might be a completely different template, # and "foo.html.py" in the module directory has no way to know that. # Update the module_directory argument to point to a directory # specifically for this lookup path. unique = hashlib.md5(":".join(str(d) for d in self.directories)).hexdigest() self.template_args['module_directory'] = os.path.join(self.__original_module_directory, unique) # Also clear the internal caches. Ick. self._collection.clear() self._uri_cache.clear() def get_template(self, uri): """ Overridden method for locating a template in either the database or the site theme. If not found, template lookup will be done in comprehensive theme for current site by prefixing path to theme. e.g if uri is `main.html` then new uri would be something like this `/red-theme/lms/static/main.html` If still unable to find a template, it will fallback to the default template directories after stripping off the prefix path to theme. """ # try to get template for the given file from microsite template = themed_template(uri) # if microsite template is not present or request is not in microsite then # let mako find and serve a template if not template: try: # Try to find themed template, i.e. see if current theme overrides the template template = super(DynamicTemplateLookup, self).get_template(get_template_path_with_theme(uri)) except TopLevelLookupException: # strip off the prefix path to theme and look in default template dirs template = super(DynamicTemplateLookup, self).get_template(strip_site_theme_templates_path(uri)) return template def clear_lookups(namespace): """ Remove mako template lookups for the given namespace. """ if namespace in LOOKUP: del LOOKUP[namespace] def add_lookup(namespace, directory, package=None, prepend=False): """ Adds a new mako template lookup directory to the given namespace. If `package` is specified, `pkg_resources` is used to look up the directory inside the given package. Otherwise `directory` is assumed to be a path in the filesystem. """ templates = LOOKUP.get(namespace) if not templates: LOOKUP[namespace] = templates = DynamicTemplateLookup( module_directory=settings.MAKO_MODULE_DIR, output_encoding='utf-8', input_encoding='utf-8', default_filters=['decode.utf8'], encoding_errors='replace', ) if package: directory = pkg_resources.resource_filename(package, directory) templates.add_directory(directory, prepend=prepend) @request_cached def lookup_template(namespace, name): """ Look up a Mako template by namespace and name. """ return LOOKUP[namespace].get_template(name) @contextlib.contextmanager def save_lookups(): """ A context manager to save and restore the Mako template lookup path. Useful for testing. """ # Make a copy of the list of directories for each namespace. namespace_dirs = {namespace: list(look.directories) for namespace, look in LOOKUP.items()} try: yield finally: # Get rid of all the lookups. LOOKUP.clear() # Re-create the lookups from our saved list. for namespace, directories in namespace_dirs.items(): for directory in directories: add_lookup(namespace, directory)