Manually merge release into master

This commit is contained in:
Renzo Lucioni
2016-04-14 14:23:03 -04:00
117 changed files with 684 additions and 3081 deletions

View File

@@ -1,22 +0,0 @@
"""
Django admin page for theming models
"""
from django.contrib import admin
from .models import (
SiteTheme,
)
class SiteThemeAdmin(admin.ModelAdmin):
""" Admin interface for the SiteTheme object. """
list_display = ('site', 'theme_dir_name')
search_fields = ('site__domain', 'theme_dir_name')
class Meta(object):
"""
Meta class for SiteTheme admin model
"""
model = SiteTheme
admin.site.register(SiteTheme, SiteThemeAdmin)

View File

@@ -1,32 +1,62 @@
"""
Core logic for Comprehensive Theming.
"""
import os.path
from path import Path as path
from path import Path
from django.conf import settings
from .helpers import (
get_project_root_name,
)
def comprehensive_theme_changes(theme_dir):
"""
Calculate the set of changes needed to enable a comprehensive theme.
Arguments:
theme_dir (path.path): the full path to the theming directory to use.
Returns:
A dict indicating the changes to make:
* 'settings': a dictionary of settings names and their new values.
* 'template_paths': a list of directories to prepend to template
lookup path.
"""
changes = {
'settings': {},
'template_paths': [],
}
root = Path(settings.PROJECT_ROOT)
if root.name == "":
root = root.parent
component_dir = theme_dir / root.name
templates_dir = component_dir / "templates"
if templates_dir.isdir():
changes['template_paths'].append(templates_dir)
staticfiles_dir = component_dir / "static"
if staticfiles_dir.isdir():
changes['settings']['STATICFILES_DIRS'] = [staticfiles_dir] + settings.STATICFILES_DIRS
locale_dir = component_dir / "conf" / "locale"
if locale_dir.isdir():
changes['settings']['LOCALE_PATHS'] = [locale_dir] + settings.LOCALE_PATHS
return changes
def enable_comprehensive_theming(themes_dir):
def enable_comprehensive_theme(theme_dir):
"""
Add directories to relevant paths for comprehensive theming.
:param themes_dir: path to base theme directory
"""
if isinstance(themes_dir, basestring):
themes_dir = path(themes_dir)
changes = comprehensive_theme_changes(theme_dir)
if themes_dir.isdir():
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].insert(0, themes_dir)
settings.MAKO_TEMPLATES['main'].insert(0, themes_dir)
for theme_dir in os.listdir(themes_dir):
staticfiles_dir = os.path.join(themes_dir, theme_dir, get_project_root_name(), "static")
if staticfiles_dir.isdir():
settings.STATICFILES_DIRS = settings.STATICFILES_DIRS + [staticfiles_dir]
locale_dir = os.path.join(themes_dir, theme_dir, get_project_root_name(), "conf", "locale")
if locale_dir.isdir():
settings.LOCALE_PATHS = (locale_dir, ) + settings.LOCALE_PATHS
# Use the changes
for name, value in changes['settings'].iteritems():
setattr(settings, name, value)
for template_dir in changes['template_paths']:
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].insert(0, template_dir)
settings.MAKO_TEMPLATES['main'].insert(0, template_dir)

View File

@@ -17,80 +17,63 @@ interface, as well.
.. _Django-Pipeline: http://django-pipeline.readthedocs.org/
.. _Django-Require: https://github.com/etianen/django-require
"""
import os
from collections import OrderedDict
from path import Path
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.contrib.staticfiles import utils
from django.contrib.staticfiles.finders import BaseFinder
from django.utils import six
from openedx.core.djangoapps.theming.helpers import get_themes
from openedx.core.djangoapps.theming.storage import ThemeStorage
from openedx.core.djangoapps.theming.storage import CachedComprehensiveThemingStorage
class ThemeFilesFinder(BaseFinder):
class ComprehensiveThemeFinder(BaseFinder):
"""
A static files finder that looks in the directory of each theme as
specified in the source_dir attribute.
A static files finder that searches the active comprehensive theme
for static files. If the ``COMPREHENSIVE_THEME_DIR`` setting is unset,
or the ``COMPREHENSIVE_THEME_DIR`` does not exist on the file system,
this finder will never find any files.
"""
storage_class = ThemeStorage
source_dir = 'static'
def __init__(self, *args, **kwargs):
# The list of themes that are handled
self.themes = []
# Mapping of theme names to storage instances
self.storages = OrderedDict()
super(ComprehensiveThemeFinder, self).__init__(*args, **kwargs)
themes = get_themes()
for theme in themes:
theme_storage = self.storage_class(
os.path.join(theme.path, self.source_dir),
prefix=theme.theme_dir,
)
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
if not theme_dir:
self.storage = None
return
self.storages[theme.theme_dir] = theme_storage
if theme.theme_dir not in self.themes:
self.themes.append(theme.theme_dir)
if not isinstance(theme_dir, basestring):
raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
super(ThemeFilesFinder, self).__init__(*args, **kwargs)
root = Path(settings.PROJECT_ROOT)
if root.name == "":
root = root.parent
def list(self, ignore_patterns):
"""
List all files in all app storages.
"""
for storage in six.itervalues(self.storages):
if storage.exists(''): # check if storage location exists
for path in utils.get_files(storage, ignore_patterns):
yield path, storage
component_dir = Path(theme_dir) / root.name
static_dir = component_dir / "static"
self.storage = CachedComprehensiveThemingStorage(location=static_dir)
def find(self, path, all=False): # pylint: disable=redefined-builtin
"""
Looks for files in the theme directories.
Looks for files in the default file storage, if it's local.
"""
matches = []
theme_dir = path.split("/", 1)[0]
if not self.storage:
return []
themes = {t.theme_dir: t for t in get_themes()}
# if path is prefixed by theme name then search in the corresponding storage other wise search all storages.
if theme_dir in themes:
theme = themes[theme_dir]
path = "/".join(path.split("/")[1:])
match = self.find_in_theme(theme.theme_dir, path)
if match:
if not all:
return match
matches.append(match)
return matches
if path.startswith(self.storage.prefix):
# strip the prefix
path = path[len(self.storage.prefix):]
def find_in_theme(self, theme, path):
if self.storage.exists(path):
match = self.storage.path(path)
if all:
match = [match]
return match
return []
def list(self, ignore_patterns):
"""
Find a requested static file in an theme's static locations.
List all files of the storage.
"""
storage = self.storages.get(theme, None)
if storage:
# only try to find a file if the source dir actually exists
if storage.exists(path):
matched_path = storage.path(path)
if matched_path:
return matched_path
if self.storage and self.storage.exists(''):
for path in utils.get_files(self.storage, ignore_patterns):
yield path, self.storage

View File

@@ -1,16 +1,9 @@
"""
Helpers for accessing comprehensive theming related variables.
"""
import re
import os
from path import Path
from django.conf import settings, ImproperlyConfigured
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
from django.conf import settings
def get_page_title_breadcrumbs(*args):
@@ -31,11 +24,7 @@ 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
return microsite.get_template_path(relative_path, **kwargs)
def is_request_in_themed_site():
@@ -45,14 +34,6 @@ def is_request_in_themed_site():
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.
@@ -71,311 +52,3 @@ def get_themed_template_path(relative_path, default_path, **kwargs):
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():
"""
Return current site.
Returns:
(django.contrib.sites.models.Site): theme directory for current site
"""
from edxmako.middleware import REQUEST_CONTEXT
request = getattr(REQUEST_CONTEXT, 'request', None)
if not request:
return None
return getattr(request, 'site', None)
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
"""
site = get_current_site()
if not site:
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:
(Path): Base theme directory path
"""
themes_dir = settings.COMPREHENSIVE_THEME_DIR
if not isinstance(themes_dir, basestring):
raise ImproperlyConfigured("COMPREHENSIVE_THEME_DIR must be a string.")
return Path(themes_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 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.THEME_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')
'/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
"""
return staticfiles_storage.url(asset)
def get_themes():
"""
get a list of all themes known to the system.
Returns:
list of themes known to the system.
"""
themes_dir = get_base_theme_dir()
# pick only directories and discard files in themes directory
theme_names = []
if themes_dir:
theme_names = [_dir for _dir in os.listdir(themes_dir) if is_theme_dir(themes_dir / _dir)]
return [Theme(name, name) for name in theme_names]
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 = ''
path = ''
def __init__(self, name='', theme_dir=''):
"""
init method for Theme
Args:
name: name if the theme
theme_dir: directory name of the theme
"""
self.name = name
self.theme_dir = theme_dir
self.path = Path(get_base_theme_dir()) / theme_dir / get_project_root_name()
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, self.path) == (other.theme_dir, other.path)
def __hash__(self):
return hash((self.theme_dir, self.path))
def __unicode__(self):
return u"<Theme: {name} at '{path}'>".format(name=self.name, path=self.path)
def __repr__(self):
return self.__unicode__()

View File

@@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sites', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='SiteTheme',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('theme_dir_name', models.CharField(max_length=255)),
('site', models.ForeignKey(related_name='themes', to='sites.Site')),
],
),
]

View File

@@ -1,19 +0,0 @@
"""
Django models supporting the Comprehensive Theming subsystem
"""
from django.db import models
from django.contrib.sites.models import Site
class SiteTheme(models.Model):
"""
This is where the information about the site's theme gets stored to the db.
`site` field is foreignkey to django Site model
`theme_dir_name` contains directory name having Site's theme
"""
site = models.ForeignKey(Site, related_name='themes')
theme_dir_name = models.CharField(max_length=255)
def __unicode__(self):
return self.theme_dir_name

View File

@@ -2,300 +2,87 @@
Comprehensive Theming support for Django's collectstatic functionality.
See https://docs.djangoproject.com/en/1.8/ref/contrib/staticfiles/
"""
import posixpath
from path import Path
import os.path
from django.conf import settings
from django.utils._os import safe_join
from django.core.exceptions import ImproperlyConfigured
from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
from django.contrib.staticfiles.finders import find
from django.utils.six.moves.urllib.parse import ( # pylint: disable=no-name-in-module, import-error
unquote, urlsplit,
)
from pipeline.storage import PipelineMixin
from openedx.core.djangoapps.theming.helpers import (
get_base_theme_dir,
get_project_root_name,
get_current_site_theme_dir,
get_themes,
)
from django.utils._os import safe_join
class ThemeStorage(StaticFilesStorage):
class ComprehensiveThemingAwareMixin(object):
"""
Comprehensive theme aware Static files storage.
Mixin for Django storage system to make it aware of the currently-active
comprehensive theme, so that it can generate theme-scoped URLs for themed
static assets.
"""
# prefix for file path, this prefix is added at the beginning of file path before saving static files during
# collectstatic command.
# e.g. having "edx.org" as prefix will cause files to be saved as "edx.org/images/logo.png"
# instead of "images/logo.png"
prefix = None
def __init__(self, location=None, base_url=None, file_permissions_mode=None,
directory_permissions_mode=None, prefix=None):
self.prefix = prefix
super(ThemeStorage, self).__init__(
location=location,
base_url=base_url,
file_permissions_mode=file_permissions_mode,
directory_permissions_mode=directory_permissions_mode,
)
def url(self, name):
"""
Returns url of the asset, themed url will be returned if the asset is themed otherwise default
asset url will be returned.
Args:
name: name of the asset, e.g. 'images/logo.png'
Returns:
url of the asset, e.g. '/static/red-theme/images/logo.png' if current theme is red-theme and logo
is provided by red-theme otherwise '/static/images/logo.png'
"""
prefix = ''
theme_dir = get_current_site_theme_dir()
# get theme prefix from site address if if asset is accessed via a url
if theme_dir:
prefix = theme_dir
# get theme prefix from storage class, if asset is accessed during collectstatic run
elif self.prefix:
prefix = self.prefix
# join theme prefix with asset name if theme is applied and themed asset exists
if prefix and self.themed(name, prefix):
name = os.path.join(prefix, name)
return super(ThemeStorage, self).url(name)
def themed(self, name, theme):
"""
Returns True if given asset override is provided by the given theme otherwise returns False.
Args:
name: asset name e.g. 'images/logo.png'
theme: theme name e.g. 'red-theme', 'edx.org'
Returns:
True if given asset override is provided by the given theme otherwise returns False
"""
# in debug mode check static asset from within the project directory
if settings.DEBUG:
themes_location = get_base_theme_dir()
# Nothing can be themed if we don't have a theme location or required params.
if not all((themes_location, theme, name)):
return False
themed_path = "/".join([
themes_location,
theme,
get_project_root_name(),
"static/"
])
name = name[1:] if name.startswith("/") else name
path = safe_join(themed_path, name)
return os.path.exists(path)
# in live mode check static asset in the static files dir defined by "STATIC_ROOT" setting
else:
return self.exists(os.path.join(theme, name))
class ComprehensiveThemingCachedFilesMixin(CachedFilesMixin):
"""
Comprehensive theme aware CachedFilesMixin.
Main purpose of subclassing CachedFilesMixin is to override the following methods.
1 - url
2 - url_converter
url:
This method takes asset name as argument and is responsible for adding hash to the name to support caching.
This method is called during both collectstatic command and live server run.
When called during collectstatic command that name argument will be asset name inside STATIC_ROOT,
for non themed assets it will be the usual path (e.g. 'images/logo.png') but for themed asset it will
also contain themes dir prefix (e.g. 'red-theme/images/logo.png'). So, here we check whether the themed asset
exists or not, if it exists we pass the same name up in the MRO chain for further processing and if it does not
exists we strip theme name and pass the new asset name to the MRO chain for further processing.
When called during server run, we get the theme dir for the current site using `get_current_site_theme_dir` and
make sure to prefix theme dir to the asset name. This is done to ensure the usage of correct hash in file name.
e.g. if our red-theme overrides 'images/logo.png' and we do not prefix theme dir to the asset name, the hash for
'{platform-dir}/lms/static/images/logo.png' would be used instead of
'{themes_base_dir}/red-theme/images/logo.png'
url_converter:
This function returns another function that is responsible for hashing urls that appear inside assets
(e.g. url("images/logo.png") inside css). The method defined in the superclass adds a hash to file and returns
relative url of the file.
e.g. for url("../images/logo.png") it would return url("../images/logo.790c9a5340cb.png"). However we would
want it to return absolute url (e.g. url("/static/images/logo.790c9a5340cb.png")) so that it works properly
with themes.
The overridden method here simply comments out the two lines that convert absolute url to relative url,
hence absolute urls are used instead of relative urls.
"""
def url(self, name, force=False):
"""
Returns themed url for the given asset.
"""
theme_dir = get_current_site_theme_dir()
if theme_dir and theme_dir not in name:
# during server run, append theme name to the asset name if it is not already there
# this is ensure that correct hash is created and default asset is not always
# used to create hash of themed assets.
name = os.path.join(theme_dir, name)
parsed_name = urlsplit(unquote(name))
clean_name = parsed_name.path.strip()
asset_name = name
if not self.exists(clean_name):
# if themed asset does not exists then use default asset
theme = name.split("/", 1)[0]
# verify that themed asset was accessed
if theme in [theme.theme_dir for theme in get_themes()]:
asset_name = "/".join(name.split("/")[1:])
return super(ComprehensiveThemingCachedFilesMixin, self).url(asset_name, force)
def url_converter(self, name, template=None):
"""
This is an override of url_converter from CachedFilesMixin.
It just comments out two lines at the end of the method.
The purpose of this override is to make converter method return absolute urls instead of relative urls.
This behavior is necessary for theme overrides, as we get 404 on assets with relative urls on a themed site.
"""
if template is None:
template = self.default_template
def converter(matchobj):
"""
Converts the matched URL depending on the parent level (`..`)
and returns the normalized and hashed URL using the url method
of the storage.
"""
matched, url = matchobj.groups()
# Completely ignore http(s) prefixed URLs,
# fragments and data-uri URLs
if url.startswith(('#', 'http:', 'https:', 'data:', '//')):
return matched
name_parts = name.split(os.sep)
# Using posix normpath here to remove duplicates
url = posixpath.normpath(url)
url_parts = url.split('/')
parent_level, sub_level = url.count('..'), url.count('/')
if url.startswith('/'):
sub_level -= 1
url_parts = url_parts[1:]
if parent_level or not url.startswith('/'):
start, end = parent_level + 1, parent_level
else:
if sub_level:
if sub_level == 1:
parent_level -= 1
start, end = parent_level, 1
else:
start, end = 1, sub_level - 1
joined_result = '/'.join(name_parts[:-start] + url_parts[end:])
hashed_url = self.url(unquote(joined_result), force=True)
# NOTE:
# following two lines are commented out so that absolute urls are used instead of relative urls
# to make themed assets work correctly.
#
# The lines are commented and not removed to make future django upgrade easier and
# show exactly what is changed in this method override
#
# file_name = hashed_url.split('/')[-1:]
# relative_url = '/'.join(url.split('/')[:-1] + file_name)
# Return the hashed version to the file
return template % unquote(hashed_url)
return converter
class ThemePipelineMixin(PipelineMixin):
"""
Mixin to make sure themed assets are also packaged and used along with non themed assets.
if a source asset for a particular package is not present then the default asset is used.
e.g. in the following package and for 'red-theme'
'style-vendor': {
'source_filenames': [
'js/vendor/afontgarde/afontgarde.css',
'css/vendor/font-awesome.css',
'css/vendor/jquery.qtip.min.css',
'css/vendor/responsive-carousel/responsive-carousel.css',
'css/vendor/responsive-carousel/responsive-carousel.slide.css',
],
'output_filename': 'css/lms-style-vendor.css'
}
'red-theme/css/vendor/responsive-carousel/responsive-carousel.css' will be used of it exists otherwise
'css/vendor/responsive-carousel/responsive-carousel.css' will be used to create 'red-theme/css/lms-style-vendor.css'
"""
packing = True
def post_process(self, paths, dry_run=False, **options):
"""
This post_process hook is used to package all themed assets.
"""
if dry_run:
def __init__(self, *args, **kwargs):
super(ComprehensiveThemingAwareMixin, self).__init__(*args, **kwargs)
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
if not theme_dir:
self.theme_location = None
return
themes = get_themes()
for theme in themes:
css_packages = self.get_themed_packages(theme.theme_dir, settings.PIPELINE_CSS)
js_packages = self.get_themed_packages(theme.theme_dir, settings.PIPELINE_JS)
if not isinstance(theme_dir, basestring):
raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
from pipeline.packager import Packager
packager = Packager(storage=self, css_packages=css_packages, js_packages=js_packages)
for package_name in packager.packages['css']:
package = packager.package_for('css', package_name)
output_file = package.output_filename
if self.packing:
packager.pack_stylesheets(package)
paths[output_file] = (self, output_file)
yield output_file, output_file, True
for package_name in packager.packages['js']:
package = packager.package_for('js', package_name)
output_file = package.output_filename
if self.packing:
packager.pack_javascripts(package)
paths[output_file] = (self, output_file)
yield output_file, output_file, True
root = Path(settings.PROJECT_ROOT)
if root.name == "":
root = root.parent
super_class = super(ThemePipelineMixin, self)
if hasattr(super_class, 'post_process'):
for name, hashed_name, processed in super_class.post_process(paths.copy(), dry_run, **options):
yield name, hashed_name, processed
component_dir = Path(theme_dir) / root.name
self.theme_location = component_dir / "static"
@staticmethod
def get_themed_packages(prefix, packages):
@property
def prefix(self):
"""
Update paths with the themed assets,
Args:
prefix: theme prefix for which to update asset paths e.g. 'red-theme', 'edx.org' etc.
packages: packages to update
Returns: list of updated paths and a boolean indicating whether any path was path or not
This is used by the ComprehensiveThemeFinder in the collection step.
"""
themed_packages = {}
for name in packages:
# collect source file names for the package
source_files = []
for path in packages[name].get('source_filenames', []):
# if themed asset exists use that, otherwise use default asset.
if find(os.path.join(prefix, path)):
source_files.append(os.path.join(prefix, path))
else:
source_files.append(path)
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
if not theme_dir:
return None
theme_name = os.path.basename(os.path.normpath(theme_dir))
return "themes/{name}/".format(name=theme_name)
themed_packages[name] = {
'output_filename': os.path.join(prefix, packages[name].get('output_filename', '')),
'source_filenames': source_files,
}
return themed_packages
def themed(self, name):
"""
Given a name, return a boolean indicating whether that name exists
as a themed asset in the comprehensive theme.
"""
# Nothing can be themed if we don't have a theme location.
if not self.theme_location:
return False
path = safe_join(self.theme_location, name)
return os.path.exists(path)
def path(self, name):
"""
Get the path to the real asset on disk
"""
if self.themed(name):
base = self.theme_location
else:
base = self.location
path = safe_join(base, name)
return os.path.normpath(path)
def url(self, name, *args, **kwargs):
"""
Add the theme prefix to the asset URL
"""
if self.themed(name):
name = self.prefix + name
return super(ComprehensiveThemingAwareMixin, self).url(name, *args, **kwargs)
class CachedComprehensiveThemingStorage(
ComprehensiveThemingAwareMixin,
CachedFilesMixin,
StaticFilesStorage
):
"""
Used by the ComprehensiveThemeFinder class. Mixes in support for cached
files and comprehensive theming in static files.
"""
pass

View File

@@ -1,27 +0,0 @@
"""
Theming aware template loaders.
"""
from django.template.loaders.filesystem import Loader as FilesystemLoader
from edxmako.makoloader import MakoLoader
from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme
class ThemeTemplateLoader(MakoLoader):
"""
This is a Django loader object which will load the template based on current request and its corresponding theme.
"""
def __call__(self, template_name, template_dirs=None):
template_name = get_template_path_with_theme(template_name).lstrip("/")
return self.load_template(template_name, template_dirs)
class ThemeFilesystemLoader(ThemeTemplateLoader):
"""
Filesystem Template loaders to pickup templates from theme directory based on the current site.
"""
is_usable = True
_accepts_engine_in_init = True
def __init__(self, *args):
ThemeTemplateLoader.__init__(self, FilesystemLoader(*args))

View File

@@ -1,78 +0,0 @@
"""
Theme aware pipeline template tags.
"""
from django import template
from django.template.loader import render_to_string
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
register = template.Library() # pylint: disable=invalid-name
class ThemeStylesheetNode(StylesheetNode):
"""
Overrides StyleSheetNode from django pipeline so that stylesheets are served based on the applied theme.
"""
def render_css(self, package, path):
"""
Override render_css from django-pipline so that stylesheets urls are based on the applied theme
"""
template_name = package.template_name or "pipeline/css.html"
context = package.extra_context
context.update({
'type': guess_type(path, 'text/css'),
'url': mark_safe(get_static_file_url(path))
})
return render_to_string(template_name, context)
class ThemeJavascriptNode(JavascriptNode):
"""
Overrides JavascriptNode from django pipeline so that js files are served based on the applied theme.
"""
def render_js(self, package, path):
"""
Override render_js from django-pipline so that js file urls are based on the applied theme
"""
template_name = package.template_name or "pipeline/js.html"
context = package.extra_context
context.update({
'type': guess_type(path, 'text/javascript'),
'url': mark_safe(get_static_file_url(path))
})
return render_to_string(template_name, context)
@register.tag
def stylesheet(parser, token): # pylint: disable=unused-argument
"""
Template tag to serve stylesheets from django-pipeline. This definition uses the theming aware ThemeStyleSheetNode.
"""
try:
_, name = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
'%r requires exactly one argument: the name of a group in the PIPELINE_CSS setting' %
token.split_contents()[0]
)
return ThemeStylesheetNode(name)
@register.tag
def javascript(parser, token): # pylint: disable=unused-argument
"""
Template tag to serve javascript from django-pipeline. This definition uses the theming aware ThemeJavascriptNode.
"""
try:
_, name = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
'%r requires exactly one argument: the name of a group in the PIPELINE_JS setting' %
token.split_contents()[0]
)
return ThemeJavascriptNode(name)

View File

@@ -6,57 +6,87 @@ from functools import wraps
import os
import os.path
import contextlib
import re
from mock import patch
from django.conf import settings
from django.contrib.sites.models import Site
from django.template import Engine
from django.test.utils import override_settings
import edxmako
from .models import SiteTheme
from .core import comprehensive_theme_changes
EDX_THEME_DIR = settings.REPO_ROOT / "themes" / "edx.org"
def with_comprehensive_theme(theme_dir_name):
def with_comprehensive_theme(theme_dir):
"""
A decorator to run a test with a comprehensive theming enabled.
A decorator to run a test with a particular comprehensive theme.
Arguments:
theme_dir_name (str): directory name of the site for which we want comprehensive theming enabled.
theme_dir (str): the full path to the theme directory to use.
This will likely use `settings.REPO_ROOT` to get the full path.
"""
# This decorator creates Site and SiteTheme models for given domain
# This decorator gets the settings changes needed for a theme, and applies
# them using the override_settings and edxmako.paths.add_lookup context
# managers.
changes = comprehensive_theme_changes(theme_dir)
def _decorator(func): # pylint: disable=missing-docstring
@wraps(func)
def _decorated(*args, **kwargs): # pylint: disable=missing-docstring
# make a domain name out of directory name
domain = "{theme_dir_name}.org".format(theme_dir_name=re.sub(r"\.org$", "", theme_dir_name))
site, __ = Site.objects.get_or_create(domain=domain, name=domain)
SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme_dir_name)
edxmako.paths.add_lookup('main', settings.COMPREHENSIVE_THEME_DIR, prepend=True)
with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme_dir',
return_value=theme_dir_name):
with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
return func(*args, **kwargs)
with override_settings(COMPREHENSIVE_THEME_DIR=theme_dir, **changes['settings']):
default_engine = Engine.get_default()
dirs = default_engine.dirs[:]
with edxmako.save_lookups():
for template_dir in changes['template_paths']:
edxmako.paths.add_lookup('main', template_dir, prepend=True)
dirs.insert(0, template_dir)
with patch.object(default_engine, 'dirs', dirs):
return func(*args, **kwargs)
return _decorated
return _decorator
@contextlib.contextmanager
def with_comprehensive_theme_context(theme=None):
def with_is_edx_domain(is_edx_domain):
"""
A function to run a test as if request was made to the given theme.
A decorator to run a test as if request originated from edX domain or not.
Arguments:
theme (str): name if the theme or None if no theme is applied
is_edx_domain (bool): are we an edX domain or not?
"""
if theme:
domain = '{theme}.org'.format(theme=re.sub(r"\.org$", "", theme))
site, __ = Site.objects.get_or_create(domain=domain, name=theme)
SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme)
edxmako.paths.add_lookup('main', settings.COMPREHENSIVE_THEME_DIR, prepend=True)
with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme_dir',
return_value=theme):
with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
# This is weird, it's a decorator that conditionally applies other
# decorators, which is confusing.
def _decorator(func): # pylint: disable=missing-docstring
if is_edx_domain:
# This applies @with_comprehensive_theme to the func.
func = with_comprehensive_theme(EDX_THEME_DIR)(func)
return func
return _decorator
@contextlib.contextmanager
def with_edx_domain_context(is_edx_domain):
"""
A function to run a test as if request originated from edX domain or not.
Arguments:
is_edx_domain (bool): are we an edX domain or not?
"""
if is_edx_domain:
changes = comprehensive_theme_changes(EDX_THEME_DIR)
with override_settings(COMPREHENSIVE_THEME_DIR=EDX_THEME_DIR, **changes['settings']):
with edxmako.save_lookups():
for template_dir in changes['template_paths']:
edxmako.paths.add_lookup('main', template_dir, prepend=True)
yield
else:
yield

View File

@@ -1,152 +0,0 @@
"""Tests of comprehensive theming."""
import unittest
from mock import patch
from django.test import TestCase, RequestFactory, override_settings
from django.conf import settings
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme, strip_site_theme_templates_path, \
get_current_site_theme_dir, get_themes, Theme
class TestHelpers(TestCase):
"""Test comprehensive theming helper functions."""
def test_get_themes(self):
"""
Tests template paths are returned from enabled theme.
"""
expected_themes = [
Theme('red-theme', 'red-theme'),
Theme('edge.edx.org', 'edge.edx.org'),
Theme('edx.org', 'edx.org'),
Theme('stanford-style', 'stanford-style'),
]
actual_themes = get_themes()
self.assertItemsEqual(expected_themes, actual_themes)
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
def test_get_themes_2(self):
"""
Tests template paths are returned from enabled theme.
"""
expected_themes = [
Theme('test-theme', 'test-theme'),
]
actual_themes = get_themes()
self.assertItemsEqual(expected_themes, actual_themes)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestHelpersLMS(TestCase):
"""Test comprehensive theming helper functions."""
@with_comprehensive_theme('red-theme')
def test_get_template_path_with_theme_enabled(self):
"""
Tests template paths are returned from enabled theme.
"""
template_path = get_template_path_with_theme('header.html')
self.assertEqual(template_path, '/red-theme/lms/templates/header.html')
@with_comprehensive_theme('red-theme')
def test_get_template_path_with_theme_for_missing_template(self):
"""
Tests default template paths are returned if template is not found in the theme.
"""
template_path = get_template_path_with_theme('course.html')
self.assertEqual(template_path, 'course.html')
def test_get_template_path_with_theme_disabled(self):
"""
Tests default template paths are returned when theme is non theme is enabled.
"""
template_path = get_template_path_with_theme('header.html')
self.assertEqual(template_path, 'header.html')
@with_comprehensive_theme('red-theme')
def test_strip_site_theme_templates_path_theme_enabled(self):
"""
Tests site theme templates path is stripped from the given template path.
"""
template_path = strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
self.assertEqual(template_path, 'header.html')
def test_strip_site_theme_templates_path_theme_disabled(self):
"""
Tests site theme templates path returned unchanged if no theme is applied.
"""
template_path = strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
self.assertEqual(template_path, '/red-theme/lms/templates/header.html')
@with_comprehensive_theme('red-theme')
def test_get_current_site_theme_dir(self):
"""
Tests current site theme name.
"""
factory = RequestFactory()
with patch(
'edxmako.middleware.REQUEST_CONTEXT.request',
factory.get('/', SERVER_NAME="red-theme.org"),
create=True,
):
current_site = get_current_site_theme_dir()
self.assertEqual(current_site, 'red-theme')
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
class TestHelpersCMS(TestCase):
"""Test comprehensive theming helper functions."""
@with_comprehensive_theme('red-theme')
def test_get_template_path_with_theme_enabled(self):
"""
Tests template paths are returned from enabled theme.
"""
template_path = get_template_path_with_theme('login.html')
self.assertEqual(template_path, '/red-theme/cms/templates/login.html')
@with_comprehensive_theme('red-theme')
def test_get_template_path_with_theme_for_missing_template(self):
"""
Tests default template paths are returned if template is not found in the theme.
"""
template_path = get_template_path_with_theme('certificates.html')
self.assertEqual(template_path, 'certificates.html')
def test_get_template_path_with_theme_disabled(self):
"""
Tests default template paths are returned when theme is non theme is enabled.
"""
template_path = get_template_path_with_theme('login.html')
self.assertEqual(template_path, 'login.html')
@with_comprehensive_theme('red-theme')
def test_strip_site_theme_templates_path_theme_enabled(self):
"""
Tests site theme templates path is stripped from the given template path.
"""
template_path = strip_site_theme_templates_path('/red-theme/cms/templates/login.html')
self.assertEqual(template_path, 'login.html')
def test_strip_site_theme_templates_path_theme_disabled(self):
"""
Tests site theme templates path returned unchanged if no theme is applied.
"""
template_path = strip_site_theme_templates_path('/red-theme/cms/templates/login.html')
self.assertEqual(template_path, '/red-theme/cms/templates/login.html')
@with_comprehensive_theme('red-theme')
def test_get_current_site_theme_dir(self):
"""
Tests current site theme name.
"""
factory = RequestFactory()
with patch(
'edxmako.middleware.REQUEST_CONTEXT.request',
factory.get('/', SERVER_NAME="red-theme.org"),
create=True,
):
current_site = get_current_site_theme_dir()
self.assertEqual(current_site, 'red-theme')

View File

@@ -1,82 +0,0 @@
"""
Tests for comprehensive theme static files storage classes.
"""
import ddt
import unittest
import re
from mock import patch
from django.test import TestCase, override_settings
from django.conf import settings
from openedx.core.djangoapps.theming.helpers import get_base_theme_dir
from openedx.core.djangoapps.theming.storage import ThemeStorage
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt
class TestStorageLMS(TestCase):
"""
Test comprehensive theming static files storage.
"""
def setUp(self):
super(TestStorageLMS, self).setUp()
self.themes_dir = get_base_theme_dir()
self.enabled_theme = "red-theme"
self.system_dir = settings.REPO_ROOT / "lms"
self.storage = ThemeStorage(location=self.themes_dir / self.enabled_theme / 'lms' / 'static')
@override_settings(DEBUG=True)
@ddt.data(
(True, "images/logo.png"),
(True, "images/favicon.ico"),
(False, "images/spinning.gif"),
)
@ddt.unpack
def test_themed(self, is_themed, asset):
"""
Verify storage returns True on themed assets
"""
self.assertEqual(is_themed, self.storage.themed(asset, self.enabled_theme))
@override_settings(DEBUG=True)
@ddt.data(
("images/logo.png", ),
("images/favicon.ico", ),
)
@ddt.unpack
def test_url(self, asset):
"""
Verify storage returns correct url depending upon the enabled theme
"""
with patch(
"openedx.core.djangoapps.theming.storage.get_current_site_theme_dir",
return_value=self.enabled_theme,
):
asset_url = self.storage.url(asset)
# remove hash key from file url
asset_url = re.sub(r"(\.\w+)(\.png|\.ico)$", r"\g<2>", asset_url)
expected_url = self.storage.base_url + self.enabled_theme + "/" + asset
self.assertEqual(asset_url, expected_url)
@override_settings(DEBUG=True)
@ddt.data(
("images/logo.png", ),
("images/favicon.ico", ),
)
@ddt.unpack
def test_path(self, asset):
"""
Verify storage returns correct file path depending upon the enabled theme
"""
with patch(
"openedx.core.djangoapps.theming.storage.get_current_site_theme_dir",
return_value=self.enabled_theme,
):
returned_path = self.storage.path(asset)
expected_path = self.themes_dir / self.enabled_theme / "lms/static/" / asset
self.assertEqual(expected_path, returned_path)

View File

@@ -1,235 +0,0 @@
"""
Tests for comprehensive themes.
"""
import unittest
from django.conf import settings
from django.test import TestCase, override_settings
from django.contrib import staticfiles
from paver.easy import call_task
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestComprehensiveThemeLMS(TestCase):
"""
Test html, sass and static file overrides for comprehensive themes.
"""
def setUp(self):
"""
Clear static file finders cache and register cleanup methods.
"""
super(TestComprehensiveThemeLMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Enable Comprehensive theme and compile sass files.
"""
# Apply Comprehensive theme and compile sass assets.
compile_sass('lms')
super(TestComprehensiveThemeLMS, cls).setUpClass()
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_footer(self):
"""
Test that theme footer is used instead of default footer.
"""
resp = self.client.get('/')
self.assertEqual(resp.status_code, 200)
# This string comes from header.html of test-theme
self.assertContains(resp, "This is a footer for test-theme.")
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_logo_image(self):
"""
Test that theme logo is used instead of default logo.
"""
result = staticfiles.finders.find('test-theme/images/logo.png')
self.assertEqual(result, settings.TEST_THEME / 'lms/static/images/logo.png')
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_css_files(self):
"""
Test that theme sass files are used instead of default sass files.
"""
result = staticfiles.finders.find('test-theme/css/lms-main-v1.css')
self.assertEqual(result, settings.TEST_THEME / "lms/static/css/lms-main-v1.css")
lms_main_css = ""
with open(result) as css_file:
lms_main_css += css_file.read()
self.assertIn("background:#00fa00", lms_main_css)
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
class TestComprehensiveThemeCMS(TestCase):
"""
Test html, sass and static file overrides for comprehensive themes.
"""
def setUp(self):
"""
Clear static file finders cache and register cleanup methods.
"""
super(TestComprehensiveThemeCMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Enable Comprehensive theme and compile sass files.
"""
# Apply Comprehensive theme and compile sass assets.
compile_sass('cms')
super(TestComprehensiveThemeCMS, cls).setUpClass()
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_template_override(self):
"""
Test that theme templates are used instead of default templates.
"""
resp = self.client.get('/signin')
self.assertEqual(resp.status_code, 200)
# This string comes from login.html of test-theme
self.assertContains(resp, "Login Page override for test-theme.")
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_css_files(self):
"""
Test that theme sass files are used instead of default sass files.
"""
result = staticfiles.finders.find('test-theme/css/studio-main-v1.css')
self.assertEqual(result, settings.TEST_THEME / "cms/static/css/studio-main-v1.css")
cms_main_css = ""
with open(result) as css_file:
cms_main_css += css_file.read()
self.assertIn("background:#00fa00", cms_main_css)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestComprehensiveThemeDisabledLMS(TestCase):
"""
Test Sass compilation order and sass overrides for comprehensive themes.
"""
def setUp(self):
"""
Clear static file finders cache.
"""
super(TestComprehensiveThemeDisabledLMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Compile sass files.
"""
# compile LMS SASS
compile_sass('lms')
super(TestComprehensiveThemeDisabledLMS, cls).setUpClass()
def test_logo(self):
"""
Test that default logo is picked in case of no comprehensive theme.
"""
result = staticfiles.finders.find('images/logo.png')
self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/logo.png')
def test_css(self):
"""
Test that default css files served without comprehensive themes applied.
"""
result = staticfiles.finders.find('css/lms-main-v1.css')
self.assertEqual(result, settings.REPO_ROOT / "lms/static/css/lms-main-v1.css")
lms_main_css = ""
with open(result) as css_file:
lms_main_css += css_file.read()
self.assertNotIn("background:#00fa00", lms_main_css)
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
class TestComprehensiveThemeDisabledCMS(TestCase):
"""
Test default html, sass and static file when no theme is applied.
"""
def setUp(self):
"""
Clear static file finders cache and register cleanup methods.
"""
super(TestComprehensiveThemeDisabledCMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Enable Comprehensive theme and compile sass files.
"""
# Apply Comprehensive theme and compile sass assets.
compile_sass('cms')
super(TestComprehensiveThemeDisabledCMS, cls).setUpClass()
def test_template_override(self):
"""
Test that defaults templates are used when no theme is applied.
"""
resp = self.client.get('/signin')
self.assertEqual(resp.status_code, 200)
self.assertNotContains(resp, "Login Page override for test-theme.")
def test_css_files(self):
"""
Test that default css files served without comprehensive themes applied..
"""
result = staticfiles.finders.find('css/studio-main-v1.css')
self.assertEqual(result, settings.REPO_ROOT / "cms/static/css/studio-main-v1.css")
cms_main_css = ""
with open(result) as css_file:
cms_main_css += css_file.read()
self.assertNotIn("background:#00fa00", cms_main_css)
def compile_sass(system):
"""
Process xmodule assets and compile sass files for the given system.
:param system - 'lms' or 'cms', specified the system to compile sass for.
"""
# Compile system sass files
call_task(
'pavelib.assets.update_assets',
args=(
system,
"--themes_dir={}".format(settings.TEST_THEME.dirname()),
"--themes={}".format(settings.TEST_THEME.basename()),
"--settings=test"),
)

View File

@@ -17,22 +17,3 @@ def cleanup_tempdir(the_dir):
"""Called on process exit to remove a temp directory."""
if os.path.exists(the_dir):
shutil.rmtree(the_dir)
def create_symlink(src, dest):
"""
Creates a symbolic link which will be deleted when the process ends.
:param src: path to source
:param dest: path to destination
"""
os.symlink(src, dest)
atexit.register(delete_symlink, dest)
def delete_symlink(link_path):
"""
Removes symbolic link for
:param link_path:
"""
if os.path.exists(link_path):
os.remove(link_path)

View File

@@ -1,18 +1,17 @@
"""
Django storage backends for Open edX.
"""
from django.contrib.staticfiles.storage import StaticFilesStorage
from pipeline.storage import NonPackagingMixin
from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
from pipeline.storage import PipelineMixin, NonPackagingMixin
from require.storage import OptimizedFilesMixin
from openedx.core.djangoapps.theming.storage import ThemeStorage, ComprehensiveThemingCachedFilesMixin, \
ThemePipelineMixin
from openedx.core.djangoapps.theming.storage import ComprehensiveThemingAwareMixin
class ProductionStorage(
ComprehensiveThemingAwareMixin,
OptimizedFilesMixin,
ThemePipelineMixin,
ComprehensiveThemingCachedFilesMixin,
ThemeStorage,
PipelineMixin,
CachedFilesMixin,
StaticFilesStorage
):
"""
@@ -23,9 +22,9 @@ class ProductionStorage(
class DevelopmentStorage(
ComprehensiveThemingAwareMixin,
NonPackagingMixin,
ThemePipelineMixin,
ThemeStorage,
PipelineMixin,
StaticFilesStorage
):
"""