Remove support for COMPREHENSIVE_THEME_DIR -

all dirs must now go into COMPREHENSIVE_THEME_DIRS.
Move comprehensive theming setup section out of startup.py and into
  settings files using new 'derived' functionality.
Add 'derive_settings' at the end of all top-level Django settings files.
Move validation of comprehensive theming settings into new apps.py
  theming file.
Split theming code into code safe to run before settings are initialized
  -and- after settings are initialized.
This commit is contained in:
John Eskew
2017-10-30 14:36:06 -04:00
parent 9a2e25c370
commit b866f35620
23 changed files with 455 additions and 286 deletions

View File

@@ -0,0 +1,83 @@
import os
import six
from django.apps import AppConfig
from django.conf import settings
from django.core.checks import Error, Tags, register
class ThemingConfig(AppConfig):
name = 'openedx.core.djangoapps.theming'
verbose_name = "Theming"
@register(Tags.compatibility)
def check_comprehensive_theme_settings(app_configs, **kwargs):
"""
Checks the comprehensive theming theme directory settings.
Raises compatibility Errors upon:
- COMPREHENSIVE_THEME_DIRS is not a list
- theme dir path is not a string
- theme dir path is not an absolute path
- path specified in COMPREHENSIVE_THEME_DIRS does not exist
Returns:
List of any Errors.
"""
if not getattr(settings, "ENABLE_COMPREHENSIVE_THEMING"):
# Only perform checks when comprehensive theming is enabled.
return []
errors = []
# COMPREHENSIVE_THEME_DIR is no longer supported - support has been removed.
if hasattr(settings, "COMPREHENSIVE_THEME_DIR"):
theme_dir = settings.COMPREHENSIVE_THEME_DIR
errors.append(
Error(
"COMPREHENSIVE_THEME_DIR setting has been removed in favor of COMPREHENSIVE_THEME_DIRS.",
hint='Transfer the COMPREHENSIVE_THEME_DIR value to COMPREHENSIVE_THEME_DIRS.',
obj=theme_dir,
id='openedx.core.djangoapps.theming.E001',
)
)
if hasattr(settings, "COMPREHENSIVE_THEME_DIRS"):
theme_dirs = settings.COMPREHENSIVE_THEME_DIRS
if not isinstance(theme_dirs, list):
errors.append(
Error(
"COMPREHENSIVE_THEME_DIRS must be a list.",
obj=theme_dirs,
id='openedx.core.djangoapps.theming.E004',
)
)
if not all([isinstance(theme_dir, six.string_types) for theme_dir in theme_dirs]):
errors.append(
Error(
"COMPREHENSIVE_THEME_DIRS must contain only strings.",
obj=theme_dirs,
id='openedx.core.djangoapps.theming.E005',
)
)
if not all([theme_dir.startswith("/") for theme_dir in theme_dirs]):
errors.append(
Error(
"COMPREHENSIVE_THEME_DIRS must contain only absolute paths to themes dirs.",
obj=theme_dirs,
id='openedx.core.djangoapps.theming.E006',
)
)
if not all([os.path.isdir(theme_dir) for theme_dir in theme_dirs]):
errors.append(
Error(
"COMPREHENSIVE_THEME_DIRS must contain valid paths.",
obj=theme_dirs,
id='openedx.core.djangoapps.theming.E007',
)
)
return errors

View File

@@ -1,38 +0,0 @@
"""
Core logic for Comprehensive Theming.
"""
from logging import getLogger
from django.conf import settings
from path import Path as path
from .helpers import get_themes
logger = getLogger(__name__) # pylint: disable=invalid-name
def enable_theming():
"""
Add directories and relevant paths to settings for comprehensive theming.
"""
# Deprecated Warnings
if hasattr(settings, "COMPREHENSIVE_THEME_DIR"):
logger.warning(
"\033[93m \nDeprecated: "
"\n\tCOMPREHENSIVE_THEME_DIR setting has been deprecated in favor of COMPREHENSIVE_THEME_DIRS.\033[00m"
)
for theme in get_themes():
if theme.themes_base_dir not in settings.MAKO_TEMPLATES['main']:
settings.MAKO_TEMPLATES['main'].insert(0, theme.themes_base_dir)
_add_theming_locales()
def _add_theming_locales():
"""
Add locale paths to settings for comprehensive theming.
"""
theme_locale_paths = settings.COMPREHENSIVE_THEME_LOCALE_PATHS
for locale_path in theme_locale_paths:
settings.LOCALE_PATHS += (path(locale_path), ) # pylint: disable=no-member

View File

@@ -5,12 +5,18 @@ import os
import re
from logging import getLogger
from django.conf import ImproperlyConfigured, settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.conf import settings
from path import Path
from microsite_configuration import microsite
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming.helpers_dirs import (
get_theme_base_dirs_from_settings,
get_themes_unchecked,
get_theme_dirs,
get_project_root_name_from_settings,
Theme
)
from request_cache.middleware import RequestCache
logger = getLogger(__name__) # pylint: disable=invalid-name
@@ -101,6 +107,23 @@ def get_all_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.
@@ -189,6 +212,7 @@ def get_current_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
@@ -232,78 +256,64 @@ def get_theme_base_dir(theme_dir_name, suppress_error=False):
))
def get_project_root_name():
def theme_exists(theme_name, themes_dir=None):
"""
Return root name for the current project
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
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 is None:
themes_dir = get_theme_base_dirs_unchecked()
return get_themes_unchecked(themes_dir, settings.PROJECT_ROOT)
def get_theme_base_dirs_unchecked():
"""
Return base directories that contains all the themes.
Example:
>> get_project_root_name()
'lms'
# from studio
>> get_project_root_name()
'cms'
>> get_theme_base_dirs_unchecked()
['/edx/app/ecommerce/ecommerce/themes']
Returns:
(str): component name of platform e.g lms, cms
(List of Paths): Base theme directory paths
"""
root = Path(settings.PROJECT_ROOT)
if root.name == "":
root = root.parent
return root.name
theme_dirs = getattr(settings, "COMPREHENSIVE_THEME_DIRS", None)
return get_theme_base_dirs_from_settings(theme_dirs)
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
Return base directories that contains all the themes.
Ensures comprehensive theming is enabled.
Example:
>> get_theme_base_dirs()
['/edx/app/ecommerce/ecommerce/themes']
Returns:
(Path): Base theme directory path
(List of Paths): Base theme directory paths
"""
# 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
return get_theme_base_dirs_unchecked()
def is_comprehensive_theming_enabled():
@@ -326,149 +336,3 @@ def is_comprehensive_theming_enabled():
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 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
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',
]

View File

@@ -0,0 +1,165 @@
"""
Code which dynamically discovers comprehensive themes. Deliberately uses no Django settings,
as the discovery happens during the initial setup of Django settings.
"""
import os
from path import Path
def get_theme_base_dirs_from_settings(theme_dirs=None):
"""
Return base directories that contains all the themes.
Example:
>> get_theme_base_dirs_from_settings('/edx/app/ecommerce/ecommerce/themes')
['/edx/app/ecommerce/ecommerce/themes']
Returns:
(List of Paths): Base theme directory paths
"""
theme_base_dirs = []
if theme_dirs:
theme_base_dirs.extend([Path(theme_dir) for theme_dir in theme_dirs])
return theme_base_dirs
def get_themes_unchecked(themes_dirs, project_root=None):
"""
Returns a list of all themes known to the system.
Args:
themes_dirs (list): Paths to themes base directory
project_root (str): (optional) Path to project root
Returns:
List of themes known to the system.
"""
themes_base_dirs = [Path(themes_dir) for themes_dir in themes_dirs]
# pick only directories and discard files in themes directory
themes = []
for themes_dir in themes_base_dirs:
themes.extend([Theme(name, name, themes_dir, project_root) 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)))
def get_project_root_name_from_settings(project_root):
"""
Return root name for the current project
Example:
>> get_project_root_name()
'lms'
# from studio
>> get_project_root_name()
'cms'
Args:
project_root (str): Root directory of the project.
Returns:
(str): component name of platform e.g lms, cms
"""
root = Path(project_root)
if root.name == "":
root = root.parent
return root.name
class Theme(object):
"""
class to encapsulate theme related information.
"""
name = ''
theme_dir_name = ''
themes_base_dir = None
project_root = None
def __init__(self, name='', theme_dir_name='', themes_base_dir=None, project_root=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
self.project_root = project_root
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_from_settings(self.project_root)
@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_from_settings(self.project_root) / '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',
]

View File

@@ -0,0 +1,19 @@
from django.contrib.staticfiles.storage import staticfiles_storage
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)

View File

@@ -92,7 +92,7 @@ class Command(BaseCommand):
if theme_dirs:
available_themes = {}
for theme_dir in theme_dirs:
available_themes.update({t.theme_dir_name: t for t in get_themes(theme_dir)})
available_themes.update({t.theme_dir_name: t for t in get_themes([theme_dir])})
else:
theme_dirs = get_theme_base_dirs()
available_themes = {t.theme_dir_name: t for t in get_themes()}

View File

@@ -9,7 +9,7 @@ from django.utils.safestring import mark_safe
from pipeline.templatetags.pipeline import StylesheetNode, JavascriptNode
from pipeline.utils import guess_type
from openedx.core.djangoapps.theming.helpers import get_static_file_url
from openedx.core.djangoapps.theming.helpers_static import get_static_file_url
register = template.Library() # pylint: disable=invalid-name

View File

@@ -22,13 +22,13 @@ class TestHelpers(TestCase):
Tests template paths are returned from enabled theme.
"""
expected_themes = [
Theme('dark-theme', 'dark-theme', get_theme_base_dir('dark-theme')),
Theme('edge.edx.org', 'edge.edx.org', get_theme_base_dir('edge.edx.org')),
Theme('edx.org', 'edx.org', get_theme_base_dir('edx.org')),
Theme('open-edx', 'open-edx', get_theme_base_dir('open-edx')),
Theme('red-theme', 'red-theme', get_theme_base_dir('red-theme')),
Theme('stanford-style', 'stanford-style', get_theme_base_dir('stanford-style')),
Theme('test-theme', 'test-theme', get_theme_base_dir('test-theme')),
Theme('dark-theme', 'dark-theme', get_theme_base_dir('dark-theme'), settings.PROJECT_ROOT),
Theme('edge.edx.org', 'edge.edx.org', get_theme_base_dir('edge.edx.org'), settings.PROJECT_ROOT),
Theme('edx.org', 'edx.org', get_theme_base_dir('edx.org'), settings.PROJECT_ROOT),
Theme('open-edx', 'open-edx', get_theme_base_dir('open-edx'), settings.PROJECT_ROOT),
Theme('red-theme', 'red-theme', get_theme_base_dir('red-theme'), settings.PROJECT_ROOT),
Theme('stanford-style', 'stanford-style', get_theme_base_dir('stanford-style'), settings.PROJECT_ROOT),
Theme('test-theme', 'test-theme', get_theme_base_dir('test-theme'), settings.PROJECT_ROOT),
]
actual_themes = get_themes()
self.assertItemsEqual(expected_themes, actual_themes)
@@ -39,7 +39,7 @@ class TestHelpers(TestCase):
Tests template paths are returned from enabled theme.
"""
expected_themes = [
Theme('test-theme', 'test-theme', get_theme_base_dir('test-theme')),
Theme('test-theme', 'test-theme', get_theme_base_dir('test-theme'), settings.PROJECT_ROOT),
]
actual_themes = get_themes()
self.assertItemsEqual(expected_themes, actual_themes)