Files
edx-platform/openedx/core/djangoapps/theming/helpers.py
Zia Fazal 954dae584a ziafazal/WL-328: Multi-Site Comprehensive Theming
ziafazal: improvements need for multi-tenancy
ziafazal: fixed broken tests
ziafazal: no need to add setting in test.py
ziafazal: added hostname validation
ziafazal: changes after feedback from mattdrayer
ziafazal: fixed branding and microsite broken tests
ziafazal: make STATICFILES_DIRS to list
ziafazal: added theme directory to mako lookup for tests
ziafazal: added more protection in test_util
saleem-latif: Enable SCSS Overrides for Comprehensive Theming
saleem-latif: Incoporate feedback changes, Correct test failures, add tests and enable theming for django templates
saleem-latif: Correct errors in python tests
mattdrayer: Fix invalid release reference
mattdrayer: Update django-wiki reference to latest release
2016-03-14 13:42:53 -04:00

337 lines
9.7 KiB
Python

"""
Helpers for accessing comprehensive theming related variables.
"""
import re
import os.path
from path import Path
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.contrib.staticfiles.storage import staticfiles_storage
from microsite_configuration import microsite
from microsite_configuration import page_title_breadcrumbs
def get_page_title_breadcrumbs(*args):
"""
This is a proxy function to hide microsite_configuration behind comprehensive theming.
"""
return page_title_breadcrumbs(*args)
def get_value(val_name, default=None, **kwargs):
"""
This is a proxy function to hide microsite_configuration behind comprehensive theming.
"""
return microsite.get_value(val_name, default=default, **kwargs)
def get_template_path(relative_path, **kwargs):
"""
This is a proxy function to hide microsite_configuration behind comprehensive theming.
"""
template_path = get_template_path_with_theme(relative_path)
if template_path == relative_path: # we don't have a theme now look into microsites
template_path = microsite.get_template_path(relative_path, **kwargs)
return template_path
def is_request_in_themed_site():
"""
This is a proxy function to hide microsite_configuration behind comprehensive theming.
"""
return 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
"""
return microsite.get_template(uri)
def get_themed_template_path(relative_path, default_path, **kwargs):
"""
This is a proxy function to hide microsite_configuration behind comprehensive theming.
The workflow considers the "Stanford theming" feature alongside of microsites. It returns
the path of the themed template (i.e. relative_path) if Stanford theming is enabled AND
microsite theming is disabled, otherwise it will return the path of either the microsite
override template or the base lms template.
:param relative_path: relative path of themed template
:param default_path: relative path of the microsite's or lms template to use if
theming is disabled or microsite is enabled
"""
is_stanford_theming_enabled = settings.FEATURES.get("USE_CUSTOM_THEME", False)
is_microsite = microsite.is_request_in_microsite()
if is_stanford_theming_enabled and not is_microsite:
return relative_path
return microsite.get_template_path(default_path, **kwargs)
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')
'/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
"""
site_theme_dir = get_current_site_theme_dir()
if not site_theme_dir:
return relative_path
base_theme_dir = get_base_theme_dir()
root_name = get_project_root_name()
template_path = "/".join([
base_theme_dir,
site_theme_dir,
root_name,
"templates"
])
# strip `/` if present at the start of relative_path
template_name = re.sub(r'^/+', '', relative_path)
search_path = os.path.join(template_path, template_name)
if os.path.isfile(search_path):
path = '/{site_theme_dir}/{root_name}/templates/{template_name}'.format(
site_theme_dir=site_theme_dir,
root_name=root_name,
template_name=template_name,
)
return path
else:
return relative_path
def get_current_theme_template_dirs():
"""
Returns template directories for the current theme.
Example:
>> get_current_theme_template_dirs('header.html')
['/edx/app/edxapp/edx-platform/themes/red-theme/lms/templates/', ]
Returns:
(list): list of directories containing theme templates.
"""
site_theme_dir = get_current_site_theme_dir()
if not site_theme_dir:
return None
base_theme_dir = get_base_theme_dir()
root_name = get_project_root_name()
template_path = "/".join([
base_theme_dir,
site_theme_dir,
root_name,
"templates"
])
return [template_path]
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.
"""
site_theme_dir = get_current_site_theme_dir()
if not site_theme_dir:
return uri
root_name = get_project_root_name()
templates_path = "/".join([
site_theme_dir,
root_name,
"templates"
])
uri = re.sub(r'^/*' + templates_path + '/*', '', uri)
return uri
def get_current_site_theme_dir():
"""
Return theme directory for the current site.
Example:
>> get_current_site_theme_dir()
'red-theme'
Returns:
(str): theme directory for current site
"""
from edxmako.middleware import REQUEST_CONTEXT
request = getattr(REQUEST_CONTEXT, 'request', None)
if not request:
return None
# if hostname is not valid
if not all((isinstance(request.get_host(), basestring), is_valid_hostname(request.get_host()))):
return None
try:
site = get_current_site(request)
except Site.DoesNotExist:
return None
site_theme_dir = cache.get(get_site_theme_cache_key(site))
# if site theme dir is not in cache and comprehensive theming is enabled then pull it from db.
if not site_theme_dir and is_comprehensive_theming_enabled():
site_theme = site.themes.first() # pylint: disable=no-member
if site_theme:
site_theme_dir = site_theme.theme_dir_name
cache_site_theme_dir(site, site_theme_dir)
return site_theme_dir
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_base_theme_dir():
"""
Return base directory that contains all the themes.
Example:
>> get_base_theme_dir()
'/edx/app/edxapp/edx-platform/themes'
Returns:
(str): Base theme directory
"""
return settings.COMPREHENSIVE_THEME_DIR
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
"""
return True if settings.COMPREHENSIVE_THEME_DIR else False
def get_site_theme_cache_key(site):
"""
Return cache key for the given site.
Example:
>> site = Site(domain='red-theme.org', name='Red Theme')
>> get_site_theme_cache_key(site)
'theming.site.red-theme.org'
Parameters:
site (django.contrib.sites.models.Site): site where key needs to generated
Returns:
(str): a key to be used as cache key
"""
cache_key = "theming.site.{domain}".format(
domain=site.domain
)
return cache_key
def is_valid_hostname(hostname):
"""
Return boolean indicating if given hostname is valid or not
Example:
>> is_valid_hostname('red-theme.org')
True
Parameters:
hostname (str): hostname that needs to be tested.
Returns:
(bool): True if given hostname is valid else False
"""
if len(hostname) > 255 or "." not in hostname:
return False
if hostname[-1] == ".":
hostname = hostname[:-1] # strip exactly one dot from the right, if present
if ":" in hostname:
hostname = hostname.split(":")[0] # strip port number if present
allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
return all(allowed.match(x) for x in hostname.split("."))
def cache_site_theme_dir(site, theme_dir):
"""
Cache site's theme directory.
Example:
>> site = Site(domain='red-theme.org', name='Red Theme')
>> cache_site_theme_dir(site, 'red-theme')
Parameters:
site (django.contrib.sites.models.Site): site for to cache
theme_dir (str): theme directory for the given site
"""
cache.set(get_site_theme_cache_key(site), theme_dir, settings.FOOTER_CACHE_TIMEOUT)
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.css', 'red-theme')
'/static/red-theme/css/lms-main.css'
Parameters:
asset (str): asset's path relative to the static files directory
Returns:
(str): static asset's url
"""
theme = get_current_site_theme_dir()
try:
return staticfiles_storage.url(asset, theme)
except (ValueError, TypeError):
# in case of an error return url without theme applied
return staticfiles_storage.url(asset)