Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,18 +631,38 @@ def find_staff_lock_source(xblock):
return find_staff_lock_source(parent)


def _get_parent_xblock(xblock, parent_xblock=None):
"""
Returns the parent xblock if provided, otherwise fetches it from the modulestore.
Returns None if the xblock has no parent (orphaned).
"""
if parent_xblock is not None:
return parent_xblock
parent_location = modulestore().get_parent_location(
xblock.location,
revision=ModuleStoreEnum.RevisionOption.draft_preferred
)
if not parent_location:
return None
return modulestore().get_item(parent_location)


def ancestor_has_staff_lock(xblock, parent_xblock=None):
"""
Returns True iff one of xblock's ancestors has staff lock.
Returns True if one of xblock's ancestors has staff lock.
Can avoid mongo query by passing in parent_xblock.
"""
parent = _get_parent_xblock(xblock, parent_xblock)
return parent.visible_to_staff_only if parent else False


def ancestor_has_optional_completion(xblock, parent_xblock=None):
"""
Returns True if one of xblock's ancestors has optional_completion.
Can avoid mongo query by passing in parent_xblock.
"""
if parent_xblock is None:
parent_location = modulestore().get_parent_location(xblock.location,
revision=ModuleStoreEnum.RevisionOption.draft_preferred)
if not parent_location:
return False
parent_xblock = modulestore().get_item(parent_location)
return parent_xblock.visible_to_staff_only
parent = _get_parent_xblock(xblock, parent_xblock)
return parent.optional_completion if parent else False


def get_sequence_usage_keys(course):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from xmodule.tabs import CourseTabList

from ..utils import (
ancestor_has_optional_completion,
ancestor_has_staff_lock,
find_release_date_source,
find_staff_lock_source,
Expand Down Expand Up @@ -1102,6 +1103,7 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
"hide_from_toc": xblock.hide_from_toc,
"enable_hide_from_toc_ui": settings.FEATURES.get("ENABLE_HIDE_FROM_TOC_UI", False),
"xblock_type": get_icon(xblock),
"optional_completion": xblock.optional_completion,
}
)

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

xblock_info["ancestor_has_optional_completion"] = ancestor_has_optional_completion(xblock, parent_xblock)

xblock_info[
"has_partition_group_components"
] = has_children_visible_to_specific_partition_groups(xblock)
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/models/settings/course_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class CourseMetadata:
'highlights_enabled_for_messaging',
'is_onboarding_exam',
'discussions_settings',
'optional_completion',
]

@classmethod
Expand Down
4 changes: 4 additions & 0 deletions cms/static/js/models/xblock_info.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ define(
*/
ancestor_has_staff_lock: null,
/**
* True if any of this xblock's ancestors has optional completion.
*/
ancestor_has_optional_completion: null,
/**
* The xblock which is determining the staff lock value. For instance, for a unit,
* this will either be the parent subsection or the grandparent section.
* This can be null if the xblock has no inherited staff lock. Will only be present if
Expand Down
54 changes: 53 additions & 1 deletion cms/static/js/views/modals/course_outline_modals.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
StaffLockEditor, UnitAccessEditor, ContentVisibilityEditor, TimedExaminationPreferenceEditor,
AccessEditor, ShowCorrectnessEditor, HighlightsEditor, HighlightsEnableXBlockModal, HighlightsEnableEditor,
DiscussionEditor, SummaryConfigurationEditor, SubsectionShareLinkXBlockModal, FullPageShareLinkEditor,
EmbedLinkShareLinkEditor;
EmbedLinkShareLinkEditor, OptionalCompletionEditor;

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

OptionalCompletionEditor = AbstractEditor.extend({
templateName: 'optional-completion-editor',
className: 'edit-optional-completion',

afterRender: function() {
AbstractEditor.prototype.afterRender.call(this);
this.setValue(this.model.get('optional_completion'));
},

setValue: function(value) {
this.$('input[name=optional_completion]').prop('checked', value);
},

currentValue: function() {
return this.$('input[name=optional_completion]').is(':checked');
},

hasChanges: function() {
return this.model.get('optional_completion') !== this.currentValue();
},

getRequestData: function() {
if (this.hasChanges()) {
return {
publish: 'republish',
metadata: {
// This variable relies on the inheritance mechanism, so we want to unset it instead of
// explicitly setting it to `false`.
optional_completion: this.currentValue() || null
}
};
} else {
return {};
}
},

getContext: function() {
return {
optional_completion: this.model.get('optional_completion'),
optional_ancestor: this.model.get('ancestor_has_optional_completion')
};
},
});

return {
getModal: function(type, xblockInfo, options) {
if (type === 'edit') {
Expand Down Expand Up @@ -1427,6 +1471,14 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
}

if (course.get('completion_tracking_enabled')) {
if (tabs.length > 0) {
tabs[0].editors.push(OptionalCompletionEditor);
} else {
editors.push(OptionalCompletionEditor);
}
}

/* globals course */
if (course.get('self_paced')) {
editors = _.without(editors, ReleaseDateEditor, DueDateEditor);
Expand Down
11 changes: 11 additions & 0 deletions cms/static/sass/elements/_modal-window.scss
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,7 @@

.edit-discussion,
.edit-staff-lock,
.edit-optional-completion,
.summary-configuration,
.edit-content-visibility,
.edit-unit-access {
Expand Down Expand Up @@ -915,9 +916,18 @@
}
}

.edit-optional-completion {
.field-message {
@extend %t-copy-sub1;
color: $gray-d1;
margin-bottom: ($baseline/4);
}
}

.edit-discussion,
.edit-unit-access,
.edit-staff-lock,
.edit-optional-completion,
.summary-configuration {
.modal-section-content {
@include font-size(16);
Expand Down Expand Up @@ -961,6 +971,7 @@
.edit-discussion,
.edit-unit-access,
.edit-staff-lock,
.edit-optional-completion,
.summary-configuration {
.modal-section-content {
@include font-size(16);
Expand Down
4 changes: 3 additions & 1 deletion cms/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
## Standard imports
<%namespace name='static' file='static_content.html'/>
<%!
from completion.waffle import ENABLE_COMPLETION_TRACKING_SWITCH
from django.utils.translation import gettext as _

from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
Expand Down Expand Up @@ -175,7 +176,8 @@
self_paced: ${ context_course.self_paced | n, dump_js_escaped_json },
is_custom_relative_dates_active: ${CUSTOM_RELATIVE_DATES.is_enabled(context_course.id) | n, dump_js_escaped_json},
start: ${context_course.start | n, dump_js_escaped_json},
discussions_settings: ${context_course.discussions_settings | n, dump_js_escaped_json}
discussions_settings: ${context_course.discussions_settings | n, dump_js_escaped_json},
completion_tracking_enabled: ${ENABLE_COMPLETION_TRACKING_SWITCH.is_enabled() | n, dump_js_escaped_json},
});
</script>
% endif
Expand Down
2 changes: 1 addition & 1 deletion cms/templates/course_outline.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

<%block name="header_extras">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
% 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']:
% 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']:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
Expand Down
8 changes: 8 additions & 0 deletions cms/templates/js/course-outline.underscore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var addStatusMessage = function (statusType, message) {
}
else if (statusType === 'partition-groups') {
statusIconClass = 'fa-eye';
} else if (statusType === 'optional-completion') {
statusIconClass = 'fa-lightbulb-o';
}

statusMessages.push({iconClass: statusIconClass, text: message});
Expand Down Expand Up @@ -105,6 +107,12 @@ if (xblockInfo.get('graded')) {
}
}

if (xblockInfo.get('optional_completion') && !xblockInfo.get('ancestor_has_optional_completion')) {
messageType = 'optional-completion';
messageText = gettext('Optional completion');
addStatusMessage(messageType, messageText);
}

var is_proctored_exam = xblockInfo.get('is_proctored_exam');
var is_practice_exam = xblockInfo.get('is_practice_exam');
var is_onboarding_exam = xblockInfo.get('is_onboarding_exam');
Expand Down
26 changes: 26 additions & 0 deletions cms/templates/js/optional-completion-editor.underscore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<form>
<h3 class="modal-section-title">
<%- gettext('Completion') %>
</h3>
<div class="modal-section-content optional-completion">
<label class="label">
<% if (optional_ancestor) { %>
<input disabled type="checkbox" id="optional_completion" name="optional_completion"
class="input input-checkbox" />
<%- gettext('Optional') %>
<p class="tip tip-warning">
<% var message = gettext('This %(xblockType)s already has an optional parent.') %>
<%- interpolate(message, { xblockType: xblockType }, true) %>
</p>
<% } else { %>
<input type="checkbox" id="optional_completion" name="optional_completion"
class="input input-checkbox" />
<%- gettext('Optional') %>
<% } %>
<p class="field-message">
<% var message = gettext('Optional %(xblockType)ss won\'t count towards course or parent completion.') %>
<%- interpolate(message, { xblockType: xblockType }, true) %>
</p>
</label>
</div>
</form>
1 change: 1 addition & 0 deletions lms/djangoapps/course_api/blocks/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def __init__(
SupportedFieldType(BlockCompletionTransformer.COMPLETION, BlockCompletionTransformer),
SupportedFieldType(BlockCompletionTransformer.COMPLETE),
SupportedFieldType(BlockCompletionTransformer.RESUME_BLOCK),
SupportedFieldType(BlockCompletionTransformer.OPTIONAL_COMPLETION),
SupportedFieldType(DiscussionsTopicLinkTransformer.EXTERNAL_ID),
SupportedFieldType(DiscussionsTopicLinkTransformer.EMBED_URL),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class BlockCompletionTransformer(BlockStructureTransformer):
COMPLETION = 'completion'
COMPLETE = 'complete'
RESUME_BLOCK = 'resume_block'
OPTIONAL_COMPLETION = 'optional_completion'

@classmethod
def name(cls):
Expand All @@ -43,7 +44,7 @@ def get_block_completion(cls, block_structure, block_key):

@classmethod
def collect(cls, block_structure):
block_structure.request_xblock_fields('completion_mode')
block_structure.request_xblock_fields('completion_mode', cls.OPTIONAL_COMPLETION)

@staticmethod
def _is_block_excluded(block_structure, block_key):
Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/course_home_api/outline/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def get_blocks(self, block): # pylint: disable=missing-function-docstring
block_key: {
'children': [child['id'] for child in children],
'complete': block.get('complete', False),
'optional_completion': block.get('optional_completion', False),
'description': description,
'display_name': display_name,
'due': block.get('due'),
Expand Down
39 changes: 26 additions & 13 deletions lms/djangoapps/courseware/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,38 +552,51 @@ def get_course_assignment_date_blocks(course, user, request, num_return=None,


@request_cached()
def get_course_blocks_completion_summary(course_key, user):
def get_course_blocks_completion_summary(course_key, user) -> dict[str, int]:
"""
Returns an object with the number of complete units, incomplete units, and units that contain gated content
Returns a dict with the number of complete units, incomplete units, and units that contain gated content
for the given course. The complete and incomplete counts only reflect units that are able to be completed by
the given user. If a unit contains gated content, it is not counted towards the incomplete count.

The object contains fields: complete_count, incomplete_count, locked_count
The dict contains fields:
- complete_count
- incomplete_count
- locked_count
- optional_complete_count
- optional_incomplete_count
- optional_locked_count
"""
if not user.id:
return []
return {}

store = modulestore()
course_usage_key = store.make_course_usage_key(course_key)
block_data = get_course_blocks(user, course_usage_key, allow_start_dates_in_future=True, include_completion=True)

complete_count, incomplete_count, locked_count = 0, 0, 0
counts = {
'complete_count': 0,
'incomplete_count': 0,
'locked_count': 0,
'optional_complete_count': 0,
'optional_incomplete_count': 0,
'optional_locked_count': 0,
}
for section_key in block_data.get_children(course_usage_key): # pylint: disable=too-many-nested-blocks
for subsection_key in block_data.get_children(section_key):
for unit_key in block_data.get_children(subsection_key):
complete = block_data.get_xblock_field(unit_key, 'complete', False)
contains_gated_content = block_data.get_xblock_field(unit_key, 'contains_gated_content', False)
optional = block_data.get_xblock_field(unit_key, 'optional_completion', False)
prefix = "optional_" if optional else ""

if contains_gated_content:
locked_count += 1
counts[f"{prefix}locked_count"] += 1
elif complete:
complete_count += 1
counts[f"{prefix}complete_count"] += 1
else:
incomplete_count += 1
counts[f"{prefix}incomplete_count"] += 1

return {
'complete_count': complete_count,
'incomplete_count': incomplete_count,
'locked_count': locked_count
}
return counts


@request_cached()
Expand Down
1 change: 1 addition & 0 deletions openedx/features/course_experience/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def recurse_mark_auth_denial(block):
'weight',
'completion',
'complete',
'optional_completion',
'resume_block',
'hide_from_toc',
'icon_class',
Expand Down
Loading
Loading