diff --git a/cms/envs/common.py b/cms/envs/common.py
index 2d8c46cc96..59d6c9328d 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -312,8 +312,11 @@ TEMPLATES = [
# Options specific to this backend.
'OPTIONS': {
'loaders': (
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
+ # We have to use mako-aware template loaders to be able to include
+ # mako templates inside django templates (such as main_django.html).
+ 'openedx.core.djangoapps.theming.template_loaders.ThemeTemplateLoader',
+ 'edxmako.makoloader.MakoFilesystemLoader',
+ 'edxmako.makoloader.MakoAppDirectoriesLoader',
),
'context_processors': (
'django.template.context_processors.request',
diff --git a/cms/static/sass/bootstrap/studio-main.scss b/cms/static/sass/bootstrap/studio-main.scss
index de32b0257a..f664fceb39 100644
--- a/cms/static/sass/bootstrap/studio-main.scss
+++ b/cms/static/sass/bootstrap/studio-main.scss
@@ -3,7 +3,7 @@
// -----------------------------
// Bootstrap theme
-@import 'bootstrap/theme';
+@import 'cms/bootstrap/theme';
@import 'bootstrap/scss/bootstrap';
// Variables
diff --git a/cms/static/sass/elements/_system-feedback.scss b/cms/static/sass/elements/_system-feedback.scss
index 2bdcc879c0..239ddb8305 100644
--- a/cms/static/sass/elements/_system-feedback.scss
+++ b/cms/static/sass/elements/_system-feedback.scss
@@ -72,6 +72,60 @@
}
}
+.page-banner {
+ max-width: $fg-max-width;
+ margin: 0 auto;
+
+ .user-messages {
+ padding-top: $baseline;
+
+ // Hack: force override the global important rule
+ // that courseware links don't have an underline.
+ a:hover {
+ color: $link-color;
+ text-decoration: underline !important;
+ }
+ }
+
+ .alert {
+ margin-bottom: $baseline !important;
+ padding: $baseline;
+ border: 1px solid;
+
+ .icon-alert {
+ margin-right: $baseline / 4;
+ }
+
+ &.alert-info {
+ color: $state-info-text;
+ background-color: $state-info-bg;
+ border-color: $state-info-border;
+ box-shadow: none;
+ }
+
+ &.alert-success {
+ color: $state-success-text;
+ background-color: $state-success-bg;
+ border-color: $state-success-border;
+ box-shadow: none;
+ }
+
+ &.alert-warning {
+ color: $state-warning-text;
+ background-color: $state-warning-bg;
+ border-color: $state-warning-border;
+ box-shadow: none;
+ }
+
+ &.alert-danger {
+ color: $state-danger-text;
+ background-color: $state-danger-bg;
+ border-color: $state-danger-border;
+ box-shadow: none;
+ }
+ }
+}
+
.alert, .notification, .prompt {
// types - confirm
diff --git a/cms/static/sass/partials/cms/base/_variables.scss b/cms/static/sass/partials/cms/base/_variables.scss
index 92aa640429..87b48c01e3 100644
--- a/cms/static/sass/partials/cms/base/_variables.scss
+++ b/cms/static/sass/partials/cms/base/_variables.scss
@@ -241,6 +241,7 @@ $ui-action-primary-color-focus: $blue-s1 !default;
$ui-link-color: $blue-u2 !default;
$ui-link-color-focus: $blue-s1 !default;
+$link-color: $ui-link-color;
// +Specific UI
// ====================
@@ -281,3 +282,23 @@ $action-primary-active-bg: #1AA1DE !default; // $m-blue
$very-light-text: $white !default;
$color-background-alternate: rgb(242, 248, 251) !default;
+
+// ----------------------------
+// #COLORS- Bootstrap-style
+// ----------------------------
+
+$state-success-text: $black !default;
+$state-success-bg: #dff0d8 !default;
+$state-success-border: darken($state-success-bg, 5%) !default;
+
+$state-info-text: $black !default;
+$state-info-bg: #d9edf7 !default;
+$state-info-border: darken($state-info-bg, 7%) !default;
+
+$state-warning-text: $black !default;
+$state-warning-bg: #fcf8e3 !default;
+$state-warning-border: darken($state-warning-bg, 5%) !default;
+
+$state-danger-text: $black !default;
+$state-danger-bg: #f2dede !default;
+$state-danger-border: darken($state-danger-bg, 5%) !default;
diff --git a/cms/static/sass/partials/bootstrap/_theme.scss b/cms/static/sass/partials/cms/bootstrap/_theme.scss
similarity index 100%
rename from cms/static/sass/partials/bootstrap/_theme.scss
rename to cms/static/sass/partials/cms/bootstrap/_theme.scss
diff --git a/cms/templates/base.html b/cms/templates/base.html
index 9cc015c745..5cba0a4051 100644
--- a/cms/templates/base.html
+++ b/cms/templates/base.html
@@ -9,9 +9,11 @@
<%!
from django.utils.translation import ugettext as _
+from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string
)
+from openedx.core.djangolib.markup import HTML
%>
<%page expression_filter="h"/>
@@ -75,17 +77,34 @@ from openedx.core.djangolib.js_utils import (
- <% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %>
- <%include file="widgets/header.html" args="online_help_token=online_help_token" />
+ <% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %>
+ <%include file="widgets/header.html" args="online_help_token=online_help_token" />
+
+ <%
+ banner_messages = list(PageLevelMessages.user_messages(request))
+ %>
+
+ % if banner_messages:
+
+
+ % for message in banner_messages:
+
+
+ ${HTML(message.message_html)}
+
+ % endfor
+
+
+ % endif
<%block name="page_alert">%block>
-
- <%block name="content">%block>
-
+
+ <%block name="content">%block>
+
% if user.is_authenticated():
diff --git a/cms/templates/fragments/standalone-page-bootstrap.html b/cms/templates/fragments/standalone-page-bootstrap.html
new file mode 100644
index 0000000000..b9c4f8c03e
--- /dev/null
+++ b/cms/templates/fragments/standalone-page-bootstrap.html
@@ -0,0 +1,13 @@
+## mako
+
+<%page expression_filter="h"/>
+
+## Override the default styles_version to use Bootstrap
+<%! main_css = "css/bootstrap/studio-main.css" %>
+
+<%inherit file="../base.html" />
+<%block name="title">${page_title if page_title else ''}%block>
+
+<%block name="content">
+ <%include file="/fragments/standalone-page-fragment.html" args="fragment=fragment"/>
+%block>
diff --git a/cms/templates/fragments/standalone-page-fragment.html b/cms/templates/fragments/standalone-page-fragment.html
new file mode 100644
index 0000000000..492f049cd6
--- /dev/null
+++ b/cms/templates/fragments/standalone-page-fragment.html
@@ -0,0 +1,15 @@
+<%! from openedx.core.djangolib.markup import HTML %>
+
+<%block name="head_extra">
+ ${HTML(fragment.head_html())}
+%block>
+
+<%block name="footer_extra">
+ ${HTML(fragment.foot_html())}
+%block>
+
+
+
+ ${HTML(fragment.body_html())}
+
+
diff --git a/cms/templates/fragments/standalone-page-v1.html b/cms/templates/fragments/standalone-page-v1.html
new file mode 100644
index 0000000000..32cd026410
--- /dev/null
+++ b/cms/templates/fragments/standalone-page-v1.html
@@ -0,0 +1,10 @@
+## mako
+
+<%page expression_filter="h"/>
+
+<%inherit file="../base.html" />
+<%block name="title">${page_title if page_title else ''}%block>
+
+<%block name="content">
+ <%include file="/fragments/standalone-page-fragment.html" args="fragment=fragment"/>
+%block>
diff --git a/cms/templates/fragments/standalone-page-v2.html b/cms/templates/fragments/standalone-page-v2.html
new file mode 100644
index 0000000000..3a78db48eb
--- /dev/null
+++ b/cms/templates/fragments/standalone-page-v2.html
@@ -0,0 +1,13 @@
+## mako
+
+<%page expression_filter="h"/>
+
+## Override the default styles_version to the Pattern Library version (version 2)
+<%! main_css = "style-main-v2" %>
+
+<%inherit file="../base.html" />
+<%block name="title">${page_title if page_title else ''}%block>
+
+<%block name="content">
+ <%include file="/fragments/standalone-page-fragment.html" args="fragment=fragment"/>
+%block>
diff --git a/cms/templates/ux/reference/bootstrap/test.html b/cms/templates/ux/reference/bootstrap/test.html
index 311a85a132..bf68ae86a4 100644
--- a/cms/templates/ux/reference/bootstrap/test.html
+++ b/cms/templates/ux/reference/bootstrap/test.html
@@ -1,20 +1,9 @@
-## Override the default styles_version to use Bootstrap
-<%! main_css = "css/bootstrap/studio-main.css" %>
+## mako
<%page expression_filter="h"/>
-<%!
-from openedx.core.djangoapps.util.user_messages import (
- register_error_message,
- register_info_message,
- register_success_message,
- register_warning_message,
-)
-%>
-
-<%
-register_info_message(request, _('This is a test message'))
-%>
+## Override the default styles_version to use Bootstrap
+<%! main_css = "css/bootstrap/studio-main.css" %>
<%inherit file="/base.html" />
<%block name="title">Bootstrap Test Page%block>
diff --git a/cms/urls.py b/cms/urls.py
index 36324081ea..03184d8f21 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -64,6 +64,9 @@ urlpatterns = patterns(
# Darklang View to change the preview language (or dark language)
url(r'^update_lang/', include('openedx.core.djangoapps.dark_lang.urls', namespace='dark_lang')),
+ # URLs for managing theming
+ url(r'^theming/', include('openedx.core.djangoapps.theming.urls', namespace='theming')),
+
# For redirecting to help pages.
url(r'^help_token/', include('help_tokens.urls')),
)
diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py
index 31f74d61d9..00b4881808 100644
--- a/lms/djangoapps/courseware/tests/test_course_info.py
+++ b/lms/djangoapps/courseware/tests/test_course_info.py
@@ -388,7 +388,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
self.assertEqual(resp.status_code, 200)
def test_num_queries_instructor_paced(self):
- self.fetch_course_info_with_queries(self.instructor_paced_course, 24, 3)
+ self.fetch_course_info_with_queries(self.instructor_paced_course, 25, 3)
def test_num_queries_self_paced(self):
- self.fetch_course_info_with_queries(self.self_paced_course, 24, 3)
+ self.fetch_course_info_with_queries(self.self_paced_course, 25, 3)
diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py
index 418a154886..4508797807 100644
--- a/lms/djangoapps/courseware/tests/test_views.py
+++ b/lms/djangoapps/courseware/tests/test_views.py
@@ -211,8 +211,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20
@ddt.data(
- (ModuleStoreEnum.Type.mongo, 10, 142),
- (ModuleStoreEnum.Type.split, 4, 142),
+ (ModuleStoreEnum.Type.mongo, 10, 143),
+ (ModuleStoreEnum.Type.split, 4, 143),
)
@ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
@@ -1464,12 +1464,12 @@ class ProgressPageTests(ProgressPageBaseTests):
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration(enabled=self_paced_enabled).save()
self.setup_course(self_paced=self_paced)
- with self.assertNumQueries(39, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1):
+ with self.assertNumQueries(40, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1):
self._get_progress_page()
@ddt.data(
- (False, 39, 25),
- (True, 32, 21)
+ (False, 40, 26),
+ (True, 33, 22)
)
@ddt.unpack
def test_progress_queries(self, enable_waffle, initial, subsequent):
diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py
index 23ccb09113..49e709833c 100644
--- a/lms/djangoapps/django_comment_client/base/tests.py
+++ b/lms/djangoapps/django_comment_client/base/tests.py
@@ -404,8 +404,8 @@ class ViewsQueryCountTestCase(
return inner
@ddt.data(
- (ModuleStoreEnum.Type.mongo, 3, 4, 31),
- (ModuleStoreEnum.Type.split, 3, 13, 31),
+ (ModuleStoreEnum.Type.mongo, 3, 4, 32),
+ (ModuleStoreEnum.Type.split, 3, 13, 32),
)
@ddt.unpack
@count_queries
@@ -413,8 +413,8 @@ class ViewsQueryCountTestCase(
self.create_thread_helper(mock_request)
@ddt.data(
- (ModuleStoreEnum.Type.mongo, 3, 3, 27),
- (ModuleStoreEnum.Type.split, 3, 10, 27),
+ (ModuleStoreEnum.Type.mongo, 3, 3, 28),
+ (ModuleStoreEnum.Type.split, 3, 10, 28),
)
@ddt.unpack
@count_queries
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 5d5ef06219..d87c336fc1 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -582,9 +582,6 @@ TEMPLATES = [
]
DEFAULT_TEMPLATE_ENGINE = TEMPLATES[0]
-# The template used to render a web fragment as a standalone page
-STANDALONE_FRAGMENT_VIEW_TEMPLATE = 'fragment-view-chromeless.html'
-
###############################################################################################
# use the ratelimit backend to prevent brute force attacks
diff --git a/lms/static/sass/bootstrap/_layouts.scss b/lms/static/sass/bootstrap/_layouts.scss
index 05ded2191a..660140e439 100644
--- a/lms/static/sass/bootstrap/_layouts.scss
+++ b/lms/static/sass/bootstrap/_layouts.scss
@@ -1,8 +1,10 @@
// LMS layouts
.content-wrapper {
+ margin-top: $baseline;
+
.course-tabs {
- padding-bottom: none;
+ padding-bottom: 0;
.nav-item {
&.active, &:hover{
diff --git a/lms/templates/fragments/standalone-page-bootstrap.html b/lms/templates/fragments/standalone-page-bootstrap.html
new file mode 100644
index 0000000000..eee76b7ac7
--- /dev/null
+++ b/lms/templates/fragments/standalone-page-bootstrap.html
@@ -0,0 +1,11 @@
+## mako
+
+<%page expression_filter="h"/>
+
+## Override the default styles_version to use Bootstrap
+<%! main_css = "css/bootstrap/lms-main.css" %>
+
+<%inherit file="/main.html" />
+<%block name="pagetitle">${page_title if page_title else ''}%block>
+
+<%include file="/fragments/standalone-page-fragment.html" args="fragment=fragment"/>
diff --git a/lms/templates/fragment-view-chromeless.html b/lms/templates/fragments/standalone-page-fragment.html
similarity index 68%
rename from lms/templates/fragment-view-chromeless.html
rename to lms/templates/fragments/standalone-page-fragment.html
index 68a13c355d..b89f69e2f7 100644
--- a/lms/templates/fragment-view-chromeless.html
+++ b/lms/templates/fragments/standalone-page-fragment.html
@@ -1,16 +1,9 @@
## mako
-<%! main_css = "style-main-v2" %>
-
<%page expression_filter="h"/>
-<%inherit file="/main.html" />
-
-<%namespace name='static' file='static_content.html'/>
<%! from openedx.core.djangolib.markup import HTML %>
-<% header_file = None %>
-
<%block name="head_extra">
${HTML(fragment.head_html())}
%block>
diff --git a/lms/templates/fragments/standalone-page-v1.html b/lms/templates/fragments/standalone-page-v1.html
new file mode 100644
index 0000000000..d9f97304af
--- /dev/null
+++ b/lms/templates/fragments/standalone-page-v1.html
@@ -0,0 +1,8 @@
+## mako
+
+<%page expression_filter="h"/>
+
+<%inherit file="/main.html" />
+<%block name="pagetitle">${page_title if page_title else ''}%block>
+
+<%include file="/fragments/standalone-page-fragment.html" args="fragment=fragment"/>
diff --git a/lms/templates/fragments/standalone-page-v2.html b/lms/templates/fragments/standalone-page-v2.html
new file mode 100644
index 0000000000..83f533387b
--- /dev/null
+++ b/lms/templates/fragments/standalone-page-v2.html
@@ -0,0 +1,11 @@
+## mako
+
+<%page expression_filter="h"/>
+
+## Override the default styles_version to the Pattern Library version (version 2)
+<%! main_css = "style-main-v2" %>
+
+<%inherit file="/main.html" />
+<%block name="pagetitle">${page_title if page_title else ''}%block>
+
+<%include file="/fragments/standalone-page-fragment.html" args="fragment=fragment"/>
diff --git a/lms/urls.py b/lms/urls.py
index 9733bf921c..86c68ef411 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -104,6 +104,9 @@ urlpatterns = (
# URLs for managing dark launches of languages
url(r'^update_lang/', include('openedx.core.djangoapps.dark_lang.urls', namespace='dark_lang')),
+ # URLs for managing theming
+ url(r'^theming/', include('openedx.core.djangoapps.theming.urls', namespace='theming')),
+
# For redirecting to help pages.
url(r'^help_token/', include('help_tokens.urls')),
diff --git a/openedx/core/djangoapps/bookmarks/tests/test_views.py b/openedx/core/djangoapps/bookmarks/tests/test_views.py
index 30ce17cc2d..e8344f70ec 100644
--- a/openedx/core/djangoapps/bookmarks/tests/test_views.py
+++ b/openedx/core/djangoapps/bookmarks/tests/test_views.py
@@ -268,7 +268,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
self.assertEqual(response.data['developer_message'], u'Parameter usage_id not provided.')
# Send empty data dictionary.
- with self.assertNumQueries(7): # No queries for bookmark table.
+ with self.assertNumQueries(8): # No queries for bookmark table.
response = self.send_post(
client=self.client,
url=reverse('bookmarks'),
diff --git a/openedx/core/djangoapps/debug/views.py b/openedx/core/djangoapps/debug/views.py
index ba2d200a2a..b0ac1f8acb 100644
--- a/openedx/core/djangoapps/debug/views.py
+++ b/openedx/core/djangoapps/debug/views.py
@@ -36,13 +36,13 @@ def show_reference_template(request, template):
# Support dynamic rendering of messages
if request.GET.get('alert'):
- register_info_message(request, request.GET.get('alert'))
+ PageLevelMessages.register_info_message(request, request.GET.get('alert'))
if request.GET.get('success'):
- register_success_message(request, request.GET.get('success'))
+ PageLevelMessages.register_success_message(request, request.GET.get('success'))
if request.GET.get('warning'):
- register_warning_message(request, request.GET.get('warning'))
+ PageLevelMessages.register_warning_message(request, request.GET.get('warning'))
if request.GET.get('error'):
- register_error_message(request, request.GET.get('error'))
+ PageLevelMessages.register_error_message(request, request.GET.get('error'))
# Add some messages to the course skeleton pages
if u'course-skeleton.html' in request.path:
diff --git a/openedx/core/djangoapps/plugin_api/views.py b/openedx/core/djangoapps/plugin_api/views.py
index 4b461bb3c4..f45260ed13 100644
--- a/openedx/core/djangoapps/plugin_api/views.py
+++ b/openedx/core/djangoapps/plugin_api/views.py
@@ -8,6 +8,7 @@ from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.http import HttpResponse
from django.shortcuts import render_to_response
+from edxmako.shortcuts import is_any_marketing_link_set, is_marketing_link_set, marketing_link
from web_fragments.views import FragmentView
log = logging.getLogger('plugin_api')
@@ -17,8 +18,6 @@ class EdxFragmentView(FragmentView):
"""
The base class of all Open edX fragment views.
"""
- USES_PATTERN_LIBRARY = True
-
page_title = None
@staticmethod
@@ -78,6 +77,44 @@ class EdxFragmentView(FragmentView):
for js_file in self.js_dependencies():
fragment.add_javascript_url(staticfiles_storage.url(js_file))
+ def create_base_standalone_context(self, request, fragment, **kwargs):
+ """
+ Creates the base context for rendering a fragment as a standalone page.
+ """
+ return {
+ 'uses_pattern_library': True,
+ 'disable_accordion': True,
+ 'allow_iframing': True,
+ 'disable_header': True,
+ 'disable_footer': True,
+ 'disable_window_wrap': True,
+ }
+
+ def _add_studio_standalone_context_variables(self, request, context):
+ """
+ Adds Studio-specific context variables for fragment standalone pages.
+
+ Note: this is meant to be a temporary hack to ensure that Studio
+ receives the context variables that are expected by some of its
+ shared templates. Ideally these templates shouldn't depend upon
+ this data being provided but should instead import the functionality
+ it needs.
+ """
+ context.update({
+ 'request': request,
+ 'settings': settings,
+ 'EDX_ROOT_URL': settings.EDX_ROOT_URL,
+ 'marketing_link': marketing_link,
+ 'is_any_marketing_link_set': is_any_marketing_link_set,
+ 'is_marketing_link_set': is_marketing_link_set,
+ })
+
+ def standalone_page_title(self, request, fragment, **kwargs):
+ """
+ Returns the page title for the standalone page, or None if there is no title.
+ """
+ return None
+
def render_standalone_response(self, request, fragment, **kwargs):
"""
Renders a standalone page for the specified fragment.
@@ -86,14 +123,18 @@ class EdxFragmentView(FragmentView):
"""
if fragment is None:
return HttpResponse(status=204)
- context = {
- 'uses-pattern-library': self.USES_PATTERN_LIBRARY,
+ context = self.create_base_standalone_context(request, fragment, **kwargs)
+ self._add_studio_standalone_context_variables(request, context)
+ context.update({
'settings': settings,
'fragment': fragment,
- 'disable_accordion': True,
- 'allow_iframing': True,
- 'disable_header': True,
- 'disable_footer': True,
- 'disable_window_wrap': True,
- }
- return render_to_response(settings.STANDALONE_FRAGMENT_VIEW_TEMPLATE, context)
+ 'page_title': self.standalone_page_title(request, fragment, **kwargs),
+ })
+ if context.get('uses_pattern_library', False):
+ template = 'fragments/standalone-page-v2.html'
+ elif context.get('uses_bootstrap', False):
+ template = 'fragments/standalone-page-bootstrap.html'
+ else:
+ template = 'fragments/standalone-page-v1.html'
+
+ return render_to_response(template, context)
diff --git a/openedx/core/djangoapps/theming/helpers.py b/openedx/core/djangoapps/theming/helpers.py
index 16b93c02be..d178c81111 100644
--- a/openedx/core/djangoapps/theming/helpers.py
+++ b/openedx/core/djangoapps/theming/helpers.py
@@ -366,6 +366,16 @@ def get_themes(themes_dir=None):
return themes
+def theme_exists(theme_name, themes_dir=None):
+ """
+ Returns True if a theme exists with the specified name.
+ """
+ for theme in get_themes(themes_dir=themes_dir):
+ if theme.theme_dir_name == theme_name:
+ return True
+ return False
+
+
def get_theme_dirs(themes_dir=None):
"""
Returns theme dirs in given dirs
diff --git a/openedx/core/djangoapps/theming/middleware.py b/openedx/core/djangoapps/theming/middleware.py
index bb4b0970ef..d2f25321e5 100644
--- a/openedx/core/djangoapps/theming/middleware.py
+++ b/openedx/core/djangoapps/theming/middleware.py
@@ -7,19 +7,25 @@ Note:
"""
from django.conf import settings
-from openedx.core.djangoapps.theming.models import SiteTheme
+from .models import SiteTheme
+from .views import get_user_preview_site_theme
class CurrentSiteThemeMiddleware(object):
"""
Middleware that sets `site_theme` attribute to request object.
"""
-
def process_request(self, request):
"""
- fetch Site Theme for the current site and add it to the request object.
+ Set the request's 'site_theme' attribute based upon the current user.
"""
- default_theme = None
- if settings.DEFAULT_SITE_THEME:
- default_theme = SiteTheme(site=request.site, theme_dir_name=settings.DEFAULT_SITE_THEME)
- request.site_theme = SiteTheme.get_theme(request.site, default=default_theme)
+ # Determine if the user has specified a preview site
+ preview_site_theme = get_user_preview_site_theme(request)
+ if preview_site_theme:
+ site_theme = preview_site_theme
+ else:
+ default_theme = None
+ if settings.DEFAULT_SITE_THEME:
+ default_theme = SiteTheme(site=request.site, theme_dir_name=settings.DEFAULT_SITE_THEME)
+ site_theme = SiteTheme.get_theme(request.site, default=default_theme)
+ request.site_theme = site_theme
diff --git a/openedx/core/djangoapps/theming/models.py b/openedx/core/djangoapps/theming/models.py
index 9efc21a3c0..da771dcbcd 100644
--- a/openedx/core/djangoapps/theming/models.py
+++ b/openedx/core/djangoapps/theming/models.py
@@ -1,7 +1,6 @@
"""
Django models supporting the Comprehensive Theming subsystem
"""
-from django.conf import settings
from django.contrib.sites.models import Site
from django.db import models
diff --git a/openedx/core/djangoapps/theming/templates/theming/theming-admin-fragment.html b/openedx/core/djangoapps/theming/templates/theming/theming-admin-fragment.html
new file mode 100644
index 0000000000..81c22b7115
--- /dev/null
+++ b/openedx/core/djangoapps/theming/templates/theming/theming-admin-fragment.html
@@ -0,0 +1,41 @@
+## mako
+
+<%page expression_filter="h"/>
+
+<%namespace name='static' file='../static_content.html'/>
+
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangoapps.theming.helpers import get_themes
+%>
+
+
+ ${_("Theming Administration")}
+
+
diff --git a/openedx/core/djangoapps/theming/tests/test_middleware.py b/openedx/core/djangoapps/theming/tests/test_middleware.py
index 5f217d5b02..a151e02106 100644
--- a/openedx/core/djangoapps/theming/tests/test_middleware.py
+++ b/openedx/core/djangoapps/theming/tests/test_middleware.py
@@ -1,14 +1,20 @@
"""
Tests for middleware for comprehensive themes.
"""
-from mock import Mock
-from django.test import TestCase, override_settings
+
+from django.contrib.messages.middleware import MessageMiddleware
+from django.test import RequestFactory, TestCase, override_settings
from django.contrib.sites.models import Site
-
from openedx.core.djangoapps.theming.middleware import CurrentSiteThemeMiddleware
+from student.tests.factories import UserFactory
+
+from ..views import set_user_preview_site_theme
+
+TEST_URL = '/test'
+TEST_THEME_NAME = 'test-theme'
-class TestCurrentSiteThemeMiddlewareLMS(TestCase):
+class TestCurrentSiteThemeMiddleware(TestCase):
"""
Test theming middleware.
"""
@@ -16,22 +22,38 @@ class TestCurrentSiteThemeMiddlewareLMS(TestCase):
"""
Initialize middleware and related objects
"""
- super(TestCurrentSiteThemeMiddlewareLMS, self).setUp()
+ super(TestCurrentSiteThemeMiddleware, self).setUp()
self.site_theme_middleware = CurrentSiteThemeMiddleware()
- self.request = Mock()
- self.request.site, __ = Site.objects.get_or_create(domain="test", name="test")
- self.request.session = {}
+ self.user = UserFactory.create()
- @override_settings(DEFAULT_SITE_THEME="test-theme")
+ def create_mock_get_request(self):
+ """
+ Returns a mock GET request.
+ """
+ request = RequestFactory().get(TEST_URL)
+ self.initialize_mock_request(request)
+ return request
+
+ def initialize_mock_request(self, request):
+ """
+ Initialize a test request.
+ """
+ request.user = self.user
+ request.site, __ = Site.objects.get_or_create(domain='test', name='test')
+ request.session = {}
+ MessageMiddleware().process_request(request)
+
+ @override_settings(DEFAULT_SITE_THEME=TEST_THEME_NAME)
def test_default_site_theme(self):
"""
Test that request.site_theme returns theme defined by DEFAULT_SITE_THEME setting
when there is no theme associated with the current site.
"""
- self.assertEqual(self.site_theme_middleware.process_request(self.request), None)
- self.assertIsNotNone(self.request.site_theme)
- self.assertEqual(self.request.site_theme.theme_dir_name, "test-theme")
+ request = self.create_mock_get_request()
+ self.assertEqual(self.site_theme_middleware.process_request(request), None)
+ self.assertIsNotNone(request.site_theme)
+ self.assertEqual(request.site_theme.theme_dir_name, TEST_THEME_NAME)
@override_settings(DEFAULT_SITE_THEME=None)
def test_default_site_theme_2(self):
@@ -39,5 +61,30 @@ class TestCurrentSiteThemeMiddlewareLMS(TestCase):
Test that request.site_theme returns None when there is no theme associated with
the current site and DEFAULT_SITE_THEME is also None.
"""
- self.assertEqual(self.site_theme_middleware.process_request(self.request), None)
- self.assertIsNone(self.request.site_theme)
+ request = self.create_mock_get_request()
+ self.assertEqual(self.site_theme_middleware.process_request(request), None)
+ self.assertIsNone(request.site_theme)
+
+ def test_preview_theme(self):
+ """
+ Verify that preview themes behaves correctly.
+ """
+ # First request a preview theme
+ post_request = RequestFactory().post('/test')
+ self.initialize_mock_request(post_request)
+ set_user_preview_site_theme(post_request, TEST_THEME_NAME)
+
+ # Next request a page and verify that the theme is returned
+ get_request = self.create_mock_get_request()
+ self.assertEqual(self.site_theme_middleware.process_request(get_request), None)
+ self.assertEqual(get_request.site_theme.theme_dir_name, TEST_THEME_NAME)
+
+ # Request to reset the theme
+ post_request = RequestFactory().post('/test')
+ self.initialize_mock_request(post_request)
+ set_user_preview_site_theme(post_request, None)
+
+ # Finally verify that no theme is returned
+ get_request = self.create_mock_get_request()
+ self.assertEqual(self.site_theme_middleware.process_request(get_request), None)
+ self.assertIsNone(get_request.site_theme)
diff --git a/openedx/core/djangoapps/theming/tests/test_views.py b/openedx/core/djangoapps/theming/tests/test_views.py
new file mode 100644
index 0000000000..e6654402d8
--- /dev/null
+++ b/openedx/core/djangoapps/theming/tests/test_views.py
@@ -0,0 +1,105 @@
+"""
+ Tests for comprehensive them
+"""
+
+from courseware.tests.factories import GlobalStaffFactory
+from django.conf import settings
+from django.contrib.messages.middleware import MessageMiddleware
+from django.test import TestCase, override_settings
+from django.contrib.sites.models import Site
+from openedx.core.djangoapps.theming.middleware import CurrentSiteThemeMiddleware
+from student.tests.factories import UserFactory
+
+THEMING_ADMIN_URL = '/theming/admin'
+TEST_THEME_NAME = 'test-theme'
+TEST_PASSWORD = 'test'
+
+
+class TestThemingViews(TestCase):
+ """
+ Test theming views.
+ """
+ def setUp(self):
+ """
+ Initialize middleware and related objects
+ """
+ super(TestThemingViews, self).setUp()
+
+ self.site_theme_middleware = CurrentSiteThemeMiddleware()
+ self.user = UserFactory.create()
+
+ def initialize_mock_request(self, request):
+ """
+ Initialize a test request.
+ """
+ request.user = self.user
+ request.site, __ = Site.objects.get_or_create(domain='test', name='test')
+ request.session = {}
+ MessageMiddleware().process_request(request)
+
+ def test_preview_theme_access(self):
+ """
+ Verify that users have the correct access to preview themes.
+ """
+ # Anonymous users get redirected to the login page
+ response = self.client.get(THEMING_ADMIN_URL)
+ self.assertRedirects(
+ response,
+ '{login_url}?next={url}'.format(
+ login_url=settings.LOGIN_REDIRECT_URL,
+ url=THEMING_ADMIN_URL,
+ )
+ )
+
+ # Logged in non-global staff get a 404
+ self.client.login(username=self.user.username, password=TEST_PASSWORD)
+ response = self.client.get(THEMING_ADMIN_URL)
+ self.assertEqual(response.status_code, 404)
+
+ # Global staff can access the page
+ global_staff = GlobalStaffFactory()
+ self.client.login(username=global_staff.username, password=TEST_PASSWORD)
+ response = self.client.get(THEMING_ADMIN_URL)
+ self.assertEqual(response.status_code, 200)
+
+ def test_preview_theme(self):
+ """
+ Verify that preview themes behaves correctly.
+ """
+ global_staff = GlobalStaffFactory()
+ self.client.login(username=global_staff.username, password=TEST_PASSWORD)
+
+ # First request a preview theme
+ post_response = self.client.post(
+ THEMING_ADMIN_URL,
+ {
+ 'action': 'set_preview_theme',
+ 'preview_theme': TEST_THEME_NAME,
+ }
+ )
+ self.assertRedirects(post_response, THEMING_ADMIN_URL)
+
+ # Next request a page and verify that the correct theme has been chosen
+ response = self.client.get(THEMING_ADMIN_URL)
+ self.assertEquals(response.status_code, 200)
+ self.assertContains(
+ response,
+ '