Commit a25caf31 by Matt Drayer

Merge pull request #11613 from edx/ziafazal/WL-328

Ziafazal/wl-328 multi-tenancy support in Comprehensive Theming
parents 01ef2c9a 954dae58
...@@ -381,6 +381,7 @@ MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = ENV_TOKENS.get( ...@@ -381,6 +381,7 @@ MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = ENV_TOKENS.get(
"MICROSITE_DATABASE_TEMPLATE_CACHE_TTL", MICROSITE_DATABASE_TEMPLATE_CACHE_TTL "MICROSITE_DATABASE_TEMPLATE_CACHE_TTL", MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
) )
FOOTER_CACHE_TIMEOUT = ENV_TOKENS.get('FOOTER_CACHE_TIMEOUT', FOOTER_CACHE_TIMEOUT)
############################ OAUTH2 Provider ################################### ############################ OAUTH2 Provider ###################################
# OpenID Connect issuer ID. Normally the URL of the authentication endpoint. # OpenID Connect issuer ID. Normally the URL of the authentication endpoint.
......
...@@ -59,9 +59,9 @@ STATIC_URL = "/static/" ...@@ -59,9 +59,9 @@ STATIC_URL = "/static/"
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
) )
STATICFILES_DIRS = ( STATICFILES_DIRS = [
(TEST_ROOT / "staticfiles" / "cms").abspath(), (TEST_ROOT / "staticfiles" / "cms").abspath(),
) ]
# Silence noisy logs # Silence noisy logs
import logging import logging
......
...@@ -441,7 +441,6 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' ...@@ -441,7 +441,6 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
# Site info # Site info
SITE_ID = 1
SITE_NAME = "localhost:8001" SITE_NAME = "localhost:8001"
HTTPS = 'on' HTTPS = 'on'
ROOT_URLCONF = 'cms.urls' ROOT_URLCONF = 'cms.urls'
...@@ -513,6 +512,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage' ...@@ -513,6 +512,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
# List of finder classes that know how to find static files in various locations. # 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 # Note: the pipeline finder is included to be able to discover optimized files
STATICFILES_FINDERS = [ STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder', 'pipeline.finders.PipelineFinder',
...@@ -1128,6 +1128,10 @@ MICROSITE_TEMPLATE_BACKEND = 'microsite_configuration.backends.filebased.Filebas ...@@ -1128,6 +1128,10 @@ MICROSITE_TEMPLATE_BACKEND = 'microsite_configuration.backends.filebased.Filebas
# TTL for microsite database template cache # TTL for microsite database template cache
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = 5 * 60 MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = 5 * 60
# Cache expiration for the version of the footer served
# by the branding API.
FOOTER_CACHE_TIMEOUT = 30 * 60
############################### PROCTORING CONFIGURATION DEFAULTS ############## ############################### PROCTORING CONFIGURATION DEFAULTS ##############
PROCTORING_BACKEND_PROVIDER = { PROCTORING_BACKEND_PROVIDER = {
'class': 'edx_proctoring.backends.null.NullBackendProvider', 'class': 'edx_proctoring.backends.null.NullBackendProvider',
......
...@@ -41,6 +41,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage' ...@@ -41,6 +41,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
# Revert to the default set of finders as we don't want the production pipeline # Revert to the default set of finders as we don't want the production pipeline
STATICFILES_FINDERS = [ STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
] ]
......
...@@ -38,6 +38,6 @@ STATIC_URL = "/static/" ...@@ -38,6 +38,6 @@ STATIC_URL = "/static/"
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
) )
STATICFILES_DIRS = ( STATICFILES_DIRS = [
(TEST_ROOT / "staticfiles" / "cms").abspath(), (TEST_ROOT / "staticfiles" / "cms").abspath(),
) ]
...@@ -30,6 +30,7 @@ from util.db import NoOpMigrationModules ...@@ -30,6 +30,7 @@ from util.db import NoOpMigrationModules
from lms.envs.test import ( from lms.envs.test import (
WIKI_ENABLED, WIKI_ENABLED,
PLATFORM_NAME, PLATFORM_NAME,
SITE_ID,
SITE_NAME, SITE_NAME,
DEFAULT_FILE_STORAGE, DEFAULT_FILE_STORAGE,
MEDIA_ROOT, MEDIA_ROOT,
...@@ -281,6 +282,8 @@ MICROSITE_CONFIGURATION = { ...@@ -281,6 +282,8 @@ MICROSITE_CONFIGURATION = {
MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver' MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver'
MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.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 # For consistency in user-experience, keep the value of this setting in sync with
# the one in lms/envs/test.py # the one in lms/envs/test.py
FEATURES['ENABLE_DISCUSSION_SERVICE'] = False FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
......
...@@ -17,7 +17,7 @@ from monkey_patch import ( ...@@ -17,7 +17,7 @@ from monkey_patch import (
import xmodule.x_module import xmodule.x_module
import cms.lib.xblock.runtime import cms.lib.xblock.runtime
from openedx.core.djangoapps.theming.core import enable_comprehensive_theme from openedx.core.djangoapps.theming.core import enable_comprehensive_theming
def run(): def run():
...@@ -30,7 +30,7 @@ def run(): ...@@ -30,7 +30,7 @@ def run():
# Comprehensive theming needs to be set up before django startup, # Comprehensive theming needs to be set up before django startup,
# because modifying django template paths after startup has no effect. # because modifying django template paths after startup has no effect.
if settings.COMPREHENSIVE_THEME_DIR: if settings.COMPREHENSIVE_THEME_DIR:
enable_comprehensive_theme(settings.COMPREHENSIVE_THEME_DIR) enable_comprehensive_theming(settings.COMPREHENSIVE_THEME_DIR)
django.setup() django.setup()
......
...@@ -22,7 +22,7 @@ from student.tests.factories import CourseEnrollmentFactory, UserFactory ...@@ -22,7 +22,7 @@ from student.tests.factories import CourseEnrollmentFactory, UserFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
import lms.djangoapps.commerce.tests.test_utils as ecomm_test_utils import lms.djangoapps.commerce.tests.test_utils as ecomm_test_utils
from course_modes.models import CourseMode, Mode from course_modes.models import CourseMode, Mode
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
@ddt.ddt @ddt.ddt
...@@ -352,7 +352,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -352,7 +352,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
self.assertEquals(course_modes, expected_modes) self.assertEquals(course_modes, expected_modes)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@with_is_edx_domain(True) @with_comprehensive_theme("edx.org")
def test_hide_nav(self): def test_hide_nav(self):
# Create the course modes # Create the course modes
for mode in ["honor", "verified"]: for mode in ["honor", "verified"]:
......
...@@ -9,9 +9,14 @@ import pkg_resources ...@@ -9,9 +9,14 @@ import pkg_resources
from django.conf import settings from django.conf import settings
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
from mako.exceptions import TopLevelLookupException
from microsite_configuration import microsite
from . import LOOKUP 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): class DynamicTemplateLookup(TemplateLookup):
...@@ -49,15 +54,25 @@ class DynamicTemplateLookup(TemplateLookup): ...@@ -49,15 +54,25 @@ class DynamicTemplateLookup(TemplateLookup):
def get_template(self, uri): def get_template(self, uri):
""" """
Overridden method which will hand-off the template lookup to the microsite subsystem Overridden method for locating a template in either the database or the site theme.
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.
""" """
microsite_template = microsite.get_template(uri) template = themed_template(uri)
return ( if not template:
microsite_template try:
if microsite_template template = super(DynamicTemplateLookup, self).get_template(get_template_path_with_theme(uri))
else super(DynamicTemplateLookup, self).get_template(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): def clear_lookups(namespace):
......
...@@ -12,16 +12,18 @@ ...@@ -12,16 +12,18 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from django.template import Context
from django.http import HttpResponse
import logging import logging
from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.template import Context
from microsite_configuration import microsite from microsite_configuration import microsite
from edxmako import lookup_template from edxmako import lookup_template
from edxmako.middleware import get_template_request_context from edxmako.middleware import get_template_request_context
from django.conf import settings from openedx.core.djangoapps.theming.helpers import get_template_path
from django.core.urlresolvers import reverse
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -113,8 +115,7 @@ def microsite_footer_context_processor(request): ...@@ -113,8 +115,7 @@ def microsite_footer_context_processor(request):
def render_to_string(template_name, dictionary, context=None, namespace='main'): def render_to_string(template_name, dictionary, context=None, namespace='main'):
# see if there is an override template defined in the microsite template_name = get_template_path(template_name)
template_name = microsite.get_template_path(template_name)
context_instance = Context(dictionary) context_instance = Context(dictionary)
# add dictionary to context_instance # add dictionary to context_instance
......
...@@ -11,7 +11,6 @@ BaseMicrositeTemplateBackend is Base Class for the microsite template backend. ...@@ -11,7 +11,6 @@ BaseMicrositeTemplateBackend is Base Class for the microsite template backend.
from __future__ import absolute_import from __future__ import absolute_import
import abc import abc
import edxmako
import os.path import os.path
import threading import threading
...@@ -272,9 +271,7 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend): ...@@ -272,9 +271,7 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend):
Configure the paths for the microsites feature Configure the paths for the microsites feature
""" """
microsites_root = settings.MICROSITE_ROOT_DIR microsites_root = settings.MICROSITE_ROOT_DIR
if os.path.isdir(microsites_root): if os.path.isdir(microsites_root):
edxmako.paths.add_lookup('main', microsites_root)
settings.STATICFILES_DIRS.insert(0, microsites_root) settings.STATICFILES_DIRS.insert(0, microsites_root)
log.info('Loading microsite path at %s', microsites_root) log.info('Loading microsite path at %s', microsites_root)
...@@ -292,6 +289,7 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend): ...@@ -292,6 +289,7 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend):
microsites_root = settings.MICROSITE_ROOT_DIR microsites_root = settings.MICROSITE_ROOT_DIR
if self.has_configuration_set(): if self.has_configuration_set():
settings.MAKO_TEMPLATES['main'].insert(0, microsites_root)
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].append(microsites_root) settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].append(microsites_root)
......
...@@ -105,6 +105,23 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase): ...@@ -105,6 +105,23 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase):
microsite.clear() microsite.clear()
self.assertIsNone(microsite.get_value('platform_name')) 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') @patch('edxmako.paths.add_lookup')
def test_enable_microsites(self, add_lookup): def test_enable_microsites(self, add_lookup):
""" """
...@@ -122,7 +139,6 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase): ...@@ -122,7 +139,6 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase):
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}): with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}):
microsite.enable_microsites(log) microsite.enable_microsites(log)
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.STATICFILES_DIRS) 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): def test_get_all_configs(self):
""" """
......
...@@ -4,7 +4,13 @@ from pipeline_mako import compressed_css, compressed_js ...@@ -4,7 +4,13 @@ from pipeline_mako import compressed_css, compressed_js
from django.utils.translation import get_language_bidi from django.utils.translation import get_language_bidi
from mako.exceptions import TemplateLookupException from mako.exceptions import TemplateLookupException
from openedx.core.djangoapps.theming.helpers import get_page_title_breadcrumbs, get_value, get_template_path, get_themed_template_path, is_request_in_themed_site from openedx.core.djangoapps.theming.helpers import (
get_page_title_breadcrumbs,
get_value,
get_template_path,
get_themed_template_path,
is_request_in_themed_site,
)
from certificates.api import get_asset_url_by_slug from certificates.api import get_asset_url_by_slug
from lang_pref.api import released_languages from lang_pref.api import released_languages
%> %>
......
...@@ -22,7 +22,7 @@ from edxmako.shortcuts import render_to_string ...@@ -22,7 +22,7 @@ from edxmako.shortcuts import render_to_string
from edxmako.tests import mako_middleware_process_request from edxmako.tests import mako_middleware_process_request
from util.request import safe_get_host from util.request import safe_get_host
from util.testing import EventTestMixin from util.testing import EventTestMixin
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
class TestException(Exception): class TestException(Exception):
...@@ -100,7 +100,7 @@ class ActivationEmailTests(TestCase): ...@@ -100,7 +100,7 @@ class ActivationEmailTests(TestCase):
self._create_account() self._create_account()
self._assert_activation_email(self.ACTIVATION_SUBJECT, self.OPENEDX_FRAGMENTS) 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): def test_activation_email_edx_domain(self):
self._create_account() self._create_account()
self._assert_activation_email(self.ACTIVATION_SUBJECT, self.EDX_DOMAIN_FRAGMENTS) self._assert_activation_email(self.ACTIVATION_SUBJECT, self.EDX_DOMAIN_FRAGMENTS)
......
...@@ -44,7 +44,6 @@ from certificates.tests.factories import GeneratedCertificateFactory # pylint: ...@@ -44,7 +44,6 @@ from certificates.tests.factories import GeneratedCertificateFactory # pylint:
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
import shoppingcart # pylint: disable=import-error import shoppingcart # pylint: disable=import-error
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
# Explicitly import the cache from ConfigurationModel so we can reset it after each test # Explicitly import the cache from ConfigurationModel so we can reset it after each test
from config_models.models import cache from config_models.models import cache
...@@ -492,7 +491,6 @@ class DashboardTest(ModuleStoreTestCase): ...@@ -492,7 +491,6 @@ class DashboardTest(ModuleStoreTestCase):
self.assertEquals(response_2.status_code, 200) self.assertEquals(response_2.status_code, 200)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @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): def test_dashboard_header_nav_has_find_courses(self):
self.client.login(username="jack", password="test") self.client.login(username="jack", password="test")
response = self.client.get(reverse("dashboard")) 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;
<%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 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')}");
});
</%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);
<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 ...@@ -10,7 +10,7 @@ import mock
import ddt import ddt
from config_models.models import cache from config_models.models import cache
from branding.models import BrandingApiConfig from branding.models import BrandingApiConfig
from openedx.core.djangoapps.theming.test_util import with_edx_domain_context from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme_context
@ddt.ddt @ddt.ddt
...@@ -30,19 +30,19 @@ class TestFooter(TestCase): ...@@ -30,19 +30,19 @@ class TestFooter(TestCase):
@ddt.data( @ddt.data(
# Open source version # Open source version
(False, "application/json", "application/json; charset=utf-8", "Open edX"), (None, "application/json", "application/json; charset=utf-8", "Open edX"),
(False, "text/html", "text/html; charset=utf-8", "lms-footer.css"), (None, "text/html", "text/html; charset=utf-8", "lms-footer.css"),
(False, "text/html", "text/html; charset=utf-8", "Open edX"), (None, "text/html", "text/html; charset=utf-8", "Open edX"),
# EdX.org version # EdX.org version
(True, "application/json", "application/json; charset=utf-8", "edX Inc"), ("edx.org", "application/json", "application/json; charset=utf-8", "edX Inc"),
(True, "text/html", "text/html; charset=utf-8", "lms-footer-edx.css"), ("edx.org", "text/html", "text/html; charset=utf-8", "lms-footer-edx.css"),
(True, "text/html", "text/html; charset=utf-8", "edX Inc"), ("edx.org", "text/html", "text/html; charset=utf-8", "edX Inc"),
) )
@ddt.unpack @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) 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) resp = self._get_footer(accepts=accepts)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
...@@ -50,10 +50,10 @@ class TestFooter(TestCase): ...@@ -50,10 +50,10 @@ class TestFooter(TestCase):
self.assertIn(content, resp.content) self.assertIn(content, resp.content)
@mock.patch.dict(settings.FEATURES, {'ENABLE_FOOTER_MOBILE_APP_LINKS': True}) @mock.patch.dict(settings.FEATURES, {'ENABLE_FOOTER_MOBILE_APP_LINKS': True})
@ddt.data(True, False) @ddt.data("edx.org", None)
def test_footer_json(self, is_edx_domain): def test_footer_json(self, theme):
self._set_feature_flag(True) self._set_feature_flag(True)
with with_edx_domain_context(is_edx_domain): with with_comprehensive_theme_context(theme):
resp = self._get_footer() resp = self._get_footer()
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
...@@ -142,18 +142,18 @@ class TestFooter(TestCase): ...@@ -142,18 +142,18 @@ class TestFooter(TestCase):
@ddt.data( @ddt.data(
# OpenEdX # OpenEdX
(False, "en", "lms-footer.css"), (None, "en", "lms-footer.css"),
(False, "ar", "lms-footer-rtl.css"), (None, "ar", "lms-footer-rtl.css"),
# EdX.org # EdX.org
(True, "en", "lms-footer-edx.css"), ("edx.org", "en", "lms-footer-edx.css"),
(True, "ar", "lms-footer-edx-rtl.css"), ("edx.org", "ar", "lms-footer-edx-rtl.css"),
) )
@ddt.unpack @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) 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}) resp = self._get_footer(accepts="text/html", params={'language': language})
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
...@@ -161,18 +161,18 @@ class TestFooter(TestCase): ...@@ -161,18 +161,18 @@ class TestFooter(TestCase):
@ddt.data( @ddt.data(
# OpenEdX # OpenEdX
(False, True), (None, True),
(False, False), (None, False),
# EdX.org # EdX.org
(True, True), ("edx.org", True),
(True, False), ("edx.org", False),
) )
@ddt.unpack @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) 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 {} params = {'show-openedx-logo': 1} if show_logo else {}
resp = self._get_footer(accepts="text/html", params=params) resp = self._get_footer(accepts="text/html", params=params)
...@@ -185,17 +185,17 @@ class TestFooter(TestCase): ...@@ -185,17 +185,17 @@ class TestFooter(TestCase):
@ddt.data( @ddt.data(
# OpenEdX # OpenEdX
(False, False), (None, False),
(False, True), (None, True),
# EdX.org # EdX.org
(True, False), ("edx.org", False),
(True, True), ("edx.org", True),
) )
@ddt.unpack @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) 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 {} params = {'include-dependencies': 1} if include_dependencies else {}
resp = self._get_footer(accepts="text/html", params=params) resp = self._get_footer(accepts="text/html", params=params)
......
...@@ -4,13 +4,12 @@ from uuid import uuid4 ...@@ -4,13 +4,12 @@ from uuid import uuid4
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
import ddt import ddt
from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
import mock import mock
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
class UserMixin(object): class UserMixin(object):
...@@ -87,7 +86,7 @@ class ReceiptViewTests(UserMixin, TestCase): ...@@ -87,7 +86,7 @@ class ReceiptViewTests(UserMixin, TestCase):
self.assertRegexpMatches(response.content, user_message if is_user_message_expected else system_message) 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) 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): def test_hide_nav_header(self):
self._login() self._login()
post_data = {'decision': 'ACCEPT', 'reason_code': '200', 'signed_field_names': 'dummy'} post_data = {'decision': 'ACCEPT', 'reason_code': '200', 'signed_field_names': 'dummy'}
......
""" """
Tests for wiki middleware. Tests for wiki middleware.
""" """
from django.conf import settings
from django.test.client import Client from django.test.client import Client
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from wiki.models import URLPath from wiki.models import URLPath
...@@ -33,7 +32,7 @@ class TestComprehensiveTheming(ModuleStoreTestCase): ...@@ -33,7 +32,7 @@ class TestComprehensiveTheming(ModuleStoreTestCase):
self.client = Client() self.client = Client()
self.client.login(username='instructor', password='secret') self.client.login(username='instructor', password='secret')
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme') @with_comprehensive_theme('red-theme')
def test_themed_footer(self): def test_themed_footer(self):
""" """
Tests that theme footer is used rather than standard Tests that theme footer is used rather than standard
......
...@@ -6,8 +6,6 @@ import re ...@@ -6,8 +6,6 @@ import re
import cgi import cgi
from django.conf import settings 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.shortcuts import redirect
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -50,18 +48,6 @@ def course_wiki_redirect(request, course_id): # pylint: disable=unused-argument ...@@ -50,18 +48,6 @@ def course_wiki_redirect(request, course_id): # pylint: disable=unused-argument
if not valid_slug: if not valid_slug:
return redirect("wiki:get", path="") 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):
raise ImproperlyConfigured("No site object was created and the SITE_ID doesn't match the newly created one. {} != {}".format(site_id, settings.SITE_ID))
try: try:
urlpath = URLPath.get_by_path(course_slug, select_related=True) urlpath = URLPath.get_by_path(course_slug, select_related=True)
......
...@@ -6,21 +6,33 @@ from django.test import TestCase ...@@ -6,21 +6,33 @@ from django.test import TestCase
from path import path # pylint: disable=no-name-in-module from path import path # pylint: disable=no-name-in-module
from django.contrib import staticfiles from django.contrib import staticfiles
from paver.easy import call_task
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
from openedx.core.lib.tempdir import mkdtemp_clean from openedx.core.lib.tempdir import mkdtemp_clean, mksym_link
class TestComprehensiveTheming(TestCase): class TestComprehensiveTheming(TestCase):
"""Test comprehensive theming.""" """Test comprehensive theming."""
@classmethod
def setUpClass(cls):
compile_sass('lms')
super(TestComprehensiveTheming, cls).setUpClass()
def setUp(self): def setUp(self):
super(TestComprehensiveTheming, self).setUp() super(TestComprehensiveTheming, self).setUp()
# Clear the internal staticfiles caches, to get test isolation. # Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear() 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): 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('/') resp = self.client.get('/')
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# This string comes from footer.html # This string comes from footer.html
...@@ -34,12 +46,16 @@ class TestComprehensiveTheming(TestCase): ...@@ -34,12 +46,16 @@ class TestComprehensiveTheming(TestCase):
# of test. # of test.
# Make a temp directory as a theme. # Make a temp directory as a theme.
tmp_theme = path(mkdtemp_clean()) themes_dir = path(mkdtemp_clean())
template_dir = tmp_theme / "lms/templates" tmp_theme = "temp_theme"
template_dir = themes_dir / tmp_theme / "lms/templates"
template_dir.makedirs() template_dir.makedirs()
with open(template_dir / "footer.html", "w") as footer: with open(template_dir / "footer.html", "w") as footer:
footer.write("<footer>TEMPORARY THEME</footer>") footer.write("<footer>TEMPORARY THEME</footer>")
dest_path = path(settings.COMPREHENSIVE_THEME_DIR) / tmp_theme
mksym_link(themes_dir / tmp_theme, dest_path)
@with_comprehensive_theme(tmp_theme) @with_comprehensive_theme(tmp_theme)
def do_the_test(self): def do_the_test(self):
"""A function to do the work so we can use the decorator.""" """A function to do the work so we can use the decorator."""
...@@ -50,16 +66,18 @@ class TestComprehensiveTheming(TestCase): ...@@ -50,16 +66,18 @@ class TestComprehensiveTheming(TestCase):
do_the_test(self) do_the_test(self)
def test_theme_adjusts_staticfiles_search_path(self): def test_theme_adjusts_staticfiles_search_path(self):
# Test that a theme adds itself to the staticfiles search path. """
Tests theme directories are added to staticfiles search path.
"""
before_finders = list(settings.STATICFILES_FINDERS) before_finders = list(settings.STATICFILES_FINDERS)
before_dirs = list(settings.STATICFILES_DIRS) before_dirs = list(settings.STATICFILES_DIRS)
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme') @with_comprehensive_theme('red-theme')
def do_the_test(self): def do_the_test(self):
"""A function to do the work so we can use the decorator.""" """A function to do the work so we can use the decorator."""
self.assertEqual(list(settings.STATICFILES_FINDERS), before_finders) self.assertEqual(list(settings.STATICFILES_FINDERS), before_finders)
self.assertEqual(settings.STATICFILES_DIRS[0], settings.REPO_ROOT / 'themes/red-theme/lms/static') self.assertIn(settings.REPO_ROOT / 'themes/red-theme/lms/static', settings.STATICFILES_DIRS)
self.assertEqual(settings.STATICFILES_DIRS[1:], before_dirs) self.assertEqual(settings.STATICFILES_DIRS, before_dirs)
do_the_test(self) do_the_test(self)
...@@ -67,9 +85,9 @@ class TestComprehensiveTheming(TestCase): ...@@ -67,9 +85,9 @@ class TestComprehensiveTheming(TestCase):
result = staticfiles.finders.find('images/logo.png') result = staticfiles.finders.find('images/logo.png')
self.assertEqual(result, settings.REPO_ROOT / 'lms/static/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): def test_overridden_logo_image(self):
result = staticfiles.finders.find('images/logo.png') result = staticfiles.finders.find('red-theme/lms/static/images/logo.png')
self.assertEqual(result, settings.REPO_ROOT / 'themes/red-theme/lms/static/images/logo.png') self.assertEqual(result, settings.REPO_ROOT / 'themes/red-theme/lms/static/images/logo.png')
def test_default_favicon(self): def test_default_favicon(self):
...@@ -79,10 +97,54 @@ class TestComprehensiveTheming(TestCase): ...@@ -79,10 +97,54 @@ class TestComprehensiveTheming(TestCase):
result = staticfiles.finders.find('images/favicon.ico') result = staticfiles.finders.find('images/favicon.ico')
self.assertEqual(result, settings.REPO_ROOT / 'lms/static/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_css(self):
"""
Test that static files finders are adjusted according to the applied comprehensive theme.
"""
result = staticfiles.finders.find('red-theme/lms/static/css/lms-main.css')
self.assertEqual(result, settings.REPO_ROOT / "themes/red-theme/lms/static/css/lms-main.css")
lms_main_css = ""
with open(result) as css_file:
lms_main_css += css_file.read()
self.assertIn("background:#fa0000", lms_main_css)
def test_default_css(self):
"""
Test default css is served if no theme is applied
"""
result = staticfiles.finders.find('css/lms-main.css')
self.assertEqual(result, settings.REPO_ROOT / "lms/static/css/lms-main.css")
lms_main_css = ""
with open(result) as css_file:
lms_main_css += css_file.read()
self.assertNotIn("background:#00fa00", lms_main_css)
@with_comprehensive_theme('red-theme')
def test_overridden_favicon(self): def test_overridden_favicon(self):
""" """
Test comprehensive theme override on favicon image. Test comprehensive theme override on favicon image.
""" """
result = staticfiles.finders.find('images/favicon.ico') result = staticfiles.finders.find('red-theme/lms/static/images/favicon.ico')
self.assertEqual(result, settings.REPO_ROOT / 'themes/red-theme/lms/static/images/favicon.ico') self.assertEqual(result, settings.REPO_ROOT / 'themes/red-theme/lms/static/images/favicon.ico')
def compile_sass(system):
"""
Process xmodule assets and compile sass files.
:param system - 'lms' or 'cms', specified the system to compile sass for.
"""
# Compile system sass files
call_task(
'pavelib.assets.update_assets',
args=(
system,
"--themes_dir={themes_dir}".format(themes_dir=settings.COMPREHENSIVE_THEME_DIR),
"--themes=red-theme",
"--settings=test"),
)
...@@ -9,7 +9,7 @@ from django.conf import settings ...@@ -9,7 +9,7 @@ from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
@attr('shard_1') @attr('shard_1')
...@@ -37,7 +37,7 @@ class TestFooter(TestCase): ...@@ -37,7 +37,7 @@ class TestFooter(TestCase):
"youtube": "https://www.youtube.com/" "youtube": "https://www.youtube.com/"
} }
@with_is_edx_domain(True) @with_comprehensive_theme("edx.org")
def test_edx_footer(self): def test_edx_footer(self):
""" """
Verify that the homepage, when accessed at edx.org, has the edX footer Verify that the homepage, when accessed at edx.org, has the edX footer
...@@ -46,7 +46,6 @@ class TestFooter(TestCase): ...@@ -46,7 +46,6 @@ class TestFooter(TestCase):
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertContains(resp, 'footer-edx-v3') self.assertContains(resp, 'footer-edx-v3')
@with_is_edx_domain(False)
def test_openedx_footer(self): def test_openedx_footer(self):
""" """
Verify that the homepage, when accessed at something other than Verify that the homepage, when accessed at something other than
...@@ -56,7 +55,7 @@ class TestFooter(TestCase): ...@@ -56,7 +55,7 @@ class TestFooter(TestCase):
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertContains(resp, 'footer-openedx') self.assertContains(resp, 'footer-openedx')
@with_is_edx_domain(True) @with_comprehensive_theme("edx.org")
@override_settings( @override_settings(
SOCIAL_MEDIA_FOOTER_NAMES=SOCIAL_MEDIA_NAMES, SOCIAL_MEDIA_FOOTER_NAMES=SOCIAL_MEDIA_NAMES,
SOCIAL_MEDIA_FOOTER_URLS=SOCIAL_MEDIA_URLS SOCIAL_MEDIA_FOOTER_URLS=SOCIAL_MEDIA_URLS
......
...@@ -26,7 +26,7 @@ from student_account.views import account_settings_context ...@@ -26,7 +26,7 @@ from student_account.views import account_settings_context
from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from openedx.core.djangoapps.theming.test_util import with_edx_domain_context from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme_context
@ddt.ddt @ddt.ddt
...@@ -241,13 +241,13 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi ...@@ -241,13 +241,13 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
self.assertRedirects(response, reverse("dashboard")) self.assertRedirects(response, reverse("dashboard"))
@ddt.data( @ddt.data(
(False, "signin_user"), (None, "signin_user"),
(False, "register_user"), (None, "register_user"),
(True, "signin_user"), ("edx.org", "signin_user"),
(True, "register_user"), ("edx.org", "register_user"),
) )
@ddt.unpack @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 = [ params = [
('course_id', 'edX/DemoX/Demo_Course'), ('course_id', 'edX/DemoX/Demo_Course'),
('enrollment_action', 'enroll'), ('enrollment_action', 'enroll'),
...@@ -255,7 +255,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi ...@@ -255,7 +255,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
# The response should have a "Sign In" button with the URL # The response should have a "Sign In" button with the URL
# that preserves the querystring params # 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) response = self.client.get(reverse(url_name), params)
expected_url = '/login?{}'.format(self._finish_auth_url_param(params + [('next', '/dashboard')])) expected_url = '/login?{}'.format(self._finish_auth_url_param(params + [('next', '/dashboard')]))
...@@ -271,7 +271,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi ...@@ -271,7 +271,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
] ]
# Verify that this parameter is also preserved # 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) response = self.client.get(reverse(url_name), params)
expected_url = '/login?{}'.format(self._finish_auth_url_param(params)) expected_url = '/login?{}'.format(self._finish_auth_url_param(params))
......
...@@ -37,7 +37,7 @@ from common.test.utils import XssTestMixin ...@@ -37,7 +37,7 @@ from common.test.utils import XssTestMixin
from commerce.tests import TEST_PAYMENT_DATA, TEST_API_URL, TEST_API_SIGNING_KEY from commerce.tests import TEST_PAYMENT_DATA, TEST_API_URL, TEST_API_SIGNING_KEY
from embargo.test_utils import restrict_course from embargo.test_utils import restrict_course
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
from shoppingcart.models import Order, CertificateItem from shoppingcart.models import Order, CertificateItem
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
...@@ -283,7 +283,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -283,7 +283,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
) )
self._assert_redirects_to_dashboard(response) 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") @ddt.data("verify_student_start_flow", "verify_student_begin_flow")
def test_pay_and_verify_hides_header_nav(self, payment_flow): def test_pay_and_verify_hides_header_nav(self, payment_flow):
course = self._create_course("verified") course = self._create_course("verified")
......
...@@ -61,9 +61,9 @@ STATIC_URL = "/static/" ...@@ -61,9 +61,9 @@ STATIC_URL = "/static/"
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
) )
STATICFILES_DIRS = ( STATICFILES_DIRS = [
(TEST_ROOT / "staticfiles" / "lms").abspath(), (TEST_ROOT / "staticfiles" / "lms").abspath(),
) ]
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
MEDIA_ROOT = TEST_ROOT / "uploads" MEDIA_ROOT = TEST_ROOT / "uploads"
......
...@@ -397,7 +397,7 @@ COURSES_ROOT = ENV_ROOT / "data" ...@@ -397,7 +397,7 @@ COURSES_ROOT = ENV_ROOT / "data"
DATA_DIR = COURSES_ROOT DATA_DIR = COURSES_ROOT
# comprehensive theming system # comprehensive theming system
COMPREHENSIVE_THEME_DIR = "" COMPREHENSIVE_THEME_DIR = REPO_ROOT / "themes"
# TODO: Remove the rest of the sys.path modification here and in cms/envs/common.py # TODO: Remove the rest of the sys.path modification here and in cms/envs/common.py
sys.path.append(REPO_ROOT) sys.path.append(REPO_ROOT)
...@@ -486,6 +486,7 @@ TEMPLATES = [ ...@@ -486,6 +486,7 @@ TEMPLATES = [
'loaders': [ 'loaders': [
# We have to use mako-aware template loaders to be able to include # We have to use mako-aware template loaders to be able to include
# mako templates inside django templates (such as main_django.html). # mako templates inside django templates (such as main_django.html).
'openedx.core.djangoapps.theming.template_loaders.ThemeFilesystemLoader',
'edxmako.makoloader.MakoFilesystemLoader', 'edxmako.makoloader.MakoFilesystemLoader',
'edxmako.makoloader.MakoAppDirectoriesLoader', 'edxmako.makoloader.MakoAppDirectoriesLoader',
], ],
...@@ -782,7 +783,6 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' ...@@ -782,7 +783,6 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
CMS_BASE = 'localhost:8001' CMS_BASE = 'localhost:8001'
# Site info # Site info
SITE_ID = 1
SITE_NAME = "example.com" SITE_NAME = "example.com"
HTTPS = 'on' HTTPS = 'on'
ROOT_URLCONF = 'lms.urls' ROOT_URLCONF = 'lms.urls'
...@@ -1172,6 +1172,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage' ...@@ -1172,6 +1172,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
# List of finder classes that know how to find static files in various locations. # 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 # Note: the pipeline finder is included to be able to discover optimized files
STATICFILES_FINDERS = [ STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder', 'pipeline.finders.PipelineFinder',
...@@ -2807,3 +2808,5 @@ AUDIT_CERT_CUTOFF_DATE = None ...@@ -2807,3 +2808,5 @@ AUDIT_CERT_CUTOFF_DATE = None
CREDENTIALS_SERVICE_USERNAME = 'credentials_service_user' CREDENTIALS_SERVICE_USERNAME = 'credentials_service_user'
CREDENTIALS_GENERATION_ROUTING_KEY = HIGH_PRIORITY_QUEUE CREDENTIALS_GENERATION_ROUTING_KEY = HIGH_PRIORITY_QUEUE
WIKI_REQUEST_CACHE_MIDDLEWARE_CLASS = "request_cache.middleware.RequestCache"
...@@ -99,6 +99,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage' ...@@ -99,6 +99,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
# Revert to the default set of finders as we don't want the production pipeline # Revert to the default set of finders as we don't want the production pipeline
STATICFILES_FINDERS = [ STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
] ]
......
...@@ -38,6 +38,6 @@ STATIC_URL = "/static/" ...@@ -38,6 +38,6 @@ STATIC_URL = "/static/"
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
) )
STATICFILES_DIRS = ( STATICFILES_DIRS = [
(TEST_ROOT / "staticfiles" / "lms").abspath(), (TEST_ROOT / "staticfiles" / "lms").abspath(),
) ]
...@@ -420,6 +420,9 @@ openid.oidutil.log = lambda message, level=0: None ...@@ -420,6 +420,9 @@ openid.oidutil.log = lambda message, level=0: None
PLATFORM_NAME = "edX" PLATFORM_NAME = "edX"
SITE_NAME = "edx.org" SITE_NAME = "edx.org"
# use default site for tests
SITE_ID = 1
# set up some testing for microsites # set up some testing for microsites
FEATURES['USE_MICROSITES'] = True FEATURES['USE_MICROSITES'] = True
MICROSITE_ROOT_DIR = COMMON_ROOT / 'test' / 'test_microsites' MICROSITE_ROOT_DIR = COMMON_ROOT / 'test' / 'test_microsites'
...@@ -489,6 +492,8 @@ MICROSITE_CONFIGURATION = { ...@@ -489,6 +492,8 @@ MICROSITE_CONFIGURATION = {
MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver' MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver'
MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver' MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
TEST_THEME = COMMON_ROOT / "test" / "test-theme"
# add extra template directory for test-only templates # add extra template directory for test-only templates
MAKO_TEMPLATES['main'].extend([ MAKO_TEMPLATES['main'].extend([
COMMON_ROOT / 'test' / 'templates', COMMON_ROOT / 'test' / 'templates',
......
...@@ -20,7 +20,7 @@ from monkey_patch import ( ...@@ -20,7 +20,7 @@ from monkey_patch import (
import xmodule.x_module import xmodule.x_module
import lms_xblock.runtime import lms_xblock.runtime
from openedx.core.djangoapps.theming.core import enable_comprehensive_theme from openedx.core.djangoapps.theming.core import enable_comprehensive_theming
from microsite_configuration import microsite from microsite_configuration import microsite
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -40,7 +40,7 @@ def run(): ...@@ -40,7 +40,7 @@ def run():
# Comprehensive theming needs to be set up before django startup, # Comprehensive theming needs to be set up before django startup,
# because modifying django template paths after startup has no effect. # because modifying django template paths after startup has no effect.
if settings.COMPREHENSIVE_THEME_DIR: if settings.COMPREHENSIVE_THEME_DIR:
enable_comprehensive_theme(settings.COMPREHENSIVE_THEME_DIR) enable_comprehensive_theming(settings.COMPREHENSIVE_THEME_DIR)
# We currently use 2 template rendering engines, mako and django_templates, # We currently use 2 template rendering engines, mako and django_templates,
# and one of them (django templates), requires the directories be added # and one of them (django templates), requires the directories be added
......
<!DOCTYPE html> <!DOCTYPE html>
{% load sekizai_tags i18n microsite pipeline optional_include %} {% load sekizai_tags i18n microsite theme_pipeline optional_include %}
{% load url from future %} {% load url from future %}
<html lang="{{LANGUAGE_CODE}}"> <html lang="{{LANGUAGE_CODE}}">
<head> <head>
......
{% extends "main_django.html" %} {% extends "main_django.html" %}
{% 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 %}<title>{% block pagetitle %}{% endblock %} | {% trans "Wiki" %} | {% platform_name %}</title>{% endblock %} {% block title %}<title>{% block pagetitle %}{% endblock %} | {% trans "Wiki" %} | {% platform_name %}</title>{% endblock %}
......
<!DOCTYPE html> <!DOCTYPE html>
{% load wiki_tags i18n %}{% load pipeline %} {% load wiki_tags i18n %}{% load theme_pipeline %}
<html lang="{{LANGUAGE_CODE}}"> <html lang="{{LANGUAGE_CODE}}">
<head> <head>
{% stylesheet 'course' %} {% stylesheet 'course' %}
......
"""
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. Core logic for Comprehensive Theming.
""" """
from path import Path import os.path
from path import Path as path
from django.conf import settings from django.conf import settings
from .helpers import (
get_project_root_name,
)
def comprehensive_theme_changes(theme_dir):
"""
Calculate the set of changes needed to enable a comprehensive theme.
Arguments:
theme_dir (path.path): the full path to the theming directory to use.
Returns:
A dict indicating the changes to make:
* 'settings': a dictionary of settings names and their new values.
* 'template_paths': a list of directories to prepend to template
lookup path.
""" def enable_comprehensive_theming(themes_dir):
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 to relevant paths for comprehensive theming.
:param themes_dir: path to base theme directory
""" """
changes = comprehensive_theme_changes(theme_dir) if isinstance(themes_dir, basestring):
themes_dir = path(themes_dir)
if themes_dir.isdir():
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].insert(0, themes_dir)
settings.MAKO_TEMPLATES['main'].insert(0, themes_dir)
for theme_dir in os.listdir(themes_dir):
staticfiles_dir = os.path.join(themes_dir, theme_dir, get_project_root_name(), "static")
if staticfiles_dir.isdir():
settings.STATICFILES_DIRS = settings.STATICFILES_DIRS + [staticfiles_dir]
# Use the changes locale_dir = os.path.join(themes_dir, theme_dir, get_project_root_name(), "conf", "locale")
for name, value in changes['settings'].iteritems(): if locale_dir.isdir():
setattr(settings, name, value) settings.LOCALE_PATHS = (locale_dir, ) + settings.LOCALE_PATHS
for template_dir in changes['template_paths']:
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].insert(0, template_dir)
settings.MAKO_TEMPLATES['main'].insert(0, template_dir)
...@@ -22,7 +22,10 @@ from django.conf import settings ...@@ -22,7 +22,10 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.contrib.staticfiles import utils from django.contrib.staticfiles import utils
from django.contrib.staticfiles.finders import BaseFinder from django.contrib.staticfiles.finders import BaseFinder
from openedx.core.djangoapps.theming.storage import CachedComprehensiveThemingStorage from openedx.core.djangoapps.theming.helpers import (
get_base_theme_dir
)
from openedx.core.djangoapps.theming.storage import ComprehensiveThemingStorage
class ComprehensiveThemeFinder(BaseFinder): class ComprehensiveThemeFinder(BaseFinder):
...@@ -33,23 +36,21 @@ class ComprehensiveThemeFinder(BaseFinder): ...@@ -33,23 +36,21 @@ class ComprehensiveThemeFinder(BaseFinder):
this finder will never find any files. this finder will never find any files.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""
Initialize finder with comprehensive theming storage if we have
a valid COMPREHENSIVE_THEME_DIR setting.
"""
super(ComprehensiveThemeFinder, self).__init__(*args, **kwargs) super(ComprehensiveThemeFinder, self).__init__(*args, **kwargs)
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "") themes_dir = get_base_theme_dir()
if not theme_dir: if not themes_dir:
self.storage = None self.storage = None
return return
if not isinstance(theme_dir, basestring): if not isinstance(themes_dir, basestring):
raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string") raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
root = Path(settings.PROJECT_ROOT) self.storage = ComprehensiveThemingStorage(location=themes_dir)
if root.name == "":
root = root.parent
component_dir = Path(theme_dir) / root.name
static_dir = component_dir / "static"
self.storage = CachedComprehensiveThemingStorage(location=static_dir)
def find(self, path, all=False): # pylint: disable=redefined-builtin def find(self, path, all=False): # pylint: disable=redefined-builtin
""" """
...@@ -58,10 +59,6 @@ class ComprehensiveThemeFinder(BaseFinder): ...@@ -58,10 +59,6 @@ class ComprehensiveThemeFinder(BaseFinder):
if not self.storage: if not self.storage:
return [] return []
if path.startswith(self.storage.prefix):
# strip the prefix
path = path[len(self.storage.prefix):]
if self.storage.exists(path): if self.storage.exists(path):
match = self.storage.path(path) match = self.storage.path(path)
if all: if all:
......
""" """
Helpers for accessing comprehensive theming related variables. Helpers for accessing comprehensive theming related variables.
""" """
import re
import os.path
from path import Path
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.contrib.staticfiles.storage import staticfiles_storage
from microsite_configuration import microsite from microsite_configuration import microsite
from microsite_configuration import page_title_breadcrumbs from microsite_configuration import page_title_breadcrumbs
from django.conf import settings
def get_page_title_breadcrumbs(*args): def get_page_title_breadcrumbs(*args):
...@@ -24,7 +33,11 @@ def get_template_path(relative_path, **kwargs): ...@@ -24,7 +33,11 @@ def get_template_path(relative_path, **kwargs):
""" """
This is a proxy function to hide microsite_configuration behind comprehensive theming. This is a proxy function to hide microsite_configuration behind comprehensive theming.
""" """
return microsite.get_template_path(relative_path, **kwargs) template_path = get_template_path_with_theme(relative_path)
if template_path == relative_path: # we don't have a theme now look into microsites
template_path = microsite.get_template_path(relative_path, **kwargs)
return template_path
def is_request_in_themed_site(): def is_request_in_themed_site():
...@@ -34,6 +47,14 @@ def is_request_in_themed_site(): ...@@ -34,6 +47,14 @@ def is_request_in_themed_site():
return microsite.is_request_in_microsite() return microsite.is_request_in_microsite()
def get_template(uri):
"""
This is a proxy function to hide microsite_configuration behind comprehensive theming.
:param uri: uri of the template
"""
return microsite.get_template(uri)
def get_themed_template_path(relative_path, default_path, **kwargs): def get_themed_template_path(relative_path, default_path, **kwargs):
""" """
This is a proxy function to hide microsite_configuration behind comprehensive theming. This is a proxy function to hide microsite_configuration behind comprehensive theming.
...@@ -52,3 +73,264 @@ def get_themed_template_path(relative_path, default_path, **kwargs): ...@@ -52,3 +73,264 @@ def get_themed_template_path(relative_path, default_path, **kwargs):
if is_stanford_theming_enabled and not is_microsite: if is_stanford_theming_enabled and not is_microsite:
return relative_path return relative_path
return microsite.get_template_path(default_path, **kwargs) return microsite.get_template_path(default_path, **kwargs)
def get_template_path_with_theme(relative_path):
"""
Returns template path in current site's theme if it finds one there otherwise returns same path.
Example:
>> get_template_path_with_theme('header')
'/red-theme/lms/templates/header.html'
Parameters:
relative_path (str): template's path relative to the templates directory e.g. 'footer.html'
Returns:
(str): template path in current site's theme
"""
site_theme_dir = get_current_site_theme_dir()
if not site_theme_dir:
return relative_path
base_theme_dir = get_base_theme_dir()
root_name = get_project_root_name()
template_path = "/".join([
base_theme_dir,
site_theme_dir,
root_name,
"templates"
])
# strip `/` if present at the start of relative_path
template_name = re.sub(r'^/+', '', relative_path)
search_path = os.path.join(template_path, template_name)
if os.path.isfile(search_path):
path = '/{site_theme_dir}/{root_name}/templates/{template_name}'.format(
site_theme_dir=site_theme_dir,
root_name=root_name,
template_name=template_name,
)
return path
else:
return relative_path
def get_current_theme_template_dirs():
"""
Returns template directories for the current theme.
Example:
>> get_current_theme_template_dirs('header.html')
['/edx/app/edxapp/edx-platform/themes/red-theme/lms/templates/', ]
Returns:
(list): list of directories containing theme templates.
"""
site_theme_dir = get_current_site_theme_dir()
if not site_theme_dir:
return None
base_theme_dir = get_base_theme_dir()
root_name = get_project_root_name()
template_path = "/".join([
base_theme_dir,
site_theme_dir,
root_name,
"templates"
])
return [template_path]
def strip_site_theme_templates_path(uri):
"""
Remove site template theme path from the uri.
Example:
>> strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
'header.html'
Arguments:
uri (str): template path from which to remove site theme path. e.g. '/red-theme/lms/templates/header.html'
Returns:
(str): template path with site theme path removed.
"""
site_theme_dir = get_current_site_theme_dir()
if not site_theme_dir:
return uri
root_name = get_project_root_name()
templates_path = "/".join([
site_theme_dir,
root_name,
"templates"
])
uri = re.sub(r'^/*' + templates_path + '/*', '', uri)
return uri
def get_current_site_theme_dir():
"""
Return theme directory for the current site.
Example:
>> get_current_site_theme_dir()
'red-theme'
Returns:
(str): theme directory for current site
"""
from edxmako.middleware import REQUEST_CONTEXT
request = getattr(REQUEST_CONTEXT, 'request', None)
if not request:
return None
# if hostname is not valid
if not all((isinstance(request.get_host(), basestring), is_valid_hostname(request.get_host()))):
return None
try:
site = get_current_site(request)
except Site.DoesNotExist:
return None
site_theme_dir = cache.get(get_site_theme_cache_key(site))
# if site theme dir is not in cache and comprehensive theming is enabled then pull it from db.
if not site_theme_dir and is_comprehensive_theming_enabled():
site_theme = site.themes.first() # pylint: disable=no-member
if site_theme:
site_theme_dir = site_theme.theme_dir_name
cache_site_theme_dir(site, site_theme_dir)
return site_theme_dir
def get_project_root_name():
"""
Return root name for the current project
Example:
>> get_project_root_name()
'lms'
# from studio
>> get_project_root_name()
'cms'
Returns:
(str): component name of platform e.g lms, cms
"""
root = Path(settings.PROJECT_ROOT)
if root.name == "":
root = root.parent
return root.name
def get_base_theme_dir():
"""
Return base directory that contains all the themes.
Example:
>> get_base_theme_dir()
'/edx/app/edxapp/edx-platform/themes'
Returns:
(str): Base theme directory
"""
return settings.COMPREHENSIVE_THEME_DIR
def is_comprehensive_theming_enabled():
"""
Returns boolean indicating whether comprehensive theming functionality is enabled or disabled.
Example:
>> is_comprehensive_theming_enabled()
True
Returns:
(bool): True if comprehensive theming is enabled else False
"""
return True if settings.COMPREHENSIVE_THEME_DIR else False
def get_site_theme_cache_key(site):
"""
Return cache key for the given site.
Example:
>> site = Site(domain='red-theme.org', name='Red Theme')
>> get_site_theme_cache_key(site)
'theming.site.red-theme.org'
Parameters:
site (django.contrib.sites.models.Site): site where key needs to generated
Returns:
(str): a key to be used as cache key
"""
cache_key = "theming.site.{domain}".format(
domain=site.domain
)
return cache_key
def is_valid_hostname(hostname):
"""
Return boolean indicating if given hostname is valid or not
Example:
>> is_valid_hostname('red-theme.org')
True
Parameters:
hostname (str): hostname that needs to be tested.
Returns:
(bool): True if given hostname is valid else False
"""
if len(hostname) > 255 or "." not in hostname:
return False
if hostname[-1] == ".":
hostname = hostname[:-1] # strip exactly one dot from the right, if present
if ":" in hostname:
hostname = hostname.split(":")[0] # strip port number if present
allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
return all(allowed.match(x) for x in hostname.split("."))
def cache_site_theme_dir(site, theme_dir):
"""
Cache site's theme directory.
Example:
>> site = Site(domain='red-theme.org', name='Red Theme')
>> cache_site_theme_dir(site, 'red-theme')
Parameters:
site (django.contrib.sites.models.Site): site for to cache
theme_dir (str): theme directory for the given site
"""
cache.set(get_site_theme_cache_key(site), theme_dir, settings.FOOTER_CACHE_TIMEOUT)
def get_static_file_url(asset):
"""
Returns url of the themed asset if asset is not themed than returns the default asset url.
Example:
>> get_static_file_url('css/lms-main.css', 'red-theme')
'/static/red-theme/css/lms-main.css'
Parameters:
asset (str): asset's path relative to the static files directory
Returns:
(str): static asset's url
"""
theme = get_current_site_theme_dir()
try:
return staticfiles_storage.url(asset, theme)
except (ValueError, TypeError):
# in case of an error return url without theme applied
return staticfiles_storage.url(asset)
# -*- 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')),
],
),
]
"""
Comprehensive Theme related models.
"""
from django.db import models
from django.contrib.sites.models import Site
class SiteTheme(models.Model):
"""
This is where the information about the site's theme gets stored to the db.
`site` field is foreignkey to django Site model
`theme_dir_name` contains directory name having Site's theme
"""
site = models.ForeignKey(Site, related_name='themes')
theme_dir_name = models.CharField(max_length=255)
def __unicode__(self):
return self.theme_dir_name
...@@ -2,87 +2,83 @@ ...@@ -2,87 +2,83 @@
Comprehensive Theming support for Django's collectstatic functionality. Comprehensive Theming support for Django's collectstatic functionality.
See https://docs.djangoproject.com/en/1.8/ref/contrib/staticfiles/ See https://docs.djangoproject.com/en/1.8/ref/contrib/staticfiles/
""" """
from path import Path
import os.path import os.path
from django.conf import settings import re
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin from django.contrib.staticfiles.storage import StaticFilesStorage
from django.utils._os import safe_join from django.utils._os import safe_join
from django.conf import settings
from openedx.core.djangoapps.theming.helpers import (
get_base_theme_dir,
get_project_root_name,
get_current_site_theme_dir,
)
class ComprehensiveThemingAwareMixin(object):
class ComprehensiveThemingStorage(StaticFilesStorage):
""" """
Mixin for Django storage system to make it aware of the currently-active Mixin for Django storage system to make it aware of the currently-active
comprehensive theme, so that it can generate theme-scoped URLs for themed comprehensive theme, so that it can generate theme-scoped URLs for themed
static assets. static assets.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ComprehensiveThemingAwareMixin, self).__init__(*args, **kwargs) super(ComprehensiveThemingStorage, self).__init__(*args, **kwargs)
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "") themes_dir = get_base_theme_dir()
if not theme_dir: if not themes_dir:
self.theme_location = None self.themes_location = None
return return
if not isinstance(theme_dir, basestring): if not isinstance(themes_dir, basestring):
raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string") raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
root = Path(settings.PROJECT_ROOT) self.themes_location = themes_dir
if root.name == "":
root = root.parent
component_dir = Path(theme_dir) / root.name
self.theme_location = component_dir / "static"
@property def themed(self, name, theme_dir):
def prefix(self):
"""
This is used by the ComprehensiveThemeFinder in the collection step.
"""
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
if not theme_dir:
return None
theme_name = os.path.basename(os.path.normpath(theme_dir))
return "themes/{name}/".format(name=theme_name)
def themed(self, name):
""" """
Given a name, return a boolean indicating whether that name exists Given a name, return a boolean indicating whether that name exists
as a themed asset in the comprehensive theme. as a themed asset in the comprehensive theme.
""" """
# Nothing can be themed if we don't have a theme location. # Nothing can be themed if we don't have a theme location or required params.
if not self.theme_location: if not all((self.themes_location, theme_dir, name)):
return False return False
path = safe_join(self.theme_location, name) themed_path = "/".join([
self.themes_location,
theme_dir,
get_project_root_name(),
"static/"
])
name = name[1:] if name.startswith("/") else name
path = safe_join(themed_path, name)
return os.path.exists(path) return os.path.exists(path)
def path(self, name): def path(self, name):
""" """
Get the path to the real asset on disk Get the path to the real asset on disk
""" """
if self.themed(name): try:
base = self.theme_location theme_dir, asset_path = name.split("/", 1)
else: if self.themed(asset_path, theme_dir):
name = asset_path
base = self.themes_location + "/" + theme_dir + "/" + get_project_root_name() + "/static/"
else:
base = self.location
except ValueError:
# in case we don't '/' in name
base = self.location base = self.location
if base == settings.STATIC_ROOT:
name = re.sub(r"/?(?P<theme>[^/]+)/(?P<system>lms|cms)/static/", r"\g<theme>/", name)
path = safe_join(base, name) path = safe_join(base, name)
return os.path.normpath(path) return os.path.normpath(path)
def url(self, name, *args, **kwargs): def url(self, name):
""" """
Add the theme prefix to the asset URL Add the theme prefix to the asset URL
""" """
if self.themed(name): theme_dir = get_current_site_theme_dir()
name = self.prefix + name if self.themed(name, theme_dir):
return super(ComprehensiveThemingAwareMixin, self).url(name, *args, **kwargs) name = theme_dir + "/" + name
return super(ComprehensiveThemingStorage, self).url(name)
class CachedComprehensiveThemingStorage(
ComprehensiveThemingAwareMixin,
CachedFilesMixin,
StaticFilesStorage
):
"""
Used by the ComprehensiveThemeFinder class. Mixes in support for cached
files and comprehensive theming in static files.
"""
pass
"""
Theming aware template loaders.
"""
import logging
from django.template.loaders.filesystem import Loader as FilesystemLoader
from edxmako.makoloader import MakoLoader
from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme
log = logging.getLogger(__name__)
class ThemeTemplateLoader(MakoLoader):
"""
This is a Django loader object which will load the template based on current request and its corresponding theme.
"""
def __call__(self, template_name, template_dirs=None):
template_name = get_template_path_with_theme(template_name).lstrip("/")
return self.load_template(template_name, template_dirs)
class ThemeFilesystemLoader(ThemeTemplateLoader):
"""
Filesystem Template loaders to pickup templates from theme directory based on the current site.
"""
is_usable = True
_accepts_engine_in_init = True
def __init__(self, *args):
ThemeTemplateLoader.__init__(self, FilesystemLoader(*args))
"""
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)
...@@ -6,87 +6,57 @@ from functools import wraps ...@@ -6,87 +6,57 @@ from functools import wraps
import os import os
import os.path import os.path
import contextlib import contextlib
import re
from mock import patch from mock import patch
from django.conf import settings from django.conf import settings
from django.template import Engine from django.contrib.sites.models import Site
from django.test.utils import override_settings
import edxmako import edxmako
from .models import SiteTheme
from .core import comprehensive_theme_changes
EDX_THEME_DIR = settings.REPO_ROOT / "themes" / "edx.org" def with_comprehensive_theme(theme_dir_name):
def with_comprehensive_theme(theme_dir):
""" """
A decorator to run a test with a particular comprehensive theme. A decorator to run a test with a comprehensive theming enabled.
Arguments: Arguments:
theme_dir (str): the full path to the theme directory to use. theme_dir_name (str): directory name of the site for which we want comprehensive theming enabled.
This will likely use `settings.REPO_ROOT` to get the full path.
""" """
# This decorator gets the settings changes needed for a theme, and applies # This decorator creates Site and SiteTheme models for given domain
# them using the override_settings and edxmako.paths.add_lookup context
# managers.
changes = comprehensive_theme_changes(theme_dir)
def _decorator(func): # pylint: disable=missing-docstring def _decorator(func): # pylint: disable=missing-docstring
@wraps(func) @wraps(func)
def _decorated(*args, **kwargs): # pylint: disable=missing-docstring def _decorated(*args, **kwargs): # pylint: disable=missing-docstring
with override_settings(COMPREHENSIVE_THEME_DIR=theme_dir, **changes['settings']): # make a domain name out of directory name
default_engine = Engine.get_default() domain = "{theme_dir_name}.org".format(theme_dir_name=re.sub(r"\.org$", "", theme_dir_name))
dirs = default_engine.dirs[:] site, __ = Site.objects.get_or_create(domain=domain, name=domain)
with edxmako.save_lookups(): SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme_dir_name)
for template_dir in changes['template_paths']: edxmako.paths.add_lookup('main', settings.COMPREHENSIVE_THEME_DIR, prepend=True)
edxmako.paths.add_lookup('main', template_dir, prepend=True) with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme_dir',
dirs.insert(0, template_dir) return_value=theme_dir_name):
with patch.object(default_engine, 'dirs', dirs): with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
return func(*args, **kwargs) return func(*args, **kwargs)
return _decorated return _decorated
return _decorator 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 @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: 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: if theme:
changes = comprehensive_theme_changes(EDX_THEME_DIR) domain = '{theme}.org'.format(theme=re.sub(r"\.org$", "", theme))
with override_settings(COMPREHENSIVE_THEME_DIR=EDX_THEME_DIR, **changes['settings']): site, __ = Site.objects.get_or_create(domain=domain, name=theme)
with edxmako.save_lookups(): SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme)
for template_dir in changes['template_paths']: edxmako.paths.add_lookup('main', settings.COMPREHENSIVE_THEME_DIR, prepend=True)
edxmako.paths.add_lookup('main', template_dir, prepend=True) with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme_dir',
return_value=theme):
with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
yield yield
else: else:
yield yield
......
"""Tests of comprehensive theming."""
import unittest
from mock import patch
from django.test import TestCase, RequestFactory
from django.conf import settings
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme, strip_site_theme_templates_path, \
get_current_site_theme_dir
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestHelpersLMS(TestCase):
"""Test comprehensive theming helper functions."""
def setUp(self):
super(TestHelpersLMS, self).setUp()
@with_comprehensive_theme('red-theme')
def test_get_template_path_with_theme_enabled(self):
"""
Tests template paths are returned from enabled theme.
"""
template_path = get_template_path_with_theme('header.html')
self.assertEqual(template_path, '/red-theme/lms/templates/header.html')
@with_comprehensive_theme('red-theme')
def test_get_template_path_with_theme_for_missing_template(self):
"""
Tests default template paths are returned if template is not found in the theme.
"""
template_path = get_template_path_with_theme('course.html')
self.assertEqual(template_path, 'course.html')
def test_get_template_path_with_theme_disabled(self):
"""
Tests default template paths are returned when theme is non theme is enabled.
"""
template_path = get_template_path_with_theme('header.html')
self.assertEqual(template_path, 'header.html')
@with_comprehensive_theme('red-theme')
def test_strip_site_theme_templates_path_theme_enabled(self):
"""
Tests site theme templates path is stripped from the given template path.
"""
template_path = strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
self.assertEqual(template_path, 'header.html')
def test_strip_site_theme_templates_path_theme_disabled(self):
"""
Tests site theme templates path returned unchanged if no theme is applied.
"""
template_path = strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
self.assertEqual(template_path, '/red-theme/lms/templates/header.html')
@with_comprehensive_theme('red-theme')
def test_get_current_site_theme_dir(self):
"""
Tests current site theme name.
"""
factory = RequestFactory()
with patch(
'edxmako.middleware.REQUEST_CONTEXT.request',
factory.get('/', SERVER_NAME="red-theme.org"),
create=True,
):
current_site = get_current_site_theme_dir()
self.assertEqual(current_site, 'red-theme')
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
class TestHelpersCMS(TestCase):
"""Test comprehensive theming helper functions."""
def setUp(self):
super(TestHelpersCMS, self).setUp()
@with_comprehensive_theme('red-theme')
def test_get_template_path_with_theme_enabled(self):
"""
Tests template paths are returned from enabled theme.
"""
template_path = get_template_path_with_theme('login.html')
self.assertEqual(template_path, '/red-theme/cms/templates/login.html')
@with_comprehensive_theme('red-theme')
def test_get_template_path_with_theme_for_missing_template(self):
"""
Tests default template paths are returned if template is not found in the theme.
"""
template_path = get_template_path_with_theme('certificates.html')
self.assertEqual(template_path, 'certificates.html')
def test_get_template_path_with_theme_disabled(self):
"""
Tests default template paths are returned when theme is non theme is enabled.
"""
template_path = get_template_path_with_theme('login.html')
self.assertEqual(template_path, 'login.html')
@with_comprehensive_theme('red-theme')
def test_strip_site_theme_templates_path_theme_enabled(self):
"""
Tests site theme templates path is stripped from the given template path.
"""
template_path = strip_site_theme_templates_path('/red-theme/cms/templates/login.html')
self.assertEqual(template_path, 'login.html')
def test_strip_site_theme_templates_path_theme_disabled(self):
"""
Tests site theme templates path returned unchanged if no theme is applied.
"""
template_path = strip_site_theme_templates_path('/red-theme/cms/templates/login.html')
self.assertEqual(template_path, '/red-theme/cms/templates/login.html')
@with_comprehensive_theme('red-theme')
def test_get_current_site_theme_dir(self):
"""
Tests current site theme name.
"""
factory = RequestFactory()
with patch(
'edxmako.middleware.REQUEST_CONTEXT.request',
factory.get('/', SERVER_NAME="red-theme.org"),
create=True,
):
current_site = get_current_site_theme_dir()
self.assertEqual(current_site, 'red-theme')
"""
Tests for comprehensive theme static files storage classes.
"""
import ddt
import unittest
import re
from mock import patch
from django.test import TestCase
from django.conf import settings
from openedx.core.djangoapps.theming.helpers import get_base_theme_dir
from openedx.core.djangoapps.theming.storage import ComprehensiveThemingStorage
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt
class TestStorageLMS(TestCase):
"""
Test comprehensive theming static files storage.
"""
def setUp(self):
super(TestStorageLMS, self).setUp()
self.themes_dir = get_base_theme_dir()
self.enabled_theme = "red-theme"
self.system_dir = settings.REPO_ROOT / "lms"
self.storage = ComprehensiveThemingStorage(location=self.themes_dir)
@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))
@ddt.data(
("images/logo.png", ),
("images/favicon.ico", ),
)
@ddt.unpack
def test_url(self, asset):
"""
Verify storage returns correct url depending upon the enabled theme
"""
with patch(
"openedx.core.djangoapps.theming.storage.get_current_site_theme_dir",
return_value=self.enabled_theme,
):
asset_url = self.storage.url(asset)
# remove hash key from file url
asset_url = re.sub(r"(\.\w+)(\.png|\.ico)$", r"\g<2>", asset_url)
expected_url = self.storage.base_url + self.enabled_theme + "/" + asset
self.assertEqual(asset_url, expected_url)
@ddt.data(
("images/logo.png", ),
("images/favicon.ico", ),
)
@ddt.unpack
def test_path(self, asset):
"""
Verify storage returns correct file path depending upon the enabled theme
"""
with patch(
"openedx.core.djangoapps.theming.storage.get_current_site_theme_dir",
return_value=self.enabled_theme,
):
asset_url = self.storage.url(asset)
asset_url = asset_url.replace(self.storage.base_url, "")
# remove hash key from file url
asset_url = re.sub(r"(\.\w+)(\.png|\.ico)$", r"\g<2>", asset_url)
returned_path = self.storage.path(asset_url)
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 paver.easy import call_task
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestComprehensiveThemeLMS(TestCase):
"""
Test html, sass and static file overrides for comprehensive themes.
"""
def setUp(self):
"""
Clear static file finders cache and register cleanup methods.
"""
super(TestComprehensiveThemeLMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Enable Comprehensive theme and compile sass files.
"""
# Apply Comprehensive theme and compile sass assets.
compile_sass('lms')
super(TestComprehensiveThemeLMS, cls).setUpClass()
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_footer(self):
"""
Test that theme footer is used instead of default footer.
"""
resp = self.client.get('/')
self.assertEqual(resp.status_code, 200)
# This string comes from header.html of test-theme
self.assertContains(resp, "This is a footer for test-theme.")
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_logo_image(self):
"""
Test that theme logo is used instead of default logo.
"""
result = staticfiles.finders.find('test-theme/images/logo.png')
self.assertEqual(result, settings.TEST_THEME / 'lms/static/images/logo.png')
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_css_files(self):
"""
Test that theme sass files are used instead of default sass files.
"""
result = staticfiles.finders.find('test-theme/css/lms-main.css')
self.assertEqual(result, settings.TEST_THEME / "lms/static/css/lms-main.css")
lms_main_css = ""
with open(result) as css_file:
lms_main_css += css_file.read()
self.assertIn("background:#00fa00", lms_main_css)
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
class TestComprehensiveThemeCMS(TestCase):
"""
Test html, sass and static file overrides for comprehensive themes.
"""
def setUp(self):
"""
Clear static file finders cache and register cleanup methods.
"""
super(TestComprehensiveThemeCMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Enable Comprehensive theme and compile sass files.
"""
# Apply Comprehensive theme and compile sass assets.
compile_sass('cms')
super(TestComprehensiveThemeCMS, cls).setUpClass()
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_template_override(self):
"""
Test that theme templates are used instead of default templates.
"""
resp = self.client.get('/signin')
self.assertEqual(resp.status_code, 200)
# This string comes from login.html of test-theme
self.assertContains(resp, "Login Page override for test-theme.")
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_css_files(self):
"""
Test that theme sass files are used instead of default sass files.
"""
result = staticfiles.finders.find('test-theme/css/studio-main.css')
self.assertEqual(result, settings.TEST_THEME / "cms/static/css/studio-main.css")
cms_main_css = ""
with open(result) as css_file:
cms_main_css += css_file.read()
self.assertIn("background:#00fa00", cms_main_css)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestComprehensiveThemeDisabledLMS(TestCase):
"""
Test Sass compilation order and sass overrides for comprehensive themes.
"""
def setUp(self):
"""
Clear static file finders cache.
"""
super(TestComprehensiveThemeDisabledLMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Compile sass files.
"""
# compile LMS SASS
compile_sass('lms')
super(TestComprehensiveThemeDisabledLMS, cls).setUpClass()
def test_logo(self):
"""
Test that default logo is picked in case of no comprehensive theme.
"""
result = staticfiles.finders.find('images/logo.png')
self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/logo.png')
def test_css(self):
"""
Test that default css files served without comprehensive themes applied.
"""
result = staticfiles.finders.find('css/lms-main.css')
self.assertEqual(result, settings.REPO_ROOT / "lms/static/css/lms-main.css")
lms_main_css = ""
with open(result) as css_file:
lms_main_css += css_file.read()
self.assertNotIn("background:#00fa00", lms_main_css)
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
class TestComprehensiveThemeDisabledCMS(TestCase):
"""
Test default html, sass and static file when no theme is applied.
"""
def setUp(self):
"""
Clear static file finders cache and register cleanup methods.
"""
super(TestComprehensiveThemeDisabledCMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Enable Comprehensive theme and compile sass files.
"""
# Apply Comprehensive theme and compile sass assets.
compile_sass('cms')
super(TestComprehensiveThemeDisabledCMS, cls).setUpClass()
def test_template_override(self):
"""
Test that defaults templates are used when no theme is applied.
"""
resp = self.client.get('/signin')
self.assertEqual(resp.status_code, 200)
self.assertNotContains(resp, "Login Page override for test-theme.")
def test_css_files(self):
"""
Test that default css files served without comprehensive themes applied..
"""
result = staticfiles.finders.find('css/studio-main.css')
self.assertEqual(result, settings.REPO_ROOT / "cms/static/css/studio-main.css")
cms_main_css = ""
with open(result) as css_file:
cms_main_css += css_file.read()
self.assertNotIn("background:#00fa00", cms_main_css)
def compile_sass(system):
"""
Process xmodule assets and compile sass files for the given system.
:param system - 'lms' or 'cms', specified the system to compile sass for.
"""
# Compile system sass files
call_task(
'pavelib.assets.update_assets',
args=(
system,
"--themes_dir={}".format(settings.TEST_THEME.dirname()),
"--themes={}".format(settings.TEST_THEME.basename()),
"--settings=test"),
)
...@@ -17,3 +17,22 @@ def cleanup_tempdir(the_dir): ...@@ -17,3 +17,22 @@ def cleanup_tempdir(the_dir):
"""Called on process exit to remove a temp directory.""" """Called on process exit to remove a temp directory."""
if os.path.exists(the_dir): if os.path.exists(the_dir):
shutil.rmtree(the_dir) shutil.rmtree(the_dir)
def mksym_link(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(cleanup_symlink, dest)
def cleanup_symlink(link_path):
"""
Removes symbolic link for
:param link_path:
"""
if os.path.exists(link_path):
os.remove(link_path)
""" """
Django storage backends for Open edX. Django storage backends for Open edX.
""" """
from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin from django.contrib.staticfiles.storage import StaticFilesStorage
from pipeline.storage import PipelineMixin, NonPackagingMixin from pipeline.storage import PipelineMixin, NonPackagingMixin
from require.storage import OptimizedFilesMixin from require.storage import OptimizedFilesMixin
from openedx.core.djangoapps.theming.storage import ComprehensiveThemingStorage
class ProductionStorage( class ProductionStorage(
ComprehensiveThemingStorage,
OptimizedFilesMixin, OptimizedFilesMixin,
PipelineMixin, PipelineMixin,
CachedFilesMixin,
StaticFilesStorage StaticFilesStorage
): ):
""" """
...@@ -20,6 +21,7 @@ class ProductionStorage( ...@@ -20,6 +21,7 @@ class ProductionStorage(
class DevelopmentStorage( class DevelopmentStorage(
ComprehensiveThemingStorage,
NonPackagingMixin, NonPackagingMixin,
PipelineMixin, PipelineMixin,
StaticFilesStorage StaticFilesStorage
......
"""Unit tests for the Paver asset tasks.""" """Unit tests for the Paver asset tasks."""
import ddt import ddt
import os
from unittest import TestCase
from paver.easy import call_task from paver.easy import call_task
from paver.easy import path
from mock import patch
from watchdog.observers import Observer
from .utils import PaverTestCase 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 @ddt.ddt
class TestPaverAssetTasks(PaverTestCase): class TestPaverAssetTasks(PaverTestCase):
...@@ -41,18 +48,157 @@ class TestPaverAssetTasks(PaverTestCase): ...@@ -41,18 +48,157 @@ class TestPaverAssetTasks(PaverTestCase):
if force: if force:
expected_messages.append("rm -rf common/static/css/*.css") expected_messages.append("rm -rf common/static/css/*.css")
expected_messages.append("libsass common/static/sass") expected_messages.append("libsass common/static/sass")
if "lms" in system: if "lms" in system:
if force: if force:
expected_messages.append("rm -rf lms/static/css/*.css") expected_messages.append("rm -rf lms/static/css/*.css")
expected_messages.append("libsass lms/static/sass") expected_messages.append("libsass lms/static/sass")
if force: 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, "themes_dir": 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("rm -rf lms/static/css/*.css")
expected_messages.append("libsass lms/static/themed_sass") expected_messages.append("libsass lms/static/sass")
if force: if force:
expected_messages.append("rm -rf lms/static/certificates/css/*.css") expected_messages.append("rm -rf lms/static/certificates/css/*.css")
expected_messages.append("libsass lms/static/certificates/sass") expected_messages.append("libsass lms/static/certificates/sass")
if "studio" in system: 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: if force:
expected_messages.append("rm -rf cms/static/css/*.css") expected_messages.append("rm -rf cms/static/css/*.css")
expected_messages.append("libsass cms/static/sass") expected_messages.append("libsass cms/static/sass")
self.assertEquals(self.task_messages, expected_messages) 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('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 test_watch_assets(self):
"""
Test the "compile_sass" task.
"""
with patch('pavelib.assets.SassWatcher.register') as mock_register:
with patch('pavelib.assets.Observer.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], Observer)
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.Observer.start'):
call_task(
'pavelib.assets.watch_assets',
options={"background": True, "themes_dir": 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], Observer)
self.assertIsInstance(sass_watcher_args[1], list)
self.assertItemsEqual(sass_watcher_args[1], self.expected_sass_directories)
def tearDown(self):
self.expected_sass_directories = []
super(TestPaverWatchAssetTasks, self).tearDown()
...@@ -18,7 +18,6 @@ EXPECTED_COMMON_SASS_DIRECTORIES = [ ...@@ -18,7 +18,6 @@ EXPECTED_COMMON_SASS_DIRECTORIES = [
] ]
EXPECTED_LMS_SASS_DIRECTORIES = [ EXPECTED_LMS_SASS_DIRECTORIES = [
"lms/static/sass", "lms/static/sass",
"lms/static/themed_sass",
"lms/static/certificates/sass", "lms/static/certificates/sass",
] ]
EXPECTED_CMS_SASS_DIRECTORIES = [ EXPECTED_CMS_SASS_DIRECTORIES = [
......
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
# Third-party: # Third-party:
git+https://github.com/cyberdelia/django-pipeline.git@1.5.3#egg=django-pipeline==1.5.3 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/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/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7fed0#egg=MongoDBProxy==0.1.0
git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6 git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6
......
<%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); $header-bg: rgb(250,0,0);
$footer-bg: rgb(250,0,0); $footer-bg: rgb(250,0,0);
$container-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';
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