Skip to content

Commit

Permalink
Add a pluggable CallToAction service for XBlocks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cpennington committed Aug 6, 2020
1 parent 30750d6 commit f9619d6
Show file tree
Hide file tree
Showing 26 changed files with 296 additions and 252 deletions.
26 changes: 5 additions & 21 deletions common/djangoapps/util/views.py
@@ -1,6 +1,3 @@


import ast
import json
import logging
import sys
Expand All @@ -14,7 +11,6 @@
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
Expand Down Expand Up @@ -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()
6 changes: 6 additions & 0 deletions common/lib/xmodule/xmodule/capa_base.py
Expand Up @@ -737,6 +737,11 @@ def get_problem_html(self, encapsulate=True, submit_notification=False):
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,
Expand Down Expand Up @@ -775,6 +780,7 @@ def get_problem_html(self, encapsulate=True, submit_notification=False):
'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)
Expand Down
1 change: 1 addition & 0 deletions common/lib/xmodule/xmodule/capa_module.py
Expand Up @@ -37,6 +37,7 @@

@XBlock.wants('user')
@XBlock.needs('i18n')
@XBlock.wants('call_to_action')
class ProblemBlock(
CapaMixin, RawMixin, XmlMixin, EditingMixin,
XModuleDescriptorToXBlockMixin, XModuleToXBlockMixin, HTMLSnippet, ResourceTemplates, XModuleMixin):
Expand Down
13 changes: 13 additions & 0 deletions common/lib/xmodule/xmodule/css/capa/display.scss
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions common/lib/xmodule/xmodule/vertical_block.py
Expand Up @@ -30,6 +30,7 @@

@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.
Expand Down Expand Up @@ -91,6 +92,9 @@ def _student_or_public_view(self, context, view):
'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 = {
Expand All @@ -101,6 +105,7 @@ def _student_or_public_view(self, context, view):
'completed': completed,
'past_due': past_due,
'subsection_format': context.get('format', ''),
'vertical_banner_ctas': vertical_banner_ctas,
}

if view == STUDENT_VIEW:
Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/courseware/tests/test_views.py
Expand Up @@ -3205,7 +3205,7 @@ def test_reset_deadlines_banner_displays(self):
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):
Expand Down
4 changes: 0 additions & 4 deletions lms/djangoapps/courseware/views/views.py
Expand Up @@ -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(),
}

Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions lms/djangoapps/lms_xblock/runtime.py
Expand Up @@ -16,6 +16,7 @@
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
Expand Down Expand Up @@ -164,6 +165,7 @@ def __init__(self, **kwargs):
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):
Expand Down
1 change: 1 addition & 0 deletions lms/static/sass/_build-course.scss
Expand Up @@ -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';
Expand Down
35 changes: 0 additions & 35 deletions lms/static/sass/base/_base.scss
Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion lms/static/sass/bootstrap/lms-main.scss
Expand Up @@ -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';
Expand Down
33 changes: 0 additions & 33 deletions lms/static/sass/course/_dates.scss
Expand Up @@ -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;
Expand Down
52 changes: 0 additions & 52 deletions lms/static/sass/course/_info.scss
Expand Up @@ -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();
}
Expand Down
15 changes: 9 additions & 6 deletions lms/static/sass/course/base/_base.scss
Expand Up @@ -155,27 +155,30 @@ 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);

&::after {
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;
}
}

Expand Down
17 changes: 0 additions & 17 deletions lms/static/sass/course/base/_mixins.scss
Expand Up @@ -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;
Expand Down

0 comments on commit f9619d6

Please sign in to comment.