""" Helpers for accessing comprehensive theming related variables. """ import re import os from path import Path from django.conf import settings, ImproperlyConfigured from django.contrib.staticfiles.storage import staticfiles_storage from request_cache.middleware import RequestCache from microsite_configuration import microsite from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from logging import getLogger logger = getLogger(__name__) # pylint: disable=invalid-name def get_template_path(relative_path, **kwargs): """ This is a proxy function to hide microsite_configuration behind comprehensive theming. """ # We need to give priority to theming over microsites # So, we apply microsite override only if there is no associated site theme # and associated microsite is present. if not current_request_has_associated_site_theme() and microsite.is_request_in_microsite(): relative_path = microsite.get_template_path(relative_path, **kwargs) return relative_path def is_request_in_themed_site(): """ This is a proxy function to hide microsite_configuration behind comprehensive theming. """ # We need to give priority to theming/site-configuration over microsites return configuration_helpers.is_site_configuration_enabled() or microsite.is_request_in_microsite() def get_template(uri): """ This is a proxy function to hide microsite_configuration behind comprehensive theming. :param uri: uri of the template """ # We need to give priority to theming over microsites # So, we apply microsite template override only when there is no associated theme, if not current_request_has_associated_site_theme(): return microsite.get_template(uri) def get_template_path_with_theme(relative_path): """ Returns template path in current site's theme if it finds one there otherwise returns same path. Example: >> get_template_path_with_theme('header.html') '/red-theme/lms/templates/header.html' Parameters: relative_path (str): template's path relative to the templates directory e.g. 'footer.html' Returns: (str): template path in current site's theme """ relative_path = os.path.normpath(relative_path) theme = get_current_theme() if not theme: return relative_path # strip `/` if present at the start of relative_path template_name = re.sub(r'^/+', '', relative_path) template_path = theme.template_path / template_name absolute_path = theme.path / "templates" / template_name if absolute_path.exists(): return str(template_path) else: return relative_path def get_all_theme_template_dirs(): """ Returns template directories for all the themes. Example: >> get_all_theme_template_dirs() [ '/edx/app/edxapp/edx-platform/themes/red-theme/lms/templates/', ] Returns: (list): list of directories containing theme templates. """ themes = get_themes() template_paths = list() for theme in themes: template_paths.extend(theme.template_dirs) return template_paths def strip_site_theme_templates_path(uri): """ Remove site template theme path from the uri. Example: >> strip_site_theme_templates_path('/red-theme/lms/templates/header.html') 'header.html' Arguments: uri (str): template path from which to remove site theme path. e.g. '/red-theme/lms/templates/header.html' Returns: (str): template path with site theme path removed. """ theme = get_current_theme() if not theme: return uri templates_path = "/".join([ theme.theme_dir_name, get_project_root_name(), "templates" ]) uri = re.sub(r'^/*' + templates_path + '/*', '', uri) return uri def get_current_request(): """ Return current request instance. Returns: (HttpRequest): returns current request """ return RequestCache.get_current_request() def get_current_site(): """ Return current site. Returns: (django.contrib.sites.models.Site): returns current site """ request = get_current_request() if not request: return None return getattr(request, 'site', None) def get_current_site_theme(): """ Return current site theme object. Returns None if theming is disabled. Returns: (ecommerce.theming.models.SiteTheme): site theme object for the current site. """ # Return None if theming is disabled if not is_comprehensive_theming_enabled(): return None request = get_current_request() if not request: return None return getattr(request, 'site_theme', None) def get_current_theme(): """ Return current theme object. Returns None if theming is disabled. Returns: (ecommerce.theming.models.SiteTheme): site theme object for the current site. """ # Return None if theming is disabled if not is_comprehensive_theming_enabled(): return None site_theme = get_current_site_theme() if not site_theme: return None try: return Theme( name=site_theme.theme_dir_name, theme_dir_name=site_theme.theme_dir_name, themes_base_dir=get_theme_base_dir(site_theme.theme_dir_name), ) except ValueError as error: # Log exception message and return None, so that open source theme is used instead logger.exception('Theme not found in any of the themes dirs. [%s]', error) return None def current_request_has_associated_site_theme(): """ True if current request has an associated SiteTheme, False otherwise. Returns: True if current request has an associated SiteTheme, False otherwise """ request = get_current_request() site_theme = getattr(request, 'site_theme', None) return bool(site_theme and site_theme.id) def get_theme_base_dir(theme_dir_name, suppress_error=False): """ Returns absolute path to the directory that contains the given theme. Args: theme_dir_name (str): theme directory name to get base path for suppress_error (bool): if True function will return None if theme is not found instead of raising an error Returns: (str): Base directory that contains the given theme """ for themes_dir in get_theme_base_dirs(): if theme_dir_name in get_theme_dirs(themes_dir): return themes_dir if suppress_error: return None raise ValueError( "Theme '{theme}' not found in any of the following themes dirs, \nTheme dirs: \n{dir}".format( theme=theme_dir_name, dir=get_theme_base_dirs(), )) def get_project_root_name(): """ Return root name for the current project Example: >> get_project_root_name() 'lms' # from studio >> get_project_root_name() 'cms' Returns: (str): component name of platform e.g lms, cms """ root = Path(settings.PROJECT_ROOT) if root.name == "": root = root.parent return root.name def get_theme_base_dirs(): """ Return base directory that contains all the themes. Raises: ImproperlyConfigured - exception is raised if 1 - COMPREHENSIVE_THEME_DIRS is not a list 1 - theme dir path is not a string 2 - theme dir path is not an absolute path 3 - path specified in COMPREHENSIVE_THEME_DIRS does not exist Example: >> get_theme_base_dirs() ['/edx/app/ecommerce/ecommerce/themes'] Returns: (Path): Base theme directory path """ # Return an empty list if theming is disabled if not is_comprehensive_theming_enabled(): return [] theme_base_dirs = [] # Legacy code for COMPREHENSIVE_THEME_DIR backward compatibility if hasattr(settings, "COMPREHENSIVE_THEME_DIR"): theme_dir = settings.COMPREHENSIVE_THEME_DIR if not isinstance(theme_dir, basestring): raise ImproperlyConfigured("COMPREHENSIVE_THEME_DIR must be a string.") if not theme_dir.startswith("/"): raise ImproperlyConfigured("COMPREHENSIVE_THEME_DIR must be an absolute paths to themes dir.") if not os.path.isdir(theme_dir): raise ImproperlyConfigured("COMPREHENSIVE_THEME_DIR must be a valid path.") theme_base_dirs.append(Path(theme_dir)) if hasattr(settings, "COMPREHENSIVE_THEME_DIRS"): theme_dirs = settings.COMPREHENSIVE_THEME_DIRS if not isinstance(theme_dirs, list): raise ImproperlyConfigured("COMPREHENSIVE_THEME_DIRS must be a list.") if not all([isinstance(theme_dir, basestring) for theme_dir in theme_dirs]): raise ImproperlyConfigured("COMPREHENSIVE_THEME_DIRS must contain only strings.") if not all([theme_dir.startswith("/") for theme_dir in theme_dirs]): raise ImproperlyConfigured("COMPREHENSIVE_THEME_DIRS must contain only absolute paths to themes dirs.") if not all([os.path.isdir(theme_dir) for theme_dir in theme_dirs]): raise ImproperlyConfigured("COMPREHENSIVE_THEME_DIRS must contain valid paths.") theme_base_dirs.extend([Path(theme_dir) for theme_dir in theme_dirs]) return theme_base_dirs def is_comprehensive_theming_enabled(): """ Returns boolean indicating whether comprehensive theming functionality is enabled or disabled. Example: >> is_comprehensive_theming_enabled() True Returns: (bool): True if comprehensive theming is enabled else False """ # We need to give priority to theming over microsites if settings.ENABLE_COMPREHENSIVE_THEMING and current_request_has_associated_site_theme(): return True # Disable theming for microsites # Microsite configurations take priority over the default site theme. if microsite.is_request_in_microsite(): return False return settings.ENABLE_COMPREHENSIVE_THEMING def get_static_file_url(asset): """ Returns url of the themed asset if asset is not themed than returns the default asset url. Example: >> get_static_file_url('css/lms-main-v1.css') '/static/red-theme/css/lms-main-v1.css' Parameters: asset (str): asset's path relative to the static files directory Returns: (str): static asset's url """ return staticfiles_storage.url(asset) def get_themes(themes_dir=None): """ get a list of all themes known to the system. Args: themes_dir (str): (Optional) Path to themes base directory Returns: list of themes known to the system. """ if not is_comprehensive_theming_enabled(): return [] themes_dirs = [Path(themes_dir)] if themes_dir else get_theme_base_dirs() # pick only directories and discard files in themes directory themes = [] for themes_dir in themes_dirs: themes.extend([Theme(name, name, themes_dir) for name in get_theme_dirs(themes_dir)]) return themes def get_theme_dirs(themes_dir=None): """ Returns theme dirs in given dirs Args: themes_dir (Path): base dir that contains themes. """ return [_dir for _dir in os.listdir(themes_dir) if is_theme_dir(themes_dir / _dir)] def is_theme_dir(_dir): """ Returns true if given dir contains theme overrides. A theme dir must have subdirectory 'lms' or 'cms' or both. Args: _dir: directory path to check for a theme Returns: Returns true if given dir is a theme directory. """ theme_sub_directories = {'lms', 'cms'} return bool(os.path.isdir(_dir) and theme_sub_directories.intersection(os.listdir(_dir))) class Theme(object): """ class to encapsulate theme related information. """ name = '' theme_dir_name = '' themes_base_dir = None def __init__(self, name='', theme_dir_name='', themes_base_dir=None): """ init method for Theme Args: name: name if the theme theme_dir_name: directory name of the theme themes_base_dir: directory path of the folder that contains the theme """ self.name = name self.theme_dir_name = theme_dir_name self.themes_base_dir = themes_base_dir def __eq__(self, other): """ Returns True if given theme is same as the self Args: other: Theme object to compare with self Returns: (bool) True if two themes are the same else False """ return (self.theme_dir_name, self.path) == (other.theme_dir_name, other.path) def __hash__(self): return hash((self.theme_dir_name, self.path)) def __unicode__(self): return u"".format(name=self.name, path=self.path) def __repr__(self): return self.__unicode__() @property def path(self): """ Get absolute path of the directory that contains current theme's templates, static assets etc. Returns: Path: absolute path to current theme's contents """ return Path(self.themes_base_dir) / self.theme_dir_name / get_project_root_name() @property def template_path(self): """ Get absolute path of current theme's template directory. Returns: Path: absolute path to current theme's template directory """ return Path(self.theme_dir_name) / get_project_root_name() / 'templates' @property def template_dirs(self): """ Get a list of all template directories for current theme. Returns: list: list of all template directories for current theme. """ return [ self.path / 'templates', ]