Merge pull request #12116 from edx/mattdrayer/rc2016-04-12/revert-theming
Revert "saleem-latif/WL-328: Multi-Site Comprehensive Theming"
This commit is contained in:
@@ -191,7 +191,6 @@ ASSET_IGNORE_REGEX = ENV_TOKENS.get('ASSET_IGNORE_REGEX', ASSET_IGNORE_REGEX)
|
||||
# Theme overrides
|
||||
THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
|
||||
COMPREHENSIVE_THEME_DIR = path(ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', COMPREHENSIVE_THEME_DIR))
|
||||
THEME_CACHE_TIMEOUT = ENV_TOKENS.get('THEME_CACHE_TIMEOUT', THEME_CACHE_TIMEOUT)
|
||||
|
||||
#Timezone overrides
|
||||
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
|
||||
|
||||
@@ -59,9 +59,9 @@ STATIC_URL = "/static/"
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
)
|
||||
STATICFILES_DIRS = [
|
||||
STATICFILES_DIRS = (
|
||||
(TEST_ROOT / "staticfiles" / "cms").abspath(),
|
||||
]
|
||||
)
|
||||
|
||||
# Silence noisy logs
|
||||
import logging
|
||||
|
||||
@@ -61,13 +61,7 @@ from lms.envs.common import (
|
||||
# Django REST framework configuration
|
||||
REST_FRAMEWORK,
|
||||
|
||||
STATICI18N_OUTPUT_DIR,
|
||||
|
||||
# Dafault site id to use in case there is no site that matches with the request headers.
|
||||
DEFAULT_SITE_ID,
|
||||
|
||||
# Cache time out settings for comprehensive theming system
|
||||
THEME_CACHE_TIMEOUT,
|
||||
STATICI18N_OUTPUT_DIR
|
||||
)
|
||||
from path import Path as path
|
||||
from warnings import simplefilter
|
||||
@@ -350,9 +344,6 @@ MIDDLEWARE_CLASSES = (
|
||||
|
||||
'codejail.django_integration.ConfigureCodeJailMiddleware',
|
||||
|
||||
# django current site middleware with default site
|
||||
'django_sites_extensions.middleware.CurrentSiteWithDefaultMiddleware',
|
||||
|
||||
# needs to run after locale middleware (or anything that modifies the request context)
|
||||
'edxmako.middleware.MakoMiddleware',
|
||||
|
||||
@@ -457,6 +448,7 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
||||
|
||||
|
||||
# Site info
|
||||
SITE_ID = 1
|
||||
SITE_NAME = "localhost:8001"
|
||||
HTTPS = 'on'
|
||||
ROOT_URLCONF = 'cms.urls'
|
||||
@@ -528,7 +520,6 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
|
||||
# List of finder classes that know how to find static files in various locations.
|
||||
# Note: the pipeline finder is included to be able to discover optimized files
|
||||
STATICFILES_FINDERS = [
|
||||
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
'pipeline.finders.PipelineFinder',
|
||||
|
||||
@@ -41,7 +41,6 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
|
||||
|
||||
# Revert to the default set of finders as we don't want the production pipeline
|
||||
STATICFILES_FINDERS = [
|
||||
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
]
|
||||
|
||||
@@ -38,6 +38,6 @@ STATIC_URL = "/static/"
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
)
|
||||
STATICFILES_DIRS = [
|
||||
STATICFILES_DIRS = (
|
||||
(TEST_ROOT / "staticfiles" / "cms").abspath(),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -30,8 +30,6 @@ from util.db import NoOpMigrationModules
|
||||
from lms.envs.test import (
|
||||
WIKI_ENABLED,
|
||||
PLATFORM_NAME,
|
||||
SITE_ID,
|
||||
DEFAULT_SITE_ID,
|
||||
SITE_NAME,
|
||||
DEFAULT_FILE_STORAGE,
|
||||
MEDIA_ROOT,
|
||||
@@ -284,8 +282,6 @@ MICROSITE_CONFIGURATION = {
|
||||
MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver'
|
||||
MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
|
||||
|
||||
TEST_THEME = COMMON_ROOT / "test" / "test-theme"
|
||||
|
||||
# For consistency in user-experience, keep the value of this setting in sync with
|
||||
# the one in lms/envs/test.py
|
||||
FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
|
||||
|
||||
@@ -17,7 +17,7 @@ from monkey_patch import (
|
||||
import xmodule.x_module
|
||||
import cms.lib.xblock.runtime
|
||||
|
||||
from openedx.core.djangoapps.theming.core import enable_comprehensive_theming
|
||||
from openedx.core.djangoapps.theming.core import enable_comprehensive_theme
|
||||
|
||||
|
||||
def run():
|
||||
@@ -30,7 +30,7 @@ def run():
|
||||
# Comprehensive theming needs to be set up before django startup,
|
||||
# because modifying django template paths after startup has no effect.
|
||||
if settings.COMPREHENSIVE_THEME_DIR:
|
||||
enable_comprehensive_theming(settings.COMPREHENSIVE_THEME_DIR)
|
||||
enable_comprehensive_theme(settings.COMPREHENSIVE_THEME_DIR)
|
||||
|
||||
django.setup()
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
import lms.djangoapps.commerce.tests.test_utils as ecomm_test_utils
|
||||
from course_modes.models import CourseMode, Mode
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
|
||||
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -352,7 +352,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
|
||||
self.assertEquals(course_modes, expected_modes)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@with_comprehensive_theme("edx.org")
|
||||
@with_is_edx_domain(True)
|
||||
def test_hide_nav(self):
|
||||
# Create the course modes
|
||||
for mode in ["honor", "verified"]:
|
||||
|
||||
@@ -9,14 +9,9 @@ import pkg_resources
|
||||
|
||||
from django.conf import settings
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako.exceptions import TopLevelLookupException
|
||||
|
||||
from microsite_configuration import microsite
|
||||
from . import LOOKUP
|
||||
from openedx.core.djangoapps.theming.helpers import (
|
||||
get_template as themed_template,
|
||||
get_template_path_with_theme,
|
||||
strip_site_theme_templates_path,
|
||||
)
|
||||
|
||||
|
||||
class DynamicTemplateLookup(TemplateLookup):
|
||||
@@ -54,25 +49,15 @@ class DynamicTemplateLookup(TemplateLookup):
|
||||
|
||||
def get_template(self, uri):
|
||||
"""
|
||||
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.
|
||||
Overridden method which will hand-off the template lookup to the microsite subsystem
|
||||
"""
|
||||
template = themed_template(uri)
|
||||
microsite_template = microsite.get_template(uri)
|
||||
|
||||
if not template:
|
||||
try:
|
||||
template = super(DynamicTemplateLookup, self).get_template(get_template_path_with_theme(uri))
|
||||
except TopLevelLookupException:
|
||||
# strip off the prefix path to theme and look in default template dirs
|
||||
template = super(DynamicTemplateLookup, self).get_template(strip_site_theme_templates_path(uri))
|
||||
|
||||
return template
|
||||
return (
|
||||
microsite_template
|
||||
if microsite_template
|
||||
else super(DynamicTemplateLookup, self).get_template(uri)
|
||||
)
|
||||
|
||||
|
||||
def clear_lookups(namespace):
|
||||
|
||||
@@ -12,18 +12,16 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse
|
||||
from django.template import Context
|
||||
from django.http import HttpResponse
|
||||
import logging
|
||||
|
||||
from microsite_configuration import microsite
|
||||
|
||||
from edxmako import lookup_template
|
||||
from edxmako.middleware import get_template_request_context
|
||||
from openedx.core.djangoapps.theming.helpers import get_template_path
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -115,7 +113,8 @@ def microsite_footer_context_processor(request):
|
||||
|
||||
def render_to_string(template_name, dictionary, context=None, namespace='main'):
|
||||
|
||||
template_name = get_template_path(template_name)
|
||||
# see if there is an override template defined in the microsite
|
||||
template_name = microsite.get_template_path(template_name)
|
||||
|
||||
context_instance = Context(dictionary)
|
||||
# add dictionary to context_instance
|
||||
|
||||
@@ -116,12 +116,8 @@ class MakoMiddlewareTest(TestCase):
|
||||
Test render_to_string() when makomiddleware has not initialized
|
||||
the threadlocal REQUEST_CONTEXT.context. This is meant to run in LMS.
|
||||
"""
|
||||
with patch("openedx.core.djangoapps.theming.helpers.get_current_site", return_value=None):
|
||||
del context_mock.context
|
||||
self.assertIn(
|
||||
"this module is temporarily unavailable",
|
||||
render_to_string("courseware/error-message.html", None),
|
||||
)
|
||||
del context_mock.context
|
||||
self.assertIn("this module is temporarily unavailable", render_to_string("courseware/error-message.html", None))
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
|
||||
@patch("edxmako.middleware.REQUEST_CONTEXT")
|
||||
@@ -130,9 +126,8 @@ class MakoMiddlewareTest(TestCase):
|
||||
Test render_to_string() when makomiddleware has not initialized
|
||||
the threadlocal REQUEST_CONTEXT.context. This is meant to run in CMS.
|
||||
"""
|
||||
with patch("openedx.core.djangoapps.theming.helpers.get_current_site", return_value=None):
|
||||
del context_mock.context
|
||||
self.assertIn("We're having trouble rendering your component", render_to_string("html_error.html", None))
|
||||
del context_mock.context
|
||||
self.assertIn("We're having trouble rendering your component", render_to_string("html_error.html", None))
|
||||
|
||||
|
||||
def mako_middleware_process_request(request):
|
||||
|
||||
@@ -11,6 +11,7 @@ BaseMicrositeTemplateBackend is Base Class for the microsite template backend.
|
||||
from __future__ import absolute_import
|
||||
|
||||
import abc
|
||||
import edxmako
|
||||
import os.path
|
||||
import threading
|
||||
|
||||
@@ -271,7 +272,9 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend):
|
||||
Configure the paths for the microsites feature
|
||||
"""
|
||||
microsites_root = settings.MICROSITE_ROOT_DIR
|
||||
|
||||
if os.path.isdir(microsites_root):
|
||||
edxmako.paths.add_lookup('main', microsites_root)
|
||||
settings.STATICFILES_DIRS.insert(0, microsites_root)
|
||||
|
||||
log.info('Loading microsite path at %s', microsites_root)
|
||||
@@ -289,7 +292,6 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend):
|
||||
microsites_root = settings.MICROSITE_ROOT_DIR
|
||||
|
||||
if self.has_configuration_set():
|
||||
settings.MAKO_TEMPLATES['main'].insert(0, microsites_root)
|
||||
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].append(microsites_root)
|
||||
|
||||
|
||||
|
||||
@@ -105,23 +105,6 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase):
|
||||
microsite.clear()
|
||||
self.assertIsNone(microsite.get_value('platform_name'))
|
||||
|
||||
def test_enable_microsites_pre_startup(self):
|
||||
"""
|
||||
Tests microsite.test_enable_microsites_pre_startup works as expected.
|
||||
"""
|
||||
# remove microsite root directory paths first
|
||||
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'] = [
|
||||
path for path in settings.DEFAULT_TEMPLATE_ENGINE['DIRS']
|
||||
if path != settings.MICROSITE_ROOT_DIR
|
||||
]
|
||||
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': False}):
|
||||
microsite.enable_microsites_pre_startup(log)
|
||||
self.assertNotIn(settings.MICROSITE_ROOT_DIR, settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
|
||||
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}):
|
||||
microsite.enable_microsites_pre_startup(log)
|
||||
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
|
||||
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.MAKO_TEMPLATES['main'])
|
||||
|
||||
@patch('edxmako.paths.add_lookup')
|
||||
def test_enable_microsites(self, add_lookup):
|
||||
"""
|
||||
@@ -139,6 +122,7 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase):
|
||||
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}):
|
||||
microsite.enable_microsites(log)
|
||||
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.STATICFILES_DIRS)
|
||||
add_lookup.assert_called_once_with('main', settings.MICROSITE_ROOT_DIR)
|
||||
|
||||
def test_get_all_configs(self):
|
||||
"""
|
||||
|
||||
@@ -4,13 +4,7 @@ from pipeline_mako import compressed_css, compressed_js
|
||||
from django.utils.translation import get_language_bidi
|
||||
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 lang_pref.api import released_languages
|
||||
%>
|
||||
|
||||
@@ -22,7 +22,7 @@ from edxmako.shortcuts import render_to_string
|
||||
from edxmako.tests import mako_middleware_process_request
|
||||
from util.request import safe_get_host
|
||||
from util.testing import EventTestMixin
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
|
||||
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
|
||||
|
||||
|
||||
class TestException(Exception):
|
||||
@@ -100,7 +100,7 @@ class ActivationEmailTests(TestCase):
|
||||
self._create_account()
|
||||
self._assert_activation_email(self.ACTIVATION_SUBJECT, self.OPENEDX_FRAGMENTS)
|
||||
|
||||
@with_comprehensive_theme("edx.org")
|
||||
@with_is_edx_domain(True)
|
||||
def test_activation_email_edx_domain(self):
|
||||
self._create_account()
|
||||
self._assert_activation_email(self.ACTIVATION_SUBJECT, self.EDX_DOMAIN_FRAGMENTS)
|
||||
|
||||
@@ -45,6 +45,7 @@ from certificates.tests.factories import GeneratedCertificateFactory # pylint:
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
import shoppingcart # pylint: disable=import-error
|
||||
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
||||
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
|
||||
|
||||
# Explicitly import the cache from ConfigurationModel so we can reset it after each test
|
||||
from config_models.models import cache
|
||||
@@ -495,6 +496,7 @@ class DashboardTest(ModuleStoreTestCase):
|
||||
self.assertEquals(response_2.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@with_is_edx_domain(True)
|
||||
def test_dashboard_header_nav_has_find_courses(self):
|
||||
self.client.login(username="jack", password="test")
|
||||
response = self.client.get(reverse("dashboard"))
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
[
|
||||
{
|
||||
"pk": 2,
|
||||
"model": "sites.Site",
|
||||
|
||||
"fields": {
|
||||
"domain": "localhost:8003",
|
||||
"name": "lms"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 3,
|
||||
"model": "sites.Site",
|
||||
|
||||
"fields": {
|
||||
"domain": "localhost:8031",
|
||||
"name": "cms"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
*.css
|
||||
@@ -1,255 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,55 +0,0 @@
|
||||
<%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>
|
||||
@@ -1 +0,0 @@
|
||||
*.css
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 493 B |
@@ -1,5 +0,0 @@
|
||||
@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);
|
||||
@@ -1,10 +0,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
|
||||
import ddt
|
||||
from config_models.models import cache
|
||||
from branding.models import BrandingApiConfig
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme_context
|
||||
from openedx.core.djangoapps.theming.test_util import with_edx_domain_context
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -30,19 +30,19 @@ class TestFooter(TestCase):
|
||||
|
||||
@ddt.data(
|
||||
# Open source version
|
||||
(None, "application/json", "application/json; charset=utf-8", "Open edX"),
|
||||
(None, "text/html", "text/html; charset=utf-8", "lms-footer.css"),
|
||||
(None, "text/html", "text/html; charset=utf-8", "Open edX"),
|
||||
(False, "application/json", "application/json; charset=utf-8", "Open edX"),
|
||||
(False, "text/html", "text/html; charset=utf-8", "lms-footer.css"),
|
||||
(False, "text/html", "text/html; charset=utf-8", "Open edX"),
|
||||
|
||||
# EdX.org version
|
||||
("edx.org", "application/json", "application/json; charset=utf-8", "edX Inc"),
|
||||
("edx.org", "text/html", "text/html; charset=utf-8", "lms-footer-edx.css"),
|
||||
("edx.org", "text/html", "text/html; charset=utf-8", "edX Inc"),
|
||||
(True, "application/json", "application/json; charset=utf-8", "edX Inc"),
|
||||
(True, "text/html", "text/html; charset=utf-8", "lms-footer-edx.css"),
|
||||
(True, "text/html", "text/html; charset=utf-8", "edX Inc"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_footer_content_types(self, theme, accepts, content_type, content):
|
||||
def test_footer_content_types(self, is_edx_domain, accepts, content_type, content):
|
||||
self._set_feature_flag(True)
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
resp = self._get_footer(accepts=accepts)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -50,10 +50,10 @@ class TestFooter(TestCase):
|
||||
self.assertIn(content, resp.content)
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {'ENABLE_FOOTER_MOBILE_APP_LINKS': True})
|
||||
@ddt.data("edx.org", None)
|
||||
def test_footer_json(self, theme):
|
||||
@ddt.data(True, False)
|
||||
def test_footer_json(self, is_edx_domain):
|
||||
self._set_feature_flag(True)
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
resp = self._get_footer()
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -142,18 +142,18 @@ class TestFooter(TestCase):
|
||||
|
||||
@ddt.data(
|
||||
# OpenEdX
|
||||
(None, "en", "lms-footer.css"),
|
||||
(None, "ar", "lms-footer-rtl.css"),
|
||||
(False, "en", "lms-footer.css"),
|
||||
(False, "ar", "lms-footer-rtl.css"),
|
||||
|
||||
# EdX.org
|
||||
("edx.org", "en", "lms-footer-edx.css"),
|
||||
("edx.org", "ar", "lms-footer-edx-rtl.css"),
|
||||
(True, "en", "lms-footer-edx.css"),
|
||||
(True, "ar", "lms-footer-edx-rtl.css"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_language_rtl(self, theme, language, static_path):
|
||||
def test_language_rtl(self, is_edx_domain, language, static_path):
|
||||
self._set_feature_flag(True)
|
||||
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
resp = self._get_footer(accepts="text/html", params={'language': language})
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -161,18 +161,18 @@ class TestFooter(TestCase):
|
||||
|
||||
@ddt.data(
|
||||
# OpenEdX
|
||||
(None, True),
|
||||
(None, False),
|
||||
(False, True),
|
||||
(False, False),
|
||||
|
||||
# EdX.org
|
||||
("edx.org", True),
|
||||
("edx.org", False),
|
||||
(True, True),
|
||||
(True, False),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_show_openedx_logo(self, theme, show_logo):
|
||||
def test_show_openedx_logo(self, is_edx_domain, show_logo):
|
||||
self._set_feature_flag(True)
|
||||
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
params = {'show-openedx-logo': 1} if show_logo else {}
|
||||
resp = self._get_footer(accepts="text/html", params=params)
|
||||
|
||||
@@ -185,17 +185,17 @@ class TestFooter(TestCase):
|
||||
|
||||
@ddt.data(
|
||||
# OpenEdX
|
||||
(None, False),
|
||||
(None, True),
|
||||
(False, False),
|
||||
(False, True),
|
||||
|
||||
# EdX.org
|
||||
("edx.org", False),
|
||||
("edx.org", True),
|
||||
(True, False),
|
||||
(True, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_include_dependencies(self, theme, include_dependencies):
|
||||
def test_include_dependencies(self, is_edx_domain, include_dependencies):
|
||||
self._set_feature_flag(True)
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
params = {'include-dependencies': 1} if include_dependencies else {}
|
||||
resp = self._get_footer(accepts="text/html", params=params)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.test import TestCase
|
||||
import mock
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
|
||||
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
|
||||
|
||||
|
||||
class UserMixin(object):
|
||||
@@ -86,7 +86,7 @@ class ReceiptViewTests(UserMixin, TestCase):
|
||||
self.assertRegexpMatches(response.content, user_message if is_user_message_expected else system_message)
|
||||
self.assertNotRegexpMatches(response.content, user_message if not is_user_message_expected else system_message)
|
||||
|
||||
@with_comprehensive_theme("edx.org")
|
||||
@with_is_edx_domain(True)
|
||||
def test_hide_nav_header(self):
|
||||
self._login()
|
||||
post_data = {'decision': 'ACCEPT', 'reason_code': '200', 'signed_field_names': 'dummy'}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Tests for wiki middleware.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.test.client import Client
|
||||
from nose.plugins.attrib import attr
|
||||
from wiki.models import URLPath
|
||||
@@ -32,7 +33,7 @@ class TestComprehensiveTheming(ModuleStoreTestCase):
|
||||
self.client = Client()
|
||||
self.client.login(username='instructor', password='secret')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
|
||||
def test_themed_footer(self):
|
||||
"""
|
||||
Tests that theme footer is used rather than standard
|
||||
|
||||
@@ -6,6 +6,8 @@ import re
|
||||
import cgi
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
@@ -48,6 +50,21 @@ def course_wiki_redirect(request, course_id): # pylint: disable=unused-argument
|
||||
if not valid_slug:
|
||||
return redirect("wiki:get", path="")
|
||||
|
||||
# The wiki needs a Site object created. We make sure it exists here
|
||||
try:
|
||||
Site.objects.get_current()
|
||||
except Site.DoesNotExist:
|
||||
new_site = Site()
|
||||
new_site.domain = settings.SITE_NAME
|
||||
new_site.name = "edX"
|
||||
new_site.save()
|
||||
site_id = str(new_site.id)
|
||||
if site_id != str(settings.SITE_ID):
|
||||
msg = "No site object was created and the SITE_ID doesn't match the newly created one. {} != {}".format(
|
||||
site_id, settings.SITE_ID
|
||||
)
|
||||
raise ImproperlyConfigured(msg)
|
||||
|
||||
try:
|
||||
urlpath = URLPath.get_by_path(course_slug, select_related=True)
|
||||
|
||||
|
||||
@@ -6,33 +6,21 @@ from django.test import TestCase
|
||||
from path import path # pylint: disable=no-name-in-module
|
||||
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.lib.tempdir import mkdtemp_clean, create_symlink, delete_symlink
|
||||
from openedx.core.lib.tempdir import mkdtemp_clean
|
||||
|
||||
|
||||
class TestComprehensiveTheming(TestCase):
|
||||
"""Test comprehensive theming."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
compile_sass('lms')
|
||||
super(TestComprehensiveTheming, cls).setUpClass()
|
||||
|
||||
def setUp(self):
|
||||
super(TestComprehensiveTheming, self).setUp()
|
||||
|
||||
# Clear the internal staticfiles caches, to get test isolation.
|
||||
staticfiles.finders.get_finder.cache_clear()
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
|
||||
def test_red_footer(self):
|
||||
"""
|
||||
Tests templates from theme are rendered if available.
|
||||
`red-theme` has header.html and footer.html so this test
|
||||
asserts presence of the content from header.html and footer.html
|
||||
"""
|
||||
resp = self.client.get('/')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# This string comes from footer.html
|
||||
@@ -46,16 +34,12 @@ class TestComprehensiveTheming(TestCase):
|
||||
# of test.
|
||||
|
||||
# Make a temp directory as a theme.
|
||||
themes_dir = path(mkdtemp_clean())
|
||||
tmp_theme = "temp_theme"
|
||||
template_dir = themes_dir / tmp_theme / "lms/templates"
|
||||
tmp_theme = path(mkdtemp_clean())
|
||||
template_dir = tmp_theme / "lms/templates"
|
||||
template_dir.makedirs()
|
||||
with open(template_dir / "footer.html", "w") as footer:
|
||||
footer.write("<footer>TEMPORARY THEME</footer>")
|
||||
|
||||
dest_path = path(settings.COMPREHENSIVE_THEME_DIR) / tmp_theme
|
||||
create_symlink(themes_dir / tmp_theme, dest_path)
|
||||
|
||||
@with_comprehensive_theme(tmp_theme)
|
||||
def do_the_test(self):
|
||||
"""A function to do the work so we can use the decorator."""
|
||||
@@ -64,22 +48,18 @@ class TestComprehensiveTheming(TestCase):
|
||||
self.assertContains(resp, "TEMPORARY THEME")
|
||||
|
||||
do_the_test(self)
|
||||
# remove symlinks before running subsequent tests
|
||||
delete_symlink(dest_path)
|
||||
|
||||
def test_theme_adjusts_staticfiles_search_path(self):
|
||||
"""
|
||||
Tests theme directories are added to staticfiles search path.
|
||||
"""
|
||||
# Test that a theme adds itself to the staticfiles search path.
|
||||
before_finders = list(settings.STATICFILES_FINDERS)
|
||||
before_dirs = list(settings.STATICFILES_DIRS)
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
|
||||
def do_the_test(self):
|
||||
"""A function to do the work so we can use the decorator."""
|
||||
self.assertEqual(list(settings.STATICFILES_FINDERS), before_finders)
|
||||
self.assertIn(settings.REPO_ROOT / 'themes/red-theme/lms/static', settings.STATICFILES_DIRS)
|
||||
self.assertEqual(settings.STATICFILES_DIRS, before_dirs)
|
||||
self.assertEqual(settings.STATICFILES_DIRS[0], settings.REPO_ROOT / 'themes/red-theme/lms/static')
|
||||
self.assertEqual(settings.STATICFILES_DIRS[1:], before_dirs)
|
||||
|
||||
do_the_test(self)
|
||||
|
||||
@@ -87,9 +67,9 @@ class TestComprehensiveTheming(TestCase):
|
||||
result = staticfiles.finders.find('images/logo.png')
|
||||
self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/logo.png')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
|
||||
def test_overridden_logo_image(self):
|
||||
result = staticfiles.finders.find('red-theme/images/logo.png')
|
||||
result = staticfiles.finders.find('images/logo.png')
|
||||
self.assertEqual(result, settings.REPO_ROOT / 'themes/red-theme/lms/static/images/logo.png')
|
||||
|
||||
def test_default_favicon(self):
|
||||
@@ -99,54 +79,10 @@ class TestComprehensiveTheming(TestCase):
|
||||
result = staticfiles.finders.find('images/favicon.ico')
|
||||
self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/favicon.ico')
|
||||
|
||||
@with_comprehensive_theme('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/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')
|
||||
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
|
||||
def test_overridden_favicon(self):
|
||||
"""
|
||||
Test comprehensive theme override on favicon image.
|
||||
"""
|
||||
result = staticfiles.finders.find('red-theme/images/favicon.ico')
|
||||
result = staticfiles.finders.find('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"),
|
||||
)
|
||||
|
||||
@@ -265,8 +265,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
|
||||
url = reverse('info', args=[unicode(course.id)])
|
||||
with self.assertNumQueries(sql_queries):
|
||||
with check_mongo_calls(mongo_queries):
|
||||
with mock.patch("openedx.core.djangoapps.theming.helpers.get_current_site", return_value=None):
|
||||
resp = self.client.get(url)
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_num_queries_instructor_paced(self):
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
|
||||
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@@ -37,7 +37,7 @@ class TestFooter(TestCase):
|
||||
"youtube": "https://www.youtube.com/"
|
||||
}
|
||||
|
||||
@with_comprehensive_theme("edx.org")
|
||||
@with_is_edx_domain(True)
|
||||
def test_edx_footer(self):
|
||||
"""
|
||||
Verify that the homepage, when accessed at edx.org, has the edX footer
|
||||
@@ -46,6 +46,7 @@ class TestFooter(TestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'footer-edx-v3')
|
||||
|
||||
@with_is_edx_domain(False)
|
||||
def test_openedx_footer(self):
|
||||
"""
|
||||
Verify that the homepage, when accessed at something other than
|
||||
@@ -55,7 +56,7 @@ class TestFooter(TestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'footer-openedx')
|
||||
|
||||
@with_comprehensive_theme("edx.org")
|
||||
@with_is_edx_domain(True)
|
||||
@override_settings(
|
||||
SOCIAL_MEDIA_FOOTER_NAMES=SOCIAL_MEDIA_NAMES,
|
||||
SOCIAL_MEDIA_FOOTER_URLS=SOCIAL_MEDIA_URLS
|
||||
|
||||
@@ -26,7 +26,7 @@ from student_account.views import account_settings_context
|
||||
from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin
|
||||
from util.testing import UrlResetMixin
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme_context
|
||||
from openedx.core.djangoapps.theming.test_util import with_edx_domain_context
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -247,13 +247,13 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
|
||||
self.assertRedirects(response, reverse("dashboard"))
|
||||
|
||||
@ddt.data(
|
||||
(None, "signin_user"),
|
||||
(None, "register_user"),
|
||||
("edx.org", "signin_user"),
|
||||
("edx.org", "register_user"),
|
||||
(False, "signin_user"),
|
||||
(False, "register_user"),
|
||||
(True, "signin_user"),
|
||||
(True, "register_user"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_login_and_registration_form_signin_preserves_params(self, theme, url_name):
|
||||
def test_login_and_registration_form_signin_preserves_params(self, is_edx_domain, url_name):
|
||||
params = [
|
||||
('course_id', 'edX/DemoX/Demo_Course'),
|
||||
('enrollment_action', 'enroll'),
|
||||
@@ -261,7 +261,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
|
||||
|
||||
# The response should have a "Sign In" button with the URL
|
||||
# that preserves the querystring params
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
response = self.client.get(reverse(url_name), params)
|
||||
|
||||
expected_url = '/login?{}'.format(self._finish_auth_url_param(params + [('next', '/dashboard')]))
|
||||
@@ -277,7 +277,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
|
||||
]
|
||||
|
||||
# Verify that this parameter is also preserved
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
response = self.client.get(reverse(url_name), params)
|
||||
|
||||
expected_url = '/login?{}'.format(self._finish_auth_url_param(params))
|
||||
|
||||
@@ -38,7 +38,7 @@ from common.test.utils import XssTestMixin
|
||||
from commerce.tests import TEST_PAYMENT_DATA, TEST_API_URL, TEST_API_SIGNING_KEY
|
||||
from embargo.test_utils import restrict_course
|
||||
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
|
||||
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
|
||||
from shoppingcart.models import Order, CertificateItem
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from student.models import CourseEnrollment
|
||||
@@ -290,7 +290,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
|
||||
)
|
||||
self._assert_redirects_to_dashboard(response)
|
||||
|
||||
@with_comprehensive_theme("edx.org")
|
||||
@with_is_edx_domain(True)
|
||||
@ddt.data("verify_student_start_flow", "verify_student_begin_flow")
|
||||
def test_pay_and_verify_hides_header_nav(self, payment_flow):
|
||||
course = self._create_course("verified")
|
||||
|
||||
@@ -243,7 +243,6 @@ BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = LOW_PRIORITY_QUEUE
|
||||
# Theme overrides
|
||||
THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
|
||||
COMPREHENSIVE_THEME_DIR = path(ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', COMPREHENSIVE_THEME_DIR))
|
||||
THEME_CACHE_TIMEOUT = ENV_TOKENS.get('THEME_CACHE_TIMEOUT', THEME_CACHE_TIMEOUT)
|
||||
|
||||
# Marketing link overrides
|
||||
MKTG_URL_LINK_MAP.update(ENV_TOKENS.get('MKTG_URL_LINK_MAP', {}))
|
||||
|
||||
@@ -61,9 +61,9 @@ STATIC_URL = "/static/"
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
)
|
||||
STATICFILES_DIRS = [
|
||||
STATICFILES_DIRS = (
|
||||
(TEST_ROOT / "staticfiles" / "lms").abspath(),
|
||||
]
|
||||
)
|
||||
|
||||
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
MEDIA_ROOT = TEST_ROOT / "uploads"
|
||||
|
||||
@@ -397,7 +397,7 @@ COURSES_ROOT = ENV_ROOT / "data"
|
||||
DATA_DIR = COURSES_ROOT
|
||||
|
||||
# comprehensive theming system
|
||||
COMPREHENSIVE_THEME_DIR = REPO_ROOT / "themes"
|
||||
COMPREHENSIVE_THEME_DIR = ""
|
||||
|
||||
# TODO: Remove the rest of the sys.path modification here and in cms/envs/common.py
|
||||
sys.path.append(REPO_ROOT)
|
||||
@@ -486,7 +486,6 @@ TEMPLATES = [
|
||||
'loaders': [
|
||||
# We have to use mako-aware template loaders to be able to include
|
||||
# mako templates inside django templates (such as main_django.html).
|
||||
'openedx.core.djangoapps.theming.template_loaders.ThemeFilesystemLoader',
|
||||
'edxmako.makoloader.MakoFilesystemLoader',
|
||||
'edxmako.makoloader.MakoAppDirectoriesLoader',
|
||||
],
|
||||
@@ -786,6 +785,7 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
||||
CMS_BASE = 'localhost:8001'
|
||||
|
||||
# Site info
|
||||
SITE_ID = 1
|
||||
SITE_NAME = "example.com"
|
||||
HTTPS = 'on'
|
||||
ROOT_URLCONF = 'lms.urls'
|
||||
@@ -1145,10 +1145,6 @@ MIDDLEWARE_CLASSES = (
|
||||
|
||||
# catches any uncaught RateLimitExceptions and returns a 403 instead of a 500
|
||||
'ratelimitbackend.middleware.RateLimitMiddleware',
|
||||
|
||||
# django current site middleware with default site
|
||||
'django_sites_extensions.middleware.CurrentSiteWithDefaultMiddleware',
|
||||
|
||||
# needs to run after locale middleware (or anything that modifies the request context)
|
||||
'edxmako.middleware.MakoMiddleware',
|
||||
|
||||
@@ -1182,7 +1178,6 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
|
||||
# List of finder classes that know how to find static files in various locations.
|
||||
# Note: the pipeline finder is included to be able to discover optimized files
|
||||
STATICFILES_FINDERS = [
|
||||
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
'pipeline.finders.PipelineFinder',
|
||||
@@ -2863,10 +2858,6 @@ WIKI_REQUEST_CACHE_MIDDLEWARE_CLASS = "request_cache.middleware.RequestCache"
|
||||
# Dafault site id to use in case there is no site that matches with the request headers.
|
||||
DEFAULT_SITE_ID = 1
|
||||
|
||||
# Cache time out settings
|
||||
# by Comprehensive Theme system
|
||||
THEME_CACHE_TIMEOUT = 30 * 60
|
||||
|
||||
# API access management
|
||||
API_ACCESS_MANAGER_EMAIL = 'api-access@example.com'
|
||||
API_ACCESS_FROM_EMAIL = 'api-requests@example.com'
|
||||
|
||||
@@ -99,7 +99,6 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
|
||||
|
||||
# Revert to the default set of finders as we don't want the production pipeline
|
||||
STATICFILES_FINDERS = [
|
||||
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
]
|
||||
|
||||
@@ -38,6 +38,6 @@ STATIC_URL = "/static/"
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
)
|
||||
STATICFILES_DIRS = [
|
||||
STATICFILES_DIRS = (
|
||||
(TEST_ROOT / "staticfiles" / "lms").abspath(),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -429,9 +429,6 @@ openid.oidutil.log = lambda message, level=0: None
|
||||
PLATFORM_NAME = "edX"
|
||||
SITE_NAME = "edx.org"
|
||||
|
||||
# use default site for tests
|
||||
SITE_ID = 1
|
||||
|
||||
# set up some testing for microsites
|
||||
FEATURES['USE_MICROSITES'] = True
|
||||
MICROSITE_ROOT_DIR = COMMON_ROOT / 'test' / 'test_microsites'
|
||||
@@ -501,8 +498,6 @@ MICROSITE_CONFIGURATION = {
|
||||
MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver'
|
||||
MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
|
||||
|
||||
TEST_THEME = COMMON_ROOT / "test" / "test-theme"
|
||||
|
||||
# add extra template directory for test-only templates
|
||||
MAKO_TEMPLATES['main'].extend([
|
||||
COMMON_ROOT / 'test' / 'templates',
|
||||
|
||||
@@ -20,7 +20,7 @@ from monkey_patch import (
|
||||
import xmodule.x_module
|
||||
import lms_xblock.runtime
|
||||
|
||||
from openedx.core.djangoapps.theming.core import enable_comprehensive_theming
|
||||
from openedx.core.djangoapps.theming.core import enable_comprehensive_theme
|
||||
from microsite_configuration import microsite
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -40,7 +40,7 @@ def run():
|
||||
# Comprehensive theming needs to be set up before django startup,
|
||||
# because modifying django template paths after startup has no effect.
|
||||
if settings.COMPREHENSIVE_THEME_DIR:
|
||||
enable_comprehensive_theming(settings.COMPREHENSIVE_THEME_DIR)
|
||||
enable_comprehensive_theme(settings.COMPREHENSIVE_THEME_DIR)
|
||||
|
||||
# We currently use 2 template rendering engines, mako and django_templates,
|
||||
# and one of them (django templates), requires the directories be added
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
{% load sekizai_tags i18n microsite theme_pipeline optional_include %}
|
||||
{% load sekizai_tags i18n microsite pipeline optional_include %}
|
||||
{% load url from future %}
|
||||
<html lang="{{LANGUAGE_CODE}}">
|
||||
<head>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "main_django.html" %}
|
||||
{% load theme_pipeline %}{% load sekizai_tags i18n microsite %}{% load url from future %}{% load staticfiles %}
|
||||
{% load pipeline %}{% load sekizai_tags i18n microsite %}{% load url from future %}{% load staticfiles %}
|
||||
|
||||
{% block title %}<title>{% block pagetitle %}{% endblock %} | {% trans "Wiki" %} | {% platform_name %}</title>{% endblock %}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
{% load wiki_tags i18n %}{% load theme_pipeline %}
|
||||
{% load wiki_tags i18n %}{% load pipeline %}
|
||||
<html lang="{{LANGUAGE_CODE}}">
|
||||
<head>
|
||||
{% stylesheet 'course' %}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
"""
|
||||
Django admin page for theming models
|
||||
"""
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import (
|
||||
SiteTheme,
|
||||
)
|
||||
|
||||
|
||||
class SiteThemeAdmin(admin.ModelAdmin):
|
||||
""" Admin interface for the SiteTheme object. """
|
||||
list_display = ('site', 'theme_dir_name')
|
||||
search_fields = ('site__domain', 'theme_dir_name')
|
||||
|
||||
class Meta(object):
|
||||
"""
|
||||
Meta class for SiteTheme admin model
|
||||
"""
|
||||
model = SiteTheme
|
||||
|
||||
admin.site.register(SiteTheme, SiteThemeAdmin)
|
||||
@@ -1,32 +1,62 @@
|
||||
"""
|
||||
Core logic for Comprehensive Theming.
|
||||
"""
|
||||
import os.path
|
||||
from path import Path as path
|
||||
from path import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from .helpers import (
|
||||
get_project_root_name,
|
||||
)
|
||||
|
||||
def comprehensive_theme_changes(theme_dir):
|
||||
"""
|
||||
Calculate the set of changes needed to enable a comprehensive theme.
|
||||
|
||||
Arguments:
|
||||
theme_dir (path.path): the full path to the theming directory to use.
|
||||
|
||||
Returns:
|
||||
A dict indicating the changes to make:
|
||||
|
||||
* 'settings': a dictionary of settings names and their new values.
|
||||
|
||||
* 'template_paths': a list of directories to prepend to template
|
||||
lookup path.
|
||||
|
||||
"""
|
||||
|
||||
changes = {
|
||||
'settings': {},
|
||||
'template_paths': [],
|
||||
}
|
||||
root = Path(settings.PROJECT_ROOT)
|
||||
if root.name == "":
|
||||
root = root.parent
|
||||
|
||||
component_dir = theme_dir / root.name
|
||||
|
||||
templates_dir = component_dir / "templates"
|
||||
if templates_dir.isdir():
|
||||
changes['template_paths'].append(templates_dir)
|
||||
|
||||
staticfiles_dir = component_dir / "static"
|
||||
if staticfiles_dir.isdir():
|
||||
changes['settings']['STATICFILES_DIRS'] = [staticfiles_dir] + settings.STATICFILES_DIRS
|
||||
|
||||
locale_dir = component_dir / "conf" / "locale"
|
||||
if locale_dir.isdir():
|
||||
changes['settings']['LOCALE_PATHS'] = [locale_dir] + settings.LOCALE_PATHS
|
||||
|
||||
return changes
|
||||
|
||||
|
||||
def enable_comprehensive_theming(themes_dir):
|
||||
def enable_comprehensive_theme(theme_dir):
|
||||
"""
|
||||
Add directories to relevant paths for comprehensive theming.
|
||||
:param themes_dir: path to base theme directory
|
||||
"""
|
||||
if isinstance(themes_dir, basestring):
|
||||
themes_dir = path(themes_dir)
|
||||
changes = comprehensive_theme_changes(theme_dir)
|
||||
|
||||
if themes_dir.isdir():
|
||||
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].insert(0, themes_dir)
|
||||
settings.MAKO_TEMPLATES['main'].insert(0, themes_dir)
|
||||
|
||||
for theme_dir in os.listdir(themes_dir):
|
||||
staticfiles_dir = os.path.join(themes_dir, theme_dir, get_project_root_name(), "static")
|
||||
if staticfiles_dir.isdir():
|
||||
settings.STATICFILES_DIRS = settings.STATICFILES_DIRS + [staticfiles_dir]
|
||||
|
||||
locale_dir = os.path.join(themes_dir, theme_dir, get_project_root_name(), "conf", "locale")
|
||||
if locale_dir.isdir():
|
||||
settings.LOCALE_PATHS = (locale_dir, ) + settings.LOCALE_PATHS
|
||||
# Use the changes
|
||||
for name, value in changes['settings'].iteritems():
|
||||
setattr(settings, name, value)
|
||||
for template_dir in changes['template_paths']:
|
||||
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].insert(0, template_dir)
|
||||
settings.MAKO_TEMPLATES['main'].insert(0, template_dir)
|
||||
|
||||
@@ -17,80 +17,63 @@ interface, as well.
|
||||
.. _Django-Pipeline: http://django-pipeline.readthedocs.org/
|
||||
.. _Django-Require: https://github.com/etianen/django-require
|
||||
"""
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
from path import Path
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.contrib.staticfiles import utils
|
||||
from django.contrib.staticfiles.finders import BaseFinder
|
||||
from django.utils import six
|
||||
|
||||
from openedx.core.djangoapps.theming.helpers import get_themes
|
||||
from openedx.core.djangoapps.theming.storage import ThemeStorage
|
||||
from openedx.core.djangoapps.theming.storage import CachedComprehensiveThemingStorage
|
||||
|
||||
|
||||
class ThemeFilesFinder(BaseFinder):
|
||||
class ComprehensiveThemeFinder(BaseFinder):
|
||||
"""
|
||||
A static files finder that looks in the directory of each theme as
|
||||
specified in the source_dir attribute.
|
||||
A static files finder that searches the active comprehensive theme
|
||||
for static files. If the ``COMPREHENSIVE_THEME_DIR`` setting is unset,
|
||||
or the ``COMPREHENSIVE_THEME_DIR`` does not exist on the file system,
|
||||
this finder will never find any files.
|
||||
"""
|
||||
storage_class = ThemeStorage
|
||||
source_dir = 'static'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# The list of themes that are handled
|
||||
self.themes = []
|
||||
# Mapping of theme names to storage instances
|
||||
self.storages = OrderedDict()
|
||||
super(ComprehensiveThemeFinder, self).__init__(*args, **kwargs)
|
||||
|
||||
themes = get_themes()
|
||||
for theme in themes:
|
||||
theme_storage = self.storage_class(
|
||||
os.path.join(theme.path, self.source_dir),
|
||||
prefix=theme.theme_dir,
|
||||
)
|
||||
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
|
||||
if not theme_dir:
|
||||
self.storage = None
|
||||
return
|
||||
|
||||
self.storages[theme.theme_dir] = theme_storage
|
||||
if theme.theme_dir not in self.themes:
|
||||
self.themes.append(theme.theme_dir)
|
||||
if not isinstance(theme_dir, basestring):
|
||||
raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
|
||||
|
||||
super(ThemeFilesFinder, self).__init__(*args, **kwargs)
|
||||
root = Path(settings.PROJECT_ROOT)
|
||||
if root.name == "":
|
||||
root = root.parent
|
||||
|
||||
def list(self, ignore_patterns):
|
||||
"""
|
||||
List all files in all app storages.
|
||||
"""
|
||||
for storage in six.itervalues(self.storages):
|
||||
if storage.exists(''): # check if storage location exists
|
||||
for path in utils.get_files(storage, ignore_patterns):
|
||||
yield path, storage
|
||||
component_dir = Path(theme_dir) / root.name
|
||||
static_dir = component_dir / "static"
|
||||
self.storage = CachedComprehensiveThemingStorage(location=static_dir)
|
||||
|
||||
def find(self, path, all=False): # pylint: disable=redefined-builtin
|
||||
"""
|
||||
Looks for files in the theme directories.
|
||||
Looks for files in the default file storage, if it's local.
|
||||
"""
|
||||
matches = []
|
||||
theme_dir = path.split("/", 1)[0]
|
||||
if not self.storage:
|
||||
return []
|
||||
|
||||
themes = {t.theme_dir: t for t in get_themes()}
|
||||
# if path is prefixed by theme name then search in the corresponding storage other wise search all storages.
|
||||
if theme_dir in themes:
|
||||
theme = themes[theme_dir]
|
||||
path = "/".join(path.split("/")[1:])
|
||||
match = self.find_in_theme(theme.theme_dir, path)
|
||||
if match:
|
||||
if not all:
|
||||
return match
|
||||
matches.append(match)
|
||||
return matches
|
||||
if path.startswith(self.storage.prefix):
|
||||
# strip the prefix
|
||||
path = path[len(self.storage.prefix):]
|
||||
|
||||
def find_in_theme(self, theme, path):
|
||||
if self.storage.exists(path):
|
||||
match = self.storage.path(path)
|
||||
if all:
|
||||
match = [match]
|
||||
return match
|
||||
|
||||
return []
|
||||
|
||||
def list(self, ignore_patterns):
|
||||
"""
|
||||
Find a requested static file in an theme's static locations.
|
||||
List all files of the storage.
|
||||
"""
|
||||
storage = self.storages.get(theme, None)
|
||||
if storage:
|
||||
# only try to find a file if the source dir actually exists
|
||||
if storage.exists(path):
|
||||
matched_path = storage.path(path)
|
||||
if matched_path:
|
||||
return matched_path
|
||||
if self.storage and self.storage.exists(''):
|
||||
for path in utils.get_files(self.storage, ignore_patterns):
|
||||
yield path, self.storage
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
"""
|
||||
Helpers for accessing comprehensive theming related variables.
|
||||
"""
|
||||
import re
|
||||
import os
|
||||
from path import Path
|
||||
|
||||
from django.conf import settings, ImproperlyConfigured
|
||||
from django.core.cache import cache
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
|
||||
from microsite_configuration import microsite
|
||||
from microsite_configuration import page_title_breadcrumbs
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def get_page_title_breadcrumbs(*args):
|
||||
@@ -31,11 +24,7 @@ def get_template_path(relative_path, **kwargs):
|
||||
"""
|
||||
This is a proxy function to hide microsite_configuration behind comprehensive theming.
|
||||
"""
|
||||
template_path = get_template_path_with_theme(relative_path)
|
||||
if template_path == relative_path: # we don't have a theme now look into microsites
|
||||
template_path = microsite.get_template_path(relative_path, **kwargs)
|
||||
|
||||
return template_path
|
||||
return microsite.get_template_path(relative_path, **kwargs)
|
||||
|
||||
|
||||
def is_request_in_themed_site():
|
||||
@@ -45,14 +34,6 @@ def is_request_in_themed_site():
|
||||
return microsite.is_request_in_microsite()
|
||||
|
||||
|
||||
def get_template(uri):
|
||||
"""
|
||||
This is a proxy function to hide microsite_configuration behind comprehensive theming.
|
||||
:param uri: uri of the template
|
||||
"""
|
||||
return microsite.get_template(uri)
|
||||
|
||||
|
||||
def get_themed_template_path(relative_path, default_path, **kwargs):
|
||||
"""
|
||||
This is a proxy function to hide microsite_configuration behind comprehensive theming.
|
||||
@@ -71,311 +52,3 @@ def get_themed_template_path(relative_path, default_path, **kwargs):
|
||||
if is_stanford_theming_enabled and not is_microsite:
|
||||
return relative_path
|
||||
return microsite.get_template_path(default_path, **kwargs)
|
||||
|
||||
|
||||
def get_template_path_with_theme(relative_path):
|
||||
"""
|
||||
Returns template path in current site's theme if it finds one there otherwise returns same path.
|
||||
|
||||
Example:
|
||||
>> get_template_path_with_theme('header')
|
||||
'/red-theme/lms/templates/header.html'
|
||||
|
||||
Parameters:
|
||||
relative_path (str): template's path relative to the templates directory e.g. 'footer.html'
|
||||
|
||||
Returns:
|
||||
(str): template path in current site's theme
|
||||
"""
|
||||
site_theme_dir = get_current_site_theme_dir()
|
||||
if not site_theme_dir:
|
||||
return relative_path
|
||||
|
||||
base_theme_dir = get_base_theme_dir()
|
||||
root_name = get_project_root_name()
|
||||
template_path = "/".join([
|
||||
base_theme_dir,
|
||||
site_theme_dir,
|
||||
root_name,
|
||||
"templates"
|
||||
])
|
||||
|
||||
# strip `/` if present at the start of relative_path
|
||||
template_name = re.sub(r'^/+', '', relative_path)
|
||||
search_path = os.path.join(template_path, template_name)
|
||||
if os.path.isfile(search_path):
|
||||
path = '/{site_theme_dir}/{root_name}/templates/{template_name}'.format(
|
||||
site_theme_dir=site_theme_dir,
|
||||
root_name=root_name,
|
||||
template_name=template_name,
|
||||
)
|
||||
return path
|
||||
else:
|
||||
return relative_path
|
||||
|
||||
|
||||
def get_current_theme_template_dirs():
|
||||
"""
|
||||
Returns template directories for the current theme.
|
||||
|
||||
Example:
|
||||
>> get_current_theme_template_dirs('header.html')
|
||||
['/edx/app/edxapp/edx-platform/themes/red-theme/lms/templates/', ]
|
||||
|
||||
Returns:
|
||||
(list): list of directories containing theme templates.
|
||||
"""
|
||||
site_theme_dir = get_current_site_theme_dir()
|
||||
if not site_theme_dir:
|
||||
return None
|
||||
|
||||
base_theme_dir = get_base_theme_dir()
|
||||
root_name = get_project_root_name()
|
||||
template_path = "/".join([
|
||||
base_theme_dir,
|
||||
site_theme_dir,
|
||||
root_name,
|
||||
"templates"
|
||||
])
|
||||
|
||||
return [template_path]
|
||||
|
||||
|
||||
def strip_site_theme_templates_path(uri):
|
||||
"""
|
||||
Remove site template theme path from the uri.
|
||||
|
||||
Example:
|
||||
>> strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
|
||||
'header.html'
|
||||
|
||||
Arguments:
|
||||
uri (str): template path from which to remove site theme path. e.g. '/red-theme/lms/templates/header.html'
|
||||
|
||||
Returns:
|
||||
(str): template path with site theme path removed.
|
||||
"""
|
||||
site_theme_dir = get_current_site_theme_dir()
|
||||
if not site_theme_dir:
|
||||
return uri
|
||||
|
||||
root_name = get_project_root_name()
|
||||
templates_path = "/".join([
|
||||
site_theme_dir,
|
||||
root_name,
|
||||
"templates"
|
||||
])
|
||||
|
||||
uri = re.sub(r'^/*' + templates_path + '/*', '', uri)
|
||||
return uri
|
||||
|
||||
|
||||
def get_current_site():
|
||||
"""
|
||||
Return current site.
|
||||
|
||||
Returns:
|
||||
(django.contrib.sites.models.Site): theme directory for current site
|
||||
"""
|
||||
from edxmako.middleware import REQUEST_CONTEXT
|
||||
request = getattr(REQUEST_CONTEXT, 'request', None)
|
||||
if not request:
|
||||
return None
|
||||
return getattr(request, 'site', None)
|
||||
|
||||
|
||||
def get_current_site_theme_dir():
|
||||
"""
|
||||
Return theme directory for the current site.
|
||||
|
||||
Example:
|
||||
>> get_current_site_theme_dir()
|
||||
'red-theme'
|
||||
|
||||
Returns:
|
||||
(str): theme directory for current site
|
||||
"""
|
||||
site = get_current_site()
|
||||
if not site:
|
||||
return None
|
||||
site_theme_dir = cache.get(get_site_theme_cache_key(site))
|
||||
|
||||
# if site theme dir is not in cache and comprehensive theming is enabled then pull it from db.
|
||||
if not site_theme_dir and is_comprehensive_theming_enabled():
|
||||
site_theme = site.themes.first() # pylint: disable=no-member
|
||||
if site_theme:
|
||||
site_theme_dir = site_theme.theme_dir_name
|
||||
cache_site_theme_dir(site, site_theme_dir)
|
||||
return site_theme_dir
|
||||
|
||||
|
||||
def get_project_root_name():
|
||||
"""
|
||||
Return root name for the current project
|
||||
|
||||
Example:
|
||||
>> get_project_root_name()
|
||||
'lms'
|
||||
# from studio
|
||||
>> get_project_root_name()
|
||||
'cms'
|
||||
|
||||
Returns:
|
||||
(str): component name of platform e.g lms, cms
|
||||
"""
|
||||
root = Path(settings.PROJECT_ROOT)
|
||||
if root.name == "":
|
||||
root = root.parent
|
||||
return root.name
|
||||
|
||||
|
||||
def get_base_theme_dir():
|
||||
"""
|
||||
Return base directory that contains all the themes.
|
||||
|
||||
Example:
|
||||
>> get_base_theme_dir()
|
||||
'/edx/app/edxapp/edx-platform/themes'
|
||||
|
||||
Returns:
|
||||
(Path): Base theme directory path
|
||||
"""
|
||||
themes_dir = settings.COMPREHENSIVE_THEME_DIR
|
||||
if not isinstance(themes_dir, basestring):
|
||||
raise ImproperlyConfigured("COMPREHENSIVE_THEME_DIR must be a string.")
|
||||
return Path(themes_dir)
|
||||
|
||||
|
||||
def is_comprehensive_theming_enabled():
|
||||
"""
|
||||
Returns boolean indicating whether comprehensive theming functionality is enabled or disabled.
|
||||
Example:
|
||||
>> is_comprehensive_theming_enabled()
|
||||
True
|
||||
|
||||
Returns:
|
||||
(bool): True if comprehensive theming is enabled else False
|
||||
"""
|
||||
return True if settings.COMPREHENSIVE_THEME_DIR else False
|
||||
|
||||
|
||||
def get_site_theme_cache_key(site):
|
||||
"""
|
||||
Return cache key for the given site.
|
||||
|
||||
Example:
|
||||
>> site = Site(domain='red-theme.org', name='Red Theme')
|
||||
>> get_site_theme_cache_key(site)
|
||||
'theming.site.red-theme.org'
|
||||
|
||||
Parameters:
|
||||
site (django.contrib.sites.models.Site): site where key needs to generated
|
||||
Returns:
|
||||
(str): a key to be used as cache key
|
||||
"""
|
||||
cache_key = "theming.site.{domain}".format(
|
||||
domain=site.domain
|
||||
)
|
||||
return cache_key
|
||||
|
||||
|
||||
def cache_site_theme_dir(site, theme_dir):
|
||||
"""
|
||||
Cache site's theme directory.
|
||||
|
||||
Example:
|
||||
>> site = Site(domain='red-theme.org', name='Red Theme')
|
||||
>> cache_site_theme_dir(site, 'red-theme')
|
||||
|
||||
Parameters:
|
||||
site (django.contrib.sites.models.Site): site for to cache
|
||||
theme_dir (str): theme directory for the given site
|
||||
"""
|
||||
cache.set(get_site_theme_cache_key(site), theme_dir, settings.THEME_CACHE_TIMEOUT)
|
||||
|
||||
|
||||
def get_static_file_url(asset):
|
||||
"""
|
||||
Returns url of the themed asset if asset is not themed than returns the default asset url.
|
||||
|
||||
Example:
|
||||
>> get_static_file_url('css/lms-main.css')
|
||||
'/static/red-theme/css/lms-main.css'
|
||||
|
||||
Parameters:
|
||||
asset (str): asset's path relative to the static files directory
|
||||
|
||||
Returns:
|
||||
(str): static asset's url
|
||||
"""
|
||||
return staticfiles_storage.url(asset)
|
||||
|
||||
|
||||
def get_themes():
|
||||
"""
|
||||
get a list of all themes known to the system.
|
||||
Returns:
|
||||
list of themes known to the system.
|
||||
"""
|
||||
themes_dir = get_base_theme_dir()
|
||||
# pick only directories and discard files in themes directory
|
||||
theme_names = []
|
||||
if themes_dir:
|
||||
theme_names = [_dir for _dir in os.listdir(themes_dir) if is_theme_dir(themes_dir / _dir)]
|
||||
|
||||
return [Theme(name, name) for name in theme_names]
|
||||
|
||||
|
||||
def is_theme_dir(_dir):
|
||||
"""
|
||||
Returns true if given dir contains theme overrides.
|
||||
A theme dir must have subdirectory 'lms' or 'cms' or both.
|
||||
|
||||
Args:
|
||||
_dir: directory path to check for a theme
|
||||
|
||||
Returns:
|
||||
Returns true if given dir is a theme directory.
|
||||
"""
|
||||
theme_sub_directories = {'lms', 'cms'}
|
||||
return bool(os.path.isdir(_dir) and theme_sub_directories.intersection(os.listdir(_dir)))
|
||||
|
||||
|
||||
class Theme(object):
|
||||
"""
|
||||
class to encapsulate theme related information.
|
||||
"""
|
||||
name = ''
|
||||
theme_dir = ''
|
||||
path = ''
|
||||
|
||||
def __init__(self, name='', theme_dir=''):
|
||||
"""
|
||||
init method for Theme
|
||||
Args:
|
||||
name: name if the theme
|
||||
theme_dir: directory name of the theme
|
||||
"""
|
||||
self.name = name
|
||||
self.theme_dir = theme_dir
|
||||
self.path = Path(get_base_theme_dir()) / theme_dir / get_project_root_name()
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Returns True if given theme is same as the self
|
||||
Args:
|
||||
other: Theme object to compare with self
|
||||
|
||||
Returns:
|
||||
(bool) True if two themes are the same else False
|
||||
"""
|
||||
return (self.theme_dir, self.path) == (other.theme_dir, other.path)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.theme_dir, self.path))
|
||||
|
||||
def __unicode__(self):
|
||||
return u"<Theme: {name} at '{path}'>".format(name=self.name, path=self.path)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sites', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SiteTheme',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('theme_dir_name', models.CharField(max_length=255)),
|
||||
('site', models.ForeignKey(related_name='themes', to='sites.Site')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
"""
|
||||
Django models supporting the Comprehensive Theming subsystem
|
||||
"""
|
||||
from django.db import models
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
|
||||
class SiteTheme(models.Model):
|
||||
"""
|
||||
This is where the information about the site's theme gets stored to the db.
|
||||
|
||||
`site` field is foreignkey to django Site model
|
||||
`theme_dir_name` contains directory name having Site's theme
|
||||
"""
|
||||
site = models.ForeignKey(Site, related_name='themes')
|
||||
theme_dir_name = models.CharField(max_length=255)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.theme_dir_name
|
||||
@@ -2,300 +2,87 @@
|
||||
Comprehensive Theming support for Django's collectstatic functionality.
|
||||
See https://docs.djangoproject.com/en/1.8/ref/contrib/staticfiles/
|
||||
"""
|
||||
import posixpath
|
||||
from path import Path
|
||||
import os.path
|
||||
from django.conf import settings
|
||||
from django.utils._os import safe_join
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
|
||||
from django.contrib.staticfiles.finders import find
|
||||
from django.utils.six.moves.urllib.parse import ( # pylint: disable=no-name-in-module, import-error
|
||||
unquote, urlsplit,
|
||||
)
|
||||
|
||||
from pipeline.storage import PipelineMixin
|
||||
|
||||
from openedx.core.djangoapps.theming.helpers import (
|
||||
get_base_theme_dir,
|
||||
get_project_root_name,
|
||||
get_current_site_theme_dir,
|
||||
get_themes,
|
||||
)
|
||||
from django.utils._os import safe_join
|
||||
|
||||
|
||||
class ThemeStorage(StaticFilesStorage):
|
||||
class ComprehensiveThemingAwareMixin(object):
|
||||
"""
|
||||
Comprehensive theme aware Static files storage.
|
||||
Mixin for Django storage system to make it aware of the currently-active
|
||||
comprehensive theme, so that it can generate theme-scoped URLs for themed
|
||||
static assets.
|
||||
"""
|
||||
# prefix for file path, this prefix is added at the beginning of file path before saving static files during
|
||||
# collectstatic command.
|
||||
# e.g. having "edx.org" as prefix will cause files to be saved as "edx.org/images/logo.png"
|
||||
# instead of "images/logo.png"
|
||||
prefix = None
|
||||
|
||||
def __init__(self, location=None, base_url=None, file_permissions_mode=None,
|
||||
directory_permissions_mode=None, prefix=None):
|
||||
|
||||
self.prefix = prefix
|
||||
super(ThemeStorage, self).__init__(
|
||||
location=location,
|
||||
base_url=base_url,
|
||||
file_permissions_mode=file_permissions_mode,
|
||||
directory_permissions_mode=directory_permissions_mode,
|
||||
)
|
||||
|
||||
def url(self, name):
|
||||
"""
|
||||
Returns url of the asset, themed url will be returned if the asset is themed otherwise default
|
||||
asset url will be returned.
|
||||
|
||||
Args:
|
||||
name: name of the asset, e.g. 'images/logo.png'
|
||||
|
||||
Returns:
|
||||
url of the asset, e.g. '/static/red-theme/images/logo.png' if current theme is red-theme and logo
|
||||
is provided by red-theme otherwise '/static/images/logo.png'
|
||||
"""
|
||||
prefix = ''
|
||||
theme_dir = get_current_site_theme_dir()
|
||||
|
||||
# get theme prefix from site address if if asset is accessed via a url
|
||||
if theme_dir:
|
||||
prefix = theme_dir
|
||||
|
||||
# get theme prefix from storage class, if asset is accessed during collectstatic run
|
||||
elif self.prefix:
|
||||
prefix = self.prefix
|
||||
|
||||
# join theme prefix with asset name if theme is applied and themed asset exists
|
||||
if prefix and self.themed(name, prefix):
|
||||
name = os.path.join(prefix, name)
|
||||
|
||||
return super(ThemeStorage, self).url(name)
|
||||
|
||||
def themed(self, name, theme):
|
||||
"""
|
||||
Returns True if given asset override is provided by the given theme otherwise returns False.
|
||||
Args:
|
||||
name: asset name e.g. 'images/logo.png'
|
||||
theme: theme name e.g. 'red-theme', 'edx.org'
|
||||
|
||||
Returns:
|
||||
True if given asset override is provided by the given theme otherwise returns False
|
||||
"""
|
||||
# in debug mode check static asset from within the project directory
|
||||
if settings.DEBUG:
|
||||
themes_location = get_base_theme_dir()
|
||||
# Nothing can be themed if we don't have a theme location or required params.
|
||||
if not all((themes_location, theme, name)):
|
||||
return False
|
||||
|
||||
themed_path = "/".join([
|
||||
themes_location,
|
||||
theme,
|
||||
get_project_root_name(),
|
||||
"static/"
|
||||
])
|
||||
name = name[1:] if name.startswith("/") else name
|
||||
path = safe_join(themed_path, name)
|
||||
return os.path.exists(path)
|
||||
# in live mode check static asset in the static files dir defined by "STATIC_ROOT" setting
|
||||
else:
|
||||
return self.exists(os.path.join(theme, name))
|
||||
|
||||
|
||||
class ComprehensiveThemingCachedFilesMixin(CachedFilesMixin):
|
||||
"""
|
||||
Comprehensive theme aware CachedFilesMixin.
|
||||
Main purpose of subclassing CachedFilesMixin is to override the following methods.
|
||||
1 - url
|
||||
2 - url_converter
|
||||
|
||||
url:
|
||||
This method takes asset name as argument and is responsible for adding hash to the name to support caching.
|
||||
This method is called during both collectstatic command and live server run.
|
||||
|
||||
When called during collectstatic command that name argument will be asset name inside STATIC_ROOT,
|
||||
for non themed assets it will be the usual path (e.g. 'images/logo.png') but for themed asset it will
|
||||
also contain themes dir prefix (e.g. 'red-theme/images/logo.png'). So, here we check whether the themed asset
|
||||
exists or not, if it exists we pass the same name up in the MRO chain for further processing and if it does not
|
||||
exists we strip theme name and pass the new asset name to the MRO chain for further processing.
|
||||
|
||||
When called during server run, we get the theme dir for the current site using `get_current_site_theme_dir` and
|
||||
make sure to prefix theme dir to the asset name. This is done to ensure the usage of correct hash in file name.
|
||||
e.g. if our red-theme overrides 'images/logo.png' and we do not prefix theme dir to the asset name, the hash for
|
||||
'{platform-dir}/lms/static/images/logo.png' would be used instead of
|
||||
'{themes_base_dir}/red-theme/images/logo.png'
|
||||
|
||||
url_converter:
|
||||
This function returns another function that is responsible for hashing urls that appear inside assets
|
||||
(e.g. url("images/logo.png") inside css). The method defined in the superclass adds a hash to file and returns
|
||||
relative url of the file.
|
||||
e.g. for url("../images/logo.png") it would return url("../images/logo.790c9a5340cb.png"). However we would
|
||||
want it to return absolute url (e.g. url("/static/images/logo.790c9a5340cb.png")) so that it works properly
|
||||
with themes.
|
||||
|
||||
The overridden method here simply comments out the two lines that convert absolute url to relative url,
|
||||
hence absolute urls are used instead of relative urls.
|
||||
"""
|
||||
|
||||
def url(self, name, force=False):
|
||||
"""
|
||||
Returns themed url for the given asset.
|
||||
"""
|
||||
theme_dir = get_current_site_theme_dir()
|
||||
if theme_dir and theme_dir not in name:
|
||||
# during server run, append theme name to the asset name if it is not already there
|
||||
# this is ensure that correct hash is created and default asset is not always
|
||||
# used to create hash of themed assets.
|
||||
name = os.path.join(theme_dir, name)
|
||||
parsed_name = urlsplit(unquote(name))
|
||||
clean_name = parsed_name.path.strip()
|
||||
asset_name = name
|
||||
if not self.exists(clean_name):
|
||||
# if themed asset does not exists then use default asset
|
||||
theme = name.split("/", 1)[0]
|
||||
# verify that themed asset was accessed
|
||||
if theme in [theme.theme_dir for theme in get_themes()]:
|
||||
asset_name = "/".join(name.split("/")[1:])
|
||||
|
||||
return super(ComprehensiveThemingCachedFilesMixin, self).url(asset_name, force)
|
||||
|
||||
def url_converter(self, name, template=None):
|
||||
"""
|
||||
This is an override of url_converter from CachedFilesMixin.
|
||||
It just comments out two lines at the end of the method.
|
||||
|
||||
The purpose of this override is to make converter method return absolute urls instead of relative urls.
|
||||
This behavior is necessary for theme overrides, as we get 404 on assets with relative urls on a themed site.
|
||||
"""
|
||||
if template is None:
|
||||
template = self.default_template
|
||||
|
||||
def converter(matchobj):
|
||||
"""
|
||||
Converts the matched URL depending on the parent level (`..`)
|
||||
and returns the normalized and hashed URL using the url method
|
||||
of the storage.
|
||||
"""
|
||||
matched, url = matchobj.groups()
|
||||
# Completely ignore http(s) prefixed URLs,
|
||||
# fragments and data-uri URLs
|
||||
if url.startswith(('#', 'http:', 'https:', 'data:', '//')):
|
||||
return matched
|
||||
name_parts = name.split(os.sep)
|
||||
# Using posix normpath here to remove duplicates
|
||||
url = posixpath.normpath(url)
|
||||
url_parts = url.split('/')
|
||||
parent_level, sub_level = url.count('..'), url.count('/')
|
||||
if url.startswith('/'):
|
||||
sub_level -= 1
|
||||
url_parts = url_parts[1:]
|
||||
if parent_level or not url.startswith('/'):
|
||||
start, end = parent_level + 1, parent_level
|
||||
else:
|
||||
if sub_level:
|
||||
if sub_level == 1:
|
||||
parent_level -= 1
|
||||
start, end = parent_level, 1
|
||||
else:
|
||||
start, end = 1, sub_level - 1
|
||||
joined_result = '/'.join(name_parts[:-start] + url_parts[end:])
|
||||
hashed_url = self.url(unquote(joined_result), force=True)
|
||||
|
||||
# NOTE:
|
||||
# following two lines are commented out so that absolute urls are used instead of relative urls
|
||||
# to make themed assets work correctly.
|
||||
#
|
||||
# The lines are commented and not removed to make future django upgrade easier and
|
||||
# show exactly what is changed in this method override
|
||||
#
|
||||
# file_name = hashed_url.split('/')[-1:]
|
||||
# relative_url = '/'.join(url.split('/')[:-1] + file_name)
|
||||
|
||||
# Return the hashed version to the file
|
||||
return template % unquote(hashed_url)
|
||||
|
||||
return converter
|
||||
|
||||
|
||||
class ThemePipelineMixin(PipelineMixin):
|
||||
"""
|
||||
Mixin to make sure themed assets are also packaged and used along with non themed assets.
|
||||
if a source asset for a particular package is not present then the default asset is used.
|
||||
|
||||
e.g. in the following package and for 'red-theme'
|
||||
'style-vendor': {
|
||||
'source_filenames': [
|
||||
'js/vendor/afontgarde/afontgarde.css',
|
||||
'css/vendor/font-awesome.css',
|
||||
'css/vendor/jquery.qtip.min.css',
|
||||
'css/vendor/responsive-carousel/responsive-carousel.css',
|
||||
'css/vendor/responsive-carousel/responsive-carousel.slide.css',
|
||||
],
|
||||
'output_filename': 'css/lms-style-vendor.css'
|
||||
}
|
||||
'red-theme/css/vendor/responsive-carousel/responsive-carousel.css' will be used of it exists otherwise
|
||||
'css/vendor/responsive-carousel/responsive-carousel.css' will be used to create 'red-theme/css/lms-style-vendor.css'
|
||||
"""
|
||||
packing = True
|
||||
|
||||
def post_process(self, paths, dry_run=False, **options):
|
||||
"""
|
||||
This post_process hook is used to package all themed assets.
|
||||
"""
|
||||
if dry_run:
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ComprehensiveThemingAwareMixin, self).__init__(*args, **kwargs)
|
||||
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
|
||||
if not theme_dir:
|
||||
self.theme_location = None
|
||||
return
|
||||
themes = get_themes()
|
||||
|
||||
for theme in themes:
|
||||
css_packages = self.get_themed_packages(theme.theme_dir, settings.PIPELINE_CSS)
|
||||
js_packages = self.get_themed_packages(theme.theme_dir, settings.PIPELINE_JS)
|
||||
if not isinstance(theme_dir, basestring):
|
||||
raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
|
||||
|
||||
from pipeline.packager import Packager
|
||||
packager = Packager(storage=self, css_packages=css_packages, js_packages=js_packages)
|
||||
for package_name in packager.packages['css']:
|
||||
package = packager.package_for('css', package_name)
|
||||
output_file = package.output_filename
|
||||
if self.packing:
|
||||
packager.pack_stylesheets(package)
|
||||
paths[output_file] = (self, output_file)
|
||||
yield output_file, output_file, True
|
||||
for package_name in packager.packages['js']:
|
||||
package = packager.package_for('js', package_name)
|
||||
output_file = package.output_filename
|
||||
if self.packing:
|
||||
packager.pack_javascripts(package)
|
||||
paths[output_file] = (self, output_file)
|
||||
yield output_file, output_file, True
|
||||
root = Path(settings.PROJECT_ROOT)
|
||||
if root.name == "":
|
||||
root = root.parent
|
||||
|
||||
super_class = super(ThemePipelineMixin, self)
|
||||
if hasattr(super_class, 'post_process'):
|
||||
for name, hashed_name, processed in super_class.post_process(paths.copy(), dry_run, **options):
|
||||
yield name, hashed_name, processed
|
||||
component_dir = Path(theme_dir) / root.name
|
||||
self.theme_location = component_dir / "static"
|
||||
|
||||
@staticmethod
|
||||
def get_themed_packages(prefix, packages):
|
||||
@property
|
||||
def prefix(self):
|
||||
"""
|
||||
Update paths with the themed assets,
|
||||
Args:
|
||||
prefix: theme prefix for which to update asset paths e.g. 'red-theme', 'edx.org' etc.
|
||||
packages: packages to update
|
||||
|
||||
Returns: list of updated paths and a boolean indicating whether any path was path or not
|
||||
This is used by the ComprehensiveThemeFinder in the collection step.
|
||||
"""
|
||||
themed_packages = {}
|
||||
for name in packages:
|
||||
# collect source file names for the package
|
||||
source_files = []
|
||||
for path in packages[name].get('source_filenames', []):
|
||||
# if themed asset exists use that, otherwise use default asset.
|
||||
if find(os.path.join(prefix, path)):
|
||||
source_files.append(os.path.join(prefix, path))
|
||||
else:
|
||||
source_files.append(path)
|
||||
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
|
||||
if not theme_dir:
|
||||
return None
|
||||
theme_name = os.path.basename(os.path.normpath(theme_dir))
|
||||
return "themes/{name}/".format(name=theme_name)
|
||||
|
||||
themed_packages[name] = {
|
||||
'output_filename': os.path.join(prefix, packages[name].get('output_filename', '')),
|
||||
'source_filenames': source_files,
|
||||
}
|
||||
return themed_packages
|
||||
def themed(self, name):
|
||||
"""
|
||||
Given a name, return a boolean indicating whether that name exists
|
||||
as a themed asset in the comprehensive theme.
|
||||
"""
|
||||
# Nothing can be themed if we don't have a theme location.
|
||||
if not self.theme_location:
|
||||
return False
|
||||
|
||||
path = safe_join(self.theme_location, name)
|
||||
return os.path.exists(path)
|
||||
|
||||
def path(self, name):
|
||||
"""
|
||||
Get the path to the real asset on disk
|
||||
"""
|
||||
if self.themed(name):
|
||||
base = self.theme_location
|
||||
else:
|
||||
base = self.location
|
||||
path = safe_join(base, name)
|
||||
return os.path.normpath(path)
|
||||
|
||||
def url(self, name, *args, **kwargs):
|
||||
"""
|
||||
Add the theme prefix to the asset URL
|
||||
"""
|
||||
if self.themed(name):
|
||||
name = self.prefix + name
|
||||
return super(ComprehensiveThemingAwareMixin, self).url(name, *args, **kwargs)
|
||||
|
||||
|
||||
class CachedComprehensiveThemingStorage(
|
||||
ComprehensiveThemingAwareMixin,
|
||||
CachedFilesMixin,
|
||||
StaticFilesStorage
|
||||
):
|
||||
"""
|
||||
Used by the ComprehensiveThemeFinder class. Mixes in support for cached
|
||||
files and comprehensive theming in static files.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
"""
|
||||
Theming aware template loaders.
|
||||
"""
|
||||
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
||||
|
||||
from edxmako.makoloader import MakoLoader
|
||||
from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme
|
||||
|
||||
|
||||
class ThemeTemplateLoader(MakoLoader):
|
||||
"""
|
||||
This is a Django loader object which will load the template based on current request and its corresponding theme.
|
||||
"""
|
||||
def __call__(self, template_name, template_dirs=None):
|
||||
template_name = get_template_path_with_theme(template_name).lstrip("/")
|
||||
return self.load_template(template_name, template_dirs)
|
||||
|
||||
|
||||
class ThemeFilesystemLoader(ThemeTemplateLoader):
|
||||
"""
|
||||
Filesystem Template loaders to pickup templates from theme directory based on the current site.
|
||||
"""
|
||||
is_usable = True
|
||||
_accepts_engine_in_init = True
|
||||
|
||||
def __init__(self, *args):
|
||||
ThemeTemplateLoader.__init__(self, FilesystemLoader(*args))
|
||||
@@ -1,78 +0,0 @@
|
||||
"""
|
||||
Theme aware pipeline template tags.
|
||||
"""
|
||||
|
||||
from django import template
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from pipeline.templatetags.pipeline import StylesheetNode, JavascriptNode
|
||||
from pipeline.utils import guess_type
|
||||
|
||||
from openedx.core.djangoapps.theming.helpers import get_static_file_url
|
||||
|
||||
register = template.Library() # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class ThemeStylesheetNode(StylesheetNode):
|
||||
"""
|
||||
Overrides StyleSheetNode from django pipeline so that stylesheets are served based on the applied theme.
|
||||
"""
|
||||
def render_css(self, package, path):
|
||||
"""
|
||||
Override render_css from django-pipline so that stylesheets urls are based on the applied theme
|
||||
"""
|
||||
template_name = package.template_name or "pipeline/css.html"
|
||||
context = package.extra_context
|
||||
context.update({
|
||||
'type': guess_type(path, 'text/css'),
|
||||
'url': mark_safe(get_static_file_url(path))
|
||||
})
|
||||
return render_to_string(template_name, context)
|
||||
|
||||
|
||||
class ThemeJavascriptNode(JavascriptNode):
|
||||
"""
|
||||
Overrides JavascriptNode from django pipeline so that js files are served based on the applied theme.
|
||||
"""
|
||||
def render_js(self, package, path):
|
||||
"""
|
||||
Override render_js from django-pipline so that js file urls are based on the applied theme
|
||||
"""
|
||||
template_name = package.template_name or "pipeline/js.html"
|
||||
context = package.extra_context
|
||||
context.update({
|
||||
'type': guess_type(path, 'text/javascript'),
|
||||
'url': mark_safe(get_static_file_url(path))
|
||||
})
|
||||
return render_to_string(template_name, context)
|
||||
|
||||
|
||||
@register.tag
|
||||
def stylesheet(parser, token): # pylint: disable=unused-argument
|
||||
"""
|
||||
Template tag to serve stylesheets from django-pipeline. This definition uses the theming aware ThemeStyleSheetNode.
|
||||
"""
|
||||
try:
|
||||
_, name = token.split_contents()
|
||||
except ValueError:
|
||||
raise template.TemplateSyntaxError(
|
||||
'%r requires exactly one argument: the name of a group in the PIPELINE_CSS setting' %
|
||||
token.split_contents()[0]
|
||||
)
|
||||
return ThemeStylesheetNode(name)
|
||||
|
||||
|
||||
@register.tag
|
||||
def javascript(parser, token): # pylint: disable=unused-argument
|
||||
"""
|
||||
Template tag to serve javascript from django-pipeline. This definition uses the theming aware ThemeJavascriptNode.
|
||||
"""
|
||||
try:
|
||||
_, name = token.split_contents()
|
||||
except ValueError:
|
||||
raise template.TemplateSyntaxError(
|
||||
'%r requires exactly one argument: the name of a group in the PIPELINE_JS setting' %
|
||||
token.split_contents()[0]
|
||||
)
|
||||
return ThemeJavascriptNode(name)
|
||||
@@ -6,57 +6,87 @@ from functools import wraps
|
||||
import os
|
||||
import os.path
|
||||
import contextlib
|
||||
import re
|
||||
|
||||
from mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.template import Engine
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import edxmako
|
||||
from .models import SiteTheme
|
||||
|
||||
from .core import comprehensive_theme_changes
|
||||
|
||||
EDX_THEME_DIR = settings.REPO_ROOT / "themes" / "edx.org"
|
||||
|
||||
|
||||
def with_comprehensive_theme(theme_dir_name):
|
||||
def with_comprehensive_theme(theme_dir):
|
||||
"""
|
||||
A decorator to run a test with a comprehensive theming enabled.
|
||||
A decorator to run a test with a particular comprehensive theme.
|
||||
|
||||
Arguments:
|
||||
theme_dir_name (str): directory name of the site for which we want comprehensive theming enabled.
|
||||
theme_dir (str): the full path to the theme directory to use.
|
||||
This will likely use `settings.REPO_ROOT` to get the full path.
|
||||
|
||||
"""
|
||||
# This decorator creates Site and SiteTheme models for given domain
|
||||
# This decorator gets the settings changes needed for a theme, and applies
|
||||
# them using the override_settings and edxmako.paths.add_lookup context
|
||||
# managers.
|
||||
|
||||
changes = comprehensive_theme_changes(theme_dir)
|
||||
|
||||
def _decorator(func): # pylint: disable=missing-docstring
|
||||
@wraps(func)
|
||||
def _decorated(*args, **kwargs): # pylint: disable=missing-docstring
|
||||
# make a domain name out of directory name
|
||||
domain = "{theme_dir_name}.org".format(theme_dir_name=re.sub(r"\.org$", "", theme_dir_name))
|
||||
site, __ = Site.objects.get_or_create(domain=domain, name=domain)
|
||||
SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme_dir_name)
|
||||
edxmako.paths.add_lookup('main', settings.COMPREHENSIVE_THEME_DIR, prepend=True)
|
||||
with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme_dir',
|
||||
return_value=theme_dir_name):
|
||||
with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
|
||||
return func(*args, **kwargs)
|
||||
with override_settings(COMPREHENSIVE_THEME_DIR=theme_dir, **changes['settings']):
|
||||
default_engine = Engine.get_default()
|
||||
dirs = default_engine.dirs[:]
|
||||
with edxmako.save_lookups():
|
||||
for template_dir in changes['template_paths']:
|
||||
edxmako.paths.add_lookup('main', template_dir, prepend=True)
|
||||
dirs.insert(0, template_dir)
|
||||
with patch.object(default_engine, 'dirs', dirs):
|
||||
return func(*args, **kwargs)
|
||||
return _decorated
|
||||
return _decorator
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def with_comprehensive_theme_context(theme=None):
|
||||
def with_is_edx_domain(is_edx_domain):
|
||||
"""
|
||||
A function to run a test as if request was made to the given theme.
|
||||
A decorator to run a test as if request originated from edX domain or not.
|
||||
|
||||
Arguments:
|
||||
theme (str): name if the theme or None if no theme is applied
|
||||
is_edx_domain (bool): are we an edX domain or not?
|
||||
|
||||
"""
|
||||
if theme:
|
||||
domain = '{theme}.org'.format(theme=re.sub(r"\.org$", "", theme))
|
||||
site, __ = Site.objects.get_or_create(domain=domain, name=theme)
|
||||
SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme)
|
||||
edxmako.paths.add_lookup('main', settings.COMPREHENSIVE_THEME_DIR, prepend=True)
|
||||
with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme_dir',
|
||||
return_value=theme):
|
||||
with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
|
||||
# This is weird, it's a decorator that conditionally applies other
|
||||
# decorators, which is confusing.
|
||||
def _decorator(func): # pylint: disable=missing-docstring
|
||||
if is_edx_domain:
|
||||
# This applies @with_comprehensive_theme to the func.
|
||||
func = with_comprehensive_theme(EDX_THEME_DIR)(func)
|
||||
|
||||
return func
|
||||
|
||||
return _decorator
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def with_edx_domain_context(is_edx_domain):
|
||||
"""
|
||||
A function to run a test as if request originated from edX domain or not.
|
||||
|
||||
Arguments:
|
||||
is_edx_domain (bool): are we an edX domain or not?
|
||||
|
||||
"""
|
||||
if is_edx_domain:
|
||||
changes = comprehensive_theme_changes(EDX_THEME_DIR)
|
||||
with override_settings(COMPREHENSIVE_THEME_DIR=EDX_THEME_DIR, **changes['settings']):
|
||||
with edxmako.save_lookups():
|
||||
for template_dir in changes['template_paths']:
|
||||
edxmako.paths.add_lookup('main', template_dir, prepend=True)
|
||||
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
"""Tests of comprehensive theming."""
|
||||
import unittest
|
||||
from mock import patch
|
||||
|
||||
from django.test import TestCase, RequestFactory, override_settings
|
||||
from django.conf import settings
|
||||
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
|
||||
from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme, strip_site_theme_templates_path, \
|
||||
get_current_site_theme_dir, get_themes, Theme
|
||||
|
||||
|
||||
class TestHelpers(TestCase):
|
||||
"""Test comprehensive theming helper functions."""
|
||||
|
||||
def test_get_themes(self):
|
||||
"""
|
||||
Tests template paths are returned from enabled theme.
|
||||
"""
|
||||
expected_themes = [
|
||||
Theme('red-theme', 'red-theme'),
|
||||
Theme('edge.edx.org', 'edge.edx.org'),
|
||||
Theme('edx.org', 'edx.org'),
|
||||
Theme('stanford-style', 'stanford-style'),
|
||||
]
|
||||
actual_themes = get_themes()
|
||||
self.assertItemsEqual(expected_themes, actual_themes)
|
||||
|
||||
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
|
||||
def test_get_themes_2(self):
|
||||
"""
|
||||
Tests template paths are returned from enabled theme.
|
||||
"""
|
||||
expected_themes = [
|
||||
Theme('test-theme', 'test-theme'),
|
||||
]
|
||||
actual_themes = get_themes()
|
||||
self.assertItemsEqual(expected_themes, actual_themes)
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TestHelpersLMS(TestCase):
|
||||
"""Test comprehensive theming helper functions."""
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_template_path_with_theme_enabled(self):
|
||||
"""
|
||||
Tests template paths are returned from enabled theme.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('header.html')
|
||||
self.assertEqual(template_path, '/red-theme/lms/templates/header.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_template_path_with_theme_for_missing_template(self):
|
||||
"""
|
||||
Tests default template paths are returned if template is not found in the theme.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('course.html')
|
||||
self.assertEqual(template_path, 'course.html')
|
||||
|
||||
def test_get_template_path_with_theme_disabled(self):
|
||||
"""
|
||||
Tests default template paths are returned when theme is non theme is enabled.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('header.html')
|
||||
self.assertEqual(template_path, 'header.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_strip_site_theme_templates_path_theme_enabled(self):
|
||||
"""
|
||||
Tests site theme templates path is stripped from the given template path.
|
||||
"""
|
||||
template_path = strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
|
||||
self.assertEqual(template_path, 'header.html')
|
||||
|
||||
def test_strip_site_theme_templates_path_theme_disabled(self):
|
||||
"""
|
||||
Tests site theme templates path returned unchanged if no theme is applied.
|
||||
"""
|
||||
template_path = strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
|
||||
self.assertEqual(template_path, '/red-theme/lms/templates/header.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_current_site_theme_dir(self):
|
||||
"""
|
||||
Tests current site theme name.
|
||||
"""
|
||||
factory = RequestFactory()
|
||||
with patch(
|
||||
'edxmako.middleware.REQUEST_CONTEXT.request',
|
||||
factory.get('/', SERVER_NAME="red-theme.org"),
|
||||
create=True,
|
||||
):
|
||||
current_site = get_current_site_theme_dir()
|
||||
self.assertEqual(current_site, 'red-theme')
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
|
||||
class TestHelpersCMS(TestCase):
|
||||
"""Test comprehensive theming helper functions."""
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_template_path_with_theme_enabled(self):
|
||||
"""
|
||||
Tests template paths are returned from enabled theme.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('login.html')
|
||||
self.assertEqual(template_path, '/red-theme/cms/templates/login.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_template_path_with_theme_for_missing_template(self):
|
||||
"""
|
||||
Tests default template paths are returned if template is not found in the theme.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('certificates.html')
|
||||
self.assertEqual(template_path, 'certificates.html')
|
||||
|
||||
def test_get_template_path_with_theme_disabled(self):
|
||||
"""
|
||||
Tests default template paths are returned when theme is non theme is enabled.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('login.html')
|
||||
self.assertEqual(template_path, 'login.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_strip_site_theme_templates_path_theme_enabled(self):
|
||||
"""
|
||||
Tests site theme templates path is stripped from the given template path.
|
||||
"""
|
||||
template_path = strip_site_theme_templates_path('/red-theme/cms/templates/login.html')
|
||||
self.assertEqual(template_path, 'login.html')
|
||||
|
||||
def test_strip_site_theme_templates_path_theme_disabled(self):
|
||||
"""
|
||||
Tests site theme templates path returned unchanged if no theme is applied.
|
||||
"""
|
||||
template_path = strip_site_theme_templates_path('/red-theme/cms/templates/login.html')
|
||||
self.assertEqual(template_path, '/red-theme/cms/templates/login.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_current_site_theme_dir(self):
|
||||
"""
|
||||
Tests current site theme name.
|
||||
"""
|
||||
factory = RequestFactory()
|
||||
with patch(
|
||||
'edxmako.middleware.REQUEST_CONTEXT.request',
|
||||
factory.get('/', SERVER_NAME="red-theme.org"),
|
||||
create=True,
|
||||
):
|
||||
current_site = get_current_site_theme_dir()
|
||||
self.assertEqual(current_site, 'red-theme')
|
||||
@@ -1,82 +0,0 @@
|
||||
"""
|
||||
Tests for comprehensive theme static files storage classes.
|
||||
"""
|
||||
import ddt
|
||||
import unittest
|
||||
import re
|
||||
|
||||
from mock import patch
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
from django.conf import settings
|
||||
|
||||
from openedx.core.djangoapps.theming.helpers import get_base_theme_dir
|
||||
from openedx.core.djangoapps.theming.storage import ThemeStorage
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@ddt.ddt
|
||||
class TestStorageLMS(TestCase):
|
||||
"""
|
||||
Test comprehensive theming static files storage.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestStorageLMS, self).setUp()
|
||||
self.themes_dir = get_base_theme_dir()
|
||||
self.enabled_theme = "red-theme"
|
||||
self.system_dir = settings.REPO_ROOT / "lms"
|
||||
self.storage = ThemeStorage(location=self.themes_dir / self.enabled_theme / 'lms' / 'static')
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@ddt.data(
|
||||
(True, "images/logo.png"),
|
||||
(True, "images/favicon.ico"),
|
||||
(False, "images/spinning.gif"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_themed(self, is_themed, asset):
|
||||
"""
|
||||
Verify storage returns True on themed assets
|
||||
"""
|
||||
self.assertEqual(is_themed, self.storage.themed(asset, self.enabled_theme))
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@ddt.data(
|
||||
("images/logo.png", ),
|
||||
("images/favicon.ico", ),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_url(self, asset):
|
||||
"""
|
||||
Verify storage returns correct url depending upon the enabled theme
|
||||
"""
|
||||
with patch(
|
||||
"openedx.core.djangoapps.theming.storage.get_current_site_theme_dir",
|
||||
return_value=self.enabled_theme,
|
||||
):
|
||||
asset_url = self.storage.url(asset)
|
||||
# remove hash key from file url
|
||||
asset_url = re.sub(r"(\.\w+)(\.png|\.ico)$", r"\g<2>", asset_url)
|
||||
expected_url = self.storage.base_url + self.enabled_theme + "/" + asset
|
||||
|
||||
self.assertEqual(asset_url, expected_url)
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@ddt.data(
|
||||
("images/logo.png", ),
|
||||
("images/favicon.ico", ),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_path(self, asset):
|
||||
"""
|
||||
Verify storage returns correct file path depending upon the enabled theme
|
||||
"""
|
||||
with patch(
|
||||
"openedx.core.djangoapps.theming.storage.get_current_site_theme_dir",
|
||||
return_value=self.enabled_theme,
|
||||
):
|
||||
returned_path = self.storage.path(asset)
|
||||
expected_path = self.themes_dir / self.enabled_theme / "lms/static/" / asset
|
||||
|
||||
self.assertEqual(expected_path, returned_path)
|
||||
@@ -1,235 +0,0 @@
|
||||
"""
|
||||
Tests for comprehensive themes.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase, override_settings
|
||||
from django.contrib import staticfiles
|
||||
|
||||
from paver.easy import call_task
|
||||
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TestComprehensiveThemeLMS(TestCase):
|
||||
"""
|
||||
Test html, sass and static file overrides for comprehensive themes.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Clear static file finders cache and register cleanup methods.
|
||||
"""
|
||||
super(TestComprehensiveThemeLMS, self).setUp()
|
||||
|
||||
# Clear the internal staticfiles caches, to get test isolation.
|
||||
staticfiles.finders.get_finder.cache_clear()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Enable Comprehensive theme and compile sass files.
|
||||
"""
|
||||
# Apply Comprehensive theme and compile sass assets.
|
||||
compile_sass('lms')
|
||||
|
||||
super(TestComprehensiveThemeLMS, cls).setUpClass()
|
||||
|
||||
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
|
||||
@with_comprehensive_theme(settings.TEST_THEME.basename())
|
||||
def test_footer(self):
|
||||
"""
|
||||
Test that theme footer is used instead of default footer.
|
||||
"""
|
||||
resp = self.client.get('/')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# This string comes from header.html of test-theme
|
||||
self.assertContains(resp, "This is a footer for test-theme.")
|
||||
|
||||
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
|
||||
@with_comprehensive_theme(settings.TEST_THEME.basename())
|
||||
def test_logo_image(self):
|
||||
"""
|
||||
Test that theme logo is used instead of default logo.
|
||||
"""
|
||||
result = staticfiles.finders.find('test-theme/images/logo.png')
|
||||
self.assertEqual(result, settings.TEST_THEME / 'lms/static/images/logo.png')
|
||||
|
||||
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
|
||||
@with_comprehensive_theme(settings.TEST_THEME.basename())
|
||||
def test_css_files(self):
|
||||
"""
|
||||
Test that theme sass files are used instead of default sass files.
|
||||
"""
|
||||
result = staticfiles.finders.find('test-theme/css/lms-main.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,22 +17,3 @@ def cleanup_tempdir(the_dir):
|
||||
"""Called on process exit to remove a temp directory."""
|
||||
if os.path.exists(the_dir):
|
||||
shutil.rmtree(the_dir)
|
||||
|
||||
|
||||
def create_symlink(src, dest):
|
||||
"""
|
||||
Creates a symbolic link which will be deleted when the process ends.
|
||||
:param src: path to source
|
||||
:param dest: path to destination
|
||||
"""
|
||||
os.symlink(src, dest)
|
||||
atexit.register(delete_symlink, dest)
|
||||
|
||||
|
||||
def delete_symlink(link_path):
|
||||
"""
|
||||
Removes symbolic link for
|
||||
:param link_path:
|
||||
"""
|
||||
if os.path.exists(link_path):
|
||||
os.remove(link_path)
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
"""
|
||||
Django storage backends for Open edX.
|
||||
"""
|
||||
from django.contrib.staticfiles.storage import StaticFilesStorage
|
||||
from pipeline.storage import NonPackagingMixin
|
||||
from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
|
||||
from pipeline.storage import PipelineMixin, NonPackagingMixin
|
||||
from require.storage import OptimizedFilesMixin
|
||||
from openedx.core.djangoapps.theming.storage import ThemeStorage, ComprehensiveThemingCachedFilesMixin, \
|
||||
ThemePipelineMixin
|
||||
|
||||
|
||||
class ProductionStorage(
|
||||
OptimizedFilesMixin,
|
||||
ThemePipelineMixin,
|
||||
ComprehensiveThemingCachedFilesMixin,
|
||||
ThemeStorage,
|
||||
PipelineMixin,
|
||||
CachedFilesMixin,
|
||||
StaticFilesStorage
|
||||
):
|
||||
"""
|
||||
@@ -24,8 +21,7 @@ class ProductionStorage(
|
||||
|
||||
class DevelopmentStorage(
|
||||
NonPackagingMixin,
|
||||
ThemePipelineMixin,
|
||||
ThemeStorage,
|
||||
PipelineMixin,
|
||||
StaticFilesStorage
|
||||
):
|
||||
"""
|
||||
|
||||
@@ -22,21 +22,20 @@ from .utils.cmd import cmd, django_cmd
|
||||
|
||||
ALL_SYSTEMS = ['lms', 'studio']
|
||||
COFFEE_DIRS = ['lms', 'cms', 'common']
|
||||
|
||||
LMS = 'lms'
|
||||
CMS = 'cms'
|
||||
|
||||
SYSTEMS = {
|
||||
'lms': LMS,
|
||||
'cms': CMS,
|
||||
'studio': CMS
|
||||
}
|
||||
|
||||
# Common lookup paths that are added to the lookup paths for all sass compilations
|
||||
COMMON_LOOKUP_DIRS = [
|
||||
path("common/static"),
|
||||
# A list of directories. Each will be paired with a sibling /css directory.
|
||||
COMMON_SASS_DIRECTORIES = [
|
||||
path("common/static/sass"),
|
||||
]
|
||||
LMS_SASS_DIRECTORIES = [
|
||||
path("lms/static/sass"),
|
||||
path("lms/static/themed_sass"),
|
||||
path("lms/static/certificates/sass"),
|
||||
]
|
||||
CMS_SASS_DIRECTORIES = [
|
||||
path("cms/static/sass"),
|
||||
]
|
||||
THEME_SASS_DIRECTORIES = []
|
||||
SASS_LOAD_PATHS = ['common/static', 'common/static/sass']
|
||||
|
||||
# A list of NPM installed libraries that should be copied into the common
|
||||
# static directory.
|
||||
@@ -48,197 +47,58 @@ NPM_INSTALLED_LIBRARIES = [
|
||||
# Directory to install static vendor files
|
||||
NPM_VENDOR_DIRECTORY = path("common/static/common/js/vendor")
|
||||
|
||||
# system specific lookup path additions, add sass dirs if one system depends on the sass files for other systems
|
||||
SASS_LOOKUP_DEPENDENCIES = {
|
||||
'cms': [path('lms') / 'static' / 'sass' / 'partials', ],
|
||||
}
|
||||
|
||||
def configure_paths():
|
||||
"""Configure our paths based on settings. Called immediately."""
|
||||
edxapp_env = Env()
|
||||
if edxapp_env.feature_flags.get('USE_CUSTOM_THEME', False):
|
||||
theme_name = edxapp_env.env_tokens.get('THEME_NAME', '')
|
||||
parent_dir = path(edxapp_env.REPO_ROOT).abspath().parent
|
||||
theme_root = parent_dir / "themes" / theme_name
|
||||
COFFEE_DIRS.append(theme_root)
|
||||
sass_dir = theme_root / "static" / "sass"
|
||||
css_dir = theme_root / "static" / "css"
|
||||
if sass_dir.isdir():
|
||||
css_dir.mkdir_p()
|
||||
THEME_SASS_DIRECTORIES.append(sass_dir)
|
||||
|
||||
if edxapp_env.env_tokens.get("COMPREHENSIVE_THEME_DIR", ""):
|
||||
theme_dir = path(edxapp_env.env_tokens["COMPREHENSIVE_THEME_DIR"])
|
||||
lms_sass = theme_dir / "lms" / "static" / "sass"
|
||||
lms_css = theme_dir / "lms" / "static" / "css"
|
||||
if lms_sass.isdir():
|
||||
lms_css.mkdir_p()
|
||||
THEME_SASS_DIRECTORIES.append(lms_sass)
|
||||
cms_sass = theme_dir / "cms" / "static" / "sass"
|
||||
cms_css = theme_dir / "cms" / "static" / "css"
|
||||
if cms_sass.isdir():
|
||||
cms_css.mkdir_p()
|
||||
THEME_SASS_DIRECTORIES.append(cms_sass)
|
||||
|
||||
configure_paths()
|
||||
|
||||
|
||||
def get_sass_directories(system, theme_dir=None):
|
||||
def applicable_sass_directories(systems=None):
|
||||
"""
|
||||
Determine the set of SASS directories to be compiled for the specified list of system and theme
|
||||
and return a list of those directories.
|
||||
Determine the applicable set of SASS directories to be
|
||||
compiled for the specified list of systems.
|
||||
|
||||
Each item in the list is dict object containing the following key-value pairs.
|
||||
{
|
||||
"sass_source_dir": "", # directory where source sass files are present
|
||||
"css_destination_dir": "", # destination where css files would be placed
|
||||
"lookup_paths": [], # list of directories to be passed as lookup paths for @import resolution.
|
||||
}
|
||||
Args:
|
||||
systems: A list of systems (defaults to all)
|
||||
|
||||
if theme_dir is empty or None then return sass directories for the given system only. (i.e. lms or cms)
|
||||
|
||||
:param system: name if the system for which to compile sass e.g. 'lms', 'cms'
|
||||
:param theme_dir: absolute path of theme for which to compile sass files.
|
||||
"""
|
||||
if system not in SYSTEMS:
|
||||
raise ValueError("'system' must be one of ({allowed_values})".format(allowed_values=', '.join(SYSTEMS.keys())))
|
||||
system = SYSTEMS[system]
|
||||
|
||||
applicable_directories = list()
|
||||
|
||||
if theme_dir:
|
||||
# Add theme sass directories
|
||||
applicable_directories.extend(
|
||||
get_theme_sass_dirs(system, theme_dir)
|
||||
)
|
||||
else:
|
||||
# add system sass directories
|
||||
applicable_directories.extend(
|
||||
get_system_sass_dirs(system)
|
||||
)
|
||||
|
||||
return applicable_directories
|
||||
|
||||
|
||||
def get_common_sass_directories():
|
||||
"""
|
||||
Determine the set of common SASS directories to be compiled for all the systems and themes.
|
||||
|
||||
Each item in the returned list is dict object containing the following key-value pairs.
|
||||
{
|
||||
"sass_source_dir": "", # directory where source sass files are present
|
||||
"css_destination_dir": "", # destination where css files would be placed
|
||||
"lookup_paths": [], # list of directories to be passed as lookup paths for @import resolution.
|
||||
}
|
||||
"""
|
||||
applicable_directories = list()
|
||||
|
||||
# add common sass directories
|
||||
applicable_directories.append({
|
||||
"sass_source_dir": path("common/static/sass"),
|
||||
"css_destination_dir": path("common/static/css"),
|
||||
"lookup_paths": [
|
||||
path("common/static"),
|
||||
path("common/static/sass"),
|
||||
],
|
||||
})
|
||||
|
||||
return applicable_directories
|
||||
|
||||
|
||||
def get_theme_sass_dirs(system, theme_dir):
|
||||
"""
|
||||
Return list of sass dirs that need to be compiled for the given theme.
|
||||
|
||||
:param system: name if the system for which to compile sass e.g. 'lms', 'cms'
|
||||
:param theme_dir: absolute path of theme for which to compile sass files.
|
||||
"""
|
||||
if system not in ('lms', 'cms'):
|
||||
raise ValueError('"system" must either be "lms" or "cms"')
|
||||
|
||||
dirs = []
|
||||
|
||||
system_sass_dir = path(system) / "static" / "sass"
|
||||
sass_dir = theme_dir / system / "static" / "sass"
|
||||
css_dir = theme_dir / system / "static" / "css"
|
||||
|
||||
dependencies = SASS_LOOKUP_DEPENDENCIES.get(system, [])
|
||||
if sass_dir.isdir():
|
||||
css_dir.mkdir_p()
|
||||
|
||||
# first compile lms sass files and place css in theme dir
|
||||
dirs.append({
|
||||
"sass_source_dir": system_sass_dir,
|
||||
"css_destination_dir": css_dir,
|
||||
"lookup_paths": dependencies + [
|
||||
sass_dir / "partials",
|
||||
system_sass_dir / "partials",
|
||||
system_sass_dir,
|
||||
],
|
||||
})
|
||||
|
||||
# now compile theme sass files and override css files generated from lms
|
||||
dirs.append({
|
||||
"sass_source_dir": sass_dir,
|
||||
"css_destination_dir": css_dir,
|
||||
"lookup_paths": dependencies + [
|
||||
sass_dir / "partials",
|
||||
system_sass_dir / "partials",
|
||||
system_sass_dir,
|
||||
],
|
||||
})
|
||||
|
||||
return dirs
|
||||
|
||||
|
||||
def get_system_sass_dirs(system):
|
||||
"""
|
||||
Return list of sass dirs that need to be compiled for the given system.
|
||||
|
||||
:param system: name if the system for which to compile sass e.g. 'lms', 'cms'
|
||||
"""
|
||||
if system not in ('lms', 'cms'):
|
||||
raise ValueError('"system" must either be "lms" or "cms"')
|
||||
|
||||
dirs = []
|
||||
sass_dir = path(system) / "static" / "sass"
|
||||
css_dir = path(system) / "static" / "css"
|
||||
|
||||
dependencies = SASS_LOOKUP_DEPENDENCIES.get(system, [])
|
||||
dirs.append({
|
||||
"sass_source_dir": sass_dir,
|
||||
"css_destination_dir": css_dir,
|
||||
"lookup_paths": dependencies + [
|
||||
sass_dir / "partials",
|
||||
sass_dir,
|
||||
],
|
||||
})
|
||||
|
||||
if system == 'lms':
|
||||
dirs.append({
|
||||
"sass_source_dir": path(system) / "static" / "certificates" / "sass",
|
||||
"css_destination_dir": path(system) / "static" / "certificates" / "css",
|
||||
"lookup_paths": [
|
||||
sass_dir / "partials",
|
||||
sass_dir
|
||||
],
|
||||
})
|
||||
|
||||
return dirs
|
||||
|
||||
|
||||
def get_watcher_dirs(themes_base_dir=None, themes=None):
|
||||
"""
|
||||
Return sass directories that need to be added to sass watcher.
|
||||
|
||||
Example:
|
||||
>> get_watcher_dirs('/edx/app/edx-platform/themes', ['red-theme'])
|
||||
[
|
||||
'common/static',
|
||||
'common/static/sass',
|
||||
'lms/static/sass',
|
||||
'lms/static/sass/partials',
|
||||
'/edx/app/edxapp/edx-platform/themes/red-theme/lms/static/sass',
|
||||
'/edx/app/edxapp/edx-platform/themes/red-theme/lms/static/sass/partials',
|
||||
'cms/static/sass',
|
||||
'cms/static/sass/partials',
|
||||
'/edx/app/edxapp/edx-platform/themes/red-theme/cms/static/sass/partials',
|
||||
]
|
||||
|
||||
Parameters:
|
||||
themes_base_dir (str): base directory that contains all the themes.
|
||||
themes (list): list containing names of themes
|
||||
Returns:
|
||||
(list): dirs that need to be added to sass watchers.
|
||||
A list of SASS directories to be compiled.
|
||||
"""
|
||||
dirs = []
|
||||
dirs.extend(COMMON_LOOKUP_DIRS)
|
||||
if themes_base_dir and themes:
|
||||
# Register sass watchers for all the given themes
|
||||
theme_dirs = [(path(themes_base_dir) / theme) for theme in themes if theme]
|
||||
for theme_dir in theme_dirs:
|
||||
for _dir in get_sass_directories('lms', theme_dir) + get_sass_directories('cms', theme_dir):
|
||||
dirs.append(_dir['sass_source_dir'])
|
||||
dirs.extend(_dir['lookup_paths'])
|
||||
# Register sass watchers for lms and cms
|
||||
for _dir in get_sass_directories('lms') + get_sass_directories('cms') + get_common_sass_directories():
|
||||
dirs.append(_dir['sass_source_dir'])
|
||||
dirs.extend(_dir['lookup_paths'])
|
||||
|
||||
# remove duplicates
|
||||
dirs = list(set(dirs))
|
||||
return dirs
|
||||
if not systems:
|
||||
systems = ALL_SYSTEMS
|
||||
applicable_directories = []
|
||||
applicable_directories.extend(COMMON_SASS_DIRECTORIES)
|
||||
if "lms" in systems:
|
||||
applicable_directories.extend(LMS_SASS_DIRECTORIES)
|
||||
if "studio" in systems or "cms" in systems:
|
||||
applicable_directories.extend(CMS_SASS_DIRECTORIES)
|
||||
applicable_directories.extend(THEME_SASS_DIRECTORIES)
|
||||
return applicable_directories
|
||||
|
||||
|
||||
def debounce(seconds=1):
|
||||
@@ -298,15 +158,11 @@ class SassWatcher(PatternMatchingEventHandler):
|
||||
patterns = ['*.scss']
|
||||
ignore_patterns = ['common/static/xmodule/*']
|
||||
|
||||
def register(self, observer, directories):
|
||||
def register(self, observer):
|
||||
"""
|
||||
register files with observer
|
||||
|
||||
Arguments:
|
||||
observer (watchdog.observers.Observer): sass file observer
|
||||
directories (list): list of directories to be register for sass watcher.
|
||||
"""
|
||||
for dirname in directories:
|
||||
for dirname in SASS_LOAD_PATHS + applicable_sass_directories():
|
||||
paths = []
|
||||
if '*' in dirname:
|
||||
paths.extend(glob.glob(dirname))
|
||||
@@ -390,125 +246,12 @@ def compile_coffeescript(*files):
|
||||
@no_help
|
||||
@cmdopts([
|
||||
('system=', 's', 'The system to compile sass for (defaults to all)'),
|
||||
('themes_dir=', '-td', 'The themes dir containing all themes (defaults to None)'),
|
||||
('themes=', '-t', 'The theme to compile sass for (defaults to None)'),
|
||||
('debug', 'd', 'Debug mode'),
|
||||
('force', '', 'Force full compilation'),
|
||||
])
|
||||
def compile_sass(options):
|
||||
"""
|
||||
Compile Sass to CSS. If command is called without any arguments, it will
|
||||
only compile lms, cms sass for the open source theme. And none of the comprehensive theme's sass would be compiled.
|
||||
|
||||
If you want to compile sass for all comprehensive themes you will have to run compile_sass
|
||||
specifying all the themes that need to be compiled..
|
||||
|
||||
The following is a list of some possible ways to use this command.
|
||||
|
||||
Command:
|
||||
paver compile_sass
|
||||
Description:
|
||||
compile sass files for both lms and cms. If command is called like above (i.e. without any arguments) it will
|
||||
only compile lms, cms sass for the open source theme. None of the theme's sass will be compiled.
|
||||
|
||||
Command:
|
||||
paver compile_sass --themes_dir=/edx/app/edxapp/edx-platform/themes --themes=red-theme
|
||||
Description:
|
||||
compile sass files for both lms and cms for 'red-theme' present in '/edx/app/edxapp/edx-platform/themes'
|
||||
|
||||
Command:
|
||||
paver compile_sass --themes_dir=/edx/app/edxapp/edx-platform/themes --themes=red-theme,stanford-style
|
||||
Description:
|
||||
compile sass files for both lms and cms for 'red-theme' and 'stanford-style' present in
|
||||
'/edx/app/edxapp/edx-platform/themes'.
|
||||
|
||||
Command:
|
||||
paver compile_sass --system=cms --themes_dir=/edx/app/edxapp/edx-platform/themes
|
||||
--themes=red-theme,stanford-style
|
||||
Description:
|
||||
compile sass files for cms only for 'red-theme' and 'stanford-style' present in
|
||||
'/edx/app/edxapp/edx-platform/themes'.
|
||||
|
||||
"""
|
||||
debug = options.get('debug')
|
||||
force = options.get('force')
|
||||
systems = getattr(options, 'system', ALL_SYSTEMS)
|
||||
themes = getattr(options, 'themes', None)
|
||||
themes_dir = getattr(options, 'themes_dir', None)
|
||||
|
||||
if not themes_dir and themes:
|
||||
# We can not compile a theme sass without knowing the directory that contains the theme.
|
||||
raise ValueError('themes_dir must be provided for compiling theme sass.')
|
||||
else:
|
||||
theme_base_dir = path(themes_dir)
|
||||
|
||||
if isinstance(systems, basestring):
|
||||
systems = systems.split(',')
|
||||
else:
|
||||
systems = systems if isinstance(systems, list) else [systems]
|
||||
if isinstance(themes, basestring):
|
||||
themes = themes.split(',')
|
||||
else:
|
||||
themes = themes if isinstance(themes, list) else [themes]
|
||||
|
||||
# Compile sass for OpenEdx theme after comprehensive themes
|
||||
if None not in themes:
|
||||
themes.append(None)
|
||||
|
||||
timing_info = []
|
||||
dry_run = tasks.environment.dry_run
|
||||
compilation_results = {'success': [], 'failure': []}
|
||||
|
||||
print("\t\tStarted compiling Sass:")
|
||||
|
||||
# compile common sass files
|
||||
is_successful = _compile_sass('common', None, debug, force, timing_info)
|
||||
if is_successful:
|
||||
print("Finished compiling 'common' sass.")
|
||||
compilation_results['success' if is_successful else 'failure'].append('"common" sass files.')
|
||||
|
||||
for system in systems:
|
||||
for theme in themes:
|
||||
print("Started compiling '{system}' Sass for '{theme}'.".format(system=system, theme=theme or 'system'))
|
||||
|
||||
# Compile sass files
|
||||
is_successful = _compile_sass(
|
||||
system=system,
|
||||
theme=theme_base_dir / theme if theme_base_dir and theme else None,
|
||||
debug=debug,
|
||||
force=force,
|
||||
timing_info=timing_info
|
||||
)
|
||||
|
||||
if is_successful:
|
||||
print("Finished compiling '{system}' Sass for '{theme}'.".format(
|
||||
system=system, theme=theme or 'system'
|
||||
))
|
||||
|
||||
compilation_results['success' if is_successful else 'failure'].append('{system} sass for {theme}.'.format(
|
||||
system=system, theme=theme or 'system',
|
||||
))
|
||||
|
||||
print("\t\tFinished compiling Sass:")
|
||||
if not dry_run:
|
||||
for sass_dir, css_dir, duration in timing_info:
|
||||
print(">> {} -> {} in {}s".format(sass_dir, css_dir, duration))
|
||||
|
||||
if compilation_results['success']:
|
||||
print("\033[92m\n\nSuccessful compilations:\n--- " + "\n--- ".join(compilation_results['success']) + "\033[00m")
|
||||
if compilation_results['failure']:
|
||||
print("\033[91m\n\nFailed compilations:\n--- " + "\n--- ".join(compilation_results['failure']) + "\033[00m")
|
||||
|
||||
|
||||
def _compile_sass(system, theme, debug, force, timing_info):
|
||||
"""
|
||||
Compile sass files for the given system and theme.
|
||||
|
||||
:param system: system to compile sass for e.g. 'lms', 'cms', 'common'
|
||||
:param theme: absolute path of the theme to compile sass for.
|
||||
:param debug: boolean showing whether to display source comments in resulted css
|
||||
:param force: boolean showing whether to remove existing css files before generating new files
|
||||
:param timing_info: list variable to keep track of timing for sass compilation
|
||||
Compile Sass to CSS.
|
||||
"""
|
||||
|
||||
# Note: import sass only when it is needed and not at the top of the file.
|
||||
@@ -516,14 +259,12 @@ def _compile_sass(system, theme, debug, force, timing_info):
|
||||
# installed. In particular, this allows the install_prereqs command to be
|
||||
# used to install the dependency.
|
||||
import sass
|
||||
if system == "common":
|
||||
sass_dirs = get_common_sass_directories()
|
||||
else:
|
||||
sass_dirs = get_sass_directories(system, theme)
|
||||
|
||||
dry_run = tasks.environment.dry_run
|
||||
|
||||
# determine css out put style and source comments enabling
|
||||
debug = options.get('debug')
|
||||
force = options.get('force')
|
||||
systems = getattr(options, 'system', ALL_SYSTEMS)
|
||||
if isinstance(systems, basestring):
|
||||
systems = systems.split(',')
|
||||
if debug:
|
||||
source_comments = True
|
||||
output_style = 'nested'
|
||||
@@ -531,18 +272,13 @@ def _compile_sass(system, theme, debug, force, timing_info):
|
||||
source_comments = False
|
||||
output_style = 'compressed'
|
||||
|
||||
for dirs in sass_dirs:
|
||||
timing_info = []
|
||||
system_sass_directories = applicable_sass_directories(systems)
|
||||
all_sass_directories = applicable_sass_directories()
|
||||
dry_run = tasks.environment.dry_run
|
||||
for sass_dir in system_sass_directories:
|
||||
start = datetime.now()
|
||||
css_dir = dirs['css_destination_dir']
|
||||
sass_source_dir = dirs['sass_source_dir']
|
||||
lookup_paths = dirs['lookup_paths']
|
||||
|
||||
if not sass_source_dir.isdir():
|
||||
print("\033[91m Sass dir '{dir}' does not exists, skipping sass compilation for '{theme}' \033[00m".format(
|
||||
dir=sass_dirs, theme=theme or system,
|
||||
))
|
||||
# theme doesn't override sass directory, so skip it
|
||||
continue
|
||||
css_dir = sass_dir.parent / "css"
|
||||
|
||||
if force:
|
||||
if dry_run:
|
||||
@@ -554,18 +290,22 @@ def _compile_sass(system, theme, debug, force, timing_info):
|
||||
|
||||
if dry_run:
|
||||
tasks.environment.info("libsass {sass_dir}".format(
|
||||
sass_dir=sass_source_dir,
|
||||
sass_dir=sass_dir,
|
||||
))
|
||||
else:
|
||||
sass.compile(
|
||||
dirname=(sass_source_dir, css_dir),
|
||||
include_paths=COMMON_LOOKUP_DIRS + lookup_paths,
|
||||
dirname=(sass_dir, css_dir),
|
||||
include_paths=SASS_LOAD_PATHS + all_sass_directories,
|
||||
source_comments=source_comments,
|
||||
output_style=output_style,
|
||||
)
|
||||
duration = datetime.now() - start
|
||||
timing_info.append((sass_source_dir, css_dir, duration))
|
||||
return True
|
||||
timing_info.append((sass_dir, css_dir, duration))
|
||||
|
||||
print("\t\tFinished compiling Sass:")
|
||||
if not dry_run:
|
||||
for sass_dir, css_dir, duration in timing_info:
|
||||
print(">> {} -> {} in {}s".format(sass_dir, css_dir, duration))
|
||||
|
||||
|
||||
def compile_templated_sass(systems, settings):
|
||||
@@ -637,11 +377,7 @@ def collect_assets(systems, settings):
|
||||
|
||||
|
||||
@task
|
||||
@cmdopts([
|
||||
('background', 'b', 'Background mode'),
|
||||
('themes_dir=', '-td', 'The themes dir containing all themes (defaults to None)'),
|
||||
('themes=', '-t', 'The themes to add sass watchers for (defaults to None)'),
|
||||
])
|
||||
@cmdopts([('background', 'b', 'Background mode')])
|
||||
def watch_assets(options):
|
||||
"""
|
||||
Watch for changes to asset files, and regenerate js/css
|
||||
@@ -650,25 +386,11 @@ def watch_assets(options):
|
||||
if tasks.environment.dry_run:
|
||||
return
|
||||
|
||||
themes = getattr(options, 'themes', None)
|
||||
themes_dir = getattr(options, 'themes_dir', None)
|
||||
if not themes_dir and themes:
|
||||
# We can not add theme sass watchers without knowing the directory that contains the themes.
|
||||
raise ValueError('themes_dir must be provided for compiling theme sass.')
|
||||
else:
|
||||
theme_base_dir = path(themes_dir)
|
||||
|
||||
if isinstance(themes, basestring):
|
||||
themes = themes.split(',')
|
||||
else:
|
||||
themes = themes if isinstance(themes, list) else [themes]
|
||||
|
||||
sass_directories = get_watcher_dirs(theme_base_dir, themes)
|
||||
observer = PollingObserver()
|
||||
|
||||
CoffeeScriptWatcher().register(observer)
|
||||
SassWatcher().register(observer, sass_directories)
|
||||
XModuleSassWatcher().register(observer, ['common/lib/xmodule/'])
|
||||
SassWatcher().register(observer)
|
||||
XModuleSassWatcher().register(observer)
|
||||
XModuleAssetsWatcher().register(observer)
|
||||
|
||||
print("Starting asset watcher...")
|
||||
@@ -714,31 +436,16 @@ def update_assets(args):
|
||||
'--watch', action='store_true', default=False,
|
||||
help="Watch files for changes",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--themes_dir', type=str, default=None,
|
||||
help="base directory where themes are placed",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--themes', type=str, nargs='*', default=None,
|
||||
help="list of themes to compile sass for",
|
||||
)
|
||||
args = parser.parse_args(args)
|
||||
|
||||
compile_templated_sass(args.system, args.settings)
|
||||
process_xmodule_assets()
|
||||
process_npm_assets()
|
||||
compile_coffeescript()
|
||||
|
||||
call_task(
|
||||
'pavelib.assets.compile_sass',
|
||||
options={'system': args.system, 'debug': args.debug, 'themes_dir': args.themes_dir, 'themes': args.themes},
|
||||
)
|
||||
call_task('pavelib.assets.compile_sass', options={'system': args.system, 'debug': args.debug})
|
||||
|
||||
if args.collect:
|
||||
collect_assets(args.system, args.settings)
|
||||
|
||||
if args.watch:
|
||||
call_task(
|
||||
'pavelib.assets.watch_assets',
|
||||
options={'background': not args.debug, 'themes_dir': args.themes_dir, 'themes': args.themes},
|
||||
)
|
||||
call_task('pavelib.assets.watch_assets', options={'background': not args.debug})
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
"""Unit tests for the Paver asset tasks."""
|
||||
|
||||
import ddt
|
||||
import os
|
||||
from unittest import TestCase
|
||||
from paver.easy import call_task
|
||||
from paver.easy import path
|
||||
from mock import patch
|
||||
from unittest import TestCase
|
||||
from watchdog.observers.polling import PollingObserver
|
||||
from .utils import PaverTestCase
|
||||
|
||||
ROOT_PATH = path(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||
TEST_THEME = ROOT_PATH / "common/test/test-theme" # pylint: disable=invalid-name
|
||||
from .utils import PaverTestCase
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -48,11 +44,13 @@ class TestPaverAssetTasks(PaverTestCase):
|
||||
if force:
|
||||
expected_messages.append("rm -rf common/static/css/*.css")
|
||||
expected_messages.append("libsass common/static/sass")
|
||||
|
||||
if "lms" in system:
|
||||
if force:
|
||||
expected_messages.append("rm -rf lms/static/css/*.css")
|
||||
expected_messages.append("libsass lms/static/sass")
|
||||
if force:
|
||||
expected_messages.append("rm -rf lms/static/css/*.css")
|
||||
expected_messages.append("libsass lms/static/themed_sass")
|
||||
if force:
|
||||
expected_messages.append("rm -rf lms/static/certificates/css/*.css")
|
||||
expected_messages.append("libsass lms/static/certificates/sass")
|
||||
@@ -60,82 +58,6 @@ class TestPaverAssetTasks(PaverTestCase):
|
||||
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("libsass lms/static/sass")
|
||||
if force:
|
||||
expected_messages.append("rm -rf lms/static/certificates/css/*.css")
|
||||
expected_messages.append("libsass lms/static/certificates/sass")
|
||||
|
||||
if "studio" in system:
|
||||
expected_messages.append("mkdir_p " + repr(TEST_THEME / "cms/static/css"))
|
||||
if force:
|
||||
expected_messages.append("rm -rf " + str(TEST_THEME) + "/cms/static/css/*.css")
|
||||
expected_messages.append("libsass cms/static/sass")
|
||||
if force:
|
||||
expected_messages.append("rm -rf " + str(TEST_THEME) + "/cms/static/css/*.css")
|
||||
expected_messages.append("libsass " + str(TEST_THEME) + "/cms/static/sass")
|
||||
|
||||
if force:
|
||||
expected_messages.append("rm -rf cms/static/css/*.css")
|
||||
expected_messages.append("libsass cms/static/sass")
|
||||
|
||||
self.assertEquals(self.task_messages, expected_messages)
|
||||
|
||||
|
||||
@@ -145,17 +67,12 @@ class TestPaverWatchAssetTasks(TestCase):
|
||||
"""
|
||||
|
||||
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 tearDown(self):
|
||||
self.expected_sass_directories = []
|
||||
super(TestPaverWatchAssetTasks, self).tearDown()
|
||||
|
||||
def test_watch_assets(self):
|
||||
"""
|
||||
Test the "compile_sass" task.
|
||||
@@ -169,36 +86,4 @@ class TestPaverWatchAssetTasks(TestCase):
|
||||
self.assertEqual(mock_register.call_count, 2)
|
||||
|
||||
sass_watcher_args = mock_register.call_args_list[0][0]
|
||||
|
||||
self.assertIsInstance(sass_watcher_args[0], PollingObserver)
|
||||
self.assertIsInstance(sass_watcher_args[1], list)
|
||||
self.assertItemsEqual(sass_watcher_args[1], self.expected_sass_directories)
|
||||
|
||||
def test_watch_theme_assets(self):
|
||||
"""
|
||||
Test the Paver watch asset tasks with theming enabled.
|
||||
"""
|
||||
self.expected_sass_directories.extend([
|
||||
path(TEST_THEME) / 'lms/static/sass',
|
||||
path(TEST_THEME) / 'lms/static/sass/partials',
|
||||
path(TEST_THEME) / 'cms/static/sass',
|
||||
path(TEST_THEME) / 'cms/static/sass/partials',
|
||||
])
|
||||
|
||||
with patch('pavelib.assets.SassWatcher.register') as mock_register:
|
||||
with patch('pavelib.assets.PollingObserver.start'):
|
||||
call_task(
|
||||
'pavelib.assets.watch_assets',
|
||||
options={"background": True, "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], PollingObserver)
|
||||
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()
|
||||
|
||||
@@ -17,6 +17,7 @@ EXPECTED_COMMON_SASS_DIRECTORIES = [
|
||||
]
|
||||
EXPECTED_LMS_SASS_DIRECTORIES = [
|
||||
u"lms/static/sass",
|
||||
u"lms/static/themed_sass",
|
||||
u"lms/static/certificates/sass",
|
||||
]
|
||||
EXPECTED_CMS_SASS_DIRECTORIES = [
|
||||
|
||||
@@ -176,6 +176,3 @@ jsonfield==1.0.3
|
||||
|
||||
# Inlines CSS styles into HTML for email notifications.
|
||||
pynliner==0.5.2
|
||||
|
||||
# django current site middleware with default site
|
||||
edx-django-sites-extensions==1.0.0
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
# Third-party:
|
||||
git+https://github.com/cyberdelia/django-pipeline.git@1.5.3#egg=django-pipeline==1.5.3
|
||||
git+https://github.com/edx/django-wiki.git@v0.0.7#egg=django-wiki==0.0.7
|
||||
git+https://github.com/edx/django-wiki.git@v0.0.5#egg=django-wiki==0.0.5
|
||||
git+https://github.com/edx/django-openid-auth.git@0.8#egg=django-openid-auth==0.8
|
||||
git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7fed0#egg=MongoDBProxy==0.1.0
|
||||
git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<%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>
|
||||
7
themes/red-theme/lms/static/sass/_overrides.scss
Executable file
7
themes/red-theme/lms/static/sass/_overrides.scss
Executable file
@@ -0,0 +1,7 @@
|
||||
// Theming overrides for sample theme
|
||||
$header-bg: rgb(250,0,0);
|
||||
$footer-bg: rgb(250,0,0);
|
||||
$container-bg: rgb(250,0,0);
|
||||
$content-wrapper-bg: rgb(250,0,0);
|
||||
$serif: 'Comic Sans', 'Comic Sans MS';
|
||||
$sans-serif: 'Comic Sans', 'Comic Sans MS';
|
||||
5
themes/red-theme/lms/static/sass/lms-main-rtl.scss
Executable file
5
themes/red-theme/lms/static/sass/lms-main-rtl.scss
Executable file
@@ -0,0 +1,5 @@
|
||||
// Theming overrides for sample theme
|
||||
@import 'overrides';
|
||||
|
||||
// import the rest of the application
|
||||
@import 'lms/static/sass/lms-main-rtl';
|
||||
5
themes/red-theme/lms/static/sass/lms-main.scss
Executable file
5
themes/red-theme/lms/static/sass/lms-main.scss
Executable file
@@ -0,0 +1,5 @@
|
||||
// Theming overrides for sample theme
|
||||
@import 'overrides';
|
||||
|
||||
// import the rest of the application
|
||||
@import 'lms/static/sass/lms-main';
|
||||
@@ -1,5 +0,0 @@
|
||||
@import 'lms/static/sass/partials/base/variables';
|
||||
|
||||
$header-bg: rgb(250,0,0);
|
||||
$footer-bg: rgb(250,0,0);
|
||||
$container-bg: rgb(250,0,0);
|
||||
@@ -1,5 +1,3 @@
|
||||
@import 'lms/static/sass/partials/base/variables';
|
||||
|
||||
// Theming overrides for sample theme
|
||||
$header-bg: rgb(140,21,21);
|
||||
$footer-bg: rgb(140,21,21);
|
||||
5
themes/stanford-style/lms/static/sass/lms-main-rtl.scss
Executable file
5
themes/stanford-style/lms/static/sass/lms-main-rtl.scss
Executable file
@@ -0,0 +1,5 @@
|
||||
// Theming overrides for sample theme
|
||||
@import 'overrides';
|
||||
|
||||
// import the rest of the application
|
||||
@import 'lms/static/sass/lms-main-rtl';
|
||||
5
themes/stanford-style/lms/static/sass/lms-main.scss
Executable file
5
themes/stanford-style/lms/static/sass/lms-main.scss
Executable file
@@ -0,0 +1,5 @@
|
||||
// Theming overrides for sample theme
|
||||
@import 'overrides';
|
||||
|
||||
// import the rest of the application
|
||||
@import 'lms/static/sass/lms-main';
|
||||
Reference in New Issue
Block a user