From f9619d6cad30aee254f0884cf04c9c0213d04e13 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 30 Jul 2020 15:53:21 -0400 Subject: [PATCH] Add a pluggable CallToAction service for XBlocks This also has an initial use case for Personalized Learner Schedules to add CTAs to capa and vertical blocks to allow users to shift their course deadlines. --- common/djangoapps/util/views.py | 26 ++----- common/lib/xmodule/xmodule/capa_base.py | 6 ++ common/lib/xmodule/xmodule/capa_module.py | 1 + .../lib/xmodule/xmodule/css/capa/display.scss | 13 ++++ common/lib/xmodule/xmodule/vertical_block.py | 5 ++ lms/djangoapps/courseware/tests/test_views.py | 2 +- lms/djangoapps/courseware/views/views.py | 4 - lms/djangoapps/lms_xblock/runtime.py | 2 + lms/static/sass/_build-course.scss | 1 + lms/static/sass/base/_base.scss | 35 --------- lms/static/sass/bootstrap/lms-main.scss | 2 +- lms/static/sass/course/_dates.scss | 33 -------- lms/static/sass/course/_info.scss | 52 ------------- lms/static/sass/course/base/_base.scss | 15 ++-- lms/static/sass/course/base/_mixins.scss | 17 ----- .../sass/course/layout/_banner_cta.scss | 73 ++++++++++++++++++ .../sass/course/layout/_dates_banner.scss | 53 ------------- lms/templates/dates_banner.html | 40 +++++----- lms/templates/problem.html | 17 +++++ lms/templates/vert_module.html | 18 +++++ openedx/core/lib/xblock_services/__init__.py | 0 .../lib/xblock_services/call_to_action.py | 42 +++++++++++ .../tests/views/test_course_outline.py | 8 +- .../course_experience/views/course_outline.py | 3 - .../call_to_action.py | 75 +++++++++++++++++++ setup.py | 5 +- 26 files changed, 296 insertions(+), 252 deletions(-) create mode 100644 lms/static/sass/course/layout/_banner_cta.scss delete mode 100644 lms/static/sass/course/layout/_dates_banner.scss create mode 100644 openedx/core/lib/xblock_services/__init__.py create mode 100644 openedx/core/lib/xblock_services/call_to_action.py create mode 100644 openedx/features/personalized_learner_schedules/call_to_action.py diff --git a/common/djangoapps/util/views.py b/common/djangoapps/util/views.py index af4e115b52..ecad99bf84 100644 --- a/common/djangoapps/util/views.py +++ b/common/djangoapps/util/views.py @@ -1,6 +1,3 @@ - - -import ast import json import logging import sys @@ -14,7 +11,6 @@ from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpRespon from django.views.decorators.csrf import ensure_csrf_cookie, requires_csrf_token from django.views.defaults import server_error from django.shortcuts import redirect -from django.urls import reverse from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey from six.moves import map @@ -201,28 +197,16 @@ def reset_course_deadlines(request): Set the start_date of a schedule to today, which in turn will adjust due dates for sequentials belonging to a self paced course """ - from lms.urls import RENDER_XBLOCK_NAME - from openedx.features.course_experience.urls import COURSE_HOME_VIEW_NAME - - detail_id_dict = ast.literal_eval(request.POST.get('reset_deadlines_redirect_url_id_dict')) - redirect_url = request.POST.get('reset_deadlines_redirect_url_base', COURSE_HOME_VIEW_NAME) - course_key = CourseKey.from_string(detail_id_dict['course_id']) - masquerade_details, masquerade_user = setup_masquerade( + course_key = CourseKey.from_string(request.POST.get('course_id')) + _course_masquerade, user = setup_masquerade( request, course_key, has_access(request.user, 'staff', course_key) ) - if masquerade_details and masquerade_details.role == 'student' and masquerade_details.user_name and ( - redirect_url == COURSE_HOME_VIEW_NAME - ): - # Masquerading as a specific student, so reset that student's schedule - user = masquerade_user - else: - user = request.user missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, user) if missed_deadlines and not missed_gated_content: reset_self_paced_schedule(user, course_key) - if redirect_url == RENDER_XBLOCK_NAME: - detail_id_dict.pop('course_id') - return redirect(reverse(redirect_url, kwargs=detail_id_dict)) + + referrer = request.META.get('HTTP_REFERER') + return redirect(referrer) if referrer else HttpResponse() diff --git a/common/lib/xmodule/xmodule/capa_base.py b/common/lib/xmodule/xmodule/capa_base.py index 3d52de8a5e..fc9c0fb609 100644 --- a/common/lib/xmodule/xmodule/capa_base.py +++ b/common/lib/xmodule/xmodule/capa_base.py @@ -737,6 +737,11 @@ class CapaMixin(ScorableXBlockMixin, CapaFields): submit_button = self.submit_button_name() submit_button_submitting = self.submit_button_submitting_name() should_enable_submit_button = self.should_enable_submit_button() + submit_disabled_ctas = None + if not should_enable_submit_button: + cta_service = self.runtime.service(self, "call_to_action") + if cta_service: + submit_disabled_ctas = cta_service.get_ctas(self, 'capa_submit_disabled') content = { 'name': self.display_name_with_default, @@ -775,6 +780,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields): 'answer_notification_message': answer_notification_message, 'has_saved_answers': self.has_saved_answers, 'save_message': save_message, + 'submit_disabled_cta': submit_disabled_ctas[0] if submit_disabled_ctas else None, } html = self.runtime.render_template('problem.html', context) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 1dbfa7429f..2fc2120f2f 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -37,6 +37,7 @@ log = logging.getLogger("edx.courseware") @XBlock.wants('user') @XBlock.needs('i18n') +@XBlock.wants('call_to_action') class ProblemBlock( CapaMixin, RawMixin, XmlMixin, EditingMixin, XModuleDescriptorToXBlockMixin, XModuleToXBlockMixin, HTMLSnippet, ResourceTemplates, XModuleMixin): diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 2f1a8eda13..29412b79ce 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -995,6 +995,19 @@ div.problem .action { white-space: nowrap; } + + .submit-cta { + display: inline-block; + } + .submit-cta-description { + margin-left: 8px; + } + .submit-cta-link-button { + background: none; + border: none; + color: $blue; + cursor: pointer; + } } .submission-feedback { diff --git a/common/lib/xmodule/xmodule/vertical_block.py b/common/lib/xmodule/xmodule/vertical_block.py index 9de73be343..11d83bec69 100644 --- a/common/lib/xmodule/xmodule/vertical_block.py +++ b/common/lib/xmodule/xmodule/vertical_block.py @@ -30,6 +30,7 @@ CLASS_PRIORITY = ['video', 'problem'] @XBlock.needs('user', 'bookmarks') @XBlock.wants('completion') +@XBlock.wants('call_to_action') class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParserMixin, MakoTemplateBlockBase, XBlock): """ Layout XBlock for rendering subblocks vertically. @@ -91,6 +92,9 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse 'content': rendered_child.content }) + cta_service = self.runtime.service(self, 'call_to_action') + vertical_banner_ctas = cta_service and cta_service.get_ctas(self, 'vertical_banner') + completed = self.is_block_complete_for_assignments(completion_service) past_due = completed is False and self.due and self.due < datetime.now(pytz.UTC) fragment_context = { @@ -101,6 +105,7 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse 'completed': completed, 'past_due': past_due, 'subsection_format': context.get('format', ''), + 'vertical_banner_ctas': vertical_banner_ctas, } if view == STUDENT_VIEW: diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 2125e0307a..98827a369f 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -3205,7 +3205,7 @@ class DatesTabTestCase(ModuleStoreTestCase): graded=True, ) response = self._get_response(self.course) - self.assertContains(response, 'div class="dates-banner-text"') + self.assertContains(response, 'div class="banner-cta-text"') class TestShowCoursewareMFE(TestCase): diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index b23e60cf0c..fd9701d2d9 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -1079,8 +1079,6 @@ def dates(request, course_id): 'missed_deadlines': missed_deadlines, 'missed_gated_content': missed_gated_content, 'reset_deadlines_url': reverse(RESET_COURSE_DEADLINES_NAME), - 'reset_deadlines_redirect_url_base': COURSE_DATES_NAME, - 'reset_deadlines_redirect_url_id_dict': {'course_id': str(course.id)}, 'has_ended': course.has_ended(), } @@ -1687,8 +1685,6 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): 'is_learning_mfe': request.META.get('HTTP_REFERER', '').startswith(settings.LEARNING_MICROFRONTEND_URL), 'is_mobile_app': is_request_from_mobile_app(request), 'reset_deadlines_url': reverse(RESET_COURSE_DEADLINES_NAME), - 'reset_deadlines_redirect_url_base': COURSE_DATES_NAME, - 'reset_deadlines_redirect_url_id_dict': {'course_id': str(course.id)} } return render_to_response('courseware/courseware-chromeless.html', context) diff --git a/lms/djangoapps/lms_xblock/runtime.py b/lms/djangoapps/lms_xblock/runtime.py index d1f1b48a98..c488d0c5af 100644 --- a/lms/djangoapps/lms_xblock/runtime.py +++ b/lms/djangoapps/lms_xblock/runtime.py @@ -16,6 +16,7 @@ from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig from lms.djangoapps.teams.services import TeamsService from openedx.core.djangoapps.user_api.course_tag import api as user_course_tag_api from openedx.core.lib.url_utils import quote_slashes +from openedx.core.lib.xblock_services.call_to_action import CallToActionService from openedx.core.lib.xblock_utils import wrap_xblock_aside, xblock_local_resource_url from xmodule.library_tools import LibraryToolsService from xmodule.modulestore.django import ModuleI18nService, modulestore @@ -164,6 +165,7 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method self.request_token = kwargs.pop('request_token', None) services['teams'] = TeamsService() services['teams_configuration'] = TeamsConfigurationService() + services['call_to_action'] = CallToActionService() super(LmsModuleSystem, self).__init__(**kwargs) def handler_url(self, *args, **kwargs): diff --git a/lms/static/sass/_build-course.scss b/lms/static/sass/_build-course.scss index ce6c72b718..8388f836ea 100644 --- a/lms/static/sass/_build-course.scss +++ b/lms/static/sass/_build-course.scss @@ -22,6 +22,7 @@ @import 'course/layout/courseware_header'; @import 'course/layout/courseware_preview'; @import 'course/layout/footer'; +@import 'course/layout/banner_cta'; @import 'course/base/mixins'; @import 'course/base/base'; @import 'course/base/extends'; diff --git a/lms/static/sass/base/_base.scss b/lms/static/sass/base/_base.scss index e572f7c5f1..4cfee159fb 100644 --- a/lms/static/sass/base/_base.scss +++ b/lms/static/sass/base/_base.scss @@ -305,38 +305,3 @@ mark { } } } - -.dates-banner { - border-radius: 4px; - border: solid 1px #9cd2e6; - background-color: #eff8fa; - margin-top: 20px; - margin-bottom: 20px; - padding: 24px; - display: flex; - flex-wrap: wrap; - justify-content: space-between; - max-width: $text-width-readability-max; - - .dates-banner-text { - font-size: 16px; - line-height: 24px; - color: #414141; - - a.mobile-dates-link { - color: #0075b4; - } - } - - &.has-button { - .dates-banner-text { - flex: 1 1 20em; - max-width: 70%; - } - } - - &.on-mobile { - margin-left: 20px; - margin-right: 20px; - } -} diff --git a/lms/static/sass/bootstrap/lms-main.scss b/lms/static/sass/bootstrap/lms-main.scss index 290b2bb84d..893a19a3c7 100644 --- a/lms/static/sass/bootstrap/lms-main.scss +++ b/lms/static/sass/bootstrap/lms-main.scss @@ -15,7 +15,7 @@ $static-path: '../..'; @import 'layouts'; @import 'components'; @import 'course/layout/courseware_preview'; -@import 'course/layout/dates_banner'; +@import 'course/layout/banner_cta'; @import 'shared/modal'; @import 'shared/help-tab'; @import './elements/banners'; diff --git a/lms/static/sass/course/_dates.scss b/lms/static/sass/course/_dates.scss index 5a69f91acc..9fa6b3753f 100644 --- a/lms/static/sass/course/_dates.scss +++ b/lms/static/sass/course/_dates.scss @@ -9,39 +9,6 @@ border-bottom: 0; } - .dates-banner { - border-radius: 4px; - border: solid 1px #9cd2e6; - background-color: #eff8fa; - margin-top: 20px; - margin-bottom: 20px; - padding: 24px; - display: flex; - flex-wrap: wrap; - justify-content: space-between; - max-width: $text-width-readability-max; - - .dates-banner-text { - font-size: 16px; - line-height: 24px; - color: #414141; - } - - .banner-has-button { - flex: 1 1 20em; - max-width: 70%; - } - - .upgrade-button { - align-self: start; - flex: none; - - button { - @include white-button-flat-outline; - } - } - } - .timeline-item { border-left: solid 1px #2d323e; color: #2d323e; diff --git a/lms/static/sass/course/_info.scss b/lms/static/sass/course/_info.scss index ab6f6a4f17..b1331985c6 100644 --- a/lms/static/sass/course/_info.scss +++ b/lms/static/sass/course/_info.scss @@ -58,58 +58,6 @@ div.info-wrapper { width: 100%; display: block; - div.dates-banner { - // This banner uses the Pattern Library's defined variables - @include border-left(0); - - border: 1px solid $border-color; - width: 100%; - display: table; - - .notification-color-border { - width: 6px; //Value defined by UX team - min-height: 100%; - margin: 0; - display: table-cell; - background: $notification-highlight-border-color; - } - - .notification-content { - display: inline-flex; - align-items: center; - align-content: flex-start; - flex-flow: row wrap; - background: $notification-background; - width: 100%; - padding: $baseline/2 0; - margin-bottom: 0; - justify-content: space-between; - - .upgrade-icon { - margin: 0; - padding: $baseline/2 $baseline; - flex-flow: row nowrap; - align-items: center; - // flex: grow, shrink, base - // The 7 was the value that allowed the icon image to grow to the UX - // desired size. - flex: 7 1 50px; - // The following dimensions were added so that the - // icon will adjust as the notification is adjusted - // but will not be smaller or larger than UX requirements. - min-height: 50px; - min-width: 80px; - max-height: 90px; - max-width: 130px; - - img { - min-height: 50px; - min-width: 80px; - } - } - } - } - > p { margin-bottom: lh(); } diff --git a/lms/static/sass/course/base/_base.scss b/lms/static/sass/course/base/_base.scss index c017cd145f..e04763c4e9 100644 --- a/lms/static/sass/course/base/_base.scss +++ b/lms/static/sass/course/base/_base.scss @@ -155,15 +155,18 @@ img { top: 0; left: 0; z-index: 99999; - padding: 0 10px; + padding: ($baseline/2); border-radius: 3px; - background: rgba(0, 0, 0, 0.85); + background: #ffffff; + border-color: #000000; + border-width: 2px; + border-style: solid; font-size: 11px; font-weight: 400; - line-height: 26px; - color: $white; + color: #000000; pointer-events: none; opacity: 0; + max-width: 200px; @include transition(opacity 0.1s linear 0s); @@ -171,11 +174,11 @@ img { content: '▾'; display: block; position: absolute; - bottom: -14px; + bottom: -($baseline - 2); left: 50%; margin-left: -7px; font-size: 20px; - color: rgba(0, 0, 0, 0.85); + color: #000000; } } diff --git a/lms/static/sass/course/base/_mixins.scss b/lms/static/sass/course/base/_mixins.scss index d8fab175eb..ac9156c82b 100644 --- a/lms/static/sass/course/base/_mixins.scss +++ b/lms/static/sass/course/base/_mixins.scss @@ -45,23 +45,6 @@ } } -@mixin white-button-flat-outline { - display: block; - border-radius: 2px; - border: solid 1px #0175b4; - background: white; - color: #2d323e; - font-size: 14px; - font-weight: bold; - line-height: 24px; - - &:hover, - &:focus, - &:active { - box-shadow: 0 2px 1px $shadow; - } -} - @mixin dark-grey-button { display: block; height: 35px; diff --git a/lms/static/sass/course/layout/_banner_cta.scss b/lms/static/sass/course/layout/_banner_cta.scss new file mode 100644 index 0000000000..b87454fa2e --- /dev/null +++ b/lms/static/sass/course/layout/_banner_cta.scss @@ -0,0 +1,73 @@ + +@mixin white-button-flat-outline { + display: block; + border-radius: 2px; + border: solid 1px #0175b4; + font-size: 14px; + font-weight: bold; + line-height: 24px; + padding: 7px 18px; + + // if you change colors here, modify them in hover block below too + background: white; + color: #2d323e; + + &:hover, + &:focus, + &:active { + cursor: pointer; + box-shadow: 0 2px 1px $shadow; + + // re-specify colors to overwrite any styling from our reset styles + background: white; + color: #2d323e; + } + } + +.banner-cta { + border-radius: 4px; + border: solid 1px #9cd2e6; + background-color: #eff8fa; + margin-top: 20px; + margin-bottom: 20px; + padding: 24px; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + max-width: $text-width-readability-max; + + .banner-cta-text { + font-size: 16px; + line-height: 24px; + color: #414141; + + a.mobile-dates-link { + color: #0075b4; + } + } + + &.has-button { + .banner-cta-text { + flex: 1 1 20em; + max-width: 70%; + } + } + + &.on-mobile { + margin-left: 20px; + margin-right: 20px; + } + + .banner-cta-button { + align-self: start; + flex: none; + + a { + text-decoration: none; + } + + button { + @include white-button-flat-outline; + } + } + } diff --git a/lms/static/sass/course/layout/_dates_banner.scss b/lms/static/sass/course/layout/_dates_banner.scss deleted file mode 100644 index 813d87fb02..0000000000 --- a/lms/static/sass/course/layout/_dates_banner.scss +++ /dev/null @@ -1,53 +0,0 @@ -.dates-banner { - border-radius: 4px; - border: solid 1px #9cd2e6; - background-color: #eff8fa; - margin-top: 20px; - margin-bottom: 20px; - padding: 24px; - display: flex; - flex-wrap: wrap; - justify-content: space-between; - max-width: $text-width-readability-max; - - .dates-banner-text { - font-size: 16px; - line-height: 24px; - color: #414141; - } - - &.has-button { - .dates-banner-text { - flex: 1 1 20em; - max-width: 70%; - } - } - - .upgrade-button { - align-self: start; - flex: none; - - a { - text-decoration: none; - } - - button { - display: block; - border-radius: 2px; - border: solid 1px #0175b4; - background: white; - color: #2d323e; - font-size: 14px; - font-weight: bold; - line-height: 24px; - padding: 7px 18px; - - &:hover, - &:focus, - &:active { - cursor: pointer; - box-shadow: 0 2px 1px $shadow; - } - } - } -} diff --git a/lms/templates/dates_banner.html b/lms/templates/dates_banner.html index 34c382711d..232b45ff11 100644 --- a/lms/templates/dates_banner.html +++ b/lms/templates/dates_banner.html @@ -13,8 +13,8 @@ additional_styling_class = 'on-mobile' if is_mobile_app and not is_learning_mfe %> <%def name="reset_dates_banner()"> -
-
+