Manually merge release into master
This commit is contained in:
@@ -191,7 +191,6 @@ ASSET_IGNORE_REGEX = ENV_TOKENS.get('ASSET_IGNORE_REGEX', ASSET_IGNORE_REGEX)
|
||||
# Theme overrides
|
||||
THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
|
||||
COMPREHENSIVE_THEME_DIR = path(ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', COMPREHENSIVE_THEME_DIR))
|
||||
THEME_CACHE_TIMEOUT = ENV_TOKENS.get('THEME_CACHE_TIMEOUT', THEME_CACHE_TIMEOUT)
|
||||
|
||||
#Timezone overrides
|
||||
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
|
||||
|
||||
@@ -59,9 +59,9 @@ STATIC_URL = "/static/"
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
)
|
||||
STATICFILES_DIRS = [
|
||||
STATICFILES_DIRS = (
|
||||
(TEST_ROOT / "staticfiles" / "cms").abspath(),
|
||||
]
|
||||
)
|
||||
|
||||
# Silence noisy logs
|
||||
import logging
|
||||
|
||||
@@ -61,13 +61,7 @@ from lms.envs.common import (
|
||||
# Django REST framework configuration
|
||||
REST_FRAMEWORK,
|
||||
|
||||
STATICI18N_OUTPUT_DIR,
|
||||
|
||||
# Dafault site id to use in case there is no site that matches with the request headers.
|
||||
DEFAULT_SITE_ID,
|
||||
|
||||
# Cache time out settings for comprehensive theming system
|
||||
THEME_CACHE_TIMEOUT,
|
||||
STATICI18N_OUTPUT_DIR
|
||||
)
|
||||
from path import Path as path
|
||||
from warnings import simplefilter
|
||||
@@ -350,9 +344,6 @@ MIDDLEWARE_CLASSES = (
|
||||
|
||||
'codejail.django_integration.ConfigureCodeJailMiddleware',
|
||||
|
||||
# django current site middleware with default site
|
||||
'django_sites_extensions.middleware.CurrentSiteWithDefaultMiddleware',
|
||||
|
||||
# needs to run after locale middleware (or anything that modifies the request context)
|
||||
'edxmako.middleware.MakoMiddleware',
|
||||
|
||||
@@ -457,6 +448,7 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
||||
|
||||
|
||||
# Site info
|
||||
SITE_ID = 1
|
||||
SITE_NAME = "localhost:8001"
|
||||
HTTPS = 'on'
|
||||
ROOT_URLCONF = 'cms.urls'
|
||||
@@ -528,7 +520,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()
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// ------------------------------
|
||||
// Studio: Shared Build Compile
|
||||
// Version 2 - introduces the Pattern Library
|
||||
|
||||
|
||||
// Configuration
|
||||
@import 'config';
|
||||
|
||||
// Extensions
|
||||
@@ -1,6 +1,5 @@
|
||||
// ------------------------------
|
||||
// Studio: Shared Build Compile
|
||||
// Version 1 styling (pre-Pattern Library)
|
||||
|
||||
// About: Sass compile for Studio that are shared between LTR and RTL UI. Configuration and vendor specific imports happen before this shared set of imports are compiled in the studio-main-*.scss files.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// ------------------------------
|
||||
// Studio configuration settings
|
||||
|
||||
|
||||
// ------------------------------
|
||||
// #VARIABLES
|
||||
// ------------------------------
|
||||
@@ -17,4 +17,4 @@
|
||||
@import 'bourbon/bourbon'; // lib - bourbon
|
||||
@import 'vendor/bi-app/bi-app-rtl'; // set the layout for right to left languages
|
||||
|
||||
@import 'build-v1'; // shared app style assets/rendering
|
||||
@import 'build'; // shared app style assets/rendering
|
||||
@@ -1,14 +0,0 @@
|
||||
// ------------------------------
|
||||
// Studio main styling
|
||||
// Version 2 - introduces the Pattern Library
|
||||
|
||||
// NOTE: This is the right-to-left (RTL) configured style compile.
|
||||
// It should mirror main-ltr w/ the exception of bi-app references.
|
||||
|
||||
|
||||
// Load the RTL version of the edX Pattern Library
|
||||
$pattern-library-path: '../edx-pattern-library' !default;
|
||||
@import 'edx-pattern-library/pattern-library/sass/edx-pattern-library-rtl';
|
||||
|
||||
// Load the shared build
|
||||
@import 'build-v2';
|
||||
@@ -1,14 +0,0 @@
|
||||
// ------------------------------
|
||||
// Studio main styling
|
||||
// Version 2 - introduces the Pattern Library
|
||||
|
||||
// NOTE: This is the left-to-right (LTR) configured style compile.
|
||||
// It should mirror main-rtl w/ the exception of bi-app references.
|
||||
|
||||
|
||||
// Load the LTR version of the edX Pattern Library
|
||||
$pattern-library-path: '../edx-pattern-library' !default;
|
||||
@import 'edx-pattern-library/pattern-library/sass/edx-pattern-library-ltr';
|
||||
|
||||
// Load the shared build
|
||||
@import 'build-v2';
|
||||
@@ -1,5 +1,4 @@
|
||||
// Studio - css architecture
|
||||
// Version 1 styling (pre-Pattern Library)
|
||||
// studio - css architecture
|
||||
// ====================
|
||||
|
||||
// Table of Contents
|
||||
@@ -18,4 +17,4 @@
|
||||
@import 'bourbon/bourbon'; // lib - bourbon
|
||||
@import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages
|
||||
|
||||
@import 'build-v1'; // shared app style assets/rendering
|
||||
@import 'build'; // shared app style assets/rendering
|
||||
@@ -13,7 +13,6 @@ from openedx.core.djangolib.js_utils import (
|
||||
dump_js_escaped_json, js_escaped_string
|
||||
)
|
||||
%>
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
<!doctype html>
|
||||
<!--[if lte IE 9]><html class="ie9 lte9" lang="${LANGUAGE_CODE}"><![endif]-->
|
||||
@@ -43,8 +42,7 @@ from openedx.core.djangolib.js_utils import (
|
||||
<%static:css group='style-vendor'/>
|
||||
<%static:css group='style-vendor-tinymce-content'/>
|
||||
<%static:css group='style-vendor-tinymce-skin'/>
|
||||
|
||||
<%static:css group='${self.attr.main_css}'/>
|
||||
<%static:css group='style-main'/>
|
||||
|
||||
<%include file="widgets/segment-io.html" />
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
<ul>
|
||||
<li><a href="container.html">Container page</a></li>
|
||||
<li><a href="unit.html">Unit page</a></li>
|
||||
<li><a href="pattern-library-test.html">Pattern Library test page</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</li>
|
||||
|
||||
@@ -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)
|
||||
@@ -180,39 +158,19 @@ class StaticContentServer(object):
|
||||
# caches a version of the response without CORS headers, in turn breaking XHR requests.
|
||||
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():
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# -*- 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
|
||||
%>
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
[
|
||||
{
|
||||
"pk": 2,
|
||||
"model": "sites.Site",
|
||||
|
||||
"fields": {
|
||||
"domain": "localhost:8003",
|
||||
"name": "lms"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 3,
|
||||
"model": "sites.Site",
|
||||
|
||||
"fields": {
|
||||
"domain": "localhost:8031",
|
||||
"name": "cms"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
*.css
|
||||
@@ -1,255 +0,0 @@
|
||||
// studio - utilities - variables
|
||||
// ====================
|
||||
|
||||
// Table of Contents
|
||||
// * +Paths
|
||||
// * +Grid
|
||||
// * +Fonts
|
||||
// * +Colors - Utility
|
||||
// * +Colors - Primary
|
||||
// * +Colors - Shadow
|
||||
// * +Color - Application
|
||||
// * +Timing
|
||||
// * +Archetype UI
|
||||
// * +Specific UI
|
||||
// * +Deprecated
|
||||
|
||||
$baseline: 20px;
|
||||
|
||||
// +Paths
|
||||
// ====================
|
||||
$static-path: '..' !default;
|
||||
|
||||
// +Grid
|
||||
// ====================
|
||||
$gw-column: ($baseline*3);
|
||||
$gw-gutter: $baseline;
|
||||
$fg-column: $gw-column;
|
||||
$fg-gutter: $gw-gutter;
|
||||
$fg-max-columns: 12;
|
||||
$fg-max-width: 1280px;
|
||||
$fg-min-width: 900px;
|
||||
|
||||
// +Fonts
|
||||
// ====================
|
||||
$f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
$f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
|
||||
|
||||
// +Colors - Utility
|
||||
// ====================
|
||||
$transparent: rgba(0,0,0,0); // used when color value is needed for UI width/transitions but element is transparent
|
||||
|
||||
// +Colors - Primary
|
||||
// ====================
|
||||
$black: rgb(0,0,0);
|
||||
$black-t0: rgba($black, 0.125);
|
||||
$black-t1: rgba($black, 0.25);
|
||||
$black-t2: rgba($black, 0.5);
|
||||
$black-t3: rgba($black, 0.75);
|
||||
|
||||
$white: rgb(255,255,255);
|
||||
$white-t0: rgba($white, 0.125);
|
||||
$white-t1: rgba($white, 0.25);
|
||||
$white-t2: rgba($white, 0.5);
|
||||
$white-t3: rgba($white, 0.75);
|
||||
|
||||
$gray: rgb(127,127,127);
|
||||
$gray-l1: tint($gray,20%);
|
||||
$gray-l2: tint($gray,40%);
|
||||
$gray-l3: tint($gray,60%);
|
||||
$gray-l4: tint($gray,80%);
|
||||
$gray-l5: tint($gray,90%);
|
||||
$gray-l6: tint($gray,95%);
|
||||
$gray-l7: tint($gray,99%);
|
||||
$gray-d1: shade($gray,20%);
|
||||
$gray-d2: shade($gray,40%);
|
||||
$gray-d3: shade($gray,60%);
|
||||
$gray-d4: shade($gray,80%);
|
||||
|
||||
$blue: rgb(0, 159, 230);
|
||||
$blue-l1: tint($blue,20%);
|
||||
$blue-l2: tint($blue,40%);
|
||||
$blue-l3: tint($blue,60%);
|
||||
$blue-l4: tint($blue,80%);
|
||||
$blue-l5: tint($blue,90%);
|
||||
$blue-d1: shade($blue,20%);
|
||||
$blue-d2: shade($blue,40%);
|
||||
$blue-d3: shade($blue,60%);
|
||||
$blue-d4: shade($blue,80%);
|
||||
$blue-s1: saturate($blue,15%);
|
||||
$blue-s2: saturate($blue,30%);
|
||||
$blue-s3: saturate($blue,45%);
|
||||
$blue-u1: desaturate($blue,15%);
|
||||
$blue-u2: desaturate($blue,30%);
|
||||
$blue-u3: desaturate($blue,45%);
|
||||
$blue-t0: rgba($blue, 0.125);
|
||||
$blue-t1: rgba($blue, 0.25);
|
||||
$blue-t2: rgba($blue, 0.50);
|
||||
$blue-t3: rgba($blue, 0.75);
|
||||
|
||||
$pink: rgb(183, 37, 103); // #b72567;
|
||||
$pink-l1: tint($pink,20%);
|
||||
$pink-l2: tint($pink,40%);
|
||||
$pink-l3: tint($pink,60%);
|
||||
$pink-l4: tint($pink,80%);
|
||||
$pink-l5: tint($pink,90%);
|
||||
$pink-d1: shade($pink,20%);
|
||||
$pink-d2: shade($pink,40%);
|
||||
$pink-d3: shade($pink,60%);
|
||||
$pink-d4: shade($pink,80%);
|
||||
$pink-s1: saturate($pink,15%);
|
||||
$pink-s2: saturate($pink,30%);
|
||||
$pink-s3: saturate($pink,45%);
|
||||
$pink-u1: desaturate($pink,15%);
|
||||
$pink-u2: desaturate($pink,30%);
|
||||
$pink-u3: desaturate($pink,45%);
|
||||
|
||||
$red: rgb(178, 6, 16); // #b20610;
|
||||
$red-l1: tint($red,20%);
|
||||
$red-l2: tint($red,40%);
|
||||
$red-l3: tint($red,60%);
|
||||
$red-l4: tint($red,80%);
|
||||
$red-l5: tint($red,90%);
|
||||
$red-d1: shade($red,20%);
|
||||
$red-d2: shade($red,40%);
|
||||
$red-d3: shade($red,60%);
|
||||
$red-d4: shade($red,80%);
|
||||
$red-s1: saturate($red,15%);
|
||||
$red-s2: saturate($red,30%);
|
||||
$red-s3: saturate($red,45%);
|
||||
$red-u1: desaturate($red,15%);
|
||||
$red-u2: desaturate($red,30%);
|
||||
$red-u3: desaturate($red,45%);
|
||||
|
||||
$green: rgb(37, 184, 90); // #25b85a
|
||||
$green-l1: tint($green,20%);
|
||||
$green-l2: tint($green,40%);
|
||||
$green-l3: tint($green,60%);
|
||||
$green-l4: tint($green,80%);
|
||||
$green-l5: tint($green,90%);
|
||||
$green-d1: shade($green,20%);
|
||||
$green-d2: shade($green,40%);
|
||||
$green-d3: shade($green,60%);
|
||||
$green-d4: shade($green,80%);
|
||||
$green-s1: saturate($green,15%);
|
||||
$green-s2: saturate($green,30%);
|
||||
$green-s3: saturate($green,45%);
|
||||
$green-u1: desaturate($green,15%);
|
||||
$green-u2: desaturate($green,30%);
|
||||
$green-u3: desaturate($green,45%);
|
||||
|
||||
$yellow: rgb(237, 189, 60);
|
||||
$yellow-l1: tint($yellow,20%);
|
||||
$yellow-l2: tint($yellow,40%);
|
||||
$yellow-l3: tint($yellow,60%);
|
||||
$yellow-l4: tint($yellow,80%);
|
||||
$yellow-l5: tint($yellow,90%);
|
||||
$yellow-d1: shade($yellow,20%);
|
||||
$yellow-d2: shade($yellow,40%);
|
||||
$yellow-d3: shade($yellow,60%);
|
||||
$yellow-d4: shade($yellow,80%);
|
||||
$yellow-s1: saturate($yellow,15%);
|
||||
$yellow-s2: saturate($yellow,30%);
|
||||
$yellow-s3: saturate($yellow,45%);
|
||||
$yellow-u1: desaturate($yellow,15%);
|
||||
$yellow-u2: desaturate($yellow,30%);
|
||||
$yellow-u3: desaturate($yellow,45%);
|
||||
|
||||
$orange: rgb(237, 189, 60);
|
||||
$orange-l1: tint($orange,20%);
|
||||
$orange-l2: tint($orange,40%);
|
||||
$orange-l3: tint($orange,60%);
|
||||
$orange-l4: tint($orange,80%);
|
||||
$orange-l5: tint($orange,90%);
|
||||
$orange-d1: shade($orange,20%);
|
||||
$orange-d2: shade($orange,40%);
|
||||
$orange-d3: shade($orange,60%);
|
||||
$orange-d4: shade($orange,80%);
|
||||
$orange-s1: saturate($orange,15%);
|
||||
$orange-s2: saturate($orange,30%);
|
||||
$orange-s3: saturate($orange,45%);
|
||||
$orange-u1: desaturate($orange,15%);
|
||||
$orange-u2: desaturate($orange,30%);
|
||||
$orange-u3: desaturate($orange,45%);
|
||||
|
||||
// +Colors - Shadows
|
||||
// ====================
|
||||
$shadow: rgba($black, 0.2);
|
||||
$shadow-l1: rgba($black, 0.1);
|
||||
$shadow-l2: rgba($black, 0.05);
|
||||
$shadow-d1: rgba($black, 0.4);
|
||||
$shadow-d2: rgba($black, 0.6);
|
||||
|
||||
// +Colors - Application
|
||||
// ====================
|
||||
$color-draft: $gray-l3;
|
||||
$color-live: $blue;
|
||||
$color-ready: $green;
|
||||
$color-warning: $orange-l2;
|
||||
$color-error: $red-l2;
|
||||
$color-staff-only: $black;
|
||||
$color-gated: $black;
|
||||
$color-visibility-set: $black;
|
||||
|
||||
$color-heading-base: $gray-d2;
|
||||
$color-copy-base: $gray-l1;
|
||||
$color-copy-emphasized: $gray-d2;
|
||||
|
||||
// +Timing
|
||||
// ====================
|
||||
// used for animation/transition mixin syncing
|
||||
$tmg-s3: 3.0s;
|
||||
$tmg-s2: 2.0s;
|
||||
$tmg-s1: 1.0s;
|
||||
$tmg-avg: 0.75s;
|
||||
$tmg-f1: 0.50s;
|
||||
$tmg-f2: 0.25s;
|
||||
$tmg-f3: 0.125s;
|
||||
|
||||
// +Archetype UI
|
||||
// ====================
|
||||
$ui-action-primary-color: $blue-u2;
|
||||
$ui-action-primary-color-focus: $blue-s1;
|
||||
|
||||
$ui-link-color: $blue-u2;
|
||||
$ui-link-color-focus: $blue-s1;
|
||||
|
||||
// +Specific UI
|
||||
// ====================
|
||||
$ui-notification-height: ($baseline*10);
|
||||
$ui-update-color: $blue-l4;
|
||||
|
||||
// +Deprecated
|
||||
// ====================
|
||||
// do not use, future clean up will use updated styles
|
||||
$baseFontColor: $gray-d2;
|
||||
$lighter-base-font-color: rgb(100,100,100);
|
||||
$offBlack: #3c3c3c;
|
||||
$green: #108614;
|
||||
$lightGrey: #edf1f5;
|
||||
$mediumGrey: #b0b6c2;
|
||||
$darkGrey: #8891a1;
|
||||
$extraDarkGrey: #3d4043;
|
||||
$paleYellow: #fffcf1;
|
||||
$yellow: rgb(255, 254, 223);
|
||||
$green: rgb(37, 184, 90);
|
||||
$brightGreen: rgb(22, 202, 87);
|
||||
$disabledGreen: rgb(124, 206, 153);
|
||||
$darkGreen: rgb(52, 133, 76);
|
||||
|
||||
// These colors are updated for testing purposes
|
||||
$lightBluishGrey: rgb(0, 250, 0);
|
||||
$lightBluishGrey2: rgb(0, 250, 0);
|
||||
$error-red: rgb(253, 87, 87);
|
||||
|
||||
|
||||
//carryover from LMS for xmodules
|
||||
$sidebar-color: rgb(246, 246, 246);
|
||||
|
||||
// type
|
||||
$sans-serif: $f-sans-serif;
|
||||
$body-line-height: golden-ratio(.875em, 1);
|
||||
|
||||
// carried over from LMS for xmodules
|
||||
$action-primary-active-bg: #1AA1DE; // $m-blue
|
||||
$very-light-text: $white;
|
||||
@@ -1,55 +0,0 @@
|
||||
<%inherit file="base.html" />
|
||||
<%def name="online_help_token()"><% return "login" %></%def>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
<%block name="title">${_("Sign In")}</%block>
|
||||
<%block name="bodyclass">not-signedin view-signin</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<header>
|
||||
<h1 class="title title-1">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</h1>
|
||||
<a href="${reverse('signup')}" class="action action-signin">${_("Don't have a {studio_name} Account? Sign up!").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
|
||||
</header>
|
||||
<!-- Login Page override for test-theme. -->
|
||||
<article class="content-primary" role="main">
|
||||
<form id="login_form" method="post" action="login_post" onsubmit="return false;">
|
||||
|
||||
<fieldset>
|
||||
<legend class="sr">${_("Required Information to Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</legend>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf }" />
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text required" id="field-email">
|
||||
<label for="email">${_("E-mail")}</label>
|
||||
<input id="email" type="email" name="email" placeholder="${_('example: username@domain.com')}"/>
|
||||
</li>
|
||||
|
||||
<li class="field text required" id="field-password">
|
||||
<label for="password">${_("Password")}</label>
|
||||
<input id="password" type="password" name="password" />
|
||||
<a href="${forgot_password_link}" class="action action-forgotpassword">${_("Forgot password?")}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" id="submit" name="submit" class="action action-primary">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</button>
|
||||
</div>
|
||||
|
||||
<!-- no honor code for CMS, but need it because we're using the lms student object -->
|
||||
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/login"], function(LoginFactory) {
|
||||
LoginFactory("${reverse('homepage')}");
|
||||
});
|
||||
</%block>
|
||||
@@ -1 +0,0 @@
|
||||
*.css
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 493 B |
@@ -1,5 +0,0 @@
|
||||
@import 'lms/static/sass/partials/base/variables';
|
||||
|
||||
$header-bg: rgb(0,250,0);
|
||||
$footer-bg: rgb(0,250,0);
|
||||
$container-bg: rgb(0,250,0);
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="wrapper wrapper-footer">
|
||||
<footer>
|
||||
<div class="colophon">
|
||||
<div class="colophon-about">
|
||||
<p>This is a footer for test-theme.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
</div>
|
||||
@@ -10,7 +10,7 @@ import mock
|
||||
import ddt
|
||||
from config_models.models import cache
|
||||
from branding.models import BrandingApiConfig
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme_context
|
||||
from openedx.core.djangoapps.theming.test_util import with_edx_domain_context
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -30,19 +30,19 @@ class TestFooter(TestCase):
|
||||
|
||||
@ddt.data(
|
||||
# Open source version
|
||||
(None, "application/json", "application/json; charset=utf-8", "Open edX"),
|
||||
(None, "text/html", "text/html; charset=utf-8", "lms-footer.css"),
|
||||
(None, "text/html", "text/html; charset=utf-8", "Open edX"),
|
||||
(False, "application/json", "application/json; charset=utf-8", "Open edX"),
|
||||
(False, "text/html", "text/html; charset=utf-8", "lms-footer.css"),
|
||||
(False, "text/html", "text/html; charset=utf-8", "Open edX"),
|
||||
|
||||
# EdX.org version
|
||||
("edx.org", "application/json", "application/json; charset=utf-8", "edX Inc"),
|
||||
("edx.org", "text/html", "text/html; charset=utf-8", "lms-footer-edx.css"),
|
||||
("edx.org", "text/html", "text/html; charset=utf-8", "edX Inc"),
|
||||
(True, "application/json", "application/json; charset=utf-8", "edX Inc"),
|
||||
(True, "text/html", "text/html; charset=utf-8", "lms-footer-edx.css"),
|
||||
(True, "text/html", "text/html; charset=utf-8", "edX Inc"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_footer_content_types(self, theme, accepts, content_type, content):
|
||||
def test_footer_content_types(self, is_edx_domain, accepts, content_type, content):
|
||||
self._set_feature_flag(True)
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
resp = self._get_footer(accepts=accepts)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -50,10 +50,10 @@ class TestFooter(TestCase):
|
||||
self.assertIn(content, resp.content)
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {'ENABLE_FOOTER_MOBILE_APP_LINKS': True})
|
||||
@ddt.data("edx.org", None)
|
||||
def test_footer_json(self, theme):
|
||||
@ddt.data(True, False)
|
||||
def test_footer_json(self, is_edx_domain):
|
||||
self._set_feature_flag(True)
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
resp = self._get_footer()
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -142,18 +142,18 @@ class TestFooter(TestCase):
|
||||
|
||||
@ddt.data(
|
||||
# OpenEdX
|
||||
(None, "en", "lms-footer.css"),
|
||||
(None, "ar", "lms-footer-rtl.css"),
|
||||
(False, "en", "lms-footer.css"),
|
||||
(False, "ar", "lms-footer-rtl.css"),
|
||||
|
||||
# EdX.org
|
||||
("edx.org", "en", "lms-footer-edx.css"),
|
||||
("edx.org", "ar", "lms-footer-edx-rtl.css"),
|
||||
(True, "en", "lms-footer-edx.css"),
|
||||
(True, "ar", "lms-footer-edx-rtl.css"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_language_rtl(self, theme, language, static_path):
|
||||
def test_language_rtl(self, is_edx_domain, language, static_path):
|
||||
self._set_feature_flag(True)
|
||||
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
resp = self._get_footer(accepts="text/html", params={'language': language})
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -161,18 +161,18 @@ class TestFooter(TestCase):
|
||||
|
||||
@ddt.data(
|
||||
# OpenEdX
|
||||
(None, True),
|
||||
(None, False),
|
||||
(False, True),
|
||||
(False, False),
|
||||
|
||||
# EdX.org
|
||||
("edx.org", True),
|
||||
("edx.org", False),
|
||||
(True, True),
|
||||
(True, False),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_show_openedx_logo(self, theme, show_logo):
|
||||
def test_show_openedx_logo(self, is_edx_domain, show_logo):
|
||||
self._set_feature_flag(True)
|
||||
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
params = {'show-openedx-logo': 1} if show_logo else {}
|
||||
resp = self._get_footer(accepts="text/html", params=params)
|
||||
|
||||
@@ -185,17 +185,17 @@ class TestFooter(TestCase):
|
||||
|
||||
@ddt.data(
|
||||
# OpenEdX
|
||||
(None, False),
|
||||
(None, True),
|
||||
(False, False),
|
||||
(False, True),
|
||||
|
||||
# EdX.org
|
||||
("edx.org", False),
|
||||
("edx.org", True),
|
||||
(True, False),
|
||||
(True, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_include_dependencies(self, theme, include_dependencies):
|
||||
def test_include_dependencies(self, is_edx_domain, include_dependencies):
|
||||
self._set_feature_flag(True)
|
||||
with with_comprehensive_theme_context(theme):
|
||||
with with_edx_domain_context(is_edx_domain):
|
||||
params = {'include-dependencies': 1} if include_dependencies else {}
|
||||
resp = self._get_footer(accepts="text/html", params=params)
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
# -*- 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'}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Tests for wiki middleware.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.test.client import Client
|
||||
from nose.plugins.attrib import attr
|
||||
from wiki.models import URLPath
|
||||
@@ -32,7 +33,7 @@ class TestComprehensiveTheming(ModuleStoreTestCase):
|
||||
self.client = Client()
|
||||
self.client.login(username='instructor', password='secret')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
|
||||
def test_themed_footer(self):
|
||||
"""
|
||||
Tests that theme footer is used rather than standard
|
||||
|
||||
@@ -6,6 +6,8 @@ import re
|
||||
import cgi
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
@@ -48,6 +50,21 @@ def course_wiki_redirect(request, course_id): # pylint: disable=unused-argument
|
||||
if not valid_slug:
|
||||
return redirect("wiki:get", path="")
|
||||
|
||||
# The wiki needs a Site object created. We make sure it exists here
|
||||
try:
|
||||
Site.objects.get_current()
|
||||
except Site.DoesNotExist:
|
||||
new_site = Site()
|
||||
new_site.domain = settings.SITE_NAME
|
||||
new_site.name = "edX"
|
||||
new_site.save()
|
||||
site_id = str(new_site.id)
|
||||
if site_id != str(settings.SITE_ID):
|
||||
msg = "No site object was created and the SITE_ID doesn't match the newly created one. {} != {}".format(
|
||||
site_id, settings.SITE_ID
|
||||
)
|
||||
raise ImproperlyConfigured(msg)
|
||||
|
||||
try:
|
||||
urlpath = URLPath.get_by_path(course_slug, select_related=True)
|
||||
|
||||
|
||||
@@ -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',
|
||||
@@ -2878,10 +2874,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
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
// ------------------------------
|
||||
// Open edX Certificates: Shared Build Compile
|
||||
|
||||
// About: Sass compile for Open edX Certificates elements that are shared between LTR and RTL UI.
|
||||
// Configuration and vendor specific imports happen before this shared set of imports are compiled
|
||||
// in the main-*.scss files.
|
||||
// About: Sass compile for Open edX Certificates elements that are shared between LTR and RTL UI. Configuration and vendor specific imports happen before this shared set of imports are compiled in the main-*.scss files.
|
||||
|
||||
|
||||
// Configuration
|
||||
// ------------------------------
|
||||
// #CONFIG + LIB
|
||||
// ------------------------------
|
||||
@import 'lib';
|
||||
@import 'config';
|
||||
@import '../../../../node_modules/edx-pattern-library/pattern-library/sass/edx-pattern-library';
|
||||
|
||||
// Extensions
|
||||
// ------------------------------
|
||||
// #EXTENSIONS
|
||||
// ------------------------------
|
||||
@import 'utilities';
|
||||
@import 'base';
|
||||
@import 'components';
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
// ------------------------------
|
||||
// Open edX Certificates: Config
|
||||
|
||||
// About: variable and configuration overrides
|
||||
|
||||
// #VARIABLES
|
||||
|
||||
// ------------------------------
|
||||
// #VARIABLES
|
||||
// ------------------------------
|
||||
$pattern-library-path: '../../edx-pattern-library' !default;
|
||||
|
||||
// certificate characteristics
|
||||
$cert-base-color: palette(grayscale-cool, dark);
|
||||
|
||||
9
lms/static/certificates/sass/_lib.scss
Normal file
9
lms/static/certificates/sass/_lib.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
// ------------------------------
|
||||
// Open edX Certificates: Main Style Compile
|
||||
|
||||
// About: third party libraries and dependencies import
|
||||
|
||||
|
||||
@import '../../../../node_modules/edx-pattern-library/node_modules/bourbon/app/assets/stylesheets/bourbon';
|
||||
@import '../../../../node_modules/edx-pattern-library/node_modules/susy/sass/susy';
|
||||
@import '../../../../node_modules/edx-pattern-library/node_modules/breakpoint-sass/stylesheets/breakpoint';
|
||||
23
lms/static/certificates/sass/_ltr.scss
Normal file
23
lms/static/certificates/sass/_ltr.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
// ------------------------------
|
||||
// Open edX Certificates: Main Style Compile
|
||||
|
||||
// About: Sass partial for defining settings and utilities for LTR-centric layouts.
|
||||
|
||||
// #SETTINGS
|
||||
// #LIB
|
||||
|
||||
|
||||
// ----------------------------
|
||||
// #SETTINGS
|
||||
// ----------------------------
|
||||
$layout-direction: ltr;
|
||||
|
||||
// currently needed since platform Sass won't obey https://github.com/edx/ux-pattern-library/blob/master/pattern-library/sass/patterns/_grid.scss#L23
|
||||
$grid-direction-default: ltr;
|
||||
$grid-direction-reversed: ltr;
|
||||
|
||||
|
||||
// ----------------------------
|
||||
// #LIB
|
||||
// ----------------------------
|
||||
@import '../../../../node_modules/edx-pattern-library/node_modules/bi-app-sass/bi-app/bi-app-ltr';
|
||||
23
lms/static/certificates/sass/_rtl.scss
Normal file
23
lms/static/certificates/sass/_rtl.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
// ------------------------------
|
||||
// Open edX Certificates: Main Style Compile
|
||||
|
||||
// About: Sass partial for defining settings and utilities for LTR-centric layouts.
|
||||
|
||||
// #SETTINGS
|
||||
// #LIB
|
||||
|
||||
|
||||
// ----------------------------
|
||||
// #SETTINGS
|
||||
// ----------------------------
|
||||
$layout-direction: rtl;
|
||||
|
||||
// currently needed since platform Sass won't obey https://github.com/edx/ux-pattern-library/blob/master/pattern-library/sass/patterns/_grid.scss#L23
|
||||
$grid-direction-default: rtl;
|
||||
$grid-direction-reversed: ltr;
|
||||
|
||||
|
||||
// ----------------------------
|
||||
// #LIB
|
||||
// ----------------------------
|
||||
@import '../../../../node_modules/edx-pattern-library/node_modules/bi-app-sass/bi-app/bi-app-rtl';
|
||||
@@ -3,13 +3,16 @@
|
||||
|
||||
// About: Sass compile for the Open edX Certificates Elements.
|
||||
|
||||
// NOTE: This is the left to right (LTR) configured style compile.
|
||||
// It should mirror main-rtl w/ the exception of bi-app references.
|
||||
// NOTE: This is the left to right (LTR) configured style compile. It should mirror main-rtl w/ the exception of bi-app references.
|
||||
|
||||
|
||||
// Load the LTR version of the edX Pattern Library
|
||||
$pattern-library-path: '../../edx-pattern-library' !default;
|
||||
@import 'edx-pattern-library/pattern-library/sass/edx-pattern-library-ltr';
|
||||
// ------------------------------
|
||||
// #CONFIG - layout direction
|
||||
// ------------------------------
|
||||
@import 'ltr'; // LTR-specifc settings and utilities
|
||||
|
||||
// Load the shared build
|
||||
@import 'build';
|
||||
|
||||
// ------------------------------
|
||||
// #BUILD
|
||||
// ------------------------------
|
||||
@import 'build'; // shared compile/build order for both LTR and RTL UI
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
|
||||
// About: Sass compile for the Open edX Certificates Elements.
|
||||
|
||||
// NOTE: This is the right to left (RTL) configured style compile.
|
||||
// It should mirror main-ltr w/ the exception of bi-app references.
|
||||
// NOTE: This is the right to left (RTL) configured style compile. It should mirror main-ltr w/ the exception of bi-app references.
|
||||
|
||||
|
||||
// Load the RTL version of the edX Pattern Library
|
||||
$pattern-library-path: '../../edx-pattern-library' !default;
|
||||
@import 'edx-pattern-library/pattern-library/sass/edx-pattern-library-rtl';
|
||||
// ------------------------------
|
||||
// #CONFIG - layout direction
|
||||
// ------------------------------
|
||||
@import 'rtl'; // RTL-specifc settings and utilities
|
||||
|
||||
// Load the shared build
|
||||
@import 'build';
|
||||
|
||||
// ------------------------------
|
||||
// #BUILD
|
||||
// ------------------------------
|
||||
@import 'build'; // shared compile/build order for both LTR and RTL UI
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// ------------------------------
|
||||
// LMS: Shared Build Compile
|
||||
// Version 2 - introduces the Pattern Library
|
||||
|
||||
|
||||
// Configuration
|
||||
@import 'config';
|
||||
|
||||
// Extensions
|
||||
@@ -1,7 +0,0 @@
|
||||
// ------------------------------
|
||||
// LMS configuration settings
|
||||
|
||||
|
||||
// ------------------------------
|
||||
// #VARIABLES
|
||||
// ------------------------------
|
||||
@@ -1,5 +1,4 @@
|
||||
// LMS - CSS application architecture
|
||||
// Version 1 styling (pre-Pattern Library)
|
||||
// lms - css application architecture
|
||||
// ====================
|
||||
|
||||
// libs and resets *do not edit*
|
||||
@@ -19,4 +18,4 @@
|
||||
// theme, for old-style deprecated theming.
|
||||
//<THEME-OVERRIDE>
|
||||
|
||||
@import 'build-lms-v1'; // shared app style assets/rendering
|
||||
@import 'build-lms'; // shared app style assets/rendering
|
||||
@@ -1,14 +0,0 @@
|
||||
// ------------------------------
|
||||
// LMS main styling
|
||||
// Version 2 - introduces the Pattern Library
|
||||
|
||||
// NOTE: This is the right-to-left (RTL) configured style compile.
|
||||
// It should mirror lms-main-v2 w/ the exception of bi-app references.
|
||||
|
||||
|
||||
// Load the RTL version of the edX Pattern Library
|
||||
$pattern-library-path: '../edx-pattern-library' !default;
|
||||
@import 'edx-pattern-library/pattern-library/sass/edx-pattern-library-rtl';
|
||||
|
||||
// Load the shared build
|
||||
@import 'build-lms-v2';
|
||||
@@ -1,14 +0,0 @@
|
||||
// ------------------------------
|
||||
// LMS main styling
|
||||
// Version 2 - introduces the Pattern Library
|
||||
|
||||
// NOTE: This is the left-to-right (LTR) configured style compile.
|
||||
// It should mirror lms-main-v2-rtl w/ the exception of bi-app references.
|
||||
|
||||
|
||||
// Load the RTL version of the edX Pattern Library
|
||||
$pattern-library-path: '../edx-pattern-library' !default;
|
||||
@import 'edx-pattern-library/pattern-library/sass/edx-pattern-library-ltr';
|
||||
|
||||
// Load the shared build
|
||||
@import 'build-lms-v2';
|
||||
@@ -1,5 +1,4 @@
|
||||
// LMS - CSS application architecture
|
||||
// Version 1 styling (pre-Pattern Library)
|
||||
// lms - css application architecture
|
||||
// ====================
|
||||
|
||||
// libs and resets *do not edit*
|
||||
@@ -18,4 +17,4 @@
|
||||
// theme, for old-style deprecated theming.
|
||||
//<THEME-OVERRIDE>
|
||||
|
||||
@import 'build-lms-v1'; // shared app style assets/rendering
|
||||
@import 'build-lms'; // shared app style assets/rendering
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -60,7 +60,17 @@ from pipeline_mako import render_require_js_path_overrides
|
||||
<link rel="icon" type="image/x-icon" href="${static.url(static.get_value('favicon_path', settings.FAVICON_PATH))}" />
|
||||
|
||||
<%static:css group='style-vendor'/>
|
||||
<%static:css group='${self.attr.main_css}'/>
|
||||
## We could do <%static:css group='style-main'/>, but that's only useful
|
||||
## if the group contains multiple files, and the 'style-main' group doesn't.
|
||||
## Instead, we'll construct this <link> element manually, to improve clarity.
|
||||
## When nothing in the system is referencing the 'style-main' group, it can
|
||||
## be removed from the environment file.
|
||||
<%
|
||||
application_css_path = "css/lms-main{rtl}.css".format(
|
||||
rtl="-rtl" if get_language_bidi() else "",
|
||||
)
|
||||
%>
|
||||
<link rel="stylesheet" href="${static.url(application_css_path)}" type="text/css" media="all" />
|
||||
|
||||
% if disable_courseware_js:
|
||||
<%static:js group='base_vendor'/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
{% load sekizai_tags i18n microsite theme_pipeline optional_include %}
|
||||
{% load sekizai_tags i18n microsite pipeline optional_include %}
|
||||
{% load url from future %}
|
||||
<html lang="{{LANGUAGE_CODE}}">
|
||||
<head>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="/main.html" />
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%block name="pagetitle">UX Reference</%block>
|
||||
<%block name="nav_skip">#content</%block>
|
||||
|
||||
<%block name="bodyclass">view-ux-reference</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="main-column">
|
||||
<article class="window unit-body">
|
||||
<h1>UX Style Reference</h1>
|
||||
|
||||
<section class="xblock xblock-student_view xmodule_display xmodule_HtmlModule">
|
||||
<h2>Page Types</h2>
|
||||
<ul>
|
||||
<li><a href="pattern-library-test.html">Pattern Library test page</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "main_django.html" %}
|
||||
{% load theme_pipeline %}{% load sekizai_tags i18n microsite %}{% load url from future %}{% load staticfiles %}
|
||||
{% load pipeline %}{% load sekizai_tags i18n microsite %}{% load url from future %}{% load staticfiles %}
|
||||
|
||||
{% block title %}<title>{% block pagetitle %}{% endblock %} | {% trans "Wiki" %} | {% platform_name %}</title>{% endblock %}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
## 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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
## mako
|
||||
<%page expression_filter="h"/>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
{% load wiki_tags i18n %}{% load theme_pipeline %}
|
||||
{% load wiki_tags i18n %}{% load pipeline %}
|
||||
<html lang="{{LANGUAGE_CODE}}">
|
||||
<head>
|
||||
{% stylesheet 'course' %}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
"""
|
||||
Django admin page for theming models
|
||||
"""
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import (
|
||||
SiteTheme,
|
||||
)
|
||||
|
||||
|
||||
class SiteThemeAdmin(admin.ModelAdmin):
|
||||
""" Admin interface for the SiteTheme object. """
|
||||
list_display = ('site', 'theme_dir_name')
|
||||
search_fields = ('site__domain', 'theme_dir_name')
|
||||
|
||||
class Meta(object):
|
||||
"""
|
||||
Meta class for SiteTheme admin model
|
||||
"""
|
||||
model = SiteTheme
|
||||
|
||||
admin.site.register(SiteTheme, SiteThemeAdmin)
|
||||
@@ -1,32 +1,62 @@
|
||||
"""
|
||||
Core logic for Comprehensive Theming.
|
||||
"""
|
||||
import os.path
|
||||
from path import Path as path
|
||||
from path import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from .helpers import (
|
||||
get_project_root_name,
|
||||
)
|
||||
|
||||
def comprehensive_theme_changes(theme_dir):
|
||||
"""
|
||||
Calculate the set of changes needed to enable a comprehensive theme.
|
||||
|
||||
Arguments:
|
||||
theme_dir (path.path): the full path to the theming directory to use.
|
||||
|
||||
Returns:
|
||||
A dict indicating the changes to make:
|
||||
|
||||
* 'settings': a dictionary of settings names and their new values.
|
||||
|
||||
* 'template_paths': a list of directories to prepend to template
|
||||
lookup path.
|
||||
|
||||
"""
|
||||
|
||||
changes = {
|
||||
'settings': {},
|
||||
'template_paths': [],
|
||||
}
|
||||
root = Path(settings.PROJECT_ROOT)
|
||||
if root.name == "":
|
||||
root = root.parent
|
||||
|
||||
component_dir = theme_dir / root.name
|
||||
|
||||
templates_dir = component_dir / "templates"
|
||||
if templates_dir.isdir():
|
||||
changes['template_paths'].append(templates_dir)
|
||||
|
||||
staticfiles_dir = component_dir / "static"
|
||||
if staticfiles_dir.isdir():
|
||||
changes['settings']['STATICFILES_DIRS'] = [staticfiles_dir] + settings.STATICFILES_DIRS
|
||||
|
||||
locale_dir = component_dir / "conf" / "locale"
|
||||
if locale_dir.isdir():
|
||||
changes['settings']['LOCALE_PATHS'] = [locale_dir] + settings.LOCALE_PATHS
|
||||
|
||||
return changes
|
||||
|
||||
|
||||
def enable_comprehensive_theming(themes_dir):
|
||||
def enable_comprehensive_theme(theme_dir):
|
||||
"""
|
||||
Add directories to relevant paths for comprehensive theming.
|
||||
:param themes_dir: path to base theme directory
|
||||
"""
|
||||
if isinstance(themes_dir, basestring):
|
||||
themes_dir = path(themes_dir)
|
||||
changes = comprehensive_theme_changes(theme_dir)
|
||||
|
||||
if themes_dir.isdir():
|
||||
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].insert(0, themes_dir)
|
||||
settings.MAKO_TEMPLATES['main'].insert(0, themes_dir)
|
||||
|
||||
for theme_dir in os.listdir(themes_dir):
|
||||
staticfiles_dir = os.path.join(themes_dir, theme_dir, get_project_root_name(), "static")
|
||||
if staticfiles_dir.isdir():
|
||||
settings.STATICFILES_DIRS = settings.STATICFILES_DIRS + [staticfiles_dir]
|
||||
|
||||
locale_dir = os.path.join(themes_dir, theme_dir, get_project_root_name(), "conf", "locale")
|
||||
if locale_dir.isdir():
|
||||
settings.LOCALE_PATHS = (locale_dir, ) + settings.LOCALE_PATHS
|
||||
# Use the changes
|
||||
for name, value in changes['settings'].iteritems():
|
||||
setattr(settings, name, value)
|
||||
for template_dir in changes['template_paths']:
|
||||
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].insert(0, template_dir)
|
||||
settings.MAKO_TEMPLATES['main'].insert(0, template_dir)
|
||||
|
||||
@@ -17,80 +17,63 @@ interface, as well.
|
||||
.. _Django-Pipeline: http://django-pipeline.readthedocs.org/
|
||||
.. _Django-Require: https://github.com/etianen/django-require
|
||||
"""
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
from path import Path
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.contrib.staticfiles import utils
|
||||
from django.contrib.staticfiles.finders import BaseFinder
|
||||
from django.utils import six
|
||||
|
||||
from openedx.core.djangoapps.theming.helpers import get_themes
|
||||
from openedx.core.djangoapps.theming.storage import ThemeStorage
|
||||
from openedx.core.djangoapps.theming.storage import CachedComprehensiveThemingStorage
|
||||
|
||||
|
||||
class ThemeFilesFinder(BaseFinder):
|
||||
class ComprehensiveThemeFinder(BaseFinder):
|
||||
"""
|
||||
A static files finder that looks in the directory of each theme as
|
||||
specified in the source_dir attribute.
|
||||
A static files finder that searches the active comprehensive theme
|
||||
for static files. If the ``COMPREHENSIVE_THEME_DIR`` setting is unset,
|
||||
or the ``COMPREHENSIVE_THEME_DIR`` does not exist on the file system,
|
||||
this finder will never find any files.
|
||||
"""
|
||||
storage_class = ThemeStorage
|
||||
source_dir = 'static'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# The list of themes that are handled
|
||||
self.themes = []
|
||||
# Mapping of theme names to storage instances
|
||||
self.storages = OrderedDict()
|
||||
super(ComprehensiveThemeFinder, self).__init__(*args, **kwargs)
|
||||
|
||||
themes = get_themes()
|
||||
for theme in themes:
|
||||
theme_storage = self.storage_class(
|
||||
os.path.join(theme.path, self.source_dir),
|
||||
prefix=theme.theme_dir,
|
||||
)
|
||||
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
|
||||
if not theme_dir:
|
||||
self.storage = None
|
||||
return
|
||||
|
||||
self.storages[theme.theme_dir] = theme_storage
|
||||
if theme.theme_dir not in self.themes:
|
||||
self.themes.append(theme.theme_dir)
|
||||
if not isinstance(theme_dir, basestring):
|
||||
raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
|
||||
|
||||
super(ThemeFilesFinder, self).__init__(*args, **kwargs)
|
||||
root = Path(settings.PROJECT_ROOT)
|
||||
if root.name == "":
|
||||
root = root.parent
|
||||
|
||||
def list(self, ignore_patterns):
|
||||
"""
|
||||
List all files in all app storages.
|
||||
"""
|
||||
for storage in six.itervalues(self.storages):
|
||||
if storage.exists(''): # check if storage location exists
|
||||
for path in utils.get_files(storage, ignore_patterns):
|
||||
yield path, storage
|
||||
component_dir = Path(theme_dir) / root.name
|
||||
static_dir = component_dir / "static"
|
||||
self.storage = CachedComprehensiveThemingStorage(location=static_dir)
|
||||
|
||||
def find(self, path, all=False): # pylint: disable=redefined-builtin
|
||||
"""
|
||||
Looks for files in the theme directories.
|
||||
Looks for files in the default file storage, if it's local.
|
||||
"""
|
||||
matches = []
|
||||
theme_dir = path.split("/", 1)[0]
|
||||
if not self.storage:
|
||||
return []
|
||||
|
||||
themes = {t.theme_dir: t for t in get_themes()}
|
||||
# if path is prefixed by theme name then search in the corresponding storage other wise search all storages.
|
||||
if theme_dir in themes:
|
||||
theme = themes[theme_dir]
|
||||
path = "/".join(path.split("/")[1:])
|
||||
match = self.find_in_theme(theme.theme_dir, path)
|
||||
if match:
|
||||
if not all:
|
||||
return match
|
||||
matches.append(match)
|
||||
return matches
|
||||
if path.startswith(self.storage.prefix):
|
||||
# strip the prefix
|
||||
path = path[len(self.storage.prefix):]
|
||||
|
||||
def find_in_theme(self, theme, path):
|
||||
if self.storage.exists(path):
|
||||
match = self.storage.path(path)
|
||||
if all:
|
||||
match = [match]
|
||||
return match
|
||||
|
||||
return []
|
||||
|
||||
def list(self, ignore_patterns):
|
||||
"""
|
||||
Find a requested static file in an theme's static locations.
|
||||
List all files of the storage.
|
||||
"""
|
||||
storage = self.storages.get(theme, None)
|
||||
if storage:
|
||||
# only try to find a file if the source dir actually exists
|
||||
if storage.exists(path):
|
||||
matched_path = storage.path(path)
|
||||
if matched_path:
|
||||
return matched_path
|
||||
if self.storage and self.storage.exists(''):
|
||||
for path in utils.get_files(self.storage, ignore_patterns):
|
||||
yield path, self.storage
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
"""
|
||||
Helpers for accessing comprehensive theming related variables.
|
||||
"""
|
||||
import re
|
||||
import os
|
||||
from path import Path
|
||||
|
||||
from django.conf import settings, ImproperlyConfigured
|
||||
from django.core.cache import cache
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
|
||||
from microsite_configuration import microsite
|
||||
from microsite_configuration import page_title_breadcrumbs
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def get_page_title_breadcrumbs(*args):
|
||||
@@ -31,11 +24,7 @@ def get_template_path(relative_path, **kwargs):
|
||||
"""
|
||||
This is a proxy function to hide microsite_configuration behind comprehensive theming.
|
||||
"""
|
||||
template_path = get_template_path_with_theme(relative_path)
|
||||
if template_path == relative_path: # we don't have a theme now look into microsites
|
||||
template_path = microsite.get_template_path(relative_path, **kwargs)
|
||||
|
||||
return template_path
|
||||
return microsite.get_template_path(relative_path, **kwargs)
|
||||
|
||||
|
||||
def is_request_in_themed_site():
|
||||
@@ -45,14 +34,6 @@ def is_request_in_themed_site():
|
||||
return microsite.is_request_in_microsite()
|
||||
|
||||
|
||||
def get_template(uri):
|
||||
"""
|
||||
This is a proxy function to hide microsite_configuration behind comprehensive theming.
|
||||
:param uri: uri of the template
|
||||
"""
|
||||
return microsite.get_template(uri)
|
||||
|
||||
|
||||
def get_themed_template_path(relative_path, default_path, **kwargs):
|
||||
"""
|
||||
This is a proxy function to hide microsite_configuration behind comprehensive theming.
|
||||
@@ -71,311 +52,3 @@ def get_themed_template_path(relative_path, default_path, **kwargs):
|
||||
if is_stanford_theming_enabled and not is_microsite:
|
||||
return relative_path
|
||||
return microsite.get_template_path(default_path, **kwargs)
|
||||
|
||||
|
||||
def get_template_path_with_theme(relative_path):
|
||||
"""
|
||||
Returns template path in current site's theme if it finds one there otherwise returns same path.
|
||||
|
||||
Example:
|
||||
>> get_template_path_with_theme('header')
|
||||
'/red-theme/lms/templates/header.html'
|
||||
|
||||
Parameters:
|
||||
relative_path (str): template's path relative to the templates directory e.g. 'footer.html'
|
||||
|
||||
Returns:
|
||||
(str): template path in current site's theme
|
||||
"""
|
||||
site_theme_dir = get_current_site_theme_dir()
|
||||
if not site_theme_dir:
|
||||
return relative_path
|
||||
|
||||
base_theme_dir = get_base_theme_dir()
|
||||
root_name = get_project_root_name()
|
||||
template_path = "/".join([
|
||||
base_theme_dir,
|
||||
site_theme_dir,
|
||||
root_name,
|
||||
"templates"
|
||||
])
|
||||
|
||||
# strip `/` if present at the start of relative_path
|
||||
template_name = re.sub(r'^/+', '', relative_path)
|
||||
search_path = os.path.join(template_path, template_name)
|
||||
if os.path.isfile(search_path):
|
||||
path = '/{site_theme_dir}/{root_name}/templates/{template_name}'.format(
|
||||
site_theme_dir=site_theme_dir,
|
||||
root_name=root_name,
|
||||
template_name=template_name,
|
||||
)
|
||||
return path
|
||||
else:
|
||||
return relative_path
|
||||
|
||||
|
||||
def get_current_theme_template_dirs():
|
||||
"""
|
||||
Returns template directories for the current theme.
|
||||
|
||||
Example:
|
||||
>> get_current_theme_template_dirs('header.html')
|
||||
['/edx/app/edxapp/edx-platform/themes/red-theme/lms/templates/', ]
|
||||
|
||||
Returns:
|
||||
(list): list of directories containing theme templates.
|
||||
"""
|
||||
site_theme_dir = get_current_site_theme_dir()
|
||||
if not site_theme_dir:
|
||||
return None
|
||||
|
||||
base_theme_dir = get_base_theme_dir()
|
||||
root_name = get_project_root_name()
|
||||
template_path = "/".join([
|
||||
base_theme_dir,
|
||||
site_theme_dir,
|
||||
root_name,
|
||||
"templates"
|
||||
])
|
||||
|
||||
return [template_path]
|
||||
|
||||
|
||||
def strip_site_theme_templates_path(uri):
|
||||
"""
|
||||
Remove site template theme path from the uri.
|
||||
|
||||
Example:
|
||||
>> strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
|
||||
'header.html'
|
||||
|
||||
Arguments:
|
||||
uri (str): template path from which to remove site theme path. e.g. '/red-theme/lms/templates/header.html'
|
||||
|
||||
Returns:
|
||||
(str): template path with site theme path removed.
|
||||
"""
|
||||
site_theme_dir = get_current_site_theme_dir()
|
||||
if not site_theme_dir:
|
||||
return uri
|
||||
|
||||
root_name = get_project_root_name()
|
||||
templates_path = "/".join([
|
||||
site_theme_dir,
|
||||
root_name,
|
||||
"templates"
|
||||
])
|
||||
|
||||
uri = re.sub(r'^/*' + templates_path + '/*', '', uri)
|
||||
return uri
|
||||
|
||||
|
||||
def get_current_site():
|
||||
"""
|
||||
Return current site.
|
||||
|
||||
Returns:
|
||||
(django.contrib.sites.models.Site): theme directory for current site
|
||||
"""
|
||||
from edxmako.middleware import REQUEST_CONTEXT
|
||||
request = getattr(REQUEST_CONTEXT, 'request', None)
|
||||
if not request:
|
||||
return None
|
||||
return getattr(request, 'site', None)
|
||||
|
||||
|
||||
def get_current_site_theme_dir():
|
||||
"""
|
||||
Return theme directory for the current site.
|
||||
|
||||
Example:
|
||||
>> get_current_site_theme_dir()
|
||||
'red-theme'
|
||||
|
||||
Returns:
|
||||
(str): theme directory for current site
|
||||
"""
|
||||
site = get_current_site()
|
||||
if not site:
|
||||
return None
|
||||
site_theme_dir = cache.get(get_site_theme_cache_key(site))
|
||||
|
||||
# if site theme dir is not in cache and comprehensive theming is enabled then pull it from db.
|
||||
if not site_theme_dir and is_comprehensive_theming_enabled():
|
||||
site_theme = site.themes.first() # pylint: disable=no-member
|
||||
if site_theme:
|
||||
site_theme_dir = site_theme.theme_dir_name
|
||||
cache_site_theme_dir(site, site_theme_dir)
|
||||
return site_theme_dir
|
||||
|
||||
|
||||
def get_project_root_name():
|
||||
"""
|
||||
Return root name for the current project
|
||||
|
||||
Example:
|
||||
>> get_project_root_name()
|
||||
'lms'
|
||||
# from studio
|
||||
>> get_project_root_name()
|
||||
'cms'
|
||||
|
||||
Returns:
|
||||
(str): component name of platform e.g lms, cms
|
||||
"""
|
||||
root = Path(settings.PROJECT_ROOT)
|
||||
if root.name == "":
|
||||
root = root.parent
|
||||
return root.name
|
||||
|
||||
|
||||
def get_base_theme_dir():
|
||||
"""
|
||||
Return base directory that contains all the themes.
|
||||
|
||||
Example:
|
||||
>> get_base_theme_dir()
|
||||
'/edx/app/edxapp/edx-platform/themes'
|
||||
|
||||
Returns:
|
||||
(Path): Base theme directory path
|
||||
"""
|
||||
themes_dir = settings.COMPREHENSIVE_THEME_DIR
|
||||
if not isinstance(themes_dir, basestring):
|
||||
raise ImproperlyConfigured("COMPREHENSIVE_THEME_DIR must be a string.")
|
||||
return Path(themes_dir)
|
||||
|
||||
|
||||
def is_comprehensive_theming_enabled():
|
||||
"""
|
||||
Returns boolean indicating whether comprehensive theming functionality is enabled or disabled.
|
||||
Example:
|
||||
>> is_comprehensive_theming_enabled()
|
||||
True
|
||||
|
||||
Returns:
|
||||
(bool): True if comprehensive theming is enabled else False
|
||||
"""
|
||||
return True if settings.COMPREHENSIVE_THEME_DIR else False
|
||||
|
||||
|
||||
def get_site_theme_cache_key(site):
|
||||
"""
|
||||
Return cache key for the given site.
|
||||
|
||||
Example:
|
||||
>> site = Site(domain='red-theme.org', name='Red Theme')
|
||||
>> get_site_theme_cache_key(site)
|
||||
'theming.site.red-theme.org'
|
||||
|
||||
Parameters:
|
||||
site (django.contrib.sites.models.Site): site where key needs to generated
|
||||
Returns:
|
||||
(str): a key to be used as cache key
|
||||
"""
|
||||
cache_key = "theming.site.{domain}".format(
|
||||
domain=site.domain
|
||||
)
|
||||
return cache_key
|
||||
|
||||
|
||||
def cache_site_theme_dir(site, theme_dir):
|
||||
"""
|
||||
Cache site's theme directory.
|
||||
|
||||
Example:
|
||||
>> site = Site(domain='red-theme.org', name='Red Theme')
|
||||
>> cache_site_theme_dir(site, 'red-theme')
|
||||
|
||||
Parameters:
|
||||
site (django.contrib.sites.models.Site): site for to cache
|
||||
theme_dir (str): theme directory for the given site
|
||||
"""
|
||||
cache.set(get_site_theme_cache_key(site), theme_dir, settings.THEME_CACHE_TIMEOUT)
|
||||
|
||||
|
||||
def get_static_file_url(asset):
|
||||
"""
|
||||
Returns url of the themed asset if asset is not themed than returns the default asset url.
|
||||
|
||||
Example:
|
||||
>> get_static_file_url('css/lms-main.css')
|
||||
'/static/red-theme/css/lms-main.css'
|
||||
|
||||
Parameters:
|
||||
asset (str): asset's path relative to the static files directory
|
||||
|
||||
Returns:
|
||||
(str): static asset's url
|
||||
"""
|
||||
return staticfiles_storage.url(asset)
|
||||
|
||||
|
||||
def get_themes():
|
||||
"""
|
||||
get a list of all themes known to the system.
|
||||
Returns:
|
||||
list of themes known to the system.
|
||||
"""
|
||||
themes_dir = get_base_theme_dir()
|
||||
# pick only directories and discard files in themes directory
|
||||
theme_names = []
|
||||
if themes_dir:
|
||||
theme_names = [_dir for _dir in os.listdir(themes_dir) if is_theme_dir(themes_dir / _dir)]
|
||||
|
||||
return [Theme(name, name) for name in theme_names]
|
||||
|
||||
|
||||
def is_theme_dir(_dir):
|
||||
"""
|
||||
Returns true if given dir contains theme overrides.
|
||||
A theme dir must have subdirectory 'lms' or 'cms' or both.
|
||||
|
||||
Args:
|
||||
_dir: directory path to check for a theme
|
||||
|
||||
Returns:
|
||||
Returns true if given dir is a theme directory.
|
||||
"""
|
||||
theme_sub_directories = {'lms', 'cms'}
|
||||
return bool(os.path.isdir(_dir) and theme_sub_directories.intersection(os.listdir(_dir)))
|
||||
|
||||
|
||||
class Theme(object):
|
||||
"""
|
||||
class to encapsulate theme related information.
|
||||
"""
|
||||
name = ''
|
||||
theme_dir = ''
|
||||
path = ''
|
||||
|
||||
def __init__(self, name='', theme_dir=''):
|
||||
"""
|
||||
init method for Theme
|
||||
Args:
|
||||
name: name if the theme
|
||||
theme_dir: directory name of the theme
|
||||
"""
|
||||
self.name = name
|
||||
self.theme_dir = theme_dir
|
||||
self.path = Path(get_base_theme_dir()) / theme_dir / get_project_root_name()
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Returns True if given theme is same as the self
|
||||
Args:
|
||||
other: Theme object to compare with self
|
||||
|
||||
Returns:
|
||||
(bool) True if two themes are the same else False
|
||||
"""
|
||||
return (self.theme_dir, self.path) == (other.theme_dir, other.path)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.theme_dir, self.path))
|
||||
|
||||
def __unicode__(self):
|
||||
return u"<Theme: {name} at '{path}'>".format(name=self.name, path=self.path)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sites', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SiteTheme',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('theme_dir_name', models.CharField(max_length=255)),
|
||||
('site', models.ForeignKey(related_name='themes', to='sites.Site')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
"""
|
||||
Django models supporting the Comprehensive Theming subsystem
|
||||
"""
|
||||
from django.db import models
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
|
||||
class SiteTheme(models.Model):
|
||||
"""
|
||||
This is where the information about the site's theme gets stored to the db.
|
||||
|
||||
`site` field is foreignkey to django Site model
|
||||
`theme_dir_name` contains directory name having Site's theme
|
||||
"""
|
||||
site = models.ForeignKey(Site, related_name='themes')
|
||||
theme_dir_name = models.CharField(max_length=255)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.theme_dir_name
|
||||
@@ -2,300 +2,87 @@
|
||||
Comprehensive Theming support for Django's collectstatic functionality.
|
||||
See https://docs.djangoproject.com/en/1.8/ref/contrib/staticfiles/
|
||||
"""
|
||||
import posixpath
|
||||
from path import Path
|
||||
import os.path
|
||||
from django.conf import settings
|
||||
from django.utils._os import safe_join
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
|
||||
from django.contrib.staticfiles.finders import find
|
||||
from django.utils.six.moves.urllib.parse import ( # pylint: disable=no-name-in-module, import-error
|
||||
unquote, urlsplit,
|
||||
)
|
||||
|
||||
from pipeline.storage import PipelineMixin
|
||||
|
||||
from openedx.core.djangoapps.theming.helpers import (
|
||||
get_base_theme_dir,
|
||||
get_project_root_name,
|
||||
get_current_site_theme_dir,
|
||||
get_themes,
|
||||
)
|
||||
from django.utils._os import safe_join
|
||||
|
||||
|
||||
class ThemeStorage(StaticFilesStorage):
|
||||
class ComprehensiveThemingAwareMixin(object):
|
||||
"""
|
||||
Comprehensive theme aware Static files storage.
|
||||
Mixin for Django storage system to make it aware of the currently-active
|
||||
comprehensive theme, so that it can generate theme-scoped URLs for themed
|
||||
static assets.
|
||||
"""
|
||||
# prefix for file path, this prefix is added at the beginning of file path before saving static files during
|
||||
# collectstatic command.
|
||||
# e.g. having "edx.org" as prefix will cause files to be saved as "edx.org/images/logo.png"
|
||||
# instead of "images/logo.png"
|
||||
prefix = None
|
||||
|
||||
def __init__(self, location=None, base_url=None, file_permissions_mode=None,
|
||||
directory_permissions_mode=None, prefix=None):
|
||||
|
||||
self.prefix = prefix
|
||||
super(ThemeStorage, self).__init__(
|
||||
location=location,
|
||||
base_url=base_url,
|
||||
file_permissions_mode=file_permissions_mode,
|
||||
directory_permissions_mode=directory_permissions_mode,
|
||||
)
|
||||
|
||||
def url(self, name):
|
||||
"""
|
||||
Returns url of the asset, themed url will be returned if the asset is themed otherwise default
|
||||
asset url will be returned.
|
||||
|
||||
Args:
|
||||
name: name of the asset, e.g. 'images/logo.png'
|
||||
|
||||
Returns:
|
||||
url of the asset, e.g. '/static/red-theme/images/logo.png' if current theme is red-theme and logo
|
||||
is provided by red-theme otherwise '/static/images/logo.png'
|
||||
"""
|
||||
prefix = ''
|
||||
theme_dir = get_current_site_theme_dir()
|
||||
|
||||
# get theme prefix from site address if if asset is accessed via a url
|
||||
if theme_dir:
|
||||
prefix = theme_dir
|
||||
|
||||
# get theme prefix from storage class, if asset is accessed during collectstatic run
|
||||
elif self.prefix:
|
||||
prefix = self.prefix
|
||||
|
||||
# join theme prefix with asset name if theme is applied and themed asset exists
|
||||
if prefix and self.themed(name, prefix):
|
||||
name = os.path.join(prefix, name)
|
||||
|
||||
return super(ThemeStorage, self).url(name)
|
||||
|
||||
def themed(self, name, theme):
|
||||
"""
|
||||
Returns True if given asset override is provided by the given theme otherwise returns False.
|
||||
Args:
|
||||
name: asset name e.g. 'images/logo.png'
|
||||
theme: theme name e.g. 'red-theme', 'edx.org'
|
||||
|
||||
Returns:
|
||||
True if given asset override is provided by the given theme otherwise returns False
|
||||
"""
|
||||
# in debug mode check static asset from within the project directory
|
||||
if settings.DEBUG:
|
||||
themes_location = get_base_theme_dir()
|
||||
# Nothing can be themed if we don't have a theme location or required params.
|
||||
if not all((themes_location, theme, name)):
|
||||
return False
|
||||
|
||||
themed_path = "/".join([
|
||||
themes_location,
|
||||
theme,
|
||||
get_project_root_name(),
|
||||
"static/"
|
||||
])
|
||||
name = name[1:] if name.startswith("/") else name
|
||||
path = safe_join(themed_path, name)
|
||||
return os.path.exists(path)
|
||||
# in live mode check static asset in the static files dir defined by "STATIC_ROOT" setting
|
||||
else:
|
||||
return self.exists(os.path.join(theme, name))
|
||||
|
||||
|
||||
class ComprehensiveThemingCachedFilesMixin(CachedFilesMixin):
|
||||
"""
|
||||
Comprehensive theme aware CachedFilesMixin.
|
||||
Main purpose of subclassing CachedFilesMixin is to override the following methods.
|
||||
1 - url
|
||||
2 - url_converter
|
||||
|
||||
url:
|
||||
This method takes asset name as argument and is responsible for adding hash to the name to support caching.
|
||||
This method is called during both collectstatic command and live server run.
|
||||
|
||||
When called during collectstatic command that name argument will be asset name inside STATIC_ROOT,
|
||||
for non themed assets it will be the usual path (e.g. 'images/logo.png') but for themed asset it will
|
||||
also contain themes dir prefix (e.g. 'red-theme/images/logo.png'). So, here we check whether the themed asset
|
||||
exists or not, if it exists we pass the same name up in the MRO chain for further processing and if it does not
|
||||
exists we strip theme name and pass the new asset name to the MRO chain for further processing.
|
||||
|
||||
When called during server run, we get the theme dir for the current site using `get_current_site_theme_dir` and
|
||||
make sure to prefix theme dir to the asset name. This is done to ensure the usage of correct hash in file name.
|
||||
e.g. if our red-theme overrides 'images/logo.png' and we do not prefix theme dir to the asset name, the hash for
|
||||
'{platform-dir}/lms/static/images/logo.png' would be used instead of
|
||||
'{themes_base_dir}/red-theme/images/logo.png'
|
||||
|
||||
url_converter:
|
||||
This function returns another function that is responsible for hashing urls that appear inside assets
|
||||
(e.g. url("images/logo.png") inside css). The method defined in the superclass adds a hash to file and returns
|
||||
relative url of the file.
|
||||
e.g. for url("../images/logo.png") it would return url("../images/logo.790c9a5340cb.png"). However we would
|
||||
want it to return absolute url (e.g. url("/static/images/logo.790c9a5340cb.png")) so that it works properly
|
||||
with themes.
|
||||
|
||||
The overridden method here simply comments out the two lines that convert absolute url to relative url,
|
||||
hence absolute urls are used instead of relative urls.
|
||||
"""
|
||||
|
||||
def url(self, name, force=False):
|
||||
"""
|
||||
Returns themed url for the given asset.
|
||||
"""
|
||||
theme_dir = get_current_site_theme_dir()
|
||||
if theme_dir and theme_dir not in name:
|
||||
# during server run, append theme name to the asset name if it is not already there
|
||||
# this is ensure that correct hash is created and default asset is not always
|
||||
# used to create hash of themed assets.
|
||||
name = os.path.join(theme_dir, name)
|
||||
parsed_name = urlsplit(unquote(name))
|
||||
clean_name = parsed_name.path.strip()
|
||||
asset_name = name
|
||||
if not self.exists(clean_name):
|
||||
# if themed asset does not exists then use default asset
|
||||
theme = name.split("/", 1)[0]
|
||||
# verify that themed asset was accessed
|
||||
if theme in [theme.theme_dir for theme in get_themes()]:
|
||||
asset_name = "/".join(name.split("/")[1:])
|
||||
|
||||
return super(ComprehensiveThemingCachedFilesMixin, self).url(asset_name, force)
|
||||
|
||||
def url_converter(self, name, template=None):
|
||||
"""
|
||||
This is an override of url_converter from CachedFilesMixin.
|
||||
It just comments out two lines at the end of the method.
|
||||
|
||||
The purpose of this override is to make converter method return absolute urls instead of relative urls.
|
||||
This behavior is necessary for theme overrides, as we get 404 on assets with relative urls on a themed site.
|
||||
"""
|
||||
if template is None:
|
||||
template = self.default_template
|
||||
|
||||
def converter(matchobj):
|
||||
"""
|
||||
Converts the matched URL depending on the parent level (`..`)
|
||||
and returns the normalized and hashed URL using the url method
|
||||
of the storage.
|
||||
"""
|
||||
matched, url = matchobj.groups()
|
||||
# Completely ignore http(s) prefixed URLs,
|
||||
# fragments and data-uri URLs
|
||||
if url.startswith(('#', 'http:', 'https:', 'data:', '//')):
|
||||
return matched
|
||||
name_parts = name.split(os.sep)
|
||||
# Using posix normpath here to remove duplicates
|
||||
url = posixpath.normpath(url)
|
||||
url_parts = url.split('/')
|
||||
parent_level, sub_level = url.count('..'), url.count('/')
|
||||
if url.startswith('/'):
|
||||
sub_level -= 1
|
||||
url_parts = url_parts[1:]
|
||||
if parent_level or not url.startswith('/'):
|
||||
start, end = parent_level + 1, parent_level
|
||||
else:
|
||||
if sub_level:
|
||||
if sub_level == 1:
|
||||
parent_level -= 1
|
||||
start, end = parent_level, 1
|
||||
else:
|
||||
start, end = 1, sub_level - 1
|
||||
joined_result = '/'.join(name_parts[:-start] + url_parts[end:])
|
||||
hashed_url = self.url(unquote(joined_result), force=True)
|
||||
|
||||
# NOTE:
|
||||
# following two lines are commented out so that absolute urls are used instead of relative urls
|
||||
# to make themed assets work correctly.
|
||||
#
|
||||
# The lines are commented and not removed to make future django upgrade easier and
|
||||
# show exactly what is changed in this method override
|
||||
#
|
||||
# file_name = hashed_url.split('/')[-1:]
|
||||
# relative_url = '/'.join(url.split('/')[:-1] + file_name)
|
||||
|
||||
# Return the hashed version to the file
|
||||
return template % unquote(hashed_url)
|
||||
|
||||
return converter
|
||||
|
||||
|
||||
class ThemePipelineMixin(PipelineMixin):
|
||||
"""
|
||||
Mixin to make sure themed assets are also packaged and used along with non themed assets.
|
||||
if a source asset for a particular package is not present then the default asset is used.
|
||||
|
||||
e.g. in the following package and for 'red-theme'
|
||||
'style-vendor': {
|
||||
'source_filenames': [
|
||||
'js/vendor/afontgarde/afontgarde.css',
|
||||
'css/vendor/font-awesome.css',
|
||||
'css/vendor/jquery.qtip.min.css',
|
||||
'css/vendor/responsive-carousel/responsive-carousel.css',
|
||||
'css/vendor/responsive-carousel/responsive-carousel.slide.css',
|
||||
],
|
||||
'output_filename': 'css/lms-style-vendor.css'
|
||||
}
|
||||
'red-theme/css/vendor/responsive-carousel/responsive-carousel.css' will be used of it exists otherwise
|
||||
'css/vendor/responsive-carousel/responsive-carousel.css' will be used to create 'red-theme/css/lms-style-vendor.css'
|
||||
"""
|
||||
packing = True
|
||||
|
||||
def post_process(self, paths, dry_run=False, **options):
|
||||
"""
|
||||
This post_process hook is used to package all themed assets.
|
||||
"""
|
||||
if dry_run:
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ComprehensiveThemingAwareMixin, self).__init__(*args, **kwargs)
|
||||
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
|
||||
if not theme_dir:
|
||||
self.theme_location = None
|
||||
return
|
||||
themes = get_themes()
|
||||
|
||||
for theme in themes:
|
||||
css_packages = self.get_themed_packages(theme.theme_dir, settings.PIPELINE_CSS)
|
||||
js_packages = self.get_themed_packages(theme.theme_dir, settings.PIPELINE_JS)
|
||||
if not isinstance(theme_dir, basestring):
|
||||
raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
|
||||
|
||||
from pipeline.packager import Packager
|
||||
packager = Packager(storage=self, css_packages=css_packages, js_packages=js_packages)
|
||||
for package_name in packager.packages['css']:
|
||||
package = packager.package_for('css', package_name)
|
||||
output_file = package.output_filename
|
||||
if self.packing:
|
||||
packager.pack_stylesheets(package)
|
||||
paths[output_file] = (self, output_file)
|
||||
yield output_file, output_file, True
|
||||
for package_name in packager.packages['js']:
|
||||
package = packager.package_for('js', package_name)
|
||||
output_file = package.output_filename
|
||||
if self.packing:
|
||||
packager.pack_javascripts(package)
|
||||
paths[output_file] = (self, output_file)
|
||||
yield output_file, output_file, True
|
||||
root = Path(settings.PROJECT_ROOT)
|
||||
if root.name == "":
|
||||
root = root.parent
|
||||
|
||||
super_class = super(ThemePipelineMixin, self)
|
||||
if hasattr(super_class, 'post_process'):
|
||||
for name, hashed_name, processed in super_class.post_process(paths.copy(), dry_run, **options):
|
||||
yield name, hashed_name, processed
|
||||
component_dir = Path(theme_dir) / root.name
|
||||
self.theme_location = component_dir / "static"
|
||||
|
||||
@staticmethod
|
||||
def get_themed_packages(prefix, packages):
|
||||
@property
|
||||
def prefix(self):
|
||||
"""
|
||||
Update paths with the themed assets,
|
||||
Args:
|
||||
prefix: theme prefix for which to update asset paths e.g. 'red-theme', 'edx.org' etc.
|
||||
packages: packages to update
|
||||
|
||||
Returns: list of updated paths and a boolean indicating whether any path was path or not
|
||||
This is used by the ComprehensiveThemeFinder in the collection step.
|
||||
"""
|
||||
themed_packages = {}
|
||||
for name in packages:
|
||||
# collect source file names for the package
|
||||
source_files = []
|
||||
for path in packages[name].get('source_filenames', []):
|
||||
# if themed asset exists use that, otherwise use default asset.
|
||||
if find(os.path.join(prefix, path)):
|
||||
source_files.append(os.path.join(prefix, path))
|
||||
else:
|
||||
source_files.append(path)
|
||||
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
|
||||
if not theme_dir:
|
||||
return None
|
||||
theme_name = os.path.basename(os.path.normpath(theme_dir))
|
||||
return "themes/{name}/".format(name=theme_name)
|
||||
|
||||
themed_packages[name] = {
|
||||
'output_filename': os.path.join(prefix, packages[name].get('output_filename', '')),
|
||||
'source_filenames': source_files,
|
||||
}
|
||||
return themed_packages
|
||||
def themed(self, name):
|
||||
"""
|
||||
Given a name, return a boolean indicating whether that name exists
|
||||
as a themed asset in the comprehensive theme.
|
||||
"""
|
||||
# Nothing can be themed if we don't have a theme location.
|
||||
if not self.theme_location:
|
||||
return False
|
||||
|
||||
path = safe_join(self.theme_location, name)
|
||||
return os.path.exists(path)
|
||||
|
||||
def path(self, name):
|
||||
"""
|
||||
Get the path to the real asset on disk
|
||||
"""
|
||||
if self.themed(name):
|
||||
base = self.theme_location
|
||||
else:
|
||||
base = self.location
|
||||
path = safe_join(base, name)
|
||||
return os.path.normpath(path)
|
||||
|
||||
def url(self, name, *args, **kwargs):
|
||||
"""
|
||||
Add the theme prefix to the asset URL
|
||||
"""
|
||||
if self.themed(name):
|
||||
name = self.prefix + name
|
||||
return super(ComprehensiveThemingAwareMixin, self).url(name, *args, **kwargs)
|
||||
|
||||
|
||||
class CachedComprehensiveThemingStorage(
|
||||
ComprehensiveThemingAwareMixin,
|
||||
CachedFilesMixin,
|
||||
StaticFilesStorage
|
||||
):
|
||||
"""
|
||||
Used by the ComprehensiveThemeFinder class. Mixes in support for cached
|
||||
files and comprehensive theming in static files.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
"""
|
||||
Theming aware template loaders.
|
||||
"""
|
||||
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
||||
|
||||
from edxmako.makoloader import MakoLoader
|
||||
from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme
|
||||
|
||||
|
||||
class ThemeTemplateLoader(MakoLoader):
|
||||
"""
|
||||
This is a Django loader object which will load the template based on current request and its corresponding theme.
|
||||
"""
|
||||
def __call__(self, template_name, template_dirs=None):
|
||||
template_name = get_template_path_with_theme(template_name).lstrip("/")
|
||||
return self.load_template(template_name, template_dirs)
|
||||
|
||||
|
||||
class ThemeFilesystemLoader(ThemeTemplateLoader):
|
||||
"""
|
||||
Filesystem Template loaders to pickup templates from theme directory based on the current site.
|
||||
"""
|
||||
is_usable = True
|
||||
_accepts_engine_in_init = True
|
||||
|
||||
def __init__(self, *args):
|
||||
ThemeTemplateLoader.__init__(self, FilesystemLoader(*args))
|
||||
@@ -1,78 +0,0 @@
|
||||
"""
|
||||
Theme aware pipeline template tags.
|
||||
"""
|
||||
|
||||
from django import template
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from pipeline.templatetags.pipeline import StylesheetNode, JavascriptNode
|
||||
from pipeline.utils import guess_type
|
||||
|
||||
from openedx.core.djangoapps.theming.helpers import get_static_file_url
|
||||
|
||||
register = template.Library() # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class ThemeStylesheetNode(StylesheetNode):
|
||||
"""
|
||||
Overrides StyleSheetNode from django pipeline so that stylesheets are served based on the applied theme.
|
||||
"""
|
||||
def render_css(self, package, path):
|
||||
"""
|
||||
Override render_css from django-pipline so that stylesheets urls are based on the applied theme
|
||||
"""
|
||||
template_name = package.template_name or "pipeline/css.html"
|
||||
context = package.extra_context
|
||||
context.update({
|
||||
'type': guess_type(path, 'text/css'),
|
||||
'url': mark_safe(get_static_file_url(path))
|
||||
})
|
||||
return render_to_string(template_name, context)
|
||||
|
||||
|
||||
class ThemeJavascriptNode(JavascriptNode):
|
||||
"""
|
||||
Overrides JavascriptNode from django pipeline so that js files are served based on the applied theme.
|
||||
"""
|
||||
def render_js(self, package, path):
|
||||
"""
|
||||
Override render_js from django-pipline so that js file urls are based on the applied theme
|
||||
"""
|
||||
template_name = package.template_name or "pipeline/js.html"
|
||||
context = package.extra_context
|
||||
context.update({
|
||||
'type': guess_type(path, 'text/javascript'),
|
||||
'url': mark_safe(get_static_file_url(path))
|
||||
})
|
||||
return render_to_string(template_name, context)
|
||||
|
||||
|
||||
@register.tag
|
||||
def stylesheet(parser, token): # pylint: disable=unused-argument
|
||||
"""
|
||||
Template tag to serve stylesheets from django-pipeline. This definition uses the theming aware ThemeStyleSheetNode.
|
||||
"""
|
||||
try:
|
||||
_, name = token.split_contents()
|
||||
except ValueError:
|
||||
raise template.TemplateSyntaxError(
|
||||
'%r requires exactly one argument: the name of a group in the PIPELINE_CSS setting' %
|
||||
token.split_contents()[0]
|
||||
)
|
||||
return ThemeStylesheetNode(name)
|
||||
|
||||
|
||||
@register.tag
|
||||
def javascript(parser, token): # pylint: disable=unused-argument
|
||||
"""
|
||||
Template tag to serve javascript from django-pipeline. This definition uses the theming aware ThemeJavascriptNode.
|
||||
"""
|
||||
try:
|
||||
_, name = token.split_contents()
|
||||
except ValueError:
|
||||
raise template.TemplateSyntaxError(
|
||||
'%r requires exactly one argument: the name of a group in the PIPELINE_JS setting' %
|
||||
token.split_contents()[0]
|
||||
)
|
||||
return ThemeJavascriptNode(name)
|
||||
@@ -6,57 +6,87 @@ from functools import wraps
|
||||
import os
|
||||
import os.path
|
||||
import contextlib
|
||||
import re
|
||||
|
||||
from mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.template import Engine
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import edxmako
|
||||
from .models import SiteTheme
|
||||
|
||||
from .core import comprehensive_theme_changes
|
||||
|
||||
EDX_THEME_DIR = settings.REPO_ROOT / "themes" / "edx.org"
|
||||
|
||||
|
||||
def with_comprehensive_theme(theme_dir_name):
|
||||
def with_comprehensive_theme(theme_dir):
|
||||
"""
|
||||
A decorator to run a test with a comprehensive theming enabled.
|
||||
A decorator to run a test with a particular comprehensive theme.
|
||||
|
||||
Arguments:
|
||||
theme_dir_name (str): directory name of the site for which we want comprehensive theming enabled.
|
||||
theme_dir (str): the full path to the theme directory to use.
|
||||
This will likely use `settings.REPO_ROOT` to get the full path.
|
||||
|
||||
"""
|
||||
# This decorator creates Site and SiteTheme models for given domain
|
||||
# This decorator gets the settings changes needed for a theme, and applies
|
||||
# them using the override_settings and edxmako.paths.add_lookup context
|
||||
# managers.
|
||||
|
||||
changes = comprehensive_theme_changes(theme_dir)
|
||||
|
||||
def _decorator(func): # pylint: disable=missing-docstring
|
||||
@wraps(func)
|
||||
def _decorated(*args, **kwargs): # pylint: disable=missing-docstring
|
||||
# make a domain name out of directory name
|
||||
domain = "{theme_dir_name}.org".format(theme_dir_name=re.sub(r"\.org$", "", theme_dir_name))
|
||||
site, __ = Site.objects.get_or_create(domain=domain, name=domain)
|
||||
SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme_dir_name)
|
||||
edxmako.paths.add_lookup('main', settings.COMPREHENSIVE_THEME_DIR, prepend=True)
|
||||
with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme_dir',
|
||||
return_value=theme_dir_name):
|
||||
with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
|
||||
return func(*args, **kwargs)
|
||||
with override_settings(COMPREHENSIVE_THEME_DIR=theme_dir, **changes['settings']):
|
||||
default_engine = Engine.get_default()
|
||||
dirs = default_engine.dirs[:]
|
||||
with edxmako.save_lookups():
|
||||
for template_dir in changes['template_paths']:
|
||||
edxmako.paths.add_lookup('main', template_dir, prepend=True)
|
||||
dirs.insert(0, template_dir)
|
||||
with patch.object(default_engine, 'dirs', dirs):
|
||||
return func(*args, **kwargs)
|
||||
return _decorated
|
||||
return _decorator
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def with_comprehensive_theme_context(theme=None):
|
||||
def with_is_edx_domain(is_edx_domain):
|
||||
"""
|
||||
A function to run a test as if request was made to the given theme.
|
||||
A decorator to run a test as if request originated from edX domain or not.
|
||||
|
||||
Arguments:
|
||||
theme (str): name if the theme or None if no theme is applied
|
||||
is_edx_domain (bool): are we an edX domain or not?
|
||||
|
||||
"""
|
||||
if theme:
|
||||
domain = '{theme}.org'.format(theme=re.sub(r"\.org$", "", theme))
|
||||
site, __ = Site.objects.get_or_create(domain=domain, name=theme)
|
||||
SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme)
|
||||
edxmako.paths.add_lookup('main', settings.COMPREHENSIVE_THEME_DIR, prepend=True)
|
||||
with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme_dir',
|
||||
return_value=theme):
|
||||
with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
|
||||
# This is weird, it's a decorator that conditionally applies other
|
||||
# decorators, which is confusing.
|
||||
def _decorator(func): # pylint: disable=missing-docstring
|
||||
if is_edx_domain:
|
||||
# This applies @with_comprehensive_theme to the func.
|
||||
func = with_comprehensive_theme(EDX_THEME_DIR)(func)
|
||||
|
||||
return func
|
||||
|
||||
return _decorator
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def with_edx_domain_context(is_edx_domain):
|
||||
"""
|
||||
A function to run a test as if request originated from edX domain or not.
|
||||
|
||||
Arguments:
|
||||
is_edx_domain (bool): are we an edX domain or not?
|
||||
|
||||
"""
|
||||
if is_edx_domain:
|
||||
changes = comprehensive_theme_changes(EDX_THEME_DIR)
|
||||
with override_settings(COMPREHENSIVE_THEME_DIR=EDX_THEME_DIR, **changes['settings']):
|
||||
with edxmako.save_lookups():
|
||||
for template_dir in changes['template_paths']:
|
||||
edxmako.paths.add_lookup('main', template_dir, prepend=True)
|
||||
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
"""Tests of comprehensive theming."""
|
||||
import unittest
|
||||
from mock import patch
|
||||
|
||||
from django.test import TestCase, RequestFactory, override_settings
|
||||
from django.conf import settings
|
||||
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
|
||||
from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme, strip_site_theme_templates_path, \
|
||||
get_current_site_theme_dir, get_themes, Theme
|
||||
|
||||
|
||||
class TestHelpers(TestCase):
|
||||
"""Test comprehensive theming helper functions."""
|
||||
|
||||
def test_get_themes(self):
|
||||
"""
|
||||
Tests template paths are returned from enabled theme.
|
||||
"""
|
||||
expected_themes = [
|
||||
Theme('red-theme', 'red-theme'),
|
||||
Theme('edge.edx.org', 'edge.edx.org'),
|
||||
Theme('edx.org', 'edx.org'),
|
||||
Theme('stanford-style', 'stanford-style'),
|
||||
]
|
||||
actual_themes = get_themes()
|
||||
self.assertItemsEqual(expected_themes, actual_themes)
|
||||
|
||||
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
|
||||
def test_get_themes_2(self):
|
||||
"""
|
||||
Tests template paths are returned from enabled theme.
|
||||
"""
|
||||
expected_themes = [
|
||||
Theme('test-theme', 'test-theme'),
|
||||
]
|
||||
actual_themes = get_themes()
|
||||
self.assertItemsEqual(expected_themes, actual_themes)
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TestHelpersLMS(TestCase):
|
||||
"""Test comprehensive theming helper functions."""
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_template_path_with_theme_enabled(self):
|
||||
"""
|
||||
Tests template paths are returned from enabled theme.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('header.html')
|
||||
self.assertEqual(template_path, '/red-theme/lms/templates/header.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_template_path_with_theme_for_missing_template(self):
|
||||
"""
|
||||
Tests default template paths are returned if template is not found in the theme.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('course.html')
|
||||
self.assertEqual(template_path, 'course.html')
|
||||
|
||||
def test_get_template_path_with_theme_disabled(self):
|
||||
"""
|
||||
Tests default template paths are returned when theme is non theme is enabled.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('header.html')
|
||||
self.assertEqual(template_path, 'header.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_strip_site_theme_templates_path_theme_enabled(self):
|
||||
"""
|
||||
Tests site theme templates path is stripped from the given template path.
|
||||
"""
|
||||
template_path = strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
|
||||
self.assertEqual(template_path, 'header.html')
|
||||
|
||||
def test_strip_site_theme_templates_path_theme_disabled(self):
|
||||
"""
|
||||
Tests site theme templates path returned unchanged if no theme is applied.
|
||||
"""
|
||||
template_path = strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
|
||||
self.assertEqual(template_path, '/red-theme/lms/templates/header.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_current_site_theme_dir(self):
|
||||
"""
|
||||
Tests current site theme name.
|
||||
"""
|
||||
factory = RequestFactory()
|
||||
with patch(
|
||||
'edxmako.middleware.REQUEST_CONTEXT.request',
|
||||
factory.get('/', SERVER_NAME="red-theme.org"),
|
||||
create=True,
|
||||
):
|
||||
current_site = get_current_site_theme_dir()
|
||||
self.assertEqual(current_site, 'red-theme')
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
|
||||
class TestHelpersCMS(TestCase):
|
||||
"""Test comprehensive theming helper functions."""
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_template_path_with_theme_enabled(self):
|
||||
"""
|
||||
Tests template paths are returned from enabled theme.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('login.html')
|
||||
self.assertEqual(template_path, '/red-theme/cms/templates/login.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_template_path_with_theme_for_missing_template(self):
|
||||
"""
|
||||
Tests default template paths are returned if template is not found in the theme.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('certificates.html')
|
||||
self.assertEqual(template_path, 'certificates.html')
|
||||
|
||||
def test_get_template_path_with_theme_disabled(self):
|
||||
"""
|
||||
Tests default template paths are returned when theme is non theme is enabled.
|
||||
"""
|
||||
template_path = get_template_path_with_theme('login.html')
|
||||
self.assertEqual(template_path, 'login.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_strip_site_theme_templates_path_theme_enabled(self):
|
||||
"""
|
||||
Tests site theme templates path is stripped from the given template path.
|
||||
"""
|
||||
template_path = strip_site_theme_templates_path('/red-theme/cms/templates/login.html')
|
||||
self.assertEqual(template_path, 'login.html')
|
||||
|
||||
def test_strip_site_theme_templates_path_theme_disabled(self):
|
||||
"""
|
||||
Tests site theme templates path returned unchanged if no theme is applied.
|
||||
"""
|
||||
template_path = strip_site_theme_templates_path('/red-theme/cms/templates/login.html')
|
||||
self.assertEqual(template_path, '/red-theme/cms/templates/login.html')
|
||||
|
||||
@with_comprehensive_theme('red-theme')
|
||||
def test_get_current_site_theme_dir(self):
|
||||
"""
|
||||
Tests current site theme name.
|
||||
"""
|
||||
factory = RequestFactory()
|
||||
with patch(
|
||||
'edxmako.middleware.REQUEST_CONTEXT.request',
|
||||
factory.get('/', SERVER_NAME="red-theme.org"),
|
||||
create=True,
|
||||
):
|
||||
current_site = get_current_site_theme_dir()
|
||||
self.assertEqual(current_site, 'red-theme')
|
||||
@@ -1,82 +0,0 @@
|
||||
"""
|
||||
Tests for comprehensive theme static files storage classes.
|
||||
"""
|
||||
import ddt
|
||||
import unittest
|
||||
import re
|
||||
|
||||
from mock import patch
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
from django.conf import settings
|
||||
|
||||
from openedx.core.djangoapps.theming.helpers import get_base_theme_dir
|
||||
from openedx.core.djangoapps.theming.storage import ThemeStorage
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@ddt.ddt
|
||||
class TestStorageLMS(TestCase):
|
||||
"""
|
||||
Test comprehensive theming static files storage.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestStorageLMS, self).setUp()
|
||||
self.themes_dir = get_base_theme_dir()
|
||||
self.enabled_theme = "red-theme"
|
||||
self.system_dir = settings.REPO_ROOT / "lms"
|
||||
self.storage = ThemeStorage(location=self.themes_dir / self.enabled_theme / 'lms' / 'static')
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@ddt.data(
|
||||
(True, "images/logo.png"),
|
||||
(True, "images/favicon.ico"),
|
||||
(False, "images/spinning.gif"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_themed(self, is_themed, asset):
|
||||
"""
|
||||
Verify storage returns True on themed assets
|
||||
"""
|
||||
self.assertEqual(is_themed, self.storage.themed(asset, self.enabled_theme))
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@ddt.data(
|
||||
("images/logo.png", ),
|
||||
("images/favicon.ico", ),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_url(self, asset):
|
||||
"""
|
||||
Verify storage returns correct url depending upon the enabled theme
|
||||
"""
|
||||
with patch(
|
||||
"openedx.core.djangoapps.theming.storage.get_current_site_theme_dir",
|
||||
return_value=self.enabled_theme,
|
||||
):
|
||||
asset_url = self.storage.url(asset)
|
||||
# remove hash key from file url
|
||||
asset_url = re.sub(r"(\.\w+)(\.png|\.ico)$", r"\g<2>", asset_url)
|
||||
expected_url = self.storage.base_url + self.enabled_theme + "/" + asset
|
||||
|
||||
self.assertEqual(asset_url, expected_url)
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@ddt.data(
|
||||
("images/logo.png", ),
|
||||
("images/favicon.ico", ),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_path(self, asset):
|
||||
"""
|
||||
Verify storage returns correct file path depending upon the enabled theme
|
||||
"""
|
||||
with patch(
|
||||
"openedx.core.djangoapps.theming.storage.get_current_site_theme_dir",
|
||||
return_value=self.enabled_theme,
|
||||
):
|
||||
returned_path = self.storage.path(asset)
|
||||
expected_path = self.themes_dir / self.enabled_theme / "lms/static/" / asset
|
||||
|
||||
self.assertEqual(expected_path, returned_path)
|
||||
@@ -1,235 +0,0 @@
|
||||
"""
|
||||
Tests for comprehensive themes.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase, override_settings
|
||||
from django.contrib import staticfiles
|
||||
|
||||
from paver.easy import call_task
|
||||
|
||||
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TestComprehensiveThemeLMS(TestCase):
|
||||
"""
|
||||
Test html, sass and static file overrides for comprehensive themes.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Clear static file finders cache and register cleanup methods.
|
||||
"""
|
||||
super(TestComprehensiveThemeLMS, self).setUp()
|
||||
|
||||
# Clear the internal staticfiles caches, to get test isolation.
|
||||
staticfiles.finders.get_finder.cache_clear()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Enable Comprehensive theme and compile sass files.
|
||||
"""
|
||||
# Apply Comprehensive theme and compile sass assets.
|
||||
compile_sass('lms')
|
||||
|
||||
super(TestComprehensiveThemeLMS, cls).setUpClass()
|
||||
|
||||
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
|
||||
@with_comprehensive_theme(settings.TEST_THEME.basename())
|
||||
def test_footer(self):
|
||||
"""
|
||||
Test that theme footer is used instead of default footer.
|
||||
"""
|
||||
resp = self.client.get('/')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# This string comes from header.html of test-theme
|
||||
self.assertContains(resp, "This is a footer for test-theme.")
|
||||
|
||||
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
|
||||
@with_comprehensive_theme(settings.TEST_THEME.basename())
|
||||
def test_logo_image(self):
|
||||
"""
|
||||
Test that theme logo is used instead of default logo.
|
||||
"""
|
||||
result = staticfiles.finders.find('test-theme/images/logo.png')
|
||||
self.assertEqual(result, settings.TEST_THEME / 'lms/static/images/logo.png')
|
||||
|
||||
@override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
|
||||
@with_comprehensive_theme(settings.TEST_THEME.basename())
|
||||
def test_css_files(self):
|
||||
"""
|
||||
Test that theme sass files are used instead of default sass files.
|
||||
"""
|
||||
result = staticfiles.finders.find('test-theme/css/lms-main-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"),
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user