Files
edx-platform/openedx/core/djangoapps/theming/helpers.py
Kyle D. McCormick 5fe131c858 fix: --theme-dirs argument to compile_sass management command
This fixes the ability to pass custom theme directories to
the management command which compiles site themes, a la:

   ./manage.py lms compile_sass --theme-dirs /my/custom/themes/dir

The exception, which was due to a incompatible use of @lru_cache, was:

   File "openedx/core/djangoapps/theming/management/commands/compile_sass.py",
   line 93, in parse_arguments:
     available_themes.update({t.theme_dir_name: t for t in get_themes([theme_dir])})
   TypeError: unhashable type: 'list'

This has been broken since the @lru_cache decorator was added, but it
wasn't noticed because:

* We weren't compiling any comprehensive themes in CI.
* Tutor supports compehensive theming, but not *site theming*, so
  it doesn't use this management command at all
  (site themeing == comp theming * site configuration).
* Although edx.org executes this management command, it does not provide
  use the `--theme-dirs` argument, so the bug was not hit.
2024-04-12 11:33:31 -04:00

364 lines
9.9 KiB
Python

"""
Helpers for accessing comprehensive theming related variables.
This file is imported at startup. Imports of models or things which import models will break startup on Django 1.9+. If
you need models here, please import them inside the function which uses them.
"""
import os
import re
from logging import getLogger
import crum
from django.conf import settings
from edx_toggles.toggles import SettingToggle
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming.helpers_dirs import (
Theme,
get_project_root_name_from_settings,
get_theme_base_dirs_from_settings,
get_theme_dirs,
get_themes_unchecked
)
from openedx.core.lib.cache_utils import request_cached
from functools import lru_cache
logger = getLogger(__name__) # pylint: disable=invalid-name
@request_cached()
def get_template_path(relative_path, **kwargs): # lint-amnesty, pylint: disable=unused-argument
"""
The calculated value is cached for the lifetime of the current request.
"""
return relative_path
def is_request_in_themed_site():
"""
This is a proxy function to site_configuration.
"""
return configuration_helpers.is_site_configuration_enabled()
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 = []
for theme in themes:
template_paths.extend(theme.template_dirs)
return template_paths
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
"""
return get_project_root_name_from_settings(settings.PROJECT_ROOT)
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 crum.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),
project_root=get_project_root_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 theme_exists(theme_name, themes_dir=None):
"""
Returns True if a theme exists with the specified name.
"""
for theme in get_themes(themes_dir=themes_dir):
if theme.theme_dir_name == theme_name:
return True
return False
@lru_cache
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 []
if themes_dir:
themes_dirs = [themes_dir]
else:
themes_dirs = get_theme_base_dirs_unchecked()
return get_themes_unchecked(themes_dirs, settings.PROJECT_ROOT)
def get_theme_base_dirs_unchecked():
"""
Return base directories that contains all the themes.
Example:
>> get_theme_base_dirs_unchecked()
['/edx/app/ecommerce/ecommerce/themes']
Returns:
(List of Paths): Base theme directory paths
"""
theme_dirs = getattr(settings, "COMPREHENSIVE_THEME_DIRS", None)
return get_theme_base_dirs_from_settings(theme_dirs)
def get_theme_base_dirs():
"""
Return base directories that contains all the themes.
Ensures comprehensive theming is enabled.
Example:
>> get_theme_base_dirs()
['/edx/app/ecommerce/ecommerce/themes']
Returns:
(List of Paths): Base theme directory paths
"""
# Return an empty list if theming is disabled
if not is_comprehensive_theming_enabled():
return []
return get_theme_base_dirs_unchecked()
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
"""
ENABLE_COMPREHENSIVE_THEMING = SettingToggle("ENABLE_COMPREHENSIVE_THEMING", default=False)
if ENABLE_COMPREHENSIVE_THEMING.is_enabled() and current_request_has_associated_site_theme():
return True
return ENABLE_COMPREHENSIVE_THEMING.is_enabled()
def get_config_value_from_site_or_settings(name, site=None, site_config_name=None):
"""
Given a configuration setting name, try to get it from the site configuration and then fall back on the settings.
If site_config_name is not specified then "name" is used as the key for both collections.
Args:
name (str): The name of the setting to get the value of.
site: The site that we are trying to fetch the value for.
site_config_name: The name of the setting within the site configuration.
Returns:
The value stored in the configuration.
"""
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
if site_config_name is None:
site_config_name = name
if site is None:
site = get_current_site()
site_configuration = None
if site is not None:
try:
site_configuration = getattr(site, "configuration", None)
except SiteConfiguration.DoesNotExist:
pass
value_from_settings = getattr(settings, name, None)
if site_configuration is not None:
return site_configuration.get_value(site_config_name, default=value_from_settings)
else:
return value_from_settings