Commit 0f808cec by Andy Armstrong

Implement theme admin UI to support previewing

LEARNER-2017
parent 2d0fc911
......@@ -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',
......
......@@ -3,7 +3,7 @@
// -----------------------------
// Bootstrap theme
@import 'bootstrap/theme';
@import 'cms/bootstrap/theme';
@import 'bootstrap/scss/bootstrap';
// Variables
......
......@@ -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
......
......@@ -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;
......@@ -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 (
<!-- view -->
<div class="wrapper wrapper-view" dir="${static.dir_rtl()}">
<% 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:
<div class="page-banner">
<div class="user-messages">
% for message in banner_messages:
<div class="alert ${message.css_class}" role="alert">
<span class="icon icon-alert fa ${message.icon_class}" aria-hidden="true"></span>
${HTML(message.message_html)}
</div>
% endfor
</div>
</div>
% endif
<div id="page-alert">
<%block name="page_alert"></%block>
</div>
<main id="main" aria-label="Content" tabindex="-1">
<div id="content">
<%block name="content"></%block>
</div>
<div id="content" class="content">
<%block name="content"></%block>
</div>
</main>
% if user.is_authenticated():
......
## 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>
<%! 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>
<div class="wrapper-content wrapper">
<section class="content">
${HTML(fragment.body_html())}
</section>
</div>
## 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>
## 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>
## 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>
......
......@@ -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')),
)
......
......@@ -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)
......@@ -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):
......
......@@ -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
......
......@@ -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
......
// LMS layouts
.content-wrapper {
margin-top: $baseline;
.course-tabs {
padding-bottom: none;
padding-bottom: 0;
.nav-item {
&.active, &:hover{
......
## 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"/>
## 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>
......
## 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"/>
## 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"/>
......@@ -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')),
......
......@@ -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'),
......
......@@ -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:
......
......@@ -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)
......@@ -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
......
......@@ -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
"""
Django models supporting the Comprehensive Theming subsystem
"""
from django.conf import settings
from django.contrib.sites.models import Site
from django.db import models
......
## 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
%>
<h3>
${_("Theming Administration")}
</h3>
<div>
<form class="form" action="${request.path}" method="post">
<div class="form-group">
<label>${_("Preview Theme")}
<select class="form-control" name="preview_theme">
<%
all_themes = list(get_themes())
all_themes.sort(key=lambda x: x.theme_dir_name)
current_theme_name = request.site_theme.theme_dir_name if request.site_theme else None
%>
% for theme in all_themes:
<% theme_name = theme.theme_dir_name %>
<option value="${theme_name}"${' selected=selected' if theme_name == current_theme_name else ''}>${theme_name}</option>
% endfor
</select>
</label>
</div>
<div class="form-actions">
<button class="btn btn-primary" type="submit" name="action" value="set_preview_theme">${_("Submit")}</button>
<button class="btn btn-secondary" type="submit" name="action" value="reset_preview_theme">${_("Reset")}</button>
</div>
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }"/>
</form>
<p>See also <a href="/admin">Django admin</a> for more theming settings.</p>
</div>
"""
Tests for middleware for comprehensive themes.
"""
from mock import Mock
from django.test import TestCase, override_settings
from django.contrib.sites.models import Site
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()
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")
@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)
"""
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,
'<option value="{theme_name}" selected=selected>'.format(theme_name=TEST_THEME_NAME)
)
# Request to reset the theme
post_response = self.client.post(
THEMING_ADMIN_URL,
{
'action': 'reset_preview_theme'
}
)
self.assertRedirects(post_response, THEMING_ADMIN_URL)
# Finally verify that the test theme is no longer selected
response = self.client.get(THEMING_ADMIN_URL)
self.assertEquals(response.status_code, 200)
self.assertContains(
response,
'<option value="{theme_name}">'.format(theme_name=TEST_THEME_NAME)
)
"""
Defines URLs for theming views.
"""
from django.conf.urls import url
from .helpers import is_comprehensive_theming_enabled
from .views import ThemingAdministrationFragmentView
if is_comprehensive_theming_enabled():
urlpatterns = [
url(
r'^admin',
ThemingAdministrationFragmentView.as_view(),
name='openedx.theming.update_theme_fragment_view',
),
]
"""
Views file for theming administration.
"""
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import Http404
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from openedx.core.djangoapps.user_api.preferences.api import (
delete_user_preference,
get_user_preference,
set_user_preference,
)
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from student.roles import GlobalStaff
from web_fragments.fragment import Fragment
from .helpers import theme_exists
from .models import SiteTheme
PREVIEW_SITE_THEME_PREFERENCE_KEY = 'preview-site-theme'
PREVIEW_THEME_FIELD = 'preview_theme'
def user_can_preview_themes(user):
"""
Returns true if the specified user is allowed to preview themes.
"""
if not user or user.is_anonymous():
return False
# In development mode, all users can preview themes
if settings.DEBUG:
return True
# Otherwise, only global staff can preview themes
return GlobalStaff().has_user(user)
def get_user_preview_site_theme(request):
"""
Returns the preview site for the current user, or None if not set.
"""
user = request.user
if not user or user.is_anonymous():
return None
preview_site_name = get_user_preference(user, PREVIEW_SITE_THEME_PREFERENCE_KEY)
if not preview_site_name:
return None
return SiteTheme(site=request.site, theme_dir_name=preview_site_name)
def set_user_preview_site_theme(request, preview_site_theme):
"""
Sets the current user's preferred preview site theme.
Args:
request: the current request
preview_site_theme (str or SiteTheme): the preview site theme or theme name.
None can be specified to remove the preview site theme.
"""
if preview_site_theme:
if isinstance(preview_site_theme, SiteTheme):
preview_site_theme_name = preview_site_theme.theme_dir_name
else:
preview_site_theme_name = preview_site_theme
if theme_exists(preview_site_theme_name):
set_user_preference(request.user, PREVIEW_SITE_THEME_PREFERENCE_KEY, preview_site_theme_name)
PageLevelMessages.register_success_message(
request,
_('Site theme changed to {site_theme}'.format(site_theme=preview_site_theme_name))
)
else:
PageLevelMessages.register_error_message(
request,
_('Theme {site_theme} does not exist'.format(site_theme=preview_site_theme_name))
)
else:
delete_user_preference(request.user, PREVIEW_SITE_THEME_PREFERENCE_KEY)
PageLevelMessages.register_success_message(request, _('Site theme reverted to the default'))
class ThemingAdministrationFragmentView(EdxFragmentView):
"""
Fragment view to allow a user to administer theming.
"""
def render_to_fragment(self, request, course_id=None, **kwargs):
"""
Renders the theming administration view as a fragment.
"""
html = render_to_string('theming/theming-admin-fragment.html', {})
return Fragment(html)
@method_decorator(login_required)
def get(self, request, *args, **kwargs):
"""
Renders the theming admin fragment to authorized users.
"""
if not user_can_preview_themes(request.user):
raise Http404
return super(ThemingAdministrationFragmentView, self).get(request, *args, **kwargs)
@method_decorator(login_required)
def post(self, request, **kwargs):
"""
Accept requests to update the theme preview.
"""
if not user_can_preview_themes(request.user):
raise Http404
action = request.POST.get('action', None)
if action == 'set_preview_theme':
preview_theme_name = request.POST.get(PREVIEW_THEME_FIELD, '')
set_user_preview_site_theme(request, preview_theme_name)
elif action == 'reset_preview_theme':
set_user_preview_site_theme(request, None)
return redirect(request.path)
def create_base_standalone_context(self, request, fragment, **kwargs):
"""
Creates the context to use when rendering a standalone page.
"""
return {
'uses_bootstrap': True,
}
def standalone_page_title(self, request, fragment, **kwargs):
"""
Returns the page title for the standalone update page.
"""
return _('Theming Administration')
......@@ -174,7 +174,7 @@ class TestOwnUsernameAPI(CacheIsolationTestCase, UserAPITestCase):
Test that a client (logged in) can get her own username.
"""
self.client.login(username=self.user.username, password=TEST_PASSWORD)
self._verify_get_own_username(14)
self._verify_get_own_username(15)
def test_get_username_inactive(self):
"""
......@@ -184,7 +184,7 @@ class TestOwnUsernameAPI(CacheIsolationTestCase, UserAPITestCase):
self.client.login(username=self.user.username, password=TEST_PASSWORD)
self.user.is_active = False
self.user.save()
self._verify_get_own_username(14)
self._verify_get_own_username(15)
def test_get_username_not_logged_in(self):
"""
......@@ -305,7 +305,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
"""
self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
self.create_mock_profile(self.user)
with self.assertNumQueries(18):
with self.assertNumQueries(19):
response = self.send_get(self.different_client)
self._verify_full_shareable_account_response(response, account_privacy=ALL_USERS_VISIBILITY)
......@@ -320,7 +320,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
"""
self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
self.create_mock_profile(self.user)
with self.assertNumQueries(18):
with self.assertNumQueries(19):
response = self.send_get(self.different_client)
self._verify_private_account_response(response, account_privacy=PRIVATE_VISIBILITY)
......@@ -395,12 +395,12 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
self.assertEqual(False, data["accomplishments_shared"])
self.client.login(username=self.user.username, password=TEST_PASSWORD)
verify_get_own_information(16)
verify_get_own_information(17)
# Now make sure that the user can get the same information, even if not active
self.user.is_active = False
self.user.save()
verify_get_own_information(10)
verify_get_own_information(11)
def test_get_account_empty_string(self):
"""
......@@ -414,7 +414,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
legacy_profile.save()
self.client.login(username=self.user.username, password=TEST_PASSWORD)
with self.assertNumQueries(16):
with self.assertNumQueries(17):
response = self.send_get(self.client)
for empty_field in ("level_of_education", "gender", "country", "bio"):
self.assertIsNone(response.data[empty_field])
......
......@@ -8,6 +8,7 @@ from django.core.urlresolvers import reverse
from django.shortcuts import render_to_response
from django.template.loader import render_to_string
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views.decorators.cache import cache_control
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View
......@@ -80,3 +81,9 @@ class CourseBookmarksFragmentView(EdxFragmentView):
self.add_fragment_resource_urls(fragment)
fragment.add_javascript(inline_js)
return fragment
def standalone_page_title(self, request, fragment, **kwargs):
"""
Returns the standalone page title.
"""
return _('Bookmarks')
......@@ -160,7 +160,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
course_home_url(self.course)
# Fetch the view and verify the query counts
with self.assertNumQueries(37, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with self.assertNumQueries(38, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with check_mongo_calls(4):
url = course_home_url(self.course)
self.client.get(url)
......
......@@ -127,7 +127,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
course_updates_url(self.course)
# Fetch the view and verify that the query counts haven't changed
with self.assertNumQueries(30, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with self.assertNumQueries(31, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with check_mongo_calls(4):
url = course_updates_url(self.course)
self.client.get(url)
// Color overrides
$white: rgb(255,255,255);
$red: #d9534f !default;
$footer-bg: $white;
$header-bg: $white;
$header-border-color: $red;
$base-font-color: $red;
$link-color: $red;
$lms-active-color: $red;
$lms-label-color: $red;
@import 'lms/static/sass/partials/base/variables';
......@@ -3,7 +3,7 @@
// Theme colors
//
// Note: define colors needed by your theme first
$red: #d9534f !default;
$red: #d9534f;
$brand-primary: $red;
// Theme fonts
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment