diff --git a/cms/envs/common.py b/cms/envs/common.py index 2d8c46cc960be115feae4cc90c7a6da6dd5dfc4b..59d6c9328dee35773b5f8b758d541c3ad24ddef9 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 de32b0257af309b114a5df3454494d3eccdbc09e..f664fceb394345efbb8db7a14cb106bee82e51e1 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 2bdcc879c0ef57610849070f6605c3c79b750ff3..239ddb83050925952e9b91ae27dd42674c99fee8 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 92aa6404295b5e61e488650f1a6ddd557d170ec4..87b48c01e3b992085f2ff8b3b6e01d5d1a5a1e70 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 9cc015c745fee33737a6e63330e04ab587950c22..5cba0a40514fec5b7c67a64ff21d906f508b7c70 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 ( <!-- 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(): diff --git a/cms/templates/fragments/standalone-page-bootstrap.html b/cms/templates/fragments/standalone-page-bootstrap.html new file mode 100644 index 0000000000000000000000000000000000000000..b9c4f8c03e2e9e7698d9910c3d203b320285629b --- /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 0000000000000000000000000000000000000000..492f049cd6ca9d1a89874980baa8edd8fcead3c3 --- /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> + +<div class="wrapper-content wrapper"> + <section class="content"> + ${HTML(fragment.body_html())} + </section> +</div> diff --git a/cms/templates/fragments/standalone-page-v1.html b/cms/templates/fragments/standalone-page-v1.html new file mode 100644 index 0000000000000000000000000000000000000000..32cd02641088c7f97428a3116d8281cb175bd0b5 --- /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 0000000000000000000000000000000000000000..3a78db48ebe171459e9c32f49f6409c9ab4f7531 --- /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 311a85a13230e6aa53c1857257c3065267a1aae3..bf68ae86a4f1e38d4b46ece697a84c99275eef53 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 36324081ea729f04c6e751d41d006e6b144e45b7..03184d8f21fd429ba0c17f61e53ce45a4c5713bd 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 31f74d61d96e79f64147cf2a5393c20d90c52600..00b48818080db917041e416b79453ccdbaaf28a8 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 418a1548868bc2c627ec6b49dbf5f5d91e025dab..45087978070b7b3ed0907f9bd8e842a64d5c620d 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 23ccb0911384e14c45b26537bfec8a060ddde37c..49e709833c3ead4ca2a4f6aa459b488a309604be 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 5d5ef06219957a33449e6d64621fb117811dc4d4..d87c336fc1fd14b4dd5aee19c5906d6d2927a788 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 05ded2191abd910d78b2c22383d22b784ae80305..660140e439fdbc544a52b96ae5d5dd2c4622bebe 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 0000000000000000000000000000000000000000..eee76b7ac7607feb8fb03221db50339e2fbc858e --- /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 68a13c355df5beb719baa7c2b121d4d9807f2403..b89f69e2f744c559c0170be9d9a27986e70c96af 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 0000000000000000000000000000000000000000..d9f97304afd876f44bec805f65a4975d758a5957 --- /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 0000000000000000000000000000000000000000..83f533387ba5e3a722a34dcb6d3216b4a6c52c6d --- /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 9733bf921c9167125de48cf5aae76ecb4158bb57..86c68ef411c91a2438167d7c12b85675c714857c 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 30ce17cc2d0eaeb69dd22b814e0e5e663c679989..e8344f70ec6bb5b577c69a5e123ed8646d344bd9 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 ba2d200a2a5e3b17140e2175afd5b0a9068febe2..b0ac1f8acbdf94abcb2cab60c108512381467e3a 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 4b461bb3c49ffefdb378c6baf23348897dad8daa..f45260ed138b27b33c472539513c2861d3b9e029 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 16b93c02be470e2c12f28eb61494ec3eb353c669..d178c811116c34f7ba14bbbce406be9ac3d0169c 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 bb4b0970ef97b0c23109373a858831aeb28db632..d2f25321e526e8be2d3e83bc67ddd18c45c7a528 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 9efc21a3c022c14c4ea5adf4257e0b073881f631..da771dcbcddeccce588f28f0b313af75fb042833 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 0000000000000000000000000000000000000000..81c22b711582070e84833df32024bb34d980956d --- /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 +%> + +<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> diff --git a/openedx/core/djangoapps/theming/tests/test_middleware.py b/openedx/core/djangoapps/theming/tests/test_middleware.py index 5f217d5b02decae3e41a8db2f2a84a62f7fde870..a151e0210666625b54019618d1f02f72dd2aed61 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.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) 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 0000000000000000000000000000000000000000..e6654402d87c78e293cdc716278e9f2ea352639a --- /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, + '<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) + ) diff --git a/openedx/core/djangoapps/theming/urls.py b/openedx/core/djangoapps/theming/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..79d25a297bdbfb7bbef873985df03c4a9e8f8e95 --- /dev/null +++ b/openedx/core/djangoapps/theming/urls.py @@ -0,0 +1,18 @@ +""" +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', + ), + ] diff --git a/openedx/core/djangoapps/theming/views.py b/openedx/core/djangoapps/theming/views.py new file mode 100644 index 0000000000000000000000000000000000000000..52502784659e797d44d504d2e09cafe8fac2ef35 --- /dev/null +++ b/openedx/core/djangoapps/theming/views.py @@ -0,0 +1,135 @@ +""" +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') diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index dd6f2dd0173be00cfa5c2e37e4d19d9de0825bad..12d911b30624205bdfed06b796b93c35886490ca 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -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]) diff --git a/openedx/features/course_bookmarks/views/course_bookmarks.py b/openedx/features/course_bookmarks/views/course_bookmarks.py index 95e753525cc5c25e1029767d69de0c34d1f0b95a..207db69965222cd7f29eaee0891e9418d7c0258e 100644 --- a/openedx/features/course_bookmarks/views/course_bookmarks.py +++ b/openedx/features/course_bookmarks/views/course_bookmarks.py @@ -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') diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index a3a08bd9cb4df450c0fe77417cb55ddc20647ba1..39478c162eacd15627109bc07e22cff6afdcf3b1 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -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) diff --git a/openedx/features/course_experience/tests/views/test_course_updates.py b/openedx/features/course_experience/tests/views/test_course_updates.py index 49c979632a1c5a41d148d48ed45903c6288bbc4f..ad2d481381005232f325c74d263b0178266712f1 100644 --- a/openedx/features/course_experience/tests/views/test_course_updates.py +++ b/openedx/features/course_experience/tests/views/test_course_updates.py @@ -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) diff --git a/themes/edge.edx.org/cms/static/sass/partials/bootstrap/_theme.scss b/themes/edge.edx.org/cms/static/sass/partials/cms/bootstrap/_theme.scss similarity index 100% rename from themes/edge.edx.org/cms/static/sass/partials/bootstrap/_theme.scss rename to themes/edge.edx.org/cms/static/sass/partials/cms/bootstrap/_theme.scss diff --git a/themes/edx.org/cms/static/sass/partials/bootstrap/_theme.scss b/themes/edx.org/cms/static/sass/partials/cms/bootstrap/_theme.scss similarity index 100% rename from themes/edx.org/cms/static/sass/partials/bootstrap/_theme.scss rename to themes/edx.org/cms/static/sass/partials/cms/bootstrap/_theme.scss diff --git a/themes/red-theme/cms/static/sass/partials/base/_variables.scss b/themes/red-theme/cms/static/sass/partials/base/_variables.scss deleted file mode 100755 index c32bb59471481c953277b5706539c83fca830dcd..0000000000000000000000000000000000000000 --- a/themes/red-theme/cms/static/sass/partials/base/_variables.scss +++ /dev/null @@ -1,14 +0,0 @@ -// 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'; diff --git a/themes/red-theme/cms/static/sass/partials/bootstrap/_theme.scss b/themes/red-theme/cms/static/sass/partials/cms/bootstrap/_theme.scss similarity index 92% rename from themes/red-theme/cms/static/sass/partials/bootstrap/_theme.scss rename to themes/red-theme/cms/static/sass/partials/cms/bootstrap/_theme.scss index 7fb5406954665f86fd6412f4db943f82f3e7b537..30d2c1a98f7d9b5feb2aab82b1bf92bd2038b79c 100644 --- a/themes/red-theme/cms/static/sass/partials/bootstrap/_theme.scss +++ b/themes/red-theme/cms/static/sass/partials/cms/bootstrap/_theme.scss @@ -3,7 +3,7 @@ // Theme colors // // Note: define colors needed by your theme first -$red: #d9534f !default; +$red: #d9534f; $brand-primary: $red; // Theme fonts