From 0f808cec7828b9df8fb97787bf653ac1eaae6199 Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Fri, 21 Jul 2017 20:42:38 -0400 Subject: [PATCH] Implement theme admin UI to support previewing LEARNER-2017 --- cms/envs/common.py | 7 +- cms/static/sass/bootstrap/studio-main.scss | 2 +- .../sass/elements/_system-feedback.scss | 54 +++++++ .../sass/partials/cms/base/_variables.scss | 21 +++ .../partials/{ => cms}/bootstrap/_theme.scss | 0 cms/templates/base.html | 29 +++- .../fragments/standalone-page-bootstrap.html | 13 ++ .../fragments/standalone-page-fragment.html | 15 ++ .../fragments/standalone-page-v1.html | 10 ++ .../fragments/standalone-page-v2.html | 13 ++ .../ux/reference/bootstrap/test.html | 17 +-- cms/urls.py | 3 + .../courseware/tests/test_course_info.py | 4 +- lms/djangoapps/courseware/tests/test_views.py | 10 +- .../django_comment_client/base/tests.py | 8 +- lms/envs/common.py | 3 - lms/static/sass/bootstrap/_layouts.scss | 4 +- .../fragments/standalone-page-bootstrap.html | 11 ++ .../standalone-page-fragment.html} | 7 - .../fragments/standalone-page-v1.html | 8 ++ .../fragments/standalone-page-v2.html | 11 ++ lms/urls.py | 3 + .../djangoapps/bookmarks/tests/test_views.py | 2 +- openedx/core/djangoapps/debug/views.py | 8 +- openedx/core/djangoapps/plugin_api/views.py | 63 ++++++-- openedx/core/djangoapps/theming/helpers.py | 10 ++ openedx/core/djangoapps/theming/middleware.py | 20 ++- openedx/core/djangoapps/theming/models.py | 1 - .../theming/theming-admin-fragment.html | 41 ++++++ .../theming/tests/test_middleware.py | 75 ++++++++-- .../djangoapps/theming/tests/test_views.py | 105 ++++++++++++++ openedx/core/djangoapps/theming/urls.py | 18 +++ openedx/core/djangoapps/theming/views.py | 135 ++++++++++++++++++ .../user_api/accounts/tests/test_views.py | 14 +- .../views/course_bookmarks.py | 7 + .../tests/views/test_course_home.py | 2 +- .../tests/views/test_course_updates.py | 2 +- .../partials/{ => cms}/bootstrap/_theme.scss | 0 .../partials/{ => cms}/bootstrap/_theme.scss | 0 .../static/sass/partials/base/_variables.scss | 14 -- .../partials/{ => cms}/bootstrap/_theme.scss | 2 +- 41 files changed, 666 insertions(+), 106 deletions(-) rename cms/static/sass/partials/{ => cms}/bootstrap/_theme.scss (100%) create mode 100644 cms/templates/fragments/standalone-page-bootstrap.html create mode 100644 cms/templates/fragments/standalone-page-fragment.html create mode 100644 cms/templates/fragments/standalone-page-v1.html create mode 100644 cms/templates/fragments/standalone-page-v2.html create mode 100644 lms/templates/fragments/standalone-page-bootstrap.html rename lms/templates/{fragment-view-chromeless.html => fragments/standalone-page-fragment.html} (68%) create mode 100644 lms/templates/fragments/standalone-page-v1.html create mode 100644 lms/templates/fragments/standalone-page-v2.html create mode 100644 openedx/core/djangoapps/theming/templates/theming/theming-admin-fragment.html create mode 100644 openedx/core/djangoapps/theming/tests/test_views.py create mode 100644 openedx/core/djangoapps/theming/urls.py create mode 100644 openedx/core/djangoapps/theming/views.py rename themes/edge.edx.org/cms/static/sass/partials/{ => cms}/bootstrap/_theme.scss (100%) rename themes/edx.org/cms/static/sass/partials/{ => cms}/bootstrap/_theme.scss (100%) delete mode 100755 themes/red-theme/cms/static/sass/partials/base/_variables.scss rename themes/red-theme/cms/static/sass/partials/{ => cms}/bootstrap/_theme.scss (92%) 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: + + % endfor +
+
+ % endif
<%block name="page_alert">
-
- <%block name="content"> -
+
+ <%block name="content"> +
% 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 name="content"> + <%include file="/fragments/standalone-page-fragment.html" args="fragment=fragment"/> + 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 name="footer_extra"> + ${HTML(fragment.foot_html())} + + +
+
+ ${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 name="content"> + <%include file="/fragments/standalone-page-fragment.html" args="fragment=fragment"/> + 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 name="content"> + <%include file="/fragments/standalone-page-fragment.html" args="fragment=fragment"/> + 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 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 ''} + +<%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())} 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 ''} + +<%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 ''} + +<%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")} +

+
+
+
+ +
+
+ + +
+ + +
+ +

See also Django admin for more theming settings.

+
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, + '