Files
edx-platform/openedx/core/djangoapps/theming/helpers.py
2016-09-01 14:57:52 +05:00

466 lines
14 KiB
Python

"""
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"<Theme: {name} at '{path}'>".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',
]