Commit 68312bdd by Matt Drayer Committed by Saleem Latif

Revert "Revert "saleem-latif/WL-328: Multi-Site Comprehensive Theming""

2. Update COMPREHNSIVE_THEME_DIR to COMPREHENSIVE_THEME_DIRS
3. Update paver commands to support multi theme dirs
4. Updating template loaders
5. Add ENABLE_COMPREHENSIVE_THEMING flag to enable or disable theming via settings
6. Update tests
7. Add backward compatibility for COMPREHEHNSIVE_THEME_DIR
parent d328328f
......@@ -211,7 +211,14 @@ ASSET_IGNORE_REGEX = ENV_TOKENS.get('ASSET_IGNORE_REGEX', ASSET_IGNORE_REGEX)
# Theme overrides
THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
COMPREHENSIVE_THEME_DIR = path(ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', COMPREHENSIVE_THEME_DIR))
# following setting is for backward compatibility
if ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', None):
COMPREHENSIVE_THEME_DIR = ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR')
COMPREHENSIVE_THEME_DIRS = ENV_TOKENS.get('COMPREHENSIVE_THEME_DIRS', COMPREHENSIVE_THEME_DIRS) or []
DEFAULT_SITE_THEME = ENV_TOKENS.get('DEFAULT_SITE_THEME', DEFAULT_SITE_THEME)
ENABLE_COMPREHENSIVE_THEMING = ENV_TOKENS.get('ENABLE_COMPREHENSIVE_THEMING', ENABLE_COMPREHENSIVE_THEMING)
#Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
......
......@@ -59,9 +59,9 @@ STATIC_URL = "/static/"
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
)
STATICFILES_DIRS = (
STATICFILES_DIRS = [
(TEST_ROOT / "staticfiles" / "cms").abspath(),
)
]
# Silence noisy logs
import logging
......
......@@ -47,7 +47,7 @@ import lms.envs.common
from lms.envs.common import (
USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, DATA_DIR, ALL_LANGUAGES, WIKI_ENABLED,
update_module_store_settings, ASSET_IGNORE_REGEX, COPYRIGHT_YEAR,
PARENTAL_CONSENT_AGE_LIMIT, COMPREHENSIVE_THEME_DIR, REGISTRATION_EMAIL_PATTERNS_ALLOWED,
PARENTAL_CONSENT_AGE_LIMIT, COMPREHENSIVE_THEME_DIRS, REGISTRATION_EMAIL_PATTERNS_ALLOWED,
# The following PROFILE_IMAGE_* settings are included as they are
# indirectly accessed through the email opt-in API, which is
# technically accessible through the CMS via legacy URLs.
......@@ -61,7 +61,20 @@ from lms.envs.common import (
# Django REST framework configuration
REST_FRAMEWORK,
STATICI18N_OUTPUT_DIR
STATICI18N_OUTPUT_DIR,
# Theme to use when no site or site theme is defined,
DEFAULT_SITE_THEME,
# Default site to use if no site exists matching request headers
SITE_ID,
# Enable or disable theming
ENABLE_COMPREHENSIVE_THEMING,
# constants for redirects app
REDIRECT_CACHE_TIMEOUT,
REDIRECT_CACHE_KEY_PREFIX,
)
from path import Path as path
from warnings import simplefilter
......@@ -318,6 +331,7 @@ MIDDLEWARE_CLASSES = (
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.sites.middleware.CurrentSiteMiddleware',
# Instead of SessionMiddleware, we use a more secure version
# 'django.contrib.sessions.middleware.SessionMiddleware',
......@@ -356,6 +370,8 @@ MIDDLEWARE_CLASSES = (
# for expiring inactive sessions
'session_inactivity_timeout.middleware.SessionInactivityTimeout',
'openedx.core.djangoapps.theming.middleware.CurrentSiteThemeMiddleware',
# use Django built in clickjacking protection
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
......@@ -451,7 +467,6 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
# Site info
SITE_ID = 1
SITE_NAME = "localhost:8001"
HTTPS = 'on'
ROOT_URLCONF = 'cms.urls'
......@@ -522,7 +537,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
# List of finder classes that know how to find static files in various locations.
# Note: the pipeline finder is included to be able to discover optimized files
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'openedx.core.lib.xblock_pipeline.finder.XBlockPipelineFinder',
......
......@@ -41,7 +41,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
# Revert to the default set of finders as we don't want the production pipeline
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
......
......@@ -41,6 +41,6 @@ STATIC_URL = "/static/"
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
)
STATICFILES_DIRS = (
STATICFILES_DIRS = [
(TEST_ROOT / "staticfiles" / "cms").abspath(),
)
]
......@@ -34,6 +34,7 @@ from lms.envs.test import (
DEFAULT_FILE_STORAGE,
MEDIA_ROOT,
MEDIA_URL,
COMPREHENSIVE_THEME_DIRS,
)
# mongo connection settings
......@@ -285,6 +286,8 @@ MICROSITE_CONFIGURATION = {
MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver'
MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
TEST_THEME = COMMON_ROOT / "test" / "test-theme"
# For consistency in user-experience, keep the value of this setting in sync with
# the one in lms/envs/test.py
FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
......
......@@ -18,7 +18,8 @@ from openedx.core.lib.xblock_utils import xblock_local_resource_url
import xmodule.x_module
import cms.lib.xblock.runtime
from openedx.core.djangoapps.theming.core import enable_comprehensive_theme
from openedx.core.djangoapps.theming.core import enable_theming
from openedx.core.djangoapps.theming.helpers import is_comprehensive_theming_enabled
def run():
......@@ -30,8 +31,8 @@ def run():
# Comprehensive theming needs to be set up before django startup,
# because modifying django template paths after startup has no effect.
if settings.COMPREHENSIVE_THEME_DIR:
enable_comprehensive_theme(settings.COMPREHENSIVE_THEME_DIR)
if is_comprehensive_theming_enabled():
enable_theming()
django.setup()
......
......@@ -25,6 +25,7 @@ from embargo.test_utils import restrict_course
from student.models import CourseEnrollment
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from util.testing import UrlResetMixin
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
@attr('shard_3')
......@@ -374,7 +375,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
self.assertEquals(course_modes, expected_modes)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@theming_test_utils.with_is_edx_domain(True)
@with_comprehensive_theme("edx.org")
def test_hide_nav(self):
# Create the course modes
for mode in ["honor", "verified"]:
......
......@@ -42,10 +42,14 @@ class MakoLoader(object):
def load_template(self, template_name, template_dirs=None):
source, file_path = self.load_template_source(template_name, template_dirs)
# In order to allow dynamic template overrides, we need to cache templates based on their absolute paths
# rather than relative paths, overriding templates would have same relative paths.
module_directory = self.module_directory.rstrip("/") + "/{dir_hash}/".format(dir_hash=hash(file_path))
if source.startswith("## mako\n"):
# This is a mako template
template = Template(filename=file_path,
module_directory=self.module_directory,
module_directory=module_directory,
input_encoding='utf-8',
output_encoding='utf-8',
default_filters=['decode.utf8'],
......
......@@ -9,9 +9,14 @@ import pkg_resources
from django.conf import settings
from mako.lookup import TemplateLookup
from mako.exceptions import TopLevelLookupException
from microsite_configuration import microsite
from . import LOOKUP
from openedx.core.djangoapps.theming.helpers import (
get_template as themed_template,
get_template_path_with_theme,
strip_site_theme_templates_path,
)
class DynamicTemplateLookup(TemplateLookup):
......@@ -49,15 +54,29 @@ class DynamicTemplateLookup(TemplateLookup):
def get_template(self, uri):
"""
Overridden method which will hand-off the template lookup to the microsite subsystem
"""
microsite_template = microsite.get_template(uri)
Overridden method for locating a template in either the database or the site theme.
return (
microsite_template
if microsite_template
else super(DynamicTemplateLookup, self).get_template(uri)
)
If not found, template lookup will be done in comprehensive theme for current site
by prefixing path to theme.
e.g if uri is `main.html` then new uri would be something like this `/red-theme/lms/static/main.html`
If still unable to find a template, it will fallback to the default template directories after stripping off
the prefix path to theme.
"""
# try to get template for the given file from microsite
template = themed_template(uri)
# if microsite template is not present or request is not in microsite then
# let mako find and serve a template
if not template:
try:
# Try to find themed template, i.e. see if current theme overrides the template
template = super(DynamicTemplateLookup, self).get_template(get_template_path_with_theme(uri))
except TopLevelLookupException:
# strip off the prefix path to theme and look in default template dirs
template = super(DynamicTemplateLookup, self).get_template(strip_site_theme_templates_path(uri))
return template
def clear_lookups(namespace):
......
......@@ -12,16 +12,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.template import Context
from django.http import HttpResponse
import logging
from django.http import HttpResponse
from django.template import Context
from microsite_configuration import microsite
from edxmako import lookup_template
from edxmako.request_context import get_template_request_context
from django.conf import settings
from django.core.urlresolvers import reverse
from openedx.core.djangoapps.theming.helpers import get_template_path
log = logging.getLogger(__name__)
......@@ -134,8 +136,7 @@ def render_to_string(template_name, dictionary, context=None, namespace='main',
this template. If not supplied, the current request will be used.
"""
# see if there is an override template defined in the microsite
template_name = microsite.get_template_path(template_name)
template_name = get_template_path(template_name)
context_instance = Context(dictionary)
# add dictionary to context_instance
......
......@@ -11,7 +11,6 @@ BaseMicrositeTemplateBackend is Base Class for the microsite template backend.
from __future__ import absolute_import
import abc
import edxmako
import os.path
import threading
......@@ -272,9 +271,7 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend):
Configure the paths for the microsites feature
"""
microsites_root = settings.MICROSITE_ROOT_DIR
if os.path.isdir(microsites_root):
edxmako.paths.add_lookup('main', microsites_root)
settings.STATICFILES_DIRS.insert(0, microsites_root)
log.info('Loading microsite path at %s', microsites_root)
......@@ -292,6 +289,7 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend):
microsites_root = settings.MICROSITE_ROOT_DIR
if self.has_configuration_set():
settings.MAKO_TEMPLATES['main'].insert(0, microsites_root)
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].append(microsites_root)
......
......@@ -105,6 +105,23 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase):
microsite.clear()
self.assertIsNone(microsite.get_value('platform_name'))
def test_enable_microsites_pre_startup(self):
"""
Tests microsite.test_enable_microsites_pre_startup works as expected.
"""
# remove microsite root directory paths first
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'] = [
path for path in settings.DEFAULT_TEMPLATE_ENGINE['DIRS']
if path != settings.MICROSITE_ROOT_DIR
]
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': False}):
microsite.enable_microsites_pre_startup(log)
self.assertNotIn(settings.MICROSITE_ROOT_DIR, settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}):
microsite.enable_microsites_pre_startup(log)
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.MAKO_TEMPLATES['main'])
@patch('edxmako.paths.add_lookup')
def test_enable_microsites(self, add_lookup):
"""
......@@ -122,7 +139,6 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase):
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}):
microsite.enable_microsites(log)
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.STATICFILES_DIRS)
add_lookup.assert_called_once_with('main', settings.MICROSITE_ROOT_DIR)
def test_get_all_configs(self):
"""
......
......@@ -49,7 +49,7 @@ class PipelineRenderTest(TestCase):
Create static assets once for all pipeline render tests.
"""
super(PipelineRenderTest, cls).setUpClass()
call_task('pavelib.assets.update_assets', args=('lms', '--settings=test'))
call_task('pavelib.assets.update_assets', args=('lms', '--settings=test', '--themes=no'))
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
@ddt.data(
......
......@@ -20,8 +20,8 @@ from django.conf import settings
from edxmako.shortcuts import render_to_string
from util.request import safe_get_host
from util.testing import EventTestMixin
from openedx.core.djangoapps.theming.tests.test_util import with_is_edx_domain
from openedx.core.djangoapps.theming import helpers as theming_helpers
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
class TestException(Exception):
......@@ -99,7 +99,7 @@ class ActivationEmailTests(TestCase):
self._create_account()
self._assert_activation_email(self.ACTIVATION_SUBJECT, self.OPENEDX_FRAGMENTS)
@with_is_edx_domain(True)
@with_comprehensive_theme("edx.org")
def test_activation_email_edx_domain(self):
self._create_account()
self._assert_activation_email(self.ACTIVATION_SUBJECT, self.EDX_DOMAIN_FRAGMENTS)
......
......@@ -46,7 +46,6 @@ from certificates.tests.factories import GeneratedCertificateFactory # pylint:
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
import shoppingcart # pylint: disable=import-error
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangoapps.theming.tests.test_util import with_is_edx_domain
# Explicitly import the cache from ConfigurationModel so we can reset it after each test
from config_models.models import cache
......@@ -484,7 +483,6 @@ class DashboardTest(ModuleStoreTestCase):
self.assertEquals(response_2.status_code, 200)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@with_is_edx_domain(True)
def test_dashboard_header_nav_has_find_courses(self):
self.client.login(username="jack", password="test")
response = self.client.get(reverse("dashboard"))
......
[
{
"pk": 2,
"model": "sites.Site",
"fields": {
"domain": "localhost:8003",
"name": "lms"
}
},
{
"pk": 3,
"model": "sites.Site",
"fields": {
"domain": "localhost:8031",
"name": "cms"
}
}
]
// studio - utilities - variables
// ====================
// Table of Contents
// * +Paths
// * +Grid
// * +Fonts
// * +Colors - Utility
// * +Colors - Primary
// * +Colors - Shadow
// * +Color - Application
// * +Timing
// * +Archetype UI
// * +Specific UI
// * +Deprecated
$baseline: 20px;
// +Paths
// ====================
$static-path: '..' !default;
// +Grid
// ====================
$gw-column: ($baseline*3);
$gw-gutter: $baseline;
$fg-column: $gw-column;
$fg-gutter: $gw-gutter;
$fg-max-columns: 12;
$fg-max-width: 1280px;
$fg-min-width: 900px;
// +Fonts
// ====================
$f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif;
$f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
// +Colors - Utility
// ====================
$transparent: rgba(0,0,0,0); // used when color value is needed for UI width/transitions but element is transparent
// +Colors - Primary
// ====================
$black: rgb(0,0,0);
$black-t0: rgba($black, 0.125);
$black-t1: rgba($black, 0.25);
$black-t2: rgba($black, 0.5);
$black-t3: rgba($black, 0.75);
$white: rgb(255,255,255);
$white-t0: rgba($white, 0.125);
$white-t1: rgba($white, 0.25);
$white-t2: rgba($white, 0.5);
$white-t3: rgba($white, 0.75);
$gray: rgb(127,127,127);
$gray-l1: tint($gray,20%);
$gray-l2: tint($gray,40%);
$gray-l3: tint($gray,60%);
$gray-l4: tint($gray,80%);
$gray-l5: tint($gray,90%);
$gray-l6: tint($gray,95%);
$gray-l7: tint($gray,99%);
$gray-d1: shade($gray,20%);
$gray-d2: shade($gray,40%);
$gray-d3: shade($gray,60%);
$gray-d4: shade($gray,80%);
$blue: rgb(0, 159, 230);
$blue-l1: tint($blue,20%);
$blue-l2: tint($blue,40%);
$blue-l3: tint($blue,60%);
$blue-l4: tint($blue,80%);
$blue-l5: tint($blue,90%);
$blue-d1: shade($blue,20%);
$blue-d2: shade($blue,40%);
$blue-d3: shade($blue,60%);
$blue-d4: shade($blue,80%);
$blue-s1: saturate($blue,15%);
$blue-s2: saturate($blue,30%);
$blue-s3: saturate($blue,45%);
$blue-u1: desaturate($blue,15%);
$blue-u2: desaturate($blue,30%);
$blue-u3: desaturate($blue,45%);
$blue-t0: rgba($blue, 0.125);
$blue-t1: rgba($blue, 0.25);
$blue-t2: rgba($blue, 0.50);
$blue-t3: rgba($blue, 0.75);
$pink: rgb(183, 37, 103); // #b72567;
$pink-l1: tint($pink,20%);
$pink-l2: tint($pink,40%);
$pink-l3: tint($pink,60%);
$pink-l4: tint($pink,80%);
$pink-l5: tint($pink,90%);
$pink-d1: shade($pink,20%);
$pink-d2: shade($pink,40%);
$pink-d3: shade($pink,60%);
$pink-d4: shade($pink,80%);
$pink-s1: saturate($pink,15%);
$pink-s2: saturate($pink,30%);
$pink-s3: saturate($pink,45%);
$pink-u1: desaturate($pink,15%);
$pink-u2: desaturate($pink,30%);
$pink-u3: desaturate($pink,45%);
$red: rgb(178, 6, 16); // #b20610;
$red-l1: tint($red,20%);
$red-l2: tint($red,40%);
$red-l3: tint($red,60%);
$red-l4: tint($red,80%);
$red-l5: tint($red,90%);
$red-d1: shade($red,20%);
$red-d2: shade($red,40%);
$red-d3: shade($red,60%);
$red-d4: shade($red,80%);
$red-s1: saturate($red,15%);
$red-s2: saturate($red,30%);
$red-s3: saturate($red,45%);
$red-u1: desaturate($red,15%);
$red-u2: desaturate($red,30%);
$red-u3: desaturate($red,45%);
$green: rgb(37, 184, 90); // #25b85a
$green-l1: tint($green,20%);
$green-l2: tint($green,40%);
$green-l3: tint($green,60%);
$green-l4: tint($green,80%);
$green-l5: tint($green,90%);
$green-d1: shade($green,20%);
$green-d2: shade($green,40%);
$green-d3: shade($green,60%);
$green-d4: shade($green,80%);
$green-s1: saturate($green,15%);
$green-s2: saturate($green,30%);
$green-s3: saturate($green,45%);
$green-u1: desaturate($green,15%);
$green-u2: desaturate($green,30%);
$green-u3: desaturate($green,45%);
$yellow: rgb(237, 189, 60);
$yellow-l1: tint($yellow,20%);
$yellow-l2: tint($yellow,40%);
$yellow-l3: tint($yellow,60%);
$yellow-l4: tint($yellow,80%);
$yellow-l5: tint($yellow,90%);
$yellow-d1: shade($yellow,20%);
$yellow-d2: shade($yellow,40%);
$yellow-d3: shade($yellow,60%);
$yellow-d4: shade($yellow,80%);
$yellow-s1: saturate($yellow,15%);
$yellow-s2: saturate($yellow,30%);
$yellow-s3: saturate($yellow,45%);
$yellow-u1: desaturate($yellow,15%);
$yellow-u2: desaturate($yellow,30%);
$yellow-u3: desaturate($yellow,45%);
$orange: rgb(237, 189, 60);
$orange-l1: tint($orange,20%);
$orange-l2: tint($orange,40%);
$orange-l3: tint($orange,60%);
$orange-l4: tint($orange,80%);
$orange-l5: tint($orange,90%);
$orange-d1: shade($orange,20%);
$orange-d2: shade($orange,40%);
$orange-d3: shade($orange,60%);
$orange-d4: shade($orange,80%);
$orange-s1: saturate($orange,15%);
$orange-s2: saturate($orange,30%);
$orange-s3: saturate($orange,45%);
$orange-u1: desaturate($orange,15%);
$orange-u2: desaturate($orange,30%);
$orange-u3: desaturate($orange,45%);
// +Colors - Shadows
// ====================
$shadow: rgba($black, 0.2);
$shadow-l1: rgba($black, 0.1);
$shadow-l2: rgba($black, 0.05);
$shadow-d1: rgba($black, 0.4);
$shadow-d2: rgba($black, 0.6);
// +Colors - Application
// ====================
$color-draft: $gray-l3;
$color-live: $blue;
$color-ready: $green;
$color-warning: $orange-l2;
$color-error: $red-l2;
$color-staff-only: $black;
$color-gated: $black;
$color-visibility-set: $black;
$color-heading-base: $gray-d2;
$color-copy-base: $gray-l1;
$color-copy-emphasized: $gray-d2;
// +Timing
// ====================
// used for animation/transition mixin syncing
$tmg-s3: 3.0s;
$tmg-s2: 2.0s;
$tmg-s1: 1.0s;
$tmg-avg: 0.75s;
$tmg-f1: 0.50s;
$tmg-f2: 0.25s;
$tmg-f3: 0.125s;
// +Archetype UI
// ====================
$ui-action-primary-color: $blue-u2;
$ui-action-primary-color-focus: $blue-s1;
$ui-link-color: $blue-u2;
$ui-link-color-focus: $blue-s1;
// +Specific UI
// ====================
$ui-notification-height: ($baseline*10);
$ui-update-color: $blue-l4;
// +Deprecated
// ====================
// do not use, future clean up will use updated styles
$baseFontColor: $gray-d2;
$lighter-base-font-color: rgb(100,100,100);
$offBlack: #3c3c3c;
$green: #108614;
$lightGrey: #edf1f5;
$mediumGrey: #b0b6c2;
$darkGrey: #8891a1;
$extraDarkGrey: #3d4043;
$paleYellow: #fffcf1;
$yellow: rgb(255, 254, 223);
$green: rgb(37, 184, 90);
$brightGreen: rgb(22, 202, 87);
$disabledGreen: rgb(124, 206, 153);
$darkGreen: rgb(52, 133, 76);
// These colors are updated for testing purposes
$lightBluishGrey: rgb(0, 250, 0);
$lightBluishGrey2: rgb(0, 250, 0);
$error-red: rgb(253, 87, 87);
//carryover from LMS for xmodules
$sidebar-color: rgb(246, 246, 246);
// type
$sans-serif: $f-sans-serif;
$body-line-height: golden-ratio(.875em, 1);
// carried over from LMS for xmodules
$action-primary-active-bg: #1AA1DE; // $m-blue
$very-light-text: $white;
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "login" %></%def>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from openedx.core.djangolib.js_utils import js_escaped_string
%>
<%block name="title">${_("Sign In")}</%block>
<%block name="bodyclass">not-signedin view-signin</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<header>
<h1 class="title title-1">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</h1>
<a href="${reverse('signup')}" class="action action-signin">${_("Don't have a {studio_name} Account? Sign up!").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
</header>
<!-- Login Page override for test-theme. -->
<article class="content-primary" role="main">
<form id="login_form" method="post" action="login_post" onsubmit="return false;">
<fieldset>
<legend class="sr">${_("Required Information to Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</legend>
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf }" />
<ol class="list-input">
<li class="field text required" id="field-email">
<label for="email">${_("E-mail")}</label>
<input id="email" type="email" name="email" placeholder="${_('example: username@domain.com')}"/>
</li>
<li class="field text required" id="field-password">
<label for="password">${_("Password")}</label>
<input id="password" type="password" name="password" />
<a href="${forgot_password_link}" class="action action-forgotpassword">${_("Forgot password?")}</a>
</li>
</ol>
</fieldset>
<div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</button>
</div>
<!-- no honor code for CMS, but need it because we're using the lms student object -->
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
</form>
</article>
</section>
</div>
</%block>
<%block name="requirejs">
require(["js/factories/login"], function(LoginFactory) {
LoginFactory("${reverse('homepage') | n, js_escaped_string }");
});
</%block>
@import 'lms/static/sass/partials/base/variables';
$header-bg: rgb(0,250,0);
$footer-bg: rgb(0,250,0);
$container-bg: rgb(0,250,0);
<%page expression_filter="h"/>
<div class="wrapper wrapper-footer">
<footer>
<div class="colophon">
<div class="colophon-about">
<p>This is a footer for test-theme.</p>
</div>
</div>
</footer>
</div>
......@@ -10,7 +10,7 @@ import mock
import ddt
from config_models.models import cache
from branding.models import BrandingApiConfig
from openedx.core.djangoapps.theming.tests.test_util import with_edx_domain_context
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme_context
@ddt.ddt
......@@ -30,19 +30,19 @@ class TestFooter(TestCase):
@ddt.data(
# Open source version
(False, "application/json", "application/json; charset=utf-8", "Open edX"),
(False, "text/html", "text/html; charset=utf-8", "lms-footer.css"),
(False, "text/html", "text/html; charset=utf-8", "Open edX"),
(None, "application/json", "application/json; charset=utf-8", "Open edX"),
(None, "text/html", "text/html; charset=utf-8", "lms-footer.css"),
(None, "text/html", "text/html; charset=utf-8", "Open edX"),
# EdX.org version
(True, "application/json", "application/json; charset=utf-8", "edX Inc"),
(True, "text/html", "text/html; charset=utf-8", "lms-footer-edx.css"),
(True, "text/html", "text/html; charset=utf-8", "edX Inc"),
("edx.org", "application/json", "application/json; charset=utf-8", "edX Inc"),
("edx.org", "text/html", "text/html; charset=utf-8", "lms-footer-edx.css"),
("edx.org", "text/html", "text/html; charset=utf-8", "edX Inc"),
)
@ddt.unpack
def test_footer_content_types(self, is_edx_domain, accepts, content_type, content):
def test_footer_content_types(self, theme, accepts, content_type, content):
self._set_feature_flag(True)
with with_edx_domain_context(is_edx_domain):
with with_comprehensive_theme_context(theme):
resp = self._get_footer(accepts=accepts)
self.assertEqual(resp.status_code, 200)
......@@ -50,10 +50,10 @@ class TestFooter(TestCase):
self.assertIn(content, resp.content)
@mock.patch.dict(settings.FEATURES, {'ENABLE_FOOTER_MOBILE_APP_LINKS': True})
@ddt.data(True, False)
def test_footer_json(self, is_edx_domain):
@ddt.data("edx.org", None)
def test_footer_json(self, theme):
self._set_feature_flag(True)
with with_edx_domain_context(is_edx_domain):
with with_comprehensive_theme_context(theme):
resp = self._get_footer()
self.assertEqual(resp.status_code, 200)
......@@ -142,18 +142,18 @@ class TestFooter(TestCase):
@ddt.data(
# OpenEdX
(False, "en", "lms-footer.css"),
(False, "ar", "lms-footer-rtl.css"),
(None, "en", "lms-footer.css"),
(None, "ar", "lms-footer-rtl.css"),
# EdX.org
(True, "en", "lms-footer-edx.css"),
(True, "ar", "lms-footer-edx-rtl.css"),
("edx.org", "en", "lms-footer-edx.css"),
("edx.org", "ar", "lms-footer-edx-rtl.css"),
)
@ddt.unpack
def test_language_rtl(self, is_edx_domain, language, static_path):
def test_language_rtl(self, theme, language, static_path):
self._set_feature_flag(True)
with with_edx_domain_context(is_edx_domain):
with with_comprehensive_theme_context(theme):
resp = self._get_footer(accepts="text/html", params={'language': language})
self.assertEqual(resp.status_code, 200)
......@@ -161,18 +161,18 @@ class TestFooter(TestCase):
@ddt.data(
# OpenEdX
(False, True),
(False, False),
(None, True),
(None, False),
# EdX.org
(True, True),
(True, False),
("edx.org", True),
("edx.org", False),
)
@ddt.unpack
def test_show_openedx_logo(self, is_edx_domain, show_logo):
def test_show_openedx_logo(self, theme, show_logo):
self._set_feature_flag(True)
with with_edx_domain_context(is_edx_domain):
with with_comprehensive_theme_context(theme):
params = {'show-openedx-logo': 1} if show_logo else {}
resp = self._get_footer(accepts="text/html", params=params)
......@@ -185,17 +185,17 @@ class TestFooter(TestCase):
@ddt.data(
# OpenEdX
(False, False),
(False, True),
(None, False),
(None, True),
# EdX.org
(True, False),
(True, True),
("edx.org", False),
("edx.org", True),
)
@ddt.unpack
def test_include_dependencies(self, is_edx_domain, include_dependencies):
def test_include_dependencies(self, theme, include_dependencies):
self._set_feature_flag(True)
with with_edx_domain_context(is_edx_domain):
with with_comprehensive_theme_context(theme):
params = {'include-dependencies': 1} if include_dependencies else {}
resp = self._get_footer(accepts="text/html", params=params)
......
......@@ -8,7 +8,7 @@ from django.test import TestCase
import mock
from student.tests.factories import UserFactory
from openedx.core.djangoapps.theming.tests.test_util import with_is_edx_domain
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
class UserMixin(object):
......@@ -85,7 +85,7 @@ class ReceiptViewTests(UserMixin, TestCase):
self.assertRegexpMatches(response.content, user_message if is_user_message_expected else system_message)
self.assertNotRegexpMatches(response.content, user_message if not is_user_message_expected else system_message)
@with_is_edx_domain(True)
@with_comprehensive_theme("edx.org")
def test_hide_nav_header(self):
self._login()
post_data = {'decision': 'ACCEPT', 'reason_code': '200', 'signed_field_names': 'dummy'}
......
"""
Tests for wiki middleware.
"""
from django.conf import settings
from django.test.client import Client
from nose.plugins.attrib import attr
from unittest import skip
......@@ -35,7 +34,7 @@ class TestComprehensiveTheming(ModuleStoreTestCase):
self.client.login(username='instructor', password='secret')
@skip("Fails when run immediately after lms.djangoapps.course_wiki.tests.test_middleware")
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
@with_comprehensive_theme('red-theme')
def test_themed_footer(self):
"""
Tests that theme footer is used rather than standard
......
......@@ -6,8 +6,6 @@ import re
import cgi
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
......@@ -51,21 +49,6 @@ def course_wiki_redirect(request, course_id): # pylint: disable=unused-argument
if not valid_slug:
return redirect("wiki:get", path="")
# The wiki needs a Site object created. We make sure it exists here
try:
Site.objects.get_current()
except Site.DoesNotExist:
new_site = Site()
new_site.domain = settings.SITE_NAME
new_site.name = "edX"
new_site.save()
site_id = str(new_site.id)
if site_id != str(settings.SITE_ID):
msg = "No site object was created and the SITE_ID doesn't match the newly created one. {} != {}".format(
site_id, settings.SITE_ID
)
raise ImproperlyConfigured(msg)
try:
urlpath = URLPath.get_by_path(course_slug, select_related=True)
......
......@@ -7,7 +7,7 @@ from path import path # pylint: disable=no-name-in-module
from django.contrib import staticfiles
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
from openedx.core.lib.tempdir import mkdtemp_clean
from openedx.core.lib.tempdir import mkdtemp_clean, create_symlink, delete_symlink
class TestComprehensiveTheming(TestCase):
......@@ -19,8 +19,13 @@ class TestComprehensiveTheming(TestCase):
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
@with_comprehensive_theme('red-theme')
def test_red_footer(self):
"""
Tests templates from theme are rendered if available.
`red-theme` has header.html and footer.html so this test
asserts presence of the content from header.html and footer.html
"""
resp = self.client.get('/')
self.assertEqual(resp.status_code, 200)
# This string comes from footer.html
......@@ -34,12 +39,16 @@ class TestComprehensiveTheming(TestCase):
# of test.
# Make a temp directory as a theme.
tmp_theme = path(mkdtemp_clean())
template_dir = tmp_theme / "lms/templates"
themes_dir = path(mkdtemp_clean())
tmp_theme = "temp_theme"
template_dir = themes_dir / tmp_theme / "lms/templates"
template_dir.makedirs()
with open(template_dir / "footer.html", "w") as footer:
footer.write("<footer>TEMPORARY THEME</footer>")
dest_path = path(settings.COMPREHENSIVE_THEME_DIRS[0]) / tmp_theme
create_symlink(themes_dir / tmp_theme, dest_path)
@with_comprehensive_theme(tmp_theme)
def do_the_test(self):
"""A function to do the work so we can use the decorator."""
......@@ -48,28 +57,16 @@ class TestComprehensiveTheming(TestCase):
self.assertContains(resp, "TEMPORARY THEME")
do_the_test(self)
def test_theme_adjusts_staticfiles_search_path(self):
# Test that a theme adds itself to the staticfiles search path.
before_finders = list(settings.STATICFILES_FINDERS)
before_dirs = list(settings.STATICFILES_DIRS)
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
def do_the_test(self):
"""A function to do the work so we can use the decorator."""
self.assertEqual(list(settings.STATICFILES_FINDERS), before_finders)
self.assertEqual(settings.STATICFILES_DIRS[0], settings.REPO_ROOT / 'themes/red-theme/lms/static')
self.assertEqual(settings.STATICFILES_DIRS[1:], before_dirs)
do_the_test(self)
# remove symlinks before running subsequent tests
delete_symlink(dest_path)
def test_default_logo_image(self):
result = staticfiles.finders.find('images/logo.png')
self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/logo.png')
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
@with_comprehensive_theme('red-theme')
def test_overridden_logo_image(self):
result = staticfiles.finders.find('images/logo.png')
result = staticfiles.finders.find('red-theme/images/logo.png')
self.assertEqual(result, settings.REPO_ROOT / 'themes/red-theme/lms/static/images/logo.png')
def test_default_favicon(self):
......@@ -79,10 +76,10 @@ class TestComprehensiveTheming(TestCase):
result = staticfiles.finders.find('images/favicon.ico')
self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/favicon.ico')
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
@with_comprehensive_theme('red-theme')
def test_overridden_favicon(self):
"""
Test comprehensive theme override on favicon image.
"""
result = staticfiles.finders.find('images/favicon.ico')
result = staticfiles.finders.find('red-theme/images/favicon.ico')
self.assertEqual(result, settings.REPO_ROOT / 'themes/red-theme/lms/static/images/favicon.ico')
......@@ -312,11 +312,12 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
url = reverse('info', args=[unicode(course.id)])
with self.assertNumQueries(sql_queries):
with check_mongo_calls(mongo_queries):
resp = self.client.get(url)
with mock.patch("openedx.core.djangoapps.theming.helpers.get_current_site", return_value=None):
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
def test_num_queries_instructor_paced(self):
self.fetch_course_info_with_queries(self.instructor_paced_course, 21, 4)
self.fetch_course_info_with_queries(self.instructor_paced_course, 22, 4)
def test_num_queries_self_paced(self):
self.fetch_course_info_with_queries(self.self_paced_course, 21, 4)
self.fetch_course_info_with_queries(self.self_paced_course, 22, 4)
......@@ -3,17 +3,22 @@ Tests related to the basic footer-switching based off SITE_NAME to ensure
edx.org uses an edx footer but other instances use an Open edX footer.
"""
import unittest
from nose.plugins.attrib import attr
from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings
from openedx.core.djangoapps.theming.tests.test_util import with_is_edx_domain
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@attr('shard_1')
class TestFooter(TestCase):
"""
Tests for edx and OpenEdX footer
"""
SOCIAL_MEDIA_NAMES = [
"facebook",
......@@ -37,7 +42,7 @@ class TestFooter(TestCase):
"youtube": "https://www.youtube.com/"
}
@with_is_edx_domain(True)
@with_comprehensive_theme("edx.org")
def test_edx_footer(self):
"""
Verify that the homepage, when accessed at edx.org, has the edX footer
......@@ -46,7 +51,6 @@ class TestFooter(TestCase):
self.assertEqual(resp.status_code, 200)
self.assertContains(resp, 'footer-edx-v3')
@with_is_edx_domain(False)
def test_openedx_footer(self):
"""
Verify that the homepage, when accessed at something other than
......@@ -56,7 +60,7 @@ class TestFooter(TestCase):
self.assertEqual(resp.status_code, 200)
self.assertContains(resp, 'footer-openedx')
@with_is_edx_domain(True)
@with_comprehensive_theme("edx.org")
@override_settings(
SOCIAL_MEDIA_FOOTER_NAMES=SOCIAL_MEDIA_NAMES,
SOCIAL_MEDIA_FOOTER_URLS=SOCIAL_MEDIA_URLS
......
......@@ -1340,7 +1340,7 @@ class ProgressPageTests(ModuleStoreTestCase):
self.assertContains(resp, u"Download Your Certificate")
@ddt.data(
*itertools.product(((46, 4, True), (46, 4, False)), (True, False))
*itertools.product(((47, 4, True), (47, 4, False)), (True, False))
)
@ddt.unpack
def test_query_counts(self, (sql_calls, mongo_calls, self_paced), self_paced_enabled):
......
......@@ -372,8 +372,8 @@ class ViewsQueryCountTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet
return inner
@ddt.data(
(ModuleStoreEnum.Type.mongo, 3, 4, 30),
(ModuleStoreEnum.Type.split, 3, 13, 30),
(ModuleStoreEnum.Type.mongo, 3, 4, 31),
(ModuleStoreEnum.Type.split, 3, 13, 31),
)
@ddt.unpack
@count_queries
......@@ -381,8 +381,8 @@ class ViewsQueryCountTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet
self.create_thread_helper(mock_request)
@ddt.data(
(ModuleStoreEnum.Type.mongo, 3, 3, 24),
(ModuleStoreEnum.Type.split, 3, 10, 24),
(ModuleStoreEnum.Type.mongo, 3, 3, 25),
(ModuleStoreEnum.Type.split, 3, 10, 25),
)
@ddt.unpack
@count_queries
......
......@@ -29,12 +29,12 @@ from openedx.core.djangoapps.user_api.accounts.api import activate_account, crea
from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH
from openedx.core.djangolib.js_utils import dump_js_escaped_json
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from openedx.core.djangoapps.theming.tests.test_util import with_edx_domain_context
from student.tests.factories import UserFactory
from student_account.views import account_settings_context, get_user_orders
from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin
from util.testing import UrlResetMixin
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme_context
@ddt.ddt
......@@ -262,13 +262,13 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
self.assertRedirects(response, reverse("dashboard"))
@ddt.data(
(False, "signin_user"),
(False, "register_user"),
(True, "signin_user"),
(True, "register_user"),
(None, "signin_user"),
(None, "register_user"),
("edx.org", "signin_user"),
("edx.org", "register_user"),
)
@ddt.unpack
def test_login_and_registration_form_signin_preserves_params(self, is_edx_domain, url_name):
def test_login_and_registration_form_signin_preserves_params(self, theme, url_name):
params = [
('course_id', 'edX/DemoX/Demo_Course'),
('enrollment_action', 'enroll'),
......@@ -276,7 +276,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
# The response should have a "Sign In" button with the URL
# that preserves the querystring params
with with_edx_domain_context(is_edx_domain):
with with_comprehensive_theme_context(theme):
response = self.client.get(reverse(url_name), params)
expected_url = '/login?{}'.format(self._finish_auth_url_param(params + [('next', '/dashboard')]))
......@@ -292,7 +292,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
]
# Verify that this parameter is also preserved
with with_edx_domain_context(is_edx_domain):
with with_comprehensive_theme_context(theme):
response = self.client.get(reverse(url_name), params)
expected_url = '/login?{}'.format(self._finish_auth_url_param(params))
......
......@@ -39,7 +39,7 @@ from commerce.models import CommerceConfiguration
from commerce.tests import TEST_PAYMENT_DATA, TEST_API_URL, TEST_API_SIGNING_KEY, TEST_PUBLIC_URL_ROOT
from embargo.test_utils import restrict_course
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
from openedx.core.djangoapps.theming.tests.test_util import with_is_edx_domain
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
from shoppingcart.models import Order, CertificateItem
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from student.models import CourseEnrollment
......@@ -321,7 +321,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
)
self._assert_redirects_to_dashboard(response)
@with_is_edx_domain(True)
@with_comprehensive_theme("edx.org")
@ddt.data("verify_student_start_flow", "verify_student_begin_flow")
def test_pay_and_verify_hides_header_nav(self, payment_flow):
course = self._create_course("verified")
......
......@@ -260,7 +260,14 @@ BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = LOW_PRIORITY_QUEUE
# Theme overrides
THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
COMPREHENSIVE_THEME_DIR = path(ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', COMPREHENSIVE_THEME_DIR))
# following setting is for backward compatibility
if ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', None):
COMPREHENSIVE_THEME_DIR = ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR')
COMPREHENSIVE_THEME_DIRS = ENV_TOKENS.get('COMPREHENSIVE_THEME_DIRS', COMPREHENSIVE_THEME_DIRS) or []
DEFAULT_SITE_THEME = ENV_TOKENS.get('DEFAULT_SITE_THEME', DEFAULT_SITE_THEME)
ENABLE_COMPREHENSIVE_THEMING = ENV_TOKENS.get('ENABLE_COMPREHENSIVE_THEMING', ENABLE_COMPREHENSIVE_THEMING)
# Marketing link overrides
MKTG_URL_LINK_MAP.update(ENV_TOKENS.get('MKTG_URL_LINK_MAP', {}))
......
......@@ -61,9 +61,9 @@ STATIC_URL = "/static/"
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
)
STATICFILES_DIRS = (
STATICFILES_DIRS = [
(TEST_ROOT / "staticfiles" / "lms").abspath(),
)
]
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
MEDIA_ROOT = TEST_ROOT / "uploads"
......
......@@ -387,9 +387,6 @@ COURSES_ROOT = ENV_ROOT / "data"
DATA_DIR = COURSES_ROOT
# comprehensive theming system
COMPREHENSIVE_THEME_DIR = ""
# TODO: Remove the rest of the sys.path modification here and in cms/envs/common.py
sys.path.append(REPO_ROOT)
sys.path.append(PROJECT_ROOT / 'djangoapps')
......@@ -489,6 +486,7 @@ TEMPLATES = [
'loaders': [
# We have to use mako-aware template loaders to be able to include
# mako templates inside django templates (such as main_django.html).
'openedx.core.djangoapps.theming.template_loaders.ThemeTemplateLoader',
'edxmako.makoloader.MakoFilesystemLoader',
'edxmako.makoloader.MakoAppDirectoriesLoader',
],
......@@ -784,7 +782,6 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
CMS_BASE = 'localhost:8001'
# Site info
SITE_ID = 1
SITE_NAME = "example.com"
HTTPS = 'on'
ROOT_URLCONF = 'lms.urls'
......@@ -1166,6 +1163,8 @@ MIDDLEWARE_CLASSES = (
'course_wiki.middleware.WikiAccessMiddleware',
'openedx.core.djangoapps.theming.middleware.CurrentSiteThemeMiddleware',
# This must be last
'microsite_configuration.middleware.MicrositeSessionCookieDomainMiddleware',
)
......@@ -1185,7 +1184,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
# List of finder classes that know how to find static files in various locations.
# Note: the pipeline finder is included to be able to discover optimized files
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'openedx.core.lib.xblock_pipeline.finder.XBlockPipelineFinder',
......@@ -2920,6 +2919,21 @@ CREDENTIALS_GENERATION_ROUTING_KEY = HIGH_PRIORITY_QUEUE
WIKI_REQUEST_CACHE_MIDDLEWARE_CLASS = "request_cache.middleware.RequestCache"
# Settings for Comprehensive Theming app
# See https://github.com/edx/edx-django-sites-extensions for more info
# Default site to use if site matching request headers does not exist
SITE_ID = 1
# dir containing all themes
COMPREHENSIVE_THEME_DIRS = [REPO_ROOT / "themes"]
# Theme to use when no site or site theme is defined,
# set to None if you want to use openedx theme
DEFAULT_SITE_THEME = None
ENABLE_COMPREHENSIVE_THEMING = True
# API access management
API_ACCESS_MANAGER_EMAIL = 'api-access@example.com'
API_ACCESS_FROM_EMAIL = 'api-requests@example.com'
......
......@@ -96,7 +96,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
# Revert to the default set of finders as we don't want the production pipeline
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
......
......@@ -41,6 +41,6 @@ STATIC_URL = "/static/"
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
)
STATICFILES_DIRS = (
STATICFILES_DIRS = [
(TEST_ROOT / "staticfiles" / "lms").abspath(),
)
]
......@@ -494,6 +494,8 @@ MICROSITE_CONFIGURATION = {
MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver'
MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
TEST_THEME = COMMON_ROOT / "test" / "test-theme"
# add extra template directory for test-only templates
MAKO_TEMPLATES['main'].extend([
COMMON_ROOT / 'test' / 'templates',
......@@ -582,3 +584,5 @@ JWT_AUTH.update({
OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application'
COURSE_CATALOG_API_URL = 'https://catalog.example.com/api/v1'
COMPREHENSIVE_THEME_DIRS = [REPO_ROOT / "themes", REPO_ROOT / "common/test"]
......@@ -20,7 +20,9 @@ from monkey_patch import (
import xmodule.x_module
import lms_xblock.runtime
from openedx.core.djangoapps.theming.core import enable_comprehensive_theme
from openedx.core.djangoapps.theming.core import enable_theming
from openedx.core.djangoapps.theming.helpers import is_comprehensive_theming_enabled
from microsite_configuration import microsite
log = logging.getLogger(__name__)
......@@ -39,8 +41,8 @@ def run():
# Comprehensive theming needs to be set up before django startup,
# because modifying django template paths after startup has no effect.
if settings.COMPREHENSIVE_THEME_DIR:
enable_comprehensive_theme(settings.COMPREHENSIVE_THEME_DIR)
if is_comprehensive_theming_enabled():
enable_theming()
# We currently use 2 template rendering engines, mako and django_templates,
# and one of them (django templates), requires the directories be added
......
......@@ -6,7 +6,7 @@ $static-path: '../..' !default;
// Configuration
@import '../config';
@import '../base/variables';
@import 'base/variables';
@import '../base-v2/extends';
// Common extensions
......
<!DOCTYPE html>
{% load sekizai_tags i18n microsite pipeline optional_include staticfiles %}
{% load sekizai_tags i18n microsite theme_pipeline optional_include staticfiles %}
{% load url from future %}
<html lang="{{LANGUAGE_CODE}}">
<head>
......
{% extends "main_django.html" %}
{% with online_help_token="wiki" %}
{% load pipeline %}{% load sekizai_tags i18n microsite %}{% load url from future %}{% load staticfiles %}
{% load theme_pipeline %}{% load sekizai_tags i18n microsite %}{% load url from future %}{% load staticfiles %}
{% block title %}
{% block pagetitle %}{% endblock %} | {% trans "Wiki" %} | {% platform_name %}
......
<!DOCTYPE html>
{% load wiki_tags i18n %}{% load pipeline %}
{% load wiki_tags i18n %}{% load theme_pipeline %}
<html lang="{{LANGUAGE_CODE}}">
<head>
{% stylesheet 'course' %}
......
......@@ -268,7 +268,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
self.assertEqual(response.data['developer_message'], u'Parameter usage_id not provided.')
# Send empty data dictionary.
with self.assertNumQueries(7): # No queries for bookmark table.
with self.assertNumQueries(8): # No queries for bookmark table.
response = self.send_post(
client=self.client,
url=reverse('bookmarks'),
......
"""
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)
"""
Core logic for Comprehensive Theming.
"""
from path import Path
from django.conf import settings
from .helpers import get_themes
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:
from logging import getLogger
logger = getLogger(__name__) # pylint: disable=invalid-name
* 'settings': a dictionary of settings names and their new values.
* 'template_paths': a list of directories to prepend to template
lookup path.
def enable_theming():
"""
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_theme(theme_dir):
"""
Add directories to relevant paths for comprehensive theming.
Add directories and relevant paths to settings for comprehensive theming.
"""
changes = comprehensive_theme_changes(theme_dir)
# 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)
# 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():
locale_dir = theme.path / "conf" / "locale"
if locale_dir.isdir():
settings.LOCALE_PATHS = (locale_dir, ) + settings.LOCALE_PATHS
if theme.themes_base_dir not in settings.MAKO_TEMPLATES['main']:
settings.MAKO_TEMPLATES['main'].insert(0, theme.themes_base_dir)
......@@ -17,63 +17,80 @@ interface, as well.
.. _Django-Pipeline: http://django-pipeline.readthedocs.org/
.. _Django-Require: https://github.com/etianen/django-require
"""
from path import Path
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
import os
from collections import OrderedDict
from django.contrib.staticfiles import utils
from django.contrib.staticfiles.finders import BaseFinder
from openedx.core.djangoapps.theming.storage import CachedComprehensiveThemingStorage
from django.utils import six
from openedx.core.djangoapps.theming.helpers import get_themes
from openedx.core.djangoapps.theming.storage import ThemeStorage
class ComprehensiveThemeFinder(BaseFinder):
class ThemeFilesFinder(BaseFinder):
"""
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.
A static files finder that looks in the directory of each theme as
specified in the source_dir attribute.
"""
storage_class = ThemeStorage
source_dir = 'static'
def __init__(self, *args, **kwargs):
super(ComprehensiveThemeFinder, self).__init__(*args, **kwargs)
# The list of themes that are handled
self.themes = []
# Mapping of theme names to storage instances
self.storages = OrderedDict()
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
if not theme_dir:
self.storage = None
return
themes = get_themes()
for theme in themes:
theme_storage = self.storage_class(
os.path.join(theme.path, self.source_dir),
prefix=theme.theme_dir_name,
)
if not isinstance(theme_dir, basestring):
raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
self.storages[theme.theme_dir_name] = theme_storage
if theme.theme_dir_name not in self.themes:
self.themes.append(theme.theme_dir_name)
root = Path(settings.PROJECT_ROOT)
if root.name == "":
root = root.parent
super(ThemeFilesFinder, self).__init__(*args, **kwargs)
component_dir = Path(theme_dir) / root.name
static_dir = component_dir / "static"
self.storage = CachedComprehensiveThemingStorage(location=static_dir)
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
def find(self, path, all=False): # pylint: disable=redefined-builtin
"""
Looks for files in the default file storage, if it's local.
Looks for files in the theme directories.
"""
if not self.storage:
return []
if path.startswith(self.storage.prefix):
# strip the prefix
path = path[len(self.storage.prefix):]
matches = []
theme_dir_name = path.split("/", 1)[0]
if self.storage.exists(path):
match = self.storage.path(path)
if all:
match = [match]
return match
themes = {t.theme_dir_name: 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_name in themes:
theme = themes[theme_dir_name]
path = "/".join(path.split("/")[1:])
match = self.find_in_theme(theme.theme_dir_name, path)
if match:
if not all:
return match
matches.append(match)
return matches
return []
def list(self, ignore_patterns):
def find_in_theme(self, theme, path):
"""
List all files of the storage.
Find a requested static file in an theme's static locations.
"""
if self.storage and self.storage.exists(''):
for path in utils.get_files(self.storage, ignore_patterns):
yield path, self.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
"""
Management commands related to Comprehensive Theming.
"""
"""
Management command for compiling sass.
"""
from __future__ import unicode_literals
from django.core.management import BaseCommand, CommandError
from paver.easy import call_task
from pavelib.assets import ALL_SYSTEMS
from openedx.core.djangoapps.theming.helpers import get_themes, get_theme_base_dirs, is_comprehensive_theming_enabled
class Command(BaseCommand):
"""
Compile theme sass and collect theme assets.
"""
help = 'Compile and collect themed assets...'
def add_arguments(self, parser):
"""
Add arguments for compile_sass command.
Args:
parser (django.core.management.base.CommandParser): parsed for parsing command line arguments.
"""
parser.add_argument(
'system', type=str, nargs='*', default=ALL_SYSTEMS,
help="lms or studio",
)
# Named (optional) arguments
parser.add_argument(
'--theme-dirs',
dest='theme_dirs',
type=str,
nargs='+',
default=None,
help="List of dirs where given themes would be looked.",
)
parser.add_argument(
'--themes',
type=str,
nargs='+',
default=["all"],
help="List of themes whose sass need to compiled. Or 'no'/'all' to compile for no/all themes.",
)
# Named (optional) arguments
parser.add_argument(
'--force',
action='store_true',
default=False,
help="Force full compilation",
)
parser.add_argument(
'--debug',
action='store_true',
default=False,
help="Disable Sass compression",
)
@staticmethod
def parse_arguments(*args, **options): # pylint: disable=unused-argument
"""
Parse and validate arguments for compile_sass command.
Args:
*args: Positional arguments passed to the update_assets command
**options: optional arguments passed to the update_assets command
Returns:
A tuple containing parsed values for themes, system, source comments and output style.
1. system (list): list of system names for whom to compile theme sass e.g. 'lms', 'cms'
2. theme_dirs (list): list of Theme objects
3. themes (list): list of Theme objects
4. force (bool): Force full compilation
5. debug (bool): Disable Sass compression
"""
system = options.get("system", ALL_SYSTEMS)
given_themes = options.get("themes", ["all"])
theme_dirs = options.get("theme_dirs", None)
force = options.get("force", True)
debug = options.get("debug", True)
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)})
else:
theme_dirs = get_theme_base_dirs()
available_themes = {t.theme_dir_name: t for t in get_themes()}
if 'no' in given_themes or 'all' in given_themes:
# Raise error if 'all' or 'no' is present and theme names are also given.
if len(given_themes) > 1:
raise CommandError("Invalid themes value, It must either be 'all' or 'no' or list of themes.")
# Raise error if any of the given theme name is invalid
# (theme name would be invalid if it does not exist in themes directory)
elif (not set(given_themes).issubset(available_themes.keys())) and is_comprehensive_theming_enabled():
raise CommandError(
"Given themes '{themes}' do not exist inside any of the theme directories '{theme_dirs}'".format(
themes=", ".join(set(given_themes) - set(available_themes.keys())),
theme_dirs=theme_dirs,
),
)
if "all" in given_themes:
themes = list(available_themes.itervalues())
elif "no" in given_themes:
themes = []
else:
# convert theme names to Theme objects, this will remove all themes if theming is disabled
themes = [available_themes.get(theme) for theme in given_themes if theme in available_themes]
return system, theme_dirs, themes, force, debug
def handle(self, *args, **options):
"""
Handle compile_sass command.
"""
system, theme_dirs, themes, force, debug = self.parse_arguments(*args, **options)
themes = [theme.theme_dir_name for theme in themes]
if options.get("themes", None) and not is_comprehensive_theming_enabled():
# log a warning message to let the user know that asset compilation for themes is skipped
self.stdout.write(
self.style.WARNING( # pylint: disable=no-member
"Skipping theme asset compilation: enable theming to process themed assets"
),
)
call_task(
'pavelib.assets.compile_sass',
options={'system': system, 'theme-dirs': theme_dirs, 'themes': themes, 'force': force, 'debug': debug},
)
"""
Middleware for theming app
Note:
This middleware depends on "django_sites_extensions" app
So it must be added to INSTALLED_APPS in django settings files.
"""
from openedx.core.djangoapps.theming.models import SiteTheme
class CurrentSiteThemeMiddleware(object):
"""
Middleware that sets `site_theme` attribute to request object.
"""
def process_request(self, request):
"""
fetch Site Theme for the current site and add it to the request object.
"""
request.site_theme = SiteTheme.get_theme(request.site)
# -*- 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')),
],
),
]
"""
Django models supporting the Comprehensive Theming subsystem
"""
from django.db import models
from django.conf import settings
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
@staticmethod
def get_theme(site):
"""
Get SiteTheme object for given site, returns default site theme if it can not
find a theme for the given site and `DEFAULT_SITE_THEME` setting has a proper value.
Args:
site (django.contrib.sites.models.Site): site object related to the current site.
Returns:
SiteTheme object for given site or a default site set by `DEFAULT_SITE_THEME`
"""
theme = site.themes.first()
if (not theme) and settings.DEFAULT_SITE_THEME:
theme = SiteTheme(site=site, theme_dir_name=settings.DEFAULT_SITE_THEME)
return theme
"""
This file contains helpers for paver commands, Django is not initialized in paver commands.
So, django settings, models etc. can not be used here.
"""
import os
from path import Path
def get_theme_paths(themes, theme_dirs):
"""
get absolute path for all the given themes, if a theme is no found
at multiple places than all paths for the theme will be included.
If a theme is not found anywhere then theme will be skipped with
an error message printed on the console.
If themes is 'None' then all themes in given dirs are returned.
Args:
themes (list): list of all theme names
theme_dirs (list): list of base dirs that contain themes
Returns:
list of absolute paths to themes.
"""
theme_paths = []
for theme in themes:
theme_base_dirs = get_theme_base_dirs(theme, theme_dirs)
if not theme_base_dirs:
print(
"\033[91m\nSkipping '{theme}': \n"
"Theme ({theme}) not found in any of the theme dirs ({theme_dirs}). \033[00m".format(
theme=theme,
theme_dirs=", ".join(theme_dirs)
),
)
theme_paths.extend(theme_base_dirs)
return theme_paths
def get_theme_base_dirs(theme, theme_dirs):
"""
Get all base dirs where the given theme can be found.
Args:
theme (str): name of the theme to find
theme_dirs (list): list of all base dirs where the given theme could be found
Returns:
list of all the dirs for the goven theme
"""
theme_paths = []
for _dir in theme_dirs:
for dir_name in {theme}.intersection(os.listdir(_dir)):
if is_theme_dir(Path(_dir) / dir_name):
theme_paths.append(Path(_dir) / dir_name)
return theme_paths
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)))
"""
Theming aware template loaders.
"""
from django.utils._os import safe_join
from django.core.exceptions import SuspiciousFileOperation
from django.template.loaders.filesystem import Loader as FilesystemLoader
from edxmako.makoloader import MakoLoader
from openedx.core.djangoapps.theming.helpers import get_current_request, \
get_current_theme, get_all_theme_template_dirs
class ThemeTemplateLoader(MakoLoader):
"""
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):
MakoLoader.__init__(self, ThemeFilesystemLoader(*args))
class ThemeFilesystemLoader(FilesystemLoader):
"""
Filesystem Template loaders to pickup templates from theme directory based on the current site.
"""
is_usable = True
_accepts_engine_in_init = True
def get_template_sources(self, template_name, template_dirs=None):
"""
Returns the absolute paths to "template_name", when appended to each
directory in "template_dirs". Any paths that don't lie inside one of the
template dirs are excluded from the result set, for security reasons.
"""
if not template_dirs:
template_dirs = self.engine.dirs
theme_dirs = self.get_theme_template_sources()
# append theme dirs to the beginning so templates are looked up inside theme dir first
if isinstance(theme_dirs, list):
template_dirs = theme_dirs + template_dirs
for template_dir in template_dirs:
try:
yield safe_join(template_dir, template_name)
except SuspiciousFileOperation:
# The joined path was located outside of this template_dir
# (it might be inside another one, so this isn't fatal).
pass
@staticmethod
def get_theme_template_sources():
"""
Return template sources for the given theme and if request object is None (this would be the case for
management commands) return template sources for all themes.
"""
if not get_current_request():
# if request object is not present, then this method is being called inside a management
# command and return all theme template sources for compression
return get_all_theme_template_dirs()
else:
# template is being accessed by a view, so return templates sources for current theme
theme = get_current_theme()
return theme and theme.template_dirs
"""
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)
"""
Tests for Management commands of comprehensive theming.
"""
from django.test import TestCase
from django.core.management import call_command, CommandError
from openedx.core.djangoapps.theming.helpers import get_themes
from openedx.core.djangoapps.theming.management.commands.compile_sass import Command
class TestUpdateAssets(TestCase):
"""
Test comprehensive theming helper functions.
"""
def setUp(self):
super(TestUpdateAssets, self).setUp()
self.themes = get_themes()
def test_errors_for_invalid_arguments(self):
"""
Test update_asset command.
"""
# make sure error is raised for invalid theme list
with self.assertRaises(CommandError):
call_command("compile_sass", themes=["all", "test-theme"])
# make sure error is raised for invalid theme list
with self.assertRaises(CommandError):
call_command("compile_sass", themes=["no", "test-theme"])
# make sure error is raised for invalid theme list
with self.assertRaises(CommandError):
call_command("compile_sass", themes=["all", "no"])
# make sure error is raised for invalid theme list
with self.assertRaises(CommandError):
call_command("compile_sass", themes=["test-theme", "non-existing-theme"])
def test_parse_arguments(self):
"""
Test parse arguments method for update_asset command.
"""
# make sure compile_sass picks all themes when called with 'themes=all' option
parsed_args = Command.parse_arguments(themes=["all"])
self.assertItemsEqual(parsed_args[2], get_themes())
# make sure compile_sass picks no themes when called with 'themes=no' option
parsed_args = Command.parse_arguments(themes=["no"])
self.assertItemsEqual(parsed_args[2], [])
# make sure compile_sass picks only specified themes
parsed_args = Command.parse_arguments(themes=["test-theme"])
self.assertItemsEqual(parsed_args[2], [theme for theme in get_themes() if theme.theme_dir_name == "test-theme"])
"""
Tests for comprehensive theme static files finders.
"""
import unittest
from django.conf import settings
from django.test import TestCase
from openedx.core.djangoapps.theming.finders import ThemeFilesFinder
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestThemeFinders(TestCase):
"""
Test comprehensive theming static files finders.
"""
def setUp(self):
super(TestThemeFinders, self).setUp()
self.finder = ThemeFilesFinder()
def test_find_first_themed_asset(self):
"""
Verify Theme Finder returns themed assets
"""
themes_dir = settings.COMPREHENSIVE_THEME_DIRS[1]
asset = "test-theme/images/logo.png"
match = self.finder.find(asset)
self.assertEqual(match, themes_dir / "test-theme" / "lms" / "static" / "images" / "logo.png")
def test_find_all_themed_asset(self):
"""
Verify Theme Finder returns themed assets
"""
themes_dir = settings.COMPREHENSIVE_THEME_DIRS[1]
asset = "test-theme/images/logo.png"
matches = self.finder.find(asset, all=True)
# Make sure only first match was returned
self.assertEqual(1, len(matches))
self.assertEqual(matches[0], themes_dir / "test-theme" / "lms" / "static" / "images" / "logo.png")
def test_find_in_theme(self):
"""
Verify find in theme method of finders returns asset from specified theme
"""
themes_dir = settings.COMPREHENSIVE_THEME_DIRS[1]
asset = "images/logo.png"
match = self.finder.find_in_theme("test-theme", asset)
self.assertEqual(match, themes_dir / "test-theme" / "lms" / "static" / "images" / "logo.png")
"""
Test helpers for Comprehensive Theming.
"""
from django.test import TestCase
import unittest
from mock import patch
from django.test import TestCase, override_settings
from django.conf import settings
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
from openedx.core.djangoapps.theming import helpers
from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme, strip_site_theme_templates_path, \
get_themes, Theme, get_theme_base_dir
class TestHelpers(TestCase):
"""Test comprehensive theming helper functions."""
class ThemingHelpersTests(TestCase):
"""
Make sure some of the theming helper functions work
"""
def test_get_themes(self):
"""
Tests template paths are returned from enabled theme.
"""
expected_themes = [
Theme('test-theme', 'test-theme', get_theme_base_dir('test-theme')),
Theme('red-theme', 'red-theme', get_theme_base_dir('red-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('stanford-style', 'stanford-style', get_theme_base_dir('stanford-style')),
]
actual_themes = get_themes()
self.assertItemsEqual(expected_themes, actual_themes)
@override_settings(COMPREHENSIVE_THEME_DIRS=[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', get_theme_base_dir('test-theme')),
]
actual_themes = get_themes()
self.assertItemsEqual(expected_themes, actual_themes)
def test_get_value_returns_override(self):
"""
......@@ -23,3 +52,89 @@ class ThemingHelpersTests(TestCase):
mock_get_value.return_value = {override_key: override_value}
jwt_auth = helpers.get_value('JWT_AUTH')
self.assertEqual(jwt_auth[override_key], override_value)
@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')
@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')
"""
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_theme_base_dirs, Theme, get_theme_base_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_theme_base_dirs()[0]
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_theme",
return_value=Theme(self.enabled_theme, self.enabled_theme, get_theme_base_dir(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_theme",
return_value=Theme(self.enabled_theme, self.enabled_theme, get_theme_base_dir(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)
"""
Tests for comprehensive themes.
"""
import unittest
from django.conf import settings
from django.test import TestCase, override_settings
from django.contrib import staticfiles
from openedx.core.djangoapps.theming.tests.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()
@override_settings(COMPREHENSIVE_THEME_DIRS=[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_DIRS=[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')
@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()
@override_settings(COMPREHENSIVE_THEME_DIRS=[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.")
@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()
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')
@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()
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.")
......@@ -6,87 +6,63 @@ from functools import wraps
import os
import os.path
import contextlib
import re
from mock import patch
from django.conf import settings
from django.template import Engine
from django.test.utils import override_settings
from django.contrib.sites.models import Site
import edxmako
from openedx.core.djangoapps.theming.models import SiteTheme
from openedx.core.djangoapps.theming.core import comprehensive_theme_changes
EDX_THEME_DIR = settings.REPO_ROOT / "themes" / "edx.org"
def with_comprehensive_theme(theme_dir):
def with_comprehensive_theme(theme_dir_name):
"""
A decorator to run a test with a particular comprehensive theme.
A decorator to run a test with a comprehensive theming enabled.
Arguments:
theme_dir (str): the full path to the theme directory to use.
This will likely use `settings.REPO_ROOT` to get the full path.
theme_dir_name (str): directory name of the site for which we want comprehensive theming enabled.
"""
# 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)
# This decorator creates Site and SiteTheme models for given domain
def _decorator(func): # pylint: disable=missing-docstring
@wraps(func)
def _decorated(*args, **kwargs): # pylint: disable=missing-docstring
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)
# 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)
site_theme, __ = SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme_dir_name)
for _dir in settings.COMPREHENSIVE_THEME_DIRS:
edxmako.paths.add_lookup('main', _dir, prepend=True)
with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme',
return_value=site_theme):
with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
return func(*args, **kwargs)
return _decorated
return _decorator
def with_is_edx_domain(is_edx_domain):
"""
A decorator 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?
"""
# 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):
def with_comprehensive_theme_context(theme=None):
"""
A function to run a test as if request originated from edX domain or not.
A function to run a test as if request was made to the given theme.
Arguments:
is_edx_domain (bool): are we an edX domain or not?
theme (str): name if the theme or None if no theme is applied
"""
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)
if theme:
domain = '{theme}.org'.format(theme=re.sub(r"\.org$", "", theme))
site, __ = Site.objects.get_or_create(domain=domain, name=theme)
site_theme, __ = SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme)
for _dir in settings.COMPREHENSIVE_THEME_DIRS:
edxmako.paths.add_lookup('main', _dir, prepend=True)
with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme',
return_value=site_theme):
with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
yield
else:
yield
......
......@@ -248,7 +248,7 @@ class TestAccountAPI(CacheIsolationTestCase, UserAPITestCase):
"""
self.different_client.login(username=self.different_user.username, password=self.test_password)
self.create_mock_profile(self.user)
with self.assertNumQueries(17):
with self.assertNumQueries(18):
response = self.send_get(self.different_client)
self._verify_full_shareable_account_response(response, account_privacy=ALL_USERS_VISIBILITY)
......@@ -263,7 +263,7 @@ class TestAccountAPI(CacheIsolationTestCase, UserAPITestCase):
"""
self.different_client.login(username=self.different_user.username, password=self.test_password)
self.create_mock_profile(self.user)
with self.assertNumQueries(17):
with self.assertNumQueries(18):
response = self.send_get(self.different_client)
self._verify_private_account_response(response, account_privacy=PRIVATE_VISIBILITY)
......@@ -337,12 +337,12 @@ class TestAccountAPI(CacheIsolationTestCase, UserAPITestCase):
self.assertEqual(False, data["accomplishments_shared"])
self.client.login(username=self.user.username, password=self.test_password)
verify_get_own_information(15)
verify_get_own_information(16)
# Now make sure that the user can get the same information, even if not active
self.user.is_active = False
self.user.save()
verify_get_own_information(10)
verify_get_own_information(11)
def test_get_account_empty_string(self):
"""
......@@ -356,7 +356,7 @@ class TestAccountAPI(CacheIsolationTestCase, UserAPITestCase):
legacy_profile.save()
self.client.login(username=self.user.username, password=self.test_password)
with self.assertNumQueries(15):
with self.assertNumQueries(16):
response = self.send_get(self.client)
for empty_field in ("level_of_education", "gender", "country", "bio"):
self.assertIsNone(response.data[empty_field])
......
......@@ -17,3 +17,22 @@ 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)
......@@ -2,18 +2,22 @@
Django storage backends for Open edX.
"""
from django_pipeline_forgiving.storages import PipelineForgivingStorage
from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
from pipeline.storage import PipelineMixin, NonPackagingMixin
from django.contrib.staticfiles.storage import StaticFilesStorage
from pipeline.storage import NonPackagingMixin
from require.storage import OptimizedFilesMixin
from openedx.core.djangoapps.theming.storage import ComprehensiveThemingAwareMixin
from openedx.core.djangoapps.theming.storage import (
ThemeStorage,
ThemeCachedFilesMixin,
ThemePipelineMixin
)
class ProductionStorage(
PipelineForgivingStorage,
ComprehensiveThemingAwareMixin,
OptimizedFilesMixin,
PipelineMixin,
CachedFilesMixin,
ThemePipelineMixin,
ThemeCachedFilesMixin,
ThemeStorage,
StaticFilesStorage
):
"""
......@@ -24,9 +28,9 @@ class ProductionStorage(
class DevelopmentStorage(
ComprehensiveThemingAwareMixin,
NonPackagingMixin,
PipelineMixin,
ThemePipelineMixin,
ThemeStorage,
StaticFilesStorage
):
"""
......
"""Unit tests for the Paver asset tasks."""
import ddt
from paver.easy import call_task
import os
from unittest import TestCase
from paver.easy import call_task, path
from mock import patch
from watchdog.observers.polling import PollingObserver
from .utils import PaverTestCase
ROOT_PATH = path(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
TEST_THEME = ROOT_PATH / "common/test/test-theme" # pylint: disable=invalid-name
@ddt.ddt
class TestPaverAssetTasks(PaverTestCase):
......@@ -43,18 +47,158 @@ class TestPaverAssetTasks(PaverTestCase):
if force:
expected_messages.append("rm -rf common/static/css/*.css")
expected_messages.append("libsass common/static/sass")
if "lms" in system:
if force:
expected_messages.append("rm -rf lms/static/css/*.css")
expected_messages.append("libsass lms/static/sass")
if force:
expected_messages.append("rm -rf lms/static/certificates/css/*.css")
expected_messages.append("libsass lms/static/certificates/sass")
if "studio" in system:
if force:
expected_messages.append("rm -rf cms/static/css/*.css")
expected_messages.append("libsass cms/static/sass")
self.assertEquals(self.task_messages, expected_messages)
@ddt.ddt
class TestPaverThemeAssetTasks(PaverTestCase):
"""
Test the Paver asset tasks.
"""
@ddt.data(
[""],
["--force"],
["--debug"],
["--system=lms"],
["--system=lms --force"],
["--system=studio"],
["--system=studio --force"],
["--system=lms,studio"],
["--system=lms,studio --force"],
)
@ddt.unpack
def test_compile_theme_sass(self, options):
"""
Test the "compile_sass" task.
"""
parameters = options.split(" ")
system = []
if "--system=studio" not in parameters:
system += ["lms"]
if "--system=lms" not in parameters:
system += ["studio"]
debug = "--debug" in parameters
force = "--force" in parameters
self.reset_task_messages()
call_task(
'pavelib.assets.compile_sass',
options={"system": system, "debug": debug, "force": force, "theme-dirs": [TEST_THEME.dirname()],
"themes": [TEST_THEME.basename()]},
)
expected_messages = []
if force:
expected_messages.append("rm -rf common/static/css/*.css")
expected_messages.append("libsass common/static/sass")
if "lms" in system:
expected_messages.append("mkdir_p " + repr(TEST_THEME / "lms/static/css"))
if force:
expected_messages.append("rm -rf " + str(TEST_THEME) + "/lms/static/css/*.css")
expected_messages.append("libsass lms/static/sass")
if force:
expected_messages.append("rm -rf " + str(TEST_THEME) + "/lms/static/css/*.css")
expected_messages.append("libsass " + str(TEST_THEME) + "/lms/static/sass")
if force:
expected_messages.append("rm -rf lms/static/css/*.css")
expected_messages.append("libsass lms/static/themed_sass")
expected_messages.append("libsass lms/static/sass")
if force:
expected_messages.append("rm -rf lms/static/certificates/css/*.css")
expected_messages.append("libsass lms/static/certificates/sass")
if "studio" in system:
expected_messages.append("mkdir_p " + repr(TEST_THEME / "cms/static/css"))
if force:
expected_messages.append("rm -rf " + str(TEST_THEME) + "/cms/static/css/*.css")
expected_messages.append("libsass cms/static/sass")
if force:
expected_messages.append("rm -rf " + str(TEST_THEME) + "/cms/static/css/*.css")
expected_messages.append("libsass " + str(TEST_THEME) + "/cms/static/sass")
if force:
expected_messages.append("rm -rf cms/static/css/*.css")
expected_messages.append("libsass cms/static/sass")
self.assertEquals(self.task_messages, expected_messages)
class TestPaverWatchAssetTasks(TestCase):
"""
Test the Paver watch asset tasks.
"""
def setUp(self):
self.expected_sass_directories = [
path('common/static/sass'),
path('common/static'),
path('node_modules'),
path('node_modules/edx-pattern-library/node_modules'),
path('lms/static/sass/partials'),
path('lms/static/sass'),
path('lms/static/certificates/sass'),
path('cms/static/sass'),
path('cms/static/sass/partials'),
]
super(TestPaverWatchAssetTasks, self).setUp()
def tearDown(self):
self.expected_sass_directories = []
super(TestPaverWatchAssetTasks, self).tearDown()
def test_watch_assets(self):
"""
Test the "compile_sass" task.
"""
with patch('pavelib.assets.SassWatcher.register') as mock_register:
with patch('pavelib.assets.PollingObserver.start'):
call_task(
'pavelib.assets.watch_assets',
options={"background": True},
)
self.assertEqual(mock_register.call_count, 2)
sass_watcher_args = mock_register.call_args_list[0][0]
self.assertIsInstance(sass_watcher_args[0], PollingObserver)
self.assertIsInstance(sass_watcher_args[1], list)
self.assertItemsEqual(sass_watcher_args[1], self.expected_sass_directories)
def test_watch_theme_assets(self):
"""
Test the Paver watch asset tasks with theming enabled.
"""
self.expected_sass_directories.extend([
path(TEST_THEME) / 'lms/static/sass',
path(TEST_THEME) / 'lms/static/sass/partials',
path(TEST_THEME) / 'cms/static/sass',
path(TEST_THEME) / 'cms/static/sass/partials',
])
with patch('pavelib.assets.SassWatcher.register') as mock_register:
with patch('pavelib.assets.PollingObserver.start'):
call_task(
'pavelib.assets.watch_assets',
options={"background": True, "theme-dirs": [TEST_THEME.dirname()],
"themes": [TEST_THEME.basename()]},
)
self.assertEqual(mock_register.call_count, 2)
sass_watcher_args = mock_register.call_args_list[0][0]
self.assertIsInstance(sass_watcher_args[0], PollingObserver)
self.assertIsInstance(sass_watcher_args[1], list)
self.assertItemsEqual(sass_watcher_args[1], self.expected_sass_directories)
......@@ -17,12 +17,17 @@ EXPECTED_COMMON_SASS_DIRECTORIES = [
]
EXPECTED_LMS_SASS_DIRECTORIES = [
u"lms/static/sass",
u"lms/static/themed_sass",
u"lms/static/certificates/sass",
]
EXPECTED_CMS_SASS_DIRECTORIES = [
u"cms/static/sass",
]
EXPECTED_LMS_SASS_COMMAND = [
u"python manage.py lms --settings={asset_settings} compile_sass lms ",
]
EXPECTED_CMS_SASS_COMMAND = [
u"python manage.py cms --settings={asset_settings} compile_sass cms ",
]
EXPECTED_PREPROCESS_ASSETS_COMMAND = (
u"python manage.py {system} --settings={asset_settings} preprocess_assets"
u" {system}/static/sass/*.scss {system}/static/themed_sass"
......@@ -234,7 +239,7 @@ class TestPaverServerTasks(PaverTestCase):
expected_messages.append(u"xmodule_assets common/static/xmodule")
expected_messages.append(u"install npm_assets")
expected_messages.append(EXPECTED_COFFEE_COMMAND.format(platform_root=self.platform_root))
expected_messages.extend(self.expected_sass_commands(system=system))
expected_messages.extend(self.expected_sass_commands(system=system, asset_settings=expected_asset_settings))
if expected_collect_static:
expected_messages.append(EXPECTED_COLLECT_STATIC_COMMAND.format(
system=system, asset_settings=expected_asset_settings
......@@ -276,7 +281,7 @@ class TestPaverServerTasks(PaverTestCase):
expected_messages.append(u"xmodule_assets common/static/xmodule")
expected_messages.append(u"install npm_assets")
expected_messages.append(EXPECTED_COFFEE_COMMAND.format(platform_root=self.platform_root))
expected_messages.extend(self.expected_sass_commands())
expected_messages.extend(self.expected_sass_commands(asset_settings=expected_asset_settings))
if expected_collect_static:
expected_messages.append(EXPECTED_COLLECT_STATIC_COMMAND.format(
system="lms", asset_settings=expected_asset_settings
......@@ -301,14 +306,13 @@ class TestPaverServerTasks(PaverTestCase):
expected_messages.append(EXPECTED_CELERY_COMMAND.format(settings="dev_with_worker"))
self.assertEquals(self.task_messages, expected_messages)
def expected_sass_commands(self, system=None):
def expected_sass_commands(self, system=None, asset_settings=u"test_static_optimized"):
"""
Returns the expected SASS commands for the specified system.
"""
expected_sass_directories = []
expected_sass_directories.extend(EXPECTED_COMMON_SASS_DIRECTORIES)
expected_sass_commands = []
if system != 'cms':
expected_sass_directories.extend(EXPECTED_LMS_SASS_DIRECTORIES)
expected_sass_commands.extend(EXPECTED_LMS_SASS_COMMAND)
if system != 'lms':
expected_sass_directories.extend(EXPECTED_CMS_SASS_DIRECTORIES)
return [EXPECTED_SASS_COMMAND.format(sass_directory=directory) for directory in expected_sass_directories]
expected_sass_commands.extend(EXPECTED_CMS_SASS_COMMAND)
return [command.format(asset_settings=asset_settings) for command in expected_sass_commands]
......@@ -45,7 +45,7 @@
# Third-party:
git+https://github.com/cyberdelia/django-pipeline.git@1.5.3#egg=django-pipeline==1.5.3
git+https://github.com/edx/django-wiki.git@v0.0.5#egg=django-wiki==0.0.5
git+https://github.com/edx/django-wiki.git@v0.0.7#egg=django-wiki==0.0.7
git+https://github.com/edx/django-openid-auth.git@0.8#egg=django-openid-auth==0.8
git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7fed0#egg=MongoDBProxy==0.1.0
git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6
......
......@@ -133,9 +133,9 @@ directory. There are two ways to do this.
$ sudo /edx/bin/update edx-platform HEAD
#. Otherwise, edit the /edx/app/edxapp/lms.env.json file to add the
``COMPREHENSIVE_THEME_DIR`` value::
``COMPREHENSIVE_THEME_DIRS`` value::
"COMPREHENSIVE_THEME_DIR": "/full/path/to/my-theme",
"COMPREHENSIVE_THEME_DIRS": ["/full/path/to/my-theme"],
Restart your site. Your changes should now be visible.
......
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "login" %></%def>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
%>
<%block name="title">${_("Sign In")}</%block>
<%block name="bodyclass">not-signedin view-signin</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<header>
<h1 class="title title-1">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</h1>
<a href="${reverse('signup')}" class="action action-signin">${_("Don't have a {studio_name} Account? Sign up!").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
</header>
<!-- Login Page override for red-theme. -->
<article class="content-primary" role="main">
<form id="login_form" method="post" action="login_post" onsubmit="return false;">
<fieldset>
<legend class="sr">${_("Required Information to Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</legend>
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf }" />
<ol class="list-input">
<li class="field text required" id="field-email">
<label for="email">${_("E-mail")}</label>
<input id="email" type="email" name="email" placeholder="${_('example: username@domain.com')}"/>
</li>
<li class="field text required" id="field-password">
<label for="password">${_("Password")}</label>
<input id="password" type="password" name="password" />
<a href="${forgot_password_link}" class="action action-forgotpassword">${_("Forgot password?")}</a>
</li>
</ol>
</fieldset>
<div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</button>
</div>
<!-- no honor code for CMS, but need it because we're using the lms student object -->
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
</form>
</article>
</section>
</div>
</%block>
<%block name="requirejs">
require(["js/factories/login"], function(LoginFactory) {
LoginFactory("${reverse('homepage')}");
});
</%block>
// Theming overrides for sample theme
@import 'overrides';
// import the rest of the application
@import 'lms/static/sass/lms-main-rtl';
// Theming overrides for sample theme
@import 'overrides';
// import the rest of the application
@import 'lms/static/sass/lms-main';
// Theming overrides for sample theme
@import 'lms/static/sass/partials/base/variables';
$header-bg: rgb(250,0,0);
$footer-bg: rgb(250,0,0);
$container-bg: rgb(250,0,0);
$content-wrapper-bg: rgb(250,0,0);
$serif: 'Comic Sans', 'Comic Sans MS';
$sans-serif: 'Comic Sans', 'Comic Sans MS';
......@@ -72,7 +72,8 @@ from django.utils.translation import ugettext as _
honor_link = u"<a href='{}'>".format(marketing_link('HONOR'))
%>
${
_("{tos_link_start}Terms of Service{tos_link_end} and {honor_link_start}Honor Code{honor_link_end}").format(
_(
"{tos_link_start}Terms of Service{tos_link_end} and {honor_link_start}Honor Code{honor_link_end}").format(
tos_link_start=tos_link,
tos_link_end="</a>",
honor_link_start=honor_link,
......
......@@ -4,6 +4,7 @@
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML
# App that handles subdomain specific branding
import branding
......@@ -36,7 +37,7 @@ site_status_msg = get_site_status_msg(course_id)
% endif
</%block>
<header id="global-navigation" class="global ${"slim" if course else ""}" >
<header id="global-navigation" class="header-global ${"slim" if course else ""}" >
<!-- This file is only for demonstration, and is horrendous! -->
<nav aria-label="${_('Global')}">
<h1 class="logo">
......@@ -53,7 +54,7 @@ site_status_msg = get_site_status_msg(course_id)
<%
display_name = course.display_name_with_default_escaped
if settings.FEATURES.get('CUSTOM_COURSES_EDX', False):
ccx = get_current_ccx()
ccx = get_current_ccx(course.id)
if ccx:
display_name = ccx.display_name
%>
......@@ -152,7 +153,7 @@ site_status_msg = get_site_status_msg(course_id)
<li class="nav-courseware-01">
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<a class="btn-brand btn-login" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Sign in")}</a>
<a class="btn btn-login" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Sign in")}</a>
% else:
<a class="btn-brand btn-login" href="/login${login_query()}">${_("Sign in")}</a>
% endif
......@@ -164,7 +165,7 @@ site_status_msg = get_site_status_msg(course_id)
</header>
% if course:
<!--[if lte IE 8]>
<div class="ie-banner" aria-hidden="true">${_('<strong>Warning:</strong> Your browser is not fully supported. We strongly recommend using {chrome_link} or {ff_link}.').format(chrome_link='<a href="https://www.google.com/intl/en/chrome/browser/" target="_blank">Chrome</a>', ff_link='<a href="http://www.mozilla.org/en-US/firefox/new/" target="_blank">Firefox</a>')}</div>
<div class="ie-banner" aria-hidden="true">${_(HTML('<strong>Warning:</strong> Your browser is not fully supported. We strongly recommend using {chrome_link} or {ff_link}.')).format(chrome_link='<a href="https://www.google.com/intl/en/chrome/browser/" target="_blank">Chrome</a>', ff_link='<a href="http://www.mozilla.org/en-US/firefox/new/" target="_blank">Firefox</a>')}</div>
<![endif]-->
% endif
......
// Theming overrides for sample theme
@import 'overrides';
// import the rest of the application
@import 'lms/static/sass/lms-main-rtl';
// Theming overrides for sample theme
@import 'overrides';
// import the rest of the application
@import 'lms/static/sass/lms-main';
@import 'lms/static/sass/partials/base/variables';
// Theming overrides for sample theme
$header-bg: rgb(140,21,21);
$footer-bg: rgb(140,21,21);
......
<%inherit file="main.html" />
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML
%>
<section class="home">
......@@ -11,7 +12,7 @@ from django.utils.translation import ugettext as _
% if homepage_overlay_html:
${homepage_overlay_html}
% else:
<h1>${_("Free courses from <strong>{university_name}</strong>").format(university_name="Stanford")}</h1>
<h1>${_(HTML("Free courses from <strong>{university_name}</strong>")).format(university_name="Stanford")}</h1>
<p>${_("For anyone, anywhere, anytime")}</p>
% endif
</div>
......
......@@ -2,6 +2,7 @@
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from openedx.core.djangolib.js_utils import js_escaped_string
%>
<%block name="pagetitle">${_("Preferences for {platform_name}").format(platform_name=settings.PLATFORM_NAME)}</%block>
......@@ -43,7 +44,7 @@ from django.core.urlresolvers import reverse
});
$('#register-form').on('ajax:success', function(event, json, xhr) {
var url = json.redirect_url || "${reverse('dashboard')}";
var url = json.redirect_url || "${reverse('dashboard') | n, js_escaped_string }";
location.href = url;
});
......@@ -65,14 +66,14 @@ from django.core.urlresolvers import reverse
removeClass('is-disabled').
attr('aria-disabled', false).
removeProp('disabled').
text("${_('Update my {platform_name} Account').format(platform_name=settings.PLATFORM_NAME)}");
text("${_('Update my {platform_name} Account').format(platform_name=settings.PLATFORM_NAME) | n, js_escaped_string }");
}
else {
$submitButton.
addClass('is-disabled').
attr('aria-disabled', true).
prop('disabled', true).
text("${_('Processing your account information')}");
text("${_('Processing your account information') | n, js_escaped_string }");
}
}
</script>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment