Commit 5bb0e908 by Renzo Lucioni

Merge pull request #12156 from edx/renzo/merge-release-into-master

Manually merge release into master
parents a804d0e9 60fab86f
......@@ -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,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
# List of finder classes that know how to find static files in various locations.
# Note: the pipeline finder is included to be able to discover optimized files
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder',
......
......@@ -41,7 +41,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
# Revert to the default set of finders as we don't want the production pipeline
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'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()
......
......@@ -4,7 +4,7 @@ that gets used when sending cachability headers back with request course assets.
"""
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin
from .models import CourseAssetCacheTtlConfig, CdnUserAgentsConfig
from .models import CourseAssetCacheTtlConfig
class CourseAssetCacheTtlConfigAdmin(ConfigurationModelAdmin):
......@@ -26,24 +26,4 @@ class CourseAssetCacheTtlConfigAdmin(ConfigurationModelAdmin):
return self.list_display
class CdnUserAgentsConfigAdmin(ConfigurationModelAdmin):
"""
Basic configuration for CDN user agent whitelist.
"""
list_display = [
'cdn_user_agents'
]
def get_list_display(self, request):
"""
Restore default list_display behavior.
ConfigurationModelAdmin overrides this, but in a way that doesn't
respect the ordering. This lets us customize it the usual Django admin
way.
"""
return self.list_display
admin.site.register(CourseAssetCacheTtlConfig, CourseAssetCacheTtlConfigAdmin)
admin.site.register(CdnUserAgentsConfig, CdnUserAgentsConfigAdmin)
......@@ -3,14 +3,13 @@ Middleware to serve assets.
"""
import logging
import datetime
import newrelic.agent
import datetime
from django.http import (
HttpResponse, HttpResponseNotModified, HttpResponseForbidden,
HttpResponseBadRequest, HttpResponseNotFound)
from student.models import CourseEnrollment
from contentserver.models import CourseAssetCacheTtlConfig, CdnUserAgentsConfig
from contentserver.models import CourseAssetCacheTtlConfig
from header_control import force_header_for_response
from xmodule.assetstore.assetmgr import AssetManager
......@@ -56,19 +55,6 @@ class StaticContentServer(object):
except (ItemNotFoundError, NotFoundError):
return HttpResponseNotFound()
# Set the basics for this request.
newrelic.agent.add_custom_parameter('course_id', loc.course_key)
newrelic.agent.add_custom_parameter('org', loc.org)
newrelic.agent.add_custom_parameter('contentserver.path', loc.path)
# Figure out if this is a CDN using us as the origin.
is_from_cdn = StaticContentServer.is_cdn_request(request)
newrelic.agent.add_custom_parameter('contentserver.from_cdn', True if is_from_cdn else False)
# Check if this content is locked or not.
locked = self.is_content_locked(content)
newrelic.agent.add_custom_parameter('contentserver.locked', True if locked else False)
# Check that user has access to the content.
if not self.is_user_authorized(request, content, loc):
return HttpResponseForbidden('Unauthorized')
......@@ -121,11 +107,8 @@ class StaticContentServer(object):
response['Content-Range'] = 'bytes {first}-{last}/{length}'.format(
first=first, last=last, length=content.length
)
range_len = last - first + 1
response['Content-Length'] = str(range_len)
response['Content-Length'] = str(last - first + 1)
response.status_code = 206 # Partial Content
newrelic.agent.add_custom_parameter('contentserver.range_len', range_len)
else:
log.warning(
u"Cannot satisfy ranges in Range header: %s for content: %s", header_value, unicode(loc)
......@@ -137,9 +120,6 @@ class StaticContentServer(object):
response = HttpResponse(content.stream_data())
response['Content-Length'] = content.length
newrelic.agent.add_custom_parameter('contentserver.content_len', content.length)
newrelic.agent.add_custom_parameter('contentserver.content_type', content.content_type)
# "Accept-Ranges: bytes" tells the user that only "bytes" ranges are allowed
response['Accept-Ranges'] = 'bytes'
response['Content-Type'] = content.content_type
......@@ -166,11 +146,9 @@ class StaticContentServer(object):
# indicate there should be no caching whatsoever.
cache_ttl = CourseAssetCacheTtlConfig.get_cache_ttl()
if cache_ttl > 0 and not is_locked:
newrelic.agent.add_custom_parameter('contentserver.cacheable', True)
response['Expires'] = StaticContentServer.get_expiration_value(datetime.datetime.utcnow(), cache_ttl)
response['Cache-Control'] = "public, max-age={ttl}, s-maxage={ttl}".format(ttl=cache_ttl)
elif is_locked:
newrelic.agent.add_custom_parameter('contentserver.cacheable', False)
response['Cache-Control'] = "private, no-cache, no-store"
response['Last-Modified'] = content.last_modified_at.strftime(HTTP_DATE_FORMAT)
......@@ -181,38 +159,18 @@ class StaticContentServer(object):
force_header_for_response(response, 'Vary', 'Origin')
@staticmethod
def is_cdn_request(request):
"""
Attempts to determine whether or not the given request is coming from a CDN.
Currently, this is a static check because edx.org only uses CloudFront, but may
be expanded in the future.
"""
cdn_user_agents = CdnUserAgentsConfig.get_cdn_user_agents()
user_agent = request.META.get('HTTP_USER_AGENT', '')
if user_agent in cdn_user_agents:
# This is a CDN request.
return True
return False
@staticmethod
def get_expiration_value(now, cache_ttl):
"""Generates an RFC1123 datetime string based on a future offset."""
expire_dt = now + datetime.timedelta(seconds=cache_ttl)
return expire_dt.strftime(HTTP_DATE_FORMAT)
def is_content_locked(self, content):
"""
Determines whether or not the given content is locked.
"""
return getattr(content, "locked", False)
def is_user_authorized(self, request, content, location):
"""
Determines whether or not the user for this request is authorized to view the given asset.
"""
if not self.is_content_locked(content):
is_locked = getattr(content, "locked", False)
if not is_locked:
return True
if not hasattr(request, "user") or not request.user.is_authenticated():
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contentserver', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CdnUserAgentsConfig',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('cdn_user_agents', models.TextField(default=b'Amazon CloudFront', help_text=b'A newline-separated list of user agents that should be considered CDNs.')),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
),
]
......@@ -2,7 +2,7 @@
Models for contentserver
"""
from django.db.models.fields import PositiveIntegerField, TextField
from django.db.models.fields import PositiveIntegerField
from config_models.models import ConfigurationModel
......@@ -27,26 +27,3 @@ class CourseAssetCacheTtlConfig(ConfigurationModel):
def __unicode__(self):
return unicode(repr(self))
class CdnUserAgentsConfig(ConfigurationModel):
"""Configuration for the user agents we expect to see from CDNs."""
class Meta(object):
app_label = 'contentserver'
cdn_user_agents = TextField(
default='Amazon CloudFront',
help_text="A newline-separated list of user agents that should be considered CDNs."
)
@classmethod
def get_cdn_user_agents(cls):
"""Gets the list of CDN user agents, if present"""
return cls.current().cdn_user_agents
def __repr__(self):
return '<WhitelistedCdnConfig(cdn_user_agents={})>'.format(self.get_cdn_user_agents())
def __unicode__(self):
return unicode(repr(self))
......@@ -10,7 +10,6 @@ import unittest
from uuid import uuid4
from django.conf import settings
from django.test import RequestFactory
from django.test.client import Client
from django.test.utils import override_settings
from mock import patch
......@@ -271,49 +270,6 @@ class ContentStoreToyCourseTest(SharedModuleStoreTestCase):
near_expire_dt = StaticContentServer.get_expiration_value(start_dt, 55)
self.assertEqual("Thu, 01 Dec 1983 20:00:55 GMT", near_expire_dt)
@patch('contentserver.models.CdnUserAgentsConfig.get_cdn_user_agents')
def test_cache_is_cdn_with_normal_request(self, mock_get_cdn_user_agents):
"""
Tests that when a normal request is made -- i.e. from an end user with their
browser -- that we don't classify the request as coming from a CDN.
"""
mock_get_cdn_user_agents.return_value = 'Amazon CloudFront'
request_factory = RequestFactory()
browser_request = request_factory.get('/fake', HTTP_USER_AGENT='Chrome 1234')
is_from_cdn = StaticContentServer.is_cdn_request(browser_request)
self.assertEqual(is_from_cdn, False)
@patch('contentserver.models.CdnUserAgentsConfig.get_cdn_user_agents')
def test_cache_is_cdn_with_cdn_request(self, mock_get_cdn_user_agents):
"""
Tests that when a CDN request is made -- i.e. from an edge node back to the
origin -- that we classify the request as coming from a CDN.
"""
mock_get_cdn_user_agents.return_value = 'Amazon CloudFront'
request_factory = RequestFactory()
browser_request = request_factory.get('/fake', HTTP_USER_AGENT='Amazon CloudFront')
is_from_cdn = StaticContentServer.is_cdn_request(browser_request)
self.assertEqual(is_from_cdn, True)
@patch('contentserver.models.CdnUserAgentsConfig.get_cdn_user_agents')
def test_cache_is_cdn_with_cdn_request_multiple_user_agents(self, mock_get_cdn_user_agents):
"""
Tests that when a CDN request is made -- i.e. from an edge node back to the
origin -- that we classify the request as coming from a CDN when multiple UAs
are configured.
"""
mock_get_cdn_user_agents.return_value = 'Amazon CloudFront\nAkamai GHost'
request_factory = RequestFactory()
browser_request = request_factory.get('/fake', HTTP_USER_AGENT='Amazon CloudFront')
is_from_cdn = StaticContentServer.is_cdn_request(browser_request)
self.assertEqual(is_from_cdn, True)
@ddt.ddt
class ParseRangeHeaderTestCase(unittest.TestCase):
......
......@@ -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
%>
......
......@@ -5,6 +5,7 @@ from unittest import skipUnless
from django.conf import settings
from django.test import TestCase
from paver.easy import call_task
from pipeline_mako import render_require_js_path_overrides, compressed_css, compressed_js
......@@ -42,6 +43,14 @@ class RequireJSPathOverridesTest(TestCase):
class PipelineRenderTest(TestCase):
"""Test individual pipeline rendering functions. """
@classmethod
def setUpClass(cls):
"""
Create static assets once for all pipeline render tests.
"""
super(PipelineRenderTest, cls).setUpClass()
call_task('pavelib.assets.update_assets', args=('lms', '--settings=test'))
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
@ddt.data(
(True,),
......
......@@ -21,7 +21,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):
......@@ -99,7 +99,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"))
......
......@@ -247,87 +247,6 @@ describe 'Problem', ->
runs ->
expect(@problem.checkButtonLabel.text).toHaveBeenCalledWith 'Check'
describe 'check button on problems', ->
beforeEach ->
@problem = new Problem($('.xblock-student_view'))
@checkDisabled = (v) -> expect(@problem.checkButton.hasClass('is-disabled')).toBe(v)
describe 'some basic tests for check button', ->
it 'should become enabled after a value is entered into the text box', ->
$('#input_example_1').val('test').trigger('input')
@checkDisabled false
$('#input_example_1').val('').trigger('input')
@checkDisabled true
describe 'some advanced tests for check button', ->
it 'should become enabled after a checkbox is checked', ->
html = '''
<div class="choicegroup">
<label for="input_1_1_1"><input type="checkbox" name="input_1_1" id="input_1_1_1" value="1"> One</label>
<label for="input_1_1_2"><input type="checkbox" name="input_1_1" id="input_1_1_2" value="2"> Two</label>
<label for="input_1_1_3"><input type="checkbox" name="input_1_1" id="input_1_1_3" value="3"> Three</label>
</div>
'''
$('#input_example_1').replaceWith(html)
@problem.checkAnswersAndCheckButton true
@checkDisabled true
$('#input_1_1_1').attr('checked', true).trigger('click')
@checkDisabled false
$('#input_1_1_1').attr('checked', false).trigger('click')
@checkDisabled true
it 'should become enabled after a radiobutton is checked', ->
html = '''
<div class="choicegroup">
<label for="input_1_1_1"><input type="radio" name="input_1_1" id="input_1_1_1" value="1"> One</label>
<label for="input_1_1_2"><input type="radio" name="input_1_1" id="input_1_1_2" value="2"> Two</label>
<label for="input_1_1_3"><input type="radio" name="input_1_1" id="input_1_1_3" value="3"> Three</label>
</div>
'''
$('#input_example_1').replaceWith(html)
@problem.checkAnswersAndCheckButton true
@checkDisabled true
$('#input_1_1_1').attr('checked', true).trigger('click')
@checkDisabled false
$('#input_1_1_1').attr('checked', false).trigger('click')
@checkDisabled true
it 'should become enabled after a value is selected in a selector', ->
html = '''
<div id="problem_sel">
<select>
<option value="val0"></option>
<option value="val1">1</option>
<option value="val2">2</option>
</select>
</div>
'''
$('#input_example_1').replaceWith(html)
@problem.checkAnswersAndCheckButton true
@checkDisabled true
$("#problem_sel select").val("val2").trigger('change')
@checkDisabled false
$("#problem_sel select").val("val0").trigger('change')
@checkDisabled true
it 'should become enabled after a radiobutton is checked and a value is entered into the text box', ->
html = '''
<div class="choicegroup">
<label for="input_1_1_1"><input type="radio" name="input_1_1" id="input_1_1_1" value="1"> One</label>
<label for="input_1_1_2"><input type="radio" name="input_1_1" id="input_1_1_2" value="2"> Two</label>
<label for="input_1_1_3"><input type="radio" name="input_1_1" id="input_1_1_3" value="3"> Three</label>
</div>
'''
$(html).insertAfter('#input_example_1')
@problem.checkAnswersAndCheckButton true
@checkDisabled true
$('#input_1_1_1').attr('checked', true).trigger('click')
@checkDisabled true
$('#input_example_1').val('111').trigger('input')
@checkDisabled false
$('#input_1_1_1').attr('checked', false).trigger('click')
@checkDisabled true
describe 'reset', ->
beforeEach ->
@problem = new Problem($('.xblock-student_view'))
......
......@@ -49,8 +49,6 @@ class @Problem
window.globalTooltipManager.hide()
@bindResetCorrectness()
if @checkButton.length
@checkAnswersAndCheckButton true
# Collapsibles
Collapsible.setCollapsibles(@el)
......@@ -454,58 +452,6 @@ class @Problem
element.CodeMirror.save() if element.CodeMirror.save
@answers = @inputs.serialize()
checkAnswersAndCheckButton: (bind=false) =>
# Used to check available answers and if something is checked (or the answer is set in some textbox)
# "Check"/"Final check" button becomes enabled. Otherwise it is disabled by default.
# params:
# 'bind' used on the first check to attach event handlers to input fields
# to change "Check"/"Final check" enable status in case of some manipulations with answers
answered = true
at_least_one_text_input_found = false
one_text_input_filled = false
@el.find("input:text").each (i, text_field) =>
at_least_one_text_input_found = true
if $(text_field).is(':visible')
if $(text_field).val() isnt ''
one_text_input_filled = true
if bind
$(text_field).on 'input', (e) =>
@checkAnswersAndCheckButton()
return
return
if at_least_one_text_input_found and not one_text_input_filled
answered = false
@el.find(".choicegroup").each (i, choicegroup_block) =>
checked = false
$(choicegroup_block).find("input[type=checkbox], input[type=radio]").each (j, checkbox_or_radio) =>
if $(checkbox_or_radio).is(':checked')
checked = true
if bind
$(checkbox_or_radio).on 'click', (e) =>
@checkAnswersAndCheckButton()
return
return
if not checked
answered = false
return
@el.find("select").each (i, select_field) =>
selected_option = $(select_field).find("option:selected").text().trim()
if selected_option is ''
answered = false
if bind
$(select_field).on 'change', (e) =>
@checkAnswersAndCheckButton()
return
return
if answered
@enableCheckButton true
else
@enableCheckButton false, false
bindResetCorrectness: ->
# Loop through all input types
# Bind the reset functions at that scope.
......
......@@ -6,7 +6,6 @@ See also lettuce tests in lms/djangoapps/courseware/features/problems.feature
import random
import textwrap
from nose import SkipTest
from abc import ABCMeta, abstractmethod
from nose.plugins.attrib import attr
from selenium.webdriver import ActionChains
......@@ -136,8 +135,6 @@ class ProblemTypeTestMixin(object):
"""
Test cases shared amongst problem types.
"""
can_submit_blank = False
@attr('shard_7')
def test_answer_correctly(self):
"""
......@@ -203,34 +200,15 @@ class ProblemTypeTestMixin(object):
Then my "<ProblemType>" answer is marked "incorrect"
And The "<ProblemType>" problem displays a "blank" answer
"""
if not self.can_submit_blank:
raise SkipTest("Test incompatible with the current problem type")
self.problem_page.wait_for(
lambda: self.problem_page.problem_name == self.problem_name,
"Make sure the correct problem is on the page"
)
# Leave the problem unchanged and click check.
self.assertNotIn('is-disabled', self.problem_page.q(css='div.problem button.check').attrs('class')[0])
self.problem_page.click_check()
self.wait_for_status('incorrect')
@attr('shard_7')
def test_cant_submit_blank_answer(self):
"""
Scenario: I can't submit a blank answer
When I try to submit blank answer
Then I can't check a problem
"""
if self.can_submit_blank:
raise SkipTest("Test incompatible with the current problem type")
self.problem_page.wait_for(
lambda: self.problem_page.problem_name == self.problem_name,
"Make sure the correct problem is on the page"
)
self.assertIn('is-disabled', self.problem_page.q(css='div.problem button.check').attrs('class')[0])
@attr('a11y')
def test_problem_type_a11y(self):
"""
......@@ -258,8 +236,6 @@ class AnnotationProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
factory = AnnotationResponseXMLFactory()
can_submit_blank = True
factory_kwargs = {
'title': 'Annotation Problem',
'text': 'The text being annotated',
......@@ -710,13 +686,6 @@ class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
"""
pass
def test_cant_submit_blank_answer(self):
"""
Overridden for script test because the testing grader always responds
with "correct"
"""
pass
class ChoiceTextProbelmTypeTestBase(ProblemTypeTestBase):
"""
......@@ -832,8 +801,6 @@ class ImageProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
factory = ImageResponseXMLFactory()
can_submit_blank = True
factory_kwargs = {
'src': '/static/images/placeholder-image.png',
'rectangle': '(0,0)-(50,50)',
......
[
{
"pk": 2,
"model": "sites.Site",
"fields": {
"domain": "localhost:8003",
"name": "lms"
}
},
{
"pk": 3,
"model": "sites.Site",
"fields": {
"domain": "localhost:8031",
"name": "cms"
}
}
]
// studio - utilities - variables
// ====================
// Table of Contents
// * +Paths
// * +Grid
// * +Fonts
// * +Colors - Utility
// * +Colors - Primary
// * +Colors - Shadow
// * +Color - Application
// * +Timing
// * +Archetype UI
// * +Specific UI
// * +Deprecated
$baseline: 20px;
// +Paths
// ====================
$static-path: '..' !default;
// +Grid
// ====================
$gw-column: ($baseline*3);
$gw-gutter: $baseline;
$fg-column: $gw-column;
$fg-gutter: $gw-gutter;
$fg-max-columns: 12;
$fg-max-width: 1280px;
$fg-min-width: 900px;
// +Fonts
// ====================
$f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif;
$f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
// +Colors - Utility
// ====================
$transparent: rgba(0,0,0,0); // used when color value is needed for UI width/transitions but element is transparent
// +Colors - Primary
// ====================
$black: rgb(0,0,0);
$black-t0: rgba($black, 0.125);
$black-t1: rgba($black, 0.25);
$black-t2: rgba($black, 0.5);
$black-t3: rgba($black, 0.75);
$white: rgb(255,255,255);
$white-t0: rgba($white, 0.125);
$white-t1: rgba($white, 0.25);
$white-t2: rgba($white, 0.5);
$white-t3: rgba($white, 0.75);
$gray: rgb(127,127,127);
$gray-l1: tint($gray,20%);
$gray-l2: tint($gray,40%);
$gray-l3: tint($gray,60%);
$gray-l4: tint($gray,80%);
$gray-l5: tint($gray,90%);
$gray-l6: tint($gray,95%);
$gray-l7: tint($gray,99%);
$gray-d1: shade($gray,20%);
$gray-d2: shade($gray,40%);
$gray-d3: shade($gray,60%);
$gray-d4: shade($gray,80%);
$blue: rgb(0, 159, 230);
$blue-l1: tint($blue,20%);
$blue-l2: tint($blue,40%);
$blue-l3: tint($blue,60%);
$blue-l4: tint($blue,80%);
$blue-l5: tint($blue,90%);
$blue-d1: shade($blue,20%);
$blue-d2: shade($blue,40%);
$blue-d3: shade($blue,60%);
$blue-d4: shade($blue,80%);
$blue-s1: saturate($blue,15%);
$blue-s2: saturate($blue,30%);
$blue-s3: saturate($blue,45%);
$blue-u1: desaturate($blue,15%);
$blue-u2: desaturate($blue,30%);
$blue-u3: desaturate($blue,45%);
$blue-t0: rgba($blue, 0.125);
$blue-t1: rgba($blue, 0.25);
$blue-t2: rgba($blue, 0.50);
$blue-t3: rgba($blue, 0.75);
$pink: rgb(183, 37, 103); // #b72567;
$pink-l1: tint($pink,20%);
$pink-l2: tint($pink,40%);
$pink-l3: tint($pink,60%);
$pink-l4: tint($pink,80%);
$pink-l5: tint($pink,90%);
$pink-d1: shade($pink,20%);
$pink-d2: shade($pink,40%);
$pink-d3: shade($pink,60%);
$pink-d4: shade($pink,80%);
$pink-s1: saturate($pink,15%);
$pink-s2: saturate($pink,30%);
$pink-s3: saturate($pink,45%);
$pink-u1: desaturate($pink,15%);
$pink-u2: desaturate($pink,30%);
$pink-u3: desaturate($pink,45%);
$red: rgb(178, 6, 16); // #b20610;
$red-l1: tint($red,20%);
$red-l2: tint($red,40%);
$red-l3: tint($red,60%);
$red-l4: tint($red,80%);
$red-l5: tint($red,90%);
$red-d1: shade($red,20%);
$red-d2: shade($red,40%);
$red-d3: shade($red,60%);
$red-d4: shade($red,80%);
$red-s1: saturate($red,15%);
$red-s2: saturate($red,30%);
$red-s3: saturate($red,45%);
$red-u1: desaturate($red,15%);
$red-u2: desaturate($red,30%);
$red-u3: desaturate($red,45%);
$green: rgb(37, 184, 90); // #25b85a
$green-l1: tint($green,20%);
$green-l2: tint($green,40%);
$green-l3: tint($green,60%);
$green-l4: tint($green,80%);
$green-l5: tint($green,90%);
$green-d1: shade($green,20%);
$green-d2: shade($green,40%);
$green-d3: shade($green,60%);
$green-d4: shade($green,80%);
$green-s1: saturate($green,15%);
$green-s2: saturate($green,30%);
$green-s3: saturate($green,45%);
$green-u1: desaturate($green,15%);
$green-u2: desaturate($green,30%);
$green-u3: desaturate($green,45%);
$yellow: rgb(237, 189, 60);
$yellow-l1: tint($yellow,20%);
$yellow-l2: tint($yellow,40%);
$yellow-l3: tint($yellow,60%);
$yellow-l4: tint($yellow,80%);
$yellow-l5: tint($yellow,90%);
$yellow-d1: shade($yellow,20%);
$yellow-d2: shade($yellow,40%);
$yellow-d3: shade($yellow,60%);
$yellow-d4: shade($yellow,80%);
$yellow-s1: saturate($yellow,15%);
$yellow-s2: saturate($yellow,30%);
$yellow-s3: saturate($yellow,45%);
$yellow-u1: desaturate($yellow,15%);
$yellow-u2: desaturate($yellow,30%);
$yellow-u3: desaturate($yellow,45%);
$orange: rgb(237, 189, 60);
$orange-l1: tint($orange,20%);
$orange-l2: tint($orange,40%);
$orange-l3: tint($orange,60%);
$orange-l4: tint($orange,80%);
$orange-l5: tint($orange,90%);
$orange-d1: shade($orange,20%);
$orange-d2: shade($orange,40%);
$orange-d3: shade($orange,60%);
$orange-d4: shade($orange,80%);
$orange-s1: saturate($orange,15%);
$orange-s2: saturate($orange,30%);
$orange-s3: saturate($orange,45%);
$orange-u1: desaturate($orange,15%);
$orange-u2: desaturate($orange,30%);
$orange-u3: desaturate($orange,45%);
// +Colors - Shadows
// ====================
$shadow: rgba($black, 0.2);
$shadow-l1: rgba($black, 0.1);
$shadow-l2: rgba($black, 0.05);
$shadow-d1: rgba($black, 0.4);
$shadow-d2: rgba($black, 0.6);
// +Colors - Application
// ====================
$color-draft: $gray-l3;
$color-live: $blue;
$color-ready: $green;
$color-warning: $orange-l2;
$color-error: $red-l2;
$color-staff-only: $black;
$color-gated: $black;
$color-visibility-set: $black;
$color-heading-base: $gray-d2;
$color-copy-base: $gray-l1;
$color-copy-emphasized: $gray-d2;
// +Timing
// ====================
// used for animation/transition mixin syncing
$tmg-s3: 3.0s;
$tmg-s2: 2.0s;
$tmg-s1: 1.0s;
$tmg-avg: 0.75s;
$tmg-f1: 0.50s;
$tmg-f2: 0.25s;
$tmg-f3: 0.125s;
// +Archetype UI
// ====================
$ui-action-primary-color: $blue-u2;
$ui-action-primary-color-focus: $blue-s1;
$ui-link-color: $blue-u2;
$ui-link-color-focus: $blue-s1;
// +Specific UI
// ====================
$ui-notification-height: ($baseline*10);
$ui-update-color: $blue-l4;
// +Deprecated
// ====================
// do not use, future clean up will use updated styles
$baseFontColor: $gray-d2;
$lighter-base-font-color: rgb(100,100,100);
$offBlack: #3c3c3c;
$green: #108614;
$lightGrey: #edf1f5;
$mediumGrey: #b0b6c2;
$darkGrey: #8891a1;
$extraDarkGrey: #3d4043;
$paleYellow: #fffcf1;
$yellow: rgb(255, 254, 223);
$green: rgb(37, 184, 90);
$brightGreen: rgb(22, 202, 87);
$disabledGreen: rgb(124, 206, 153);
$darkGreen: rgb(52, 133, 76);
// These colors are updated for testing purposes
$lightBluishGrey: rgb(0, 250, 0);
$lightBluishGrey2: rgb(0, 250, 0);
$error-red: rgb(253, 87, 87);
//carryover from LMS for xmodules
$sidebar-color: rgb(246, 246, 246);
// type
$sans-serif: $f-sans-serif;
$body-line-height: golden-ratio(.875em, 1);
// carried over from LMS for xmodules
$action-primary-active-bg: #1AA1DE; // $m-blue
$very-light-text: $white;
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "login" %></%def>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
%>
<%block name="title">${_("Sign In")}</%block>
<%block name="bodyclass">not-signedin view-signin</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<header>
<h1 class="title title-1">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</h1>
<a href="${reverse('signup')}" class="action action-signin">${_("Don't have a {studio_name} Account? Sign up!").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
</header>
<!-- Login Page override for test-theme. -->
<article class="content-primary" role="main">
<form id="login_form" method="post" action="login_post" onsubmit="return false;">
<fieldset>
<legend class="sr">${_("Required Information to Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</legend>
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf }" />
<ol class="list-input">
<li class="field text required" id="field-email">
<label for="email">${_("E-mail")}</label>
<input id="email" type="email" name="email" placeholder="${_('example: username@domain.com')}"/>
</li>
<li class="field text required" id="field-password">
<label for="password">${_("Password")}</label>
<input id="password" type="password" name="password" />
<a href="${forgot_password_link}" class="action action-forgotpassword">${_("Forgot password?")}</a>
</li>
</ol>
</fieldset>
<div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</button>
</div>
<!-- no honor code for CMS, but need it because we're using the lms student object -->
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
</form>
</article>
</section>
</div>
</%block>
<%block name="requirejs">
require(["js/factories/login"], function(LoginFactory) {
LoginFactory("${reverse('homepage')}");
});
</%block>
@import 'lms/static/sass/partials/base/variables';
$header-bg: rgb(0,250,0);
$footer-bg: rgb(0,250,0);
$container-bg: rgb(0,250,0);
<div class="wrapper wrapper-footer">
<footer>
<div class="colophon">
<div class="colophon-about">
<p>This is a footer for test-theme.</p>
</div>
</div>
</footer>
</div>
......@@ -10,7 +10,7 @@ import mock
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)
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
from ccx_keys.locator import CCXLocator
from courseware.courses import get_course_by_id
from django.db import migrations
from django.http import Http404
from lms.djangoapps.ccx.utils import (
add_master_course_staff_to_ccx,
remove_master_course_staff_from_ccx,
)
log = logging.getLogger("edx.ccx")
def add_master_course_staff_to_ccx_for_existing_ccx(apps, schema_editor):
"""
......@@ -23,16 +28,24 @@ def add_master_course_staff_to_ccx_for_existing_ccx(apps, schema_editor):
CustomCourseForEdX = apps.get_model("ccx", "CustomCourseForEdX")
list_ccx = CustomCourseForEdX.objects.all()
for ccx in list_ccx:
if ccx.course_id.deprecated:
# prevent migration for deprecated course ids.
if not ccx.course_id or ccx.course_id.deprecated:
# prevent migration for deprecated course ids or invalid ids.
continue
ccx_locator = CCXLocator.from_course_locator(ccx.course_id, unicode(ccx.id))
add_master_course_staff_to_ccx(
get_course_by_id(ccx.course_id),
ccx_locator,
ccx.display_name,
send_email=False
)
try:
course = get_course_by_id(ccx.course_id)
add_master_course_staff_to_ccx(
course,
ccx_locator,
ccx.display_name,
send_email=False
)
except Http404:
log.warning(
"Unable to add instructors and staff of master course %s to ccx %s.",
ccx.course_id,
ccx_locator
)
def remove_master_course_staff_from_ccx_for_existing_ccx(apps, schema_editor):
......@@ -47,17 +60,24 @@ def remove_master_course_staff_from_ccx_for_existing_ccx(apps, schema_editor):
CustomCourseForEdX = apps.get_model("ccx", "CustomCourseForEdX")
list_ccx = CustomCourseForEdX.objects.all()
for ccx in list_ccx:
if ccx.course_id.deprecated:
# prevent migration for deprecated course ids.
if not ccx.course_id or ccx.course_id.deprecated:
# prevent migration for deprecated course ids or invalid ids.
continue
ccx_locator = CCXLocator.from_course_locator(ccx.course_id, unicode(ccx.id))
remove_master_course_staff_from_ccx(
get_course_by_id(ccx.course_id),
ccx_locator,
ccx.display_name,
send_email=False
)
try:
course = get_course_by_id(ccx.course_id)
remove_master_course_staff_from_ccx(
course,
ccx_locator,
ccx.display_name,
send_email=False
)
except Http404:
log.warning(
"Unable to remove instructors and staff of master course %s from ccx %s.",
ccx.course_id,
ccx_locator
)
class Migration(migrations.Migration):
......@@ -65,6 +85,7 @@ class Migration(migrations.Migration):
('ccx', '0001_initial'),
('ccx', '0002_customcourseforedx_structure_json'),
('course_overviews','0010_auto_20160329_2317'), # because we use course overview and are in the same release as that table is modified
('verified_track_content','0001_initial'), # because we use enrollement code and are in the same release as an enrollement related table is created
]
operations = [
......
......@@ -8,7 +8,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):
......@@ -85,7 +85,7 @@ class ReceiptViewTests(UserMixin, TestCase):
self.assertRegexpMatches(response.content, user_message if is_user_message_expected else system_message)
self.assertNotRegexpMatches(response.content, user_message if not is_user_message_expected else system_message)
@with_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'}
......
"""
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)
......
......@@ -180,22 +180,16 @@ Feature: LMS.Answer problems
Examples:
| ProblemType | Points Possible |
| drop down | 1 point possible |
| multiple choice | 1 point possible |
| checkbox | 1 point possible |
| radio | 1 point possible |
#| string | 1 point possible |
| numerical | 1 point possible |
| formula | 1 point possible |
| script | 2 points possible |
| image | 1 point possible |
Scenario: I can't submit a blank answer
Given I am viewing a "<ProblemType>" problem
Then I can't check a problem
Examples:
| ProblemType |
| drop down |
| multiple choice |
| checkbox |
| radio |
| string |
| numerical |
| formula |
| script |
Scenario: I can reset the correctness of a problem after changing my answer
Given I am viewing a "<ProblemType>" problem
......@@ -240,3 +234,21 @@ Feature: LMS.Answer problems
| multiple choice | incorrect | correct |
| radio | correct | incorrect |
| radio | incorrect | correct |
Scenario: I can reset the correctness of a problem after submitting a blank answer
Given I am viewing a "<ProblemType>" problem
When I check a problem
And I input an answer on a "<ProblemType>" problem "correctly"
Then my "<ProblemType>" answer is marked "unanswered"
Examples:
| ProblemType |
| drop down |
| multiple choice |
| checkbox |
| radio |
#| string |
| numerical |
| formula |
| script |
......@@ -92,21 +92,12 @@ def check_problem(step):
# first scroll down so the loading mathjax button does not
# cover up the Check button
world.browser.execute_script("window.scrollTo(0,1024)")
assert world.is_css_not_present("button.check.is-disabled")
world.css_click("button.check")
# Wait for the problem to finish re-rendering
world.wait_for_ajax_complete()
@step(u"I can't check a problem")
def assert_cant_check_problem(step): # pylint: disable=unused-argument
# first scroll down so the loading mathjax button does not
# cover up the Check button
world.browser.execute_script("window.scrollTo(0,1024)")
assert world.is_css_present("button.check.is-disabled")
@step(u'The "([^"]*)" problem displays a "([^"]*)" answer')
def assert_problem_has_answer(step, problem_type, answer_class):
'''
......
......@@ -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-v1.css')
self.assertEqual(result, settings.REPO_ROOT / "themes/red-theme/lms/static/css/lms-main-v1.css")
lms_main_css = ""
with open(result) as css_file:
lms_main_css += css_file.read()
self.assertIn("background:#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-v1.css')
self.assertEqual(result, settings.REPO_ROOT / "lms/static/css/lms-main-v1.css")
lms_main_css = ""
with open(result) as css_file:
lms_main_css += css_file.read()
self.assertNotIn("background:#00fa00", lms_main_css)
@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))
......
......@@ -39,7 +39,7 @@ from commerce.models import CommerceConfiguration
from commerce.tests import TEST_PAYMENT_DATA, TEST_API_URL, TEST_API_SIGNING_KEY, TEST_PUBLIC_URL_ROOT
from embargo.test_utils import restrict_course
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
from openedx.core.djangoapps.theming.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
......@@ -319,7 +319,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', {}))
......@@ -445,6 +444,7 @@ AWS_STORAGE_BUCKET_NAME = AUTH_TOKENS.get('AWS_STORAGE_BUCKET_NAME', 'edxuploads
# Disabling querystring auth instructs Boto to exclude the querystring parameters (e.g. signature, access key) it
# normally appends to every returned URL.
AWS_QUERYSTRING_AUTH = AUTH_TOKENS.get('AWS_QUERYSTRING_AUTH', True)
AWS_S3_CUSTOM_DOMAIN = AUTH_TOKENS.get('AWS_S3_CUSTOM_DOMAIN', 'edxuploads.s3.amazonaws.com')
if AUTH_TOKENS.get('DEFAULT_FILE_STORAGE'):
DEFAULT_FILE_STORAGE = AUTH_TOKENS.get('DEFAULT_FILE_STORAGE')
......
......@@ -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"
......
......@@ -396,7 +396,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)
......@@ -485,7 +485,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',
],
......@@ -785,6 +784,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,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
# List of finder classes that know how to find static files in various locations.
# Note: the pipeline finder is included to be able to discover optimized files
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder',
......@@ -2879,10 +2875,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,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
# Revert to the default set of finders as we don't want the production pipeline
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'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
......
......@@ -8,6 +8,7 @@ from django.template import RequestContext
import third_party_auth
from third_party_auth import pipeline
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
from openedx.core.djangolib.markup import Text, HTML
%>
<%
......@@ -200,7 +201,7 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
<header>
<h2 id="email-settings-title">
${_("Email Settings for {course_number}").format(course_number='<span id="email_settings_course_number"></span>')}
${Text(_("Email Settings for {course_number}")).format(course_number=HTML('<span id="email_settings_course_number"></span>'))}
<span class="sr">,
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
${_("window open")}
......
......@@ -8,6 +8,7 @@ from django.utils.translation import ungettext
from django.core.urlresolvers import reverse
from course_modes.models import CourseMode
from course_modes.helpers import enrollment_mode_display
from openedx.core.djangolib.js_utils import dump_js_escaped_json
from openedx.core.djangolib.markup import Text, HTML
from student.helpers import (
VERIFY_STATUS_NEED_TO_VERIFY,
......@@ -317,7 +318,13 @@ from student.helpers import (
<h4 class="message-title">${_('Your verification will expire soon!')}</h4>
## Translators: start_link and end_link will be replaced with HTML tags;
## please do not translate these.
<p class="message-copy">${Text(_('Your current verification will expire before the verification deadline for this course. {start_link}Re-verify your identity now{end_link} using a webcam and a government-issued ID.')).format(start_link=HTML('<a href="{href}">'.format(href=reverse('verify_student_reverify'))), end_link=HTML('</a>'))}</p>
<p class="message-copy">${Text(_('Your current verification will expire before the verification deadline '
'for this course. {start_link}Re-verify your identity now{end_link} using a webcam and a '
'government-issued ID.')).format(
start_link=HTML('<a href="{href}">').format(href=reverse('verify_student_reverify')),
end_link=HTML('</a>')
)}
</p>
% endif
</div>
% endif
......@@ -334,10 +341,10 @@ from student.helpers import (
"It's a proven motivator to complete the course. {line_break}"
"{link_start}Learn more about the verified {cert_name_long}{link_end}.")).format(
line_break=HTML('<br>'),
link_start=HTML('<a href="{}" class="verified-info" data-course-key="{}">'.format(
link_start=HTML('<a href="{}" class="verified-info" data-course-key="{}">').format(
marketing_link('WHAT_IS_VERIFIED_CERT'),
enrollment.course_id
)),
),
link_end=HTML('</a>'),
cert_name_long=cert_name_long
)}
......@@ -394,7 +401,7 @@ from student.helpers import (
<li class="prerequisites">
<p class="tip">
${Text(_("You must successfully complete {link_start}{prc_display}{link_end} before you begin this course.")).format(
link_start=HTML('<a href="{}">'.format(prc_target)),
link_start=HTML('<a href="{}">').format(prc_target),
link_end=HTML('</a>'),
prc_display=course_requirements['courses'][0]['display'],
)}
......@@ -409,7 +416,7 @@ from student.helpers import (
<script>
$( document ).ready(function() {
if("${is_course_blocked}" == "True"){
if("${is_course_blocked | n, dump_js_escaped_json}" == 'true'){
$( "#unregister_block_course" ).click(function() {
$('.disable-look-unregister').click();
});
......
<!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>
......
......@@ -22,8 +22,8 @@ from django.conf import settings
"If you did not mean to do this, {undo_link_start}you can re-subscribe{link_end}."
)).format(
platform_name=settings.PLATFORM_NAME,
dashboard_link_start=HTML("<a href='{}'>".format(reverse('dashboard'))),
undo_link_start=HTML("<a id='resub_link' href='{}'>".format(reverse('resubscribe_forum_update', args=[token]))),
dashboard_link_start=HTML("<a href='{}'>").format(reverse('dashboard')),
undo_link_start=HTML("<a id='resub_link' href='{}'>").format(reverse('resubscribe_forum_update', args=[token])),
link_end=HTML("</a>"),
)}
</p>
......
{% 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 %}
......
## mako
<%page expression_filter="h"/>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import Text, HTML
from wiki.core.permissions import can_change_permissions
%>
......@@ -11,7 +9,7 @@
<a href="${reverse('wiki:get', kwargs={'article_id' : article.id, 'path' : urlpath.path})}">
<i class="icon fa fa-eye"></i>
${_("View")}
${Text(_("{span_start}(active){span_end}")).format(span_start=HTML("<span class='sr'>"), span_end=HTML("</span>")) if selected_tab == "view" else ""}
${_("{span_start}(active){span_end}").format(span_start="<span class='sr'>", span_end="</span>") if selected_tab == "view" else ""}
</a>
</li>
......@@ -20,7 +18,7 @@
<a href="${reverse('wiki:edit', kwargs={'article_id' : article.id, 'path' : urlpath.path})}">
<i class="icon fa fa-pencil"></i>
${_("Edit")}
${Text(_("{span_start}(active){span_end}")).format(span_start=HTML("<span class='sr'>"), span_end=HTML("</span>")) if selected_tab == "edit" else ""}
${_("{span_start}(active){span_end}").format(span_start="<span class='sr'>", span_end="</span>") if selected_tab == "edit" else ""}
</a>
</li>
%endif
......@@ -29,7 +27,7 @@
<a href="${reverse('wiki:history', kwargs={'article_id' : article.id, 'path' : urlpath.path})}">
<i class="icon fa fa-clock-o"></i>
${_("Changes")}
${Text(_("{span_start}(active){span_end}")).format(span_start=HTML("<span class='sr'>"), span_end=HTML("</span>")) if selected_tab == "history" else ""}
${_("{span_start}(active){span_end}").format(span_start="<span class='sr'>", span_end="</span>") if selected_tab == "history" else ""}
</a>
</li>
......@@ -39,7 +37,7 @@
<a href="${reverse('wiki:plugin', kwargs={'slug' : plugin.slug, 'article_id' : article.id, 'path' : urlpath.path}) }">
<i class="icon fa fa-file ${plugin.article_tab[1]}"></i>
${plugin.article_tab[0]}
${Text(_("{span_start}(active){span_end}")).format(span_start=HTML("<span class='sr'>"), span_end=HTML("</span>")) if selected_tab == plugin.slug else ""}
${_("{span_start}(active){span_end}").format(span_start="<span class='sr'>", span_end="</span>") if selected_tab == plugin.slug else ""}
</a>
</li>
%endif
......@@ -55,7 +53,7 @@ ${_("This should be enabled for all non-anonymous users once the notifications a
<a href="${reverse('wiki:settings', kwargs={'article_id' : article.id, 'path' : urlpath.path})}">
<i class="icon fa fa-cog"></i>
${_("Settings")}
${Text(_("{span_start}active{span_end}")).format(span_start=HTML("<span class='sr'>"), span_end=HTML("</span>")) if selected_tab == "settings" else ""}
${_("{span_start}active{span_end}").format(span_start="<span class='sr'>(", span_end=")</span>") if selected_tab == "settings" else ""}
</a>
</li>
%endif
......
## mako
<%page expression_filter="h"/>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
......
<!DOCTYPE html>
{% load wiki_tags i18n %}{% load theme_pipeline %}
{% load wiki_tags i18n %}{% load pipeline %}
<html lang="{{LANGUAGE_CODE}}">
<head>
{% stylesheet 'course' %}
......
"""
Django admin page for theming models
"""
from django.contrib import admin
from .models import (
SiteTheme,
)
class SiteThemeAdmin(admin.ModelAdmin):
""" Admin interface for the SiteTheme object. """
list_display = ('site', 'theme_dir_name')
search_fields = ('site__domain', 'theme_dir_name')
class Meta(object):
"""
Meta class for SiteTheme admin model
"""
model = SiteTheme
admin.site.register(SiteTheme, SiteThemeAdmin)
"""
Core logic for Comprehensive Theming.
"""
import os.path
from path import Path as path
from django.conf import settings
from path import Path
from .helpers import (
get_project_root_name,
)
from django.conf import settings
def enable_comprehensive_theming(themes_dir):
def comprehensive_theme_changes(theme_dir):
"""
Add directories to relevant paths for comprehensive theming.
:param themes_dir: path to base theme directory
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.
"""
if isinstance(themes_dir, basestring):
themes_dir = path(themes_dir)
if themes_dir.isdir():
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].insert(0, themes_dir)
settings.MAKO_TEMPLATES['main'].insert(0, themes_dir)
changes = {
'settings': {},
'template_paths': [],
}
root = Path(settings.PROJECT_ROOT)
if root.name == "":
root = root.parent
component_dir = theme_dir / root.name
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]
templates_dir = component_dir / "templates"
if templates_dir.isdir():
changes['template_paths'].append(templates_dir)
staticfiles_dir = component_dir / "static"
if staticfiles_dir.isdir():
changes['settings']['STATICFILES_DIRS'] = [staticfiles_dir] + settings.STATICFILES_DIRS
locale_dir = component_dir / "conf" / "locale"
if locale_dir.isdir():
changes['settings']['LOCALE_PATHS'] = [locale_dir] + settings.LOCALE_PATHS
return changes
def enable_comprehensive_theme(theme_dir):
"""
Add directories to relevant paths for comprehensive theming.
"""
changes = comprehensive_theme_changes(theme_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 []
if path.startswith(self.storage.prefix):
# strip the prefix
path = path[len(self.storage.prefix):]
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 self.storage.exists(path):
match = self.storage.path(path)
if all:
match = [match]
return match
def find_in_theme(self, theme, path):
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
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sites', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='SiteTheme',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('theme_dir_name', models.CharField(max_length=255)),
('site', models.ForeignKey(related_name='themes', to='sites.Site')),
],
),
]
"""
Django models supporting the Comprehensive Theming subsystem
"""
from django.db import models
from django.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
"""
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))
"""
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
def with_comprehensive_theme(theme_dir_name):
EDX_THEME_DIR = settings.REPO_ROOT / "themes" / "edx.org"
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
def with_is_edx_domain(is_edx_domain):
"""
A decorator to run a test as if request originated from edX domain or not.
Arguments:
is_edx_domain (bool): are we an edX domain or not?
"""
# This is weird, it's a decorator that conditionally applies other
# decorators, which is confusing.
def _decorator(func): # pylint: disable=missing-docstring
if is_edx_domain:
# This applies @with_comprehensive_theme to the func.
func = with_comprehensive_theme(EDX_THEME_DIR)(func)
return func
return _decorator
@contextlib.contextmanager
def with_comprehensive_theme_context(theme=None):
def with_edx_domain_context(is_edx_domain):
"""
A function to run a test as if request was made to the given theme.
A function 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):
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
......
"""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')
"""
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)
"""
Tests for comprehensive themes.
"""
import unittest
from django.conf import settings
from django.test import TestCase, override_settings
from django.contrib import staticfiles
from paver.easy import call_task
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestComprehensiveThemeLMS(TestCase):
"""
Test html, sass and static file overrides for comprehensive themes.
"""
def setUp(self):
"""
Clear static file finders cache and register cleanup methods.
"""
super(TestComprehensiveThemeLMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Enable Comprehensive theme and compile sass files.
"""
# Apply Comprehensive theme and compile sass assets.
compile_sass('lms')
super(TestComprehensiveThemeLMS, cls).setUpClass()
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_footer(self):
"""
Test that theme footer is used instead of default footer.
"""
resp = self.client.get('/')
self.assertEqual(resp.status_code, 200)
# This string comes from header.html of test-theme
self.assertContains(resp, "This is a footer for test-theme.")
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_logo_image(self):
"""
Test that theme logo is used instead of default logo.
"""
result = staticfiles.finders.find('test-theme/images/logo.png')
self.assertEqual(result, settings.TEST_THEME / 'lms/static/images/logo.png')
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_css_files(self):
"""
Test that theme sass files are used instead of default sass files.
"""
result = staticfiles.finders.find('test-theme/css/lms-main-v1.css')
self.assertEqual(result, settings.TEST_THEME / "lms/static/css/lms-main-v1.css")
lms_main_css = ""
with open(result) as css_file:
lms_main_css += css_file.read()
self.assertIn("background:#00fa00", lms_main_css)
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
class TestComprehensiveThemeCMS(TestCase):
"""
Test html, sass and static file overrides for comprehensive themes.
"""
def setUp(self):
"""
Clear static file finders cache and register cleanup methods.
"""
super(TestComprehensiveThemeCMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Enable Comprehensive theme and compile sass files.
"""
# Apply Comprehensive theme and compile sass assets.
compile_sass('cms')
super(TestComprehensiveThemeCMS, cls).setUpClass()
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_template_override(self):
"""
Test that theme templates are used instead of default templates.
"""
resp = self.client.get('/signin')
self.assertEqual(resp.status_code, 200)
# This string comes from login.html of test-theme
self.assertContains(resp, "Login Page override for test-theme.")
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
@with_comprehensive_theme(settings.TEST_THEME.basename())
def test_css_files(self):
"""
Test that theme sass files are used instead of default sass files.
"""
result = staticfiles.finders.find('test-theme/css/studio-main-v1.css')
self.assertEqual(result, settings.TEST_THEME / "cms/static/css/studio-main-v1.css")
cms_main_css = ""
with open(result) as css_file:
cms_main_css += css_file.read()
self.assertIn("background:#00fa00", cms_main_css)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestComprehensiveThemeDisabledLMS(TestCase):
"""
Test Sass compilation order and sass overrides for comprehensive themes.
"""
def setUp(self):
"""
Clear static file finders cache.
"""
super(TestComprehensiveThemeDisabledLMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Compile sass files.
"""
# compile LMS SASS
compile_sass('lms')
super(TestComprehensiveThemeDisabledLMS, cls).setUpClass()
def test_logo(self):
"""
Test that default logo is picked in case of no comprehensive theme.
"""
result = staticfiles.finders.find('images/logo.png')
self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/logo.png')
def test_css(self):
"""
Test that default css files served without comprehensive themes applied.
"""
result = staticfiles.finders.find('css/lms-main-v1.css')
self.assertEqual(result, settings.REPO_ROOT / "lms/static/css/lms-main-v1.css")
lms_main_css = ""
with open(result) as css_file:
lms_main_css += css_file.read()
self.assertNotIn("background:#00fa00", lms_main_css)
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
class TestComprehensiveThemeDisabledCMS(TestCase):
"""
Test default html, sass and static file when no theme is applied.
"""
def setUp(self):
"""
Clear static file finders cache and register cleanup methods.
"""
super(TestComprehensiveThemeDisabledCMS, self).setUp()
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@classmethod
def setUpClass(cls):
"""
Enable Comprehensive theme and compile sass files.
"""
# Apply Comprehensive theme and compile sass assets.
compile_sass('cms')
super(TestComprehensiveThemeDisabledCMS, cls).setUpClass()
def test_template_override(self):
"""
Test that defaults templates are used when no theme is applied.
"""
resp = self.client.get('/signin')
self.assertEqual(resp.status_code, 200)
self.assertNotContains(resp, "Login Page override for test-theme.")
def test_css_files(self):
"""
Test that default css files served without comprehensive themes applied..
"""
result = staticfiles.finders.find('css/studio-main-v1.css')
self.assertEqual(result, settings.REPO_ROOT / "cms/static/css/studio-main-v1.css")
cms_main_css = ""
with open(result) as css_file:
cms_main_css += css_file.read()
self.assertNotIn("background:#00fa00", cms_main_css)
def compile_sass(system):
"""
Process xmodule assets and compile sass files for the given system.
:param system - 'lms' or 'cms', specified the system to compile sass for.
"""
# Compile system sass files
call_task(
'pavelib.assets.update_assets',
args=(
system,
"--themes_dir={}".format(settings.TEST_THEME.dirname()),
"--themes={}".format(settings.TEST_THEME.basename()),
"--settings=test"),
)
......@@ -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)
"""
Django storage backends for Open edX.
"""
from django.contrib.staticfiles.storage import StaticFilesStorage
from pipeline.storage import NonPackagingMixin
from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
from pipeline.storage import PipelineMixin, NonPackagingMixin
from require.storage import OptimizedFilesMixin
from openedx.core.djangoapps.theming.storage import ThemeStorage, ComprehensiveThemingCachedFilesMixin, \
ThemePipelineMixin
from openedx.core.djangoapps.theming.storage import ComprehensiveThemingAwareMixin
class ProductionStorage(
ComprehensiveThemingAwareMixin,
OptimizedFilesMixin,
ThemePipelineMixin,
ComprehensiveThemingCachedFilesMixin,
ThemeStorage,
PipelineMixin,
CachedFilesMixin,
StaticFilesStorage
):
"""
......@@ -23,9 +22,9 @@ class ProductionStorage(
class DevelopmentStorage(
ComprehensiveThemingAwareMixin,
NonPackagingMixin,
ThemePipelineMixin,
ThemeStorage,
PipelineMixin,
StaticFilesStorage
):
"""
......
"""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 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,159 +43,18 @@ class TestPaverAssetTasks(PaverTestCase):
if force:
expected_messages.append("rm -rf common/static/css/*.css")
expected_messages.append("libsass common/static/sass")
if "lms" in system:
if force:
expected_messages.append("rm -rf lms/static/css/*.css")
expected_messages.append("libsass lms/static/sass")
if force:
expected_messages.append("rm -rf lms/static/certificates/css/*.css")
expected_messages.append("libsass lms/static/certificates/sass")
if "studio" in system:
if force:
expected_messages.append("rm -rf cms/static/css/*.css")
expected_messages.append("libsass cms/static/sass")
self.assertEquals(self.task_messages, expected_messages)
@ddt.ddt
class TestPaverThemeAssetTasks(PaverTestCase):
"""
Test the Paver asset tasks.
"""
@ddt.data(
[""],
["--force"],
["--debug"],
["--system=lms"],
["--system=lms --force"],
["--system=studio"],
["--system=studio --force"],
["--system=lms,studio"],
["--system=lms,studio --force"],
)
@ddt.unpack
def test_compile_theme_sass(self, options):
"""
Test the "compile_sass" task.
"""
parameters = options.split(" ")
system = []
if "--system=studio" not in parameters:
system += ["lms"]
if "--system=lms" not in parameters:
system += ["studio"]
debug = "--debug" in parameters
force = "--force" in parameters
self.reset_task_messages()
call_task(
'pavelib.assets.compile_sass',
options={"system": system, "debug": debug, "force": force, "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")
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")
if "studio" in system:
expected_messages.append("mkdir_p " + repr(TEST_THEME / "cms/static/css"))
if force:
expected_messages.append("rm -rf " + str(TEST_THEME) + "/cms/static/css/*.css")
expected_messages.append("libsass cms/static/sass")
if force:
expected_messages.append("rm -rf " + str(TEST_THEME) + "/cms/static/css/*.css")
expected_messages.append("libsass " + str(TEST_THEME) + "/cms/static/sass")
if force:
expected_messages.append("rm -rf cms/static/css/*.css")
expected_messages.append("libsass cms/static/sass")
self.assertEquals(self.task_messages, expected_messages)
class TestPaverWatchAssetTasks(TestCase):
"""
Test the Paver watch asset tasks.
"""
def setUp(self):
self.expected_sass_directories = [
path('common/static/sass'),
path('common/static'),
path('node_modules'),
path('node_modules/edx-pattern-library/node_modules'),
path('lms/static/sass/partials'),
path('lms/static/sass'),
path('lms/static/certificates/sass'),
path('cms/static/sass'),
path('cms/static/sass/partials'),
]
super(TestPaverWatchAssetTasks, self).setUp()
def tearDown(self):
self.expected_sass_directories = []
super(TestPaverWatchAssetTasks, self).tearDown()
def test_watch_assets(self):
"""
Test the "compile_sass" task.
"""
with patch('pavelib.assets.SassWatcher.register') as mock_register:
with patch('pavelib.assets.PollingObserver.start'):
call_task(
'pavelib.assets.watch_assets',
options={"background": True},
)
self.assertEqual(mock_register.call_count, 2)
sass_watcher_args = mock_register.call_args_list[0][0]
self.assertIsInstance(sass_watcher_args[0], PollingObserver)
self.assertIsInstance(sass_watcher_args[1], list)
self.assertItemsEqual(sass_watcher_args[1], self.expected_sass_directories)
def test_watch_theme_assets(self):
"""
Test the Paver watch asset tasks with theming enabled.
"""
self.expected_sass_directories.extend([
path(TEST_THEME) / 'lms/static/sass',
path(TEST_THEME) / 'lms/static/sass/partials',
path(TEST_THEME) / 'cms/static/sass',
path(TEST_THEME) / 'cms/static/sass/partials',
])
with patch('pavelib.assets.SassWatcher.register') as mock_register:
with patch('pavelib.assets.PollingObserver.start'):
call_task(
'pavelib.assets.watch_assets',
options={"background": True, "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)
......@@ -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
......
......@@ -65,13 +65,13 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
<div class="dashboard-notifications" tabindex="-1">
%if message:
<section class="dashboard-banner">
${message}
${message | n, unicode}
</section>
%endif
%if enrollment_message:
<section class="dashboard-banner">
${enrollment_message}
${enrollment_message | n, unicode}
</section>
%endif
</div>
......
<%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>
@import 'lms/static/sass/partials/base/variables';
// 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';
// Theming overrides for sample theme
@import 'overrides';
// import the rest of the application
@import 'lms/static/sass/lms-main-rtl';
// Theming overrides for sample theme
@import 'overrides';
// import the rest of the application
@import 'lms/static/sass/lms-main';
@import 'lms/static/sass/partials/base/variables';
// Theming overrides for sample theme
$header-bg: rgb(140,21,21);
$footer-bg: rgb(140,21,21);
......
// Theming overrides for sample theme
@import 'overrides';
// import the rest of the application
@import 'lms/static/sass/lms-main-rtl';
// Theming overrides for sample theme
@import 'overrides';
// import the rest of the application
@import 'lms/static/sass/lms-main';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment