Skip to content

Commit f2a4bd2

Browse files
committed
feat: add optional completion editor to Studio
1 parent 2d41f44 commit f2a4bd2

File tree

10 files changed

+139
-11
lines changed

10 files changed

+139
-11
lines changed

cms/djangoapps/contentstore/utils.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -631,18 +631,38 @@ def find_staff_lock_source(xblock):
631631
return find_staff_lock_source(parent)
632632

633633

634+
def _get_parent_xblock(xblock, parent_xblock=None):
635+
"""
636+
Returns the parent xblock if provided, otherwise fetches it from the modulestore.
637+
Returns None if the xblock has no parent (orphaned).
638+
"""
639+
if parent_xblock is not None:
640+
return parent_xblock
641+
parent_location = modulestore().get_parent_location(
642+
xblock.location,
643+
revision=ModuleStoreEnum.RevisionOption.draft_preferred
644+
)
645+
if not parent_location:
646+
return None
647+
return modulestore().get_item(parent_location)
648+
649+
634650
def ancestor_has_staff_lock(xblock, parent_xblock=None):
635651
"""
636-
Returns True iff one of xblock's ancestors has staff lock.
652+
Returns True if one of xblock's ancestors has staff lock.
653+
Can avoid mongo query by passing in parent_xblock.
654+
"""
655+
parent = _get_parent_xblock(xblock, parent_xblock)
656+
return parent.visible_to_staff_only if parent else False
657+
658+
659+
def ancestor_has_optional_completion(xblock, parent_xblock=None):
660+
"""
661+
Returns True if one of xblock's ancestors has optional_completion.
637662
Can avoid mongo query by passing in parent_xblock.
638663
"""
639-
if parent_xblock is None:
640-
parent_location = modulestore().get_parent_location(xblock.location,
641-
revision=ModuleStoreEnum.RevisionOption.draft_preferred)
642-
if not parent_location:
643-
return False
644-
parent_xblock = modulestore().get_item(parent_location)
645-
return parent_xblock.visible_to_staff_only
664+
parent = _get_parent_xblock(xblock, parent_xblock)
665+
return parent.optional_completion if parent else False
646666

647667

648668
def get_sequence_usage_keys(course):

cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from xmodule.tabs import CourseTabList
6060

6161
from ..utils import (
62+
ancestor_has_optional_completion,
6263
ancestor_has_staff_lock,
6364
find_release_date_source,
6465
find_staff_lock_source,
@@ -1102,6 +1103,7 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
11021103
"hide_from_toc": xblock.hide_from_toc,
11031104
"enable_hide_from_toc_ui": settings.FEATURES.get("ENABLE_HIDE_FROM_TOC_UI", False),
11041105
"xblock_type": get_icon(xblock),
1106+
"optional_completion": xblock.optional_completion,
11051107
}
11061108
)
11071109

@@ -1252,6 +1254,8 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
12521254
xblock_info["course_tags_count"] = _get_course_tags_count(course.id)
12531255
xblock_info["tag_counts_by_block"] = _get_course_block_tags(xblock.location.context_key)
12541256

1257+
xblock_info["ancestor_has_optional_completion"] = ancestor_has_optional_completion(xblock, parent_xblock)
1258+
12551259
xblock_info[
12561260
"has_partition_group_components"
12571261
] = has_children_visible_to_specific_partition_groups(xblock)

cms/djangoapps/models/settings/course_metadata.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class CourseMetadata:
8181
'highlights_enabled_for_messaging',
8282
'is_onboarding_exam',
8383
'discussions_settings',
84+
'optional_completion',
8485
]
8586

8687
@classmethod

cms/static/js/models/xblock_info.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ define(
121121
*/
122122
ancestor_has_staff_lock: null,
123123
/**
124+
* True if any of this xblock's ancestors has optional completion.
125+
*/
126+
ancestor_has_optional_completion: null,
127+
/**
124128
* The xblock which is determining the staff lock value. For instance, for a unit,
125129
* this will either be the parent subsection or the grandparent section.
126130
* This can be null if the xblock has no inherited staff lock. Will only be present if

cms/static/js/views/modals/course_outline_modals.js

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
2020
StaffLockEditor, UnitAccessEditor, ContentVisibilityEditor, TimedExaminationPreferenceEditor,
2121
AccessEditor, ShowCorrectnessEditor, HighlightsEditor, HighlightsEnableXBlockModal, HighlightsEnableEditor,
2222
DiscussionEditor, SummaryConfigurationEditor, SubsectionShareLinkXBlockModal, FullPageShareLinkEditor,
23-
EmbedLinkShareLinkEditor;
23+
EmbedLinkShareLinkEditor, OptionalCompletionEditor;
2424

2525
CourseOutlineXBlockModal = BaseModal.extend({
2626
events: _.extend({}, BaseModal.prototype.events, {
@@ -1359,6 +1359,50 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
13591359
}
13601360
});
13611361

1362+
OptionalCompletionEditor = AbstractEditor.extend({
1363+
templateName: 'optional-completion-editor',
1364+
className: 'edit-optional-completion',
1365+
1366+
afterRender: function() {
1367+
AbstractEditor.prototype.afterRender.call(this);
1368+
this.setValue(this.model.get('optional_completion'));
1369+
},
1370+
1371+
setValue: function(value) {
1372+
this.$('input[name=optional_completion]').prop('checked', value);
1373+
},
1374+
1375+
currentValue: function() {
1376+
return this.$('input[name=optional_completion]').is(':checked');
1377+
},
1378+
1379+
hasChanges: function() {
1380+
return this.model.get('optional_completion') !== this.currentValue();
1381+
},
1382+
1383+
getRequestData: function() {
1384+
if (this.hasChanges()) {
1385+
return {
1386+
publish: 'republish',
1387+
metadata: {
1388+
// This variable relies on the inheritance mechanism, so we want to unset it instead of
1389+
// explicitly setting it to `false`.
1390+
optional_completion: this.currentValue() || null
1391+
}
1392+
};
1393+
} else {
1394+
return {};
1395+
}
1396+
},
1397+
1398+
getContext: function() {
1399+
return {
1400+
optional_completion: this.model.get('optional_completion'),
1401+
optional_ancestor: this.model.get('ancestor_has_optional_completion')
1402+
};
1403+
},
1404+
});
1405+
13621406
return {
13631407
getModal: function(type, xblockInfo, options) {
13641408
if (type === 'edit') {
@@ -1427,6 +1471,14 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
14271471
}
14281472
}
14291473

1474+
if (course.get('completion_tracking_enabled')) {
1475+
if (tabs.length > 0) {
1476+
tabs[0].editors.push(OptionalCompletionEditor);
1477+
} else {
1478+
editors.push(OptionalCompletionEditor);
1479+
}
1480+
}
1481+
14301482
/* globals course */
14311483
if (course.get('self_paced')) {
14321484
editors = _.without(editors, ReleaseDateEditor, DueDateEditor);

cms/static/sass/elements/_modal-window.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,7 @@
830830

831831
.edit-discussion,
832832
.edit-staff-lock,
833+
.edit-optional-completion,
833834
.summary-configuration,
834835
.edit-content-visibility,
835836
.edit-unit-access {
@@ -915,9 +916,18 @@
915916
}
916917
}
917918

919+
.edit-optional-completion {
920+
.field-message {
921+
@extend %t-copy-sub1;
922+
color: $gray-d1;
923+
margin-bottom: ($baseline/4);
924+
}
925+
}
926+
918927
.edit-discussion,
919928
.edit-unit-access,
920929
.edit-staff-lock,
930+
.edit-optional-completion,
921931
.summary-configuration {
922932
.modal-section-content {
923933
@include font-size(16);
@@ -961,6 +971,7 @@
961971
.edit-discussion,
962972
.edit-unit-access,
963973
.edit-staff-lock,
974+
.edit-optional-completion,
964975
.summary-configuration {
965976
.modal-section-content {
966977
@include font-size(16);

cms/templates/base.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
## Standard imports
99
<%namespace name='static' file='static_content.html'/>
1010
<%!
11+
from completion.waffle import ENABLE_COMPLETION_TRACKING_SWITCH
1112
from django.utils.translation import gettext as _
1213

1314
from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
@@ -175,7 +176,8 @@
175176
self_paced: ${ context_course.self_paced | n, dump_js_escaped_json },
176177
is_custom_relative_dates_active: ${CUSTOM_RELATIVE_DATES.is_enabled(context_course.id) | n, dump_js_escaped_json},
177178
start: ${context_course.start | n, dump_js_escaped_json},
178-
discussions_settings: ${context_course.discussions_settings | n, dump_js_escaped_json}
179+
discussions_settings: ${context_course.discussions_settings | n, dump_js_escaped_json},
180+
completion_tracking_enabled: ${ENABLE_COMPLETION_TRACKING_SWITCH.is_enabled() | n, dump_js_escaped_json},
179181
});
180182
</script>
181183
% endif

cms/templates/course_outline.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
<%block name="header_extras">
3131
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
32-
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'self-paced-due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'unit-access-editor', 'discussion-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs', 'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor', 'course-highlights-enable', 'course-manage-tags', 'course-video-sharing-enable', 'summary-configuration-editor', 'tag-count', 'subsection-share-link-modal-tabs', 'full-page-share-link-editor', 'embed-link-share-link-editor']:
32+
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'self-paced-due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'unit-access-editor', 'discussion-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs', 'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor', 'course-highlights-enable', 'course-manage-tags', 'course-video-sharing-enable', 'summary-configuration-editor', 'tag-count', 'subsection-share-link-modal-tabs', 'full-page-share-link-editor', 'embed-link-share-link-editor', 'optional-completion-editor']:
3333
<script type="text/template" id="${template_name}-tpl">
3434
<%static:include path="js/${template_name}.underscore" />
3535
</script>

cms/templates/js/course-outline.underscore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ var addStatusMessage = function (statusType, message) {
3030
}
3131
else if (statusType === 'partition-groups') {
3232
statusIconClass = 'fa-eye';
33+
} else if (statusType === 'optional-completion') {
34+
statusIconClass = 'fa-lightbulb-o';
3335
}
3436

3537
statusMessages.push({iconClass: statusIconClass, text: message});
@@ -105,6 +107,12 @@ if (xblockInfo.get('graded')) {
105107
}
106108
}
107109

110+
if (xblockInfo.get('optional_completion') && !xblockInfo.get('ancestor_has_optional_completion')) {
111+
messageType = 'optional-completion';
112+
messageText = gettext('Optional completion');
113+
addStatusMessage(messageType, messageText);
114+
}
115+
108116
var is_proctored_exam = xblockInfo.get('is_proctored_exam');
109117
var is_practice_exam = xblockInfo.get('is_practice_exam');
110118
var is_onboarding_exam = xblockInfo.get('is_onboarding_exam');
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<form>
2+
<h3 class="modal-section-title">
3+
<%- gettext('Completion') %>
4+
</h3>
5+
<div class="modal-section-content optional-completion">
6+
<label class="label">
7+
<% if (optional_ancestor) { %>
8+
<input disabled type="checkbox" id="optional_completion" name="optional_completion"
9+
class="input input-checkbox" />
10+
<%- gettext('Optional') %>
11+
<p class="tip tip-warning">
12+
<% var message = gettext('This %(xblockType)s already has an optional parent.') %>
13+
<%- interpolate(message, { xblockType: xblockType }, true) %>
14+
</p>
15+
<% } else { %>
16+
<input type="checkbox" id="optional_completion" name="optional_completion"
17+
class="input input-checkbox" />
18+
<%- gettext('Optional') %>
19+
<% } %>
20+
<p class="field-message">
21+
<% var message = gettext('Optional %(xblockType)ss won\'t count towards course or parent completion.') %>
22+
<%- interpolate(message, { xblockType: xblockType }, true) %>
23+
</p>
24+
</label>
25+
</div>
26+
</form>

0 commit comments

Comments
 (0)