From d8c7d47702023f820435a0944b16220d8fa7e70e Mon Sep 17 00:00:00 2001
From: Daniel Valenzuela
Date: Sat, 10 Feb 2024 15:43:18 -0300
Subject: [PATCH 01/10] feat: optional xblocks
---
cms/djangoapps/contentstore/views/block.py | 1 +
.../models/settings/course_metadata.py | 1 +
.../js/views/modals/course_outline_modals.js | 46 +++++++++++++++++--
cms/static/sass/elements/_modal-window.scss | 8 +++-
cms/templates/course_outline.html | 2 +-
cms/templates/js/course-outline.underscore | 3 ++
.../js/optional-content-editor.underscore | 5 ++
.../course_api/blocks/serializers.py | 1 +
.../blocks/transformers/block_completion.py | 2 +-
.../course_home_api/outline/serializers.py | 1 +
.../outline/tests/test_view.py | 15 ++++++
.../course_home_api/progress/serializers.py | 1 +
.../progress/tests/test_views.py | 10 ++++
.../course_home_api/progress/views.py | 1 +
lms/djangoapps/courseware/courses.py | 10 +++-
openedx/features/course_experience/utils.py | 1 +
xmodule/modulestore/inheritance.py | 11 +++++
17 files changed, 111 insertions(+), 8 deletions(-)
create mode 100644 cms/templates/js/optional-content-editor.underscore
diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py
index 7305bc9adc89..8b9680c2a3a3 100644
--- a/cms/djangoapps/contentstore/views/block.py
+++ b/cms/djangoapps/contentstore/views/block.py
@@ -1321,6 +1321,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'group_access': xblock.group_access,
'user_partitions': user_partitions,
'show_correctness': xblock.show_correctness,
+ 'optional_content': xblock.optional_content,
})
if xblock.category == 'sequential':
diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py
index c072a4ae552b..4be641f82546 100644
--- a/cms/djangoapps/models/settings/course_metadata.py
+++ b/cms/djangoapps/models/settings/course_metadata.py
@@ -78,6 +78,7 @@ class CourseMetadata:
'highlights_enabled_for_messaging',
'is_onboarding_exam',
'discussions_settings',
+ 'optional',
]
@classmethod
diff --git a/cms/static/js/views/modals/course_outline_modals.js b/cms/static/js/views/modals/course_outline_modals.js
index a10a4f23749d..1197af63e65a 100644
--- a/cms/static/js/views/modals/course_outline_modals.js
+++ b/cms/static/js/views/modals/course_outline_modals.js
@@ -18,7 +18,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
ReleaseDateEditor, DueDateEditor, SelfPacedDueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor,
StaffLockEditor, UnitAccessEditor, ContentVisibilityEditor, TimedExaminationPreferenceEditor,
AccessEditor, ShowCorrectnessEditor, HighlightsEditor, HighlightsEnableXBlockModal, HighlightsEnableEditor,
- DiscussionEditor;
+ DiscussionEditor, OptionalContentEditor;
CourseOutlineXBlockModal = BaseModal.extend({
events: _.extend({}, BaseModal.prototype.events, {
@@ -1201,6 +1201,46 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
});
+ OptionalContentEditor = AbstractEditor.extend(
+ {
+ templateName: 'optional-content-editor',
+ className: 'edit-optional-content',
+
+ afterRender: function() {
+ AbstractEditor.prototype.afterRender.call(this);
+ this.setValue(this.model.get("optional_content"));
+ },
+
+ setValue: function(value) {
+ this.$('input[name=optional_content]').prop('checked', value);
+ },
+
+ currentValue: function() {
+ return this.$('input[name=optional_content]').is(':checked');
+ },
+
+ hasChanges: function() {
+ return this.model.get('optional_content') !== this.currentValue();
+ },
+
+ getRequestData: function() {
+ if (this.hasChanges()) {
+ return {
+ publish: 'republish',
+ metadata: {
+ optional_content: this.currentValue()
+ }
+ };
+ } else {
+ return {};
+ }
+ },
+ getContext: function() {
+ return {
+ optional_content: this.model.get('optional_content')
+ };
+ },
+ })
return {
getModal: function(type, xblockInfo, options) {
if (type === 'edit') {
@@ -1240,10 +1280,10 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
];
if (xblockInfo.isChapter()) {
- tabs[0].editors = [ReleaseDateEditor];
+ tabs[0].editors = [ReleaseDateEditor, OptionalContentEditor];
tabs[1].editors = [StaffLockEditor];
} else if (xblockInfo.isSequential()) {
- tabs[0].editors = [ReleaseDateEditor, GradingEditor, DueDateEditor];
+ tabs[0].editors = [ReleaseDateEditor, GradingEditor, DueDateEditor, OptionalContentEditor];
tabs[1].editors = [ContentVisibilityEditor, ShowCorrectnessEditor];
if (course.get('self_paced') && course.get('is_custom_relative_dates_active')) {
tabs[0].editors.push(SelfPacedDueDateEditor);
diff --git a/cms/static/sass/elements/_modal-window.scss b/cms/static/sass/elements/_modal-window.scss
index b6508ef49a01..4a91aced1316 100644
--- a/cms/static/sass/elements/_modal-window.scss
+++ b/cms/static/sass/elements/_modal-window.scss
@@ -747,6 +747,7 @@
.edit-discussion,
.edit-staff-lock,
+ .edit-optional-content,
.edit-content-visibility,
.edit-unit-access {
margin-bottom: $baseline;
@@ -760,19 +761,20 @@
// UI: staff lock and discussion
.edit-discussion,
.edit-staff-lock,
+ .edit-optional-content,
.edit-settings-timed-examination,
.edit-unit-access {
.checkbox-cosmetic .input-checkbox {
@extend %cont-text-sr;
// CASE: unchecked
- ~ .tip-warning {
+ ~.tip-warning {
display: block;
}
// CASE: checked
&:checked {
- ~ .tip-warning {
+ ~.tip-warning {
display: none;
}
}
@@ -832,6 +834,7 @@
.edit-discussion,
.edit-unit-access,
+ .edit-optional-content,
.edit-staff-lock {
.modal-section-content {
@include font-size(16);
@@ -874,6 +877,7 @@
.edit-discussion,
.edit-unit-access,
+ .edit-optional-content,
.edit-staff-lock {
.modal-section-content {
@include font-size(16);
diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html
index f9f499c87bd8..f24a574be9fe 100644
--- a/cms/templates/course_outline.html
+++ b/cms/templates/course_outline.html
@@ -29,7 +29,7 @@
<%block name="header_extras">
-% 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']:
+% 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', 'optional-content-editor']:
diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore
index 7f82410196bb..636d37bd131a 100644
--- a/cms/templates/js/course-outline.underscore
+++ b/cms/templates/js/course-outline.underscore
@@ -225,6 +225,9 @@ if (is_proctored_exam) {
<% if (xblockInfo.get('release_date')) { %>
<%- xblockInfo.get('release_date') %>
<% } %>
+ <% if (xblockInfo.get('optional_content')) { %>
+ - <%- gettext('Optional') %>
+ <% } %>
<% } %>
diff --git a/cms/templates/js/optional-content-editor.underscore b/cms/templates/js/optional-content-editor.underscore
new file mode 100644
index 000000000000..8b6101488cc3
--- /dev/null
+++ b/cms/templates/js/optional-content-editor.underscore
@@ -0,0 +1,5 @@
+
+
+ <%- gettext('Mark as optional') %>
+
diff --git a/lms/djangoapps/course_api/blocks/serializers.py b/lms/djangoapps/course_api/blocks/serializers.py
index ac031600a02c..68c4ab0f1840 100644
--- a/lms/djangoapps/course_api/blocks/serializers.py
+++ b/lms/djangoapps/course_api/blocks/serializers.py
@@ -56,6 +56,7 @@ def __init__(
SupportedFieldType('has_score'),
SupportedFieldType('has_scheduled_content'),
SupportedFieldType('weight'),
+ SupportedFieldType('optional_content'),
SupportedFieldType('show_correctness'),
# 'student_view_data'
SupportedFieldType(StudentViewTransformer.STUDENT_VIEW_DATA, StudentViewTransformer),
diff --git a/lms/djangoapps/course_api/blocks/transformers/block_completion.py b/lms/djangoapps/course_api/blocks/transformers/block_completion.py
index 472555c4c7f9..3825137e1abc 100644
--- a/lms/djangoapps/course_api/blocks/transformers/block_completion.py
+++ b/lms/djangoapps/course_api/blocks/transformers/block_completion.py
@@ -43,7 +43,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', 'optional_content')
@staticmethod
def _is_block_excluded(block_structure, block_key):
diff --git a/lms/djangoapps/course_home_api/outline/serializers.py b/lms/djangoapps/course_home_api/outline/serializers.py
index 20b205e3040d..d337130fda18 100644
--- a/lms/djangoapps/course_home_api/outline/serializers.py
+++ b/lms/djangoapps/course_home_api/outline/serializers.py
@@ -54,6 +54,7 @@ def get_blocks(self, block): # pylint: disable=missing-function-docstring
'resume_block': block.get('resume_block', False),
'type': block_type,
'has_scheduled_content': block.get('has_scheduled_content'),
+ 'optional_content': block.get('optional_content'),
},
}
for child in children:
diff --git a/lms/djangoapps/course_home_api/outline/tests/test_view.py b/lms/djangoapps/course_home_api/outline/tests/test_view.py
index f02ad646ef48..79a037118b45 100644
--- a/lms/djangoapps/course_home_api/outline/tests/test_view.py
+++ b/lms/djangoapps/course_home_api/outline/tests/test_view.py
@@ -437,3 +437,18 @@ def test_cannot_enroll_if_full(self):
self.update_course_and_overview()
CourseEnrollment.enroll(UserFactory(), self.course.id) # grr, some rando took our spot!
self.assert_can_enroll(False)
+
+ def test_optional_content(self):
+ CourseEnrollment.enroll(self.user, self.course.id)
+ assert not self.course.optional_content
+ response = self.client.get(self.url)
+ for block in response.data['course_blocks']['blocks'].values():
+ assert not block['optional_content']
+
+ def test_optional_content_true(self):
+ self.course.optional_content = True
+ self.update_course_and_overview()
+ CourseEnrollment.enroll(self.user, self.course.id)
+ response = self.client.get(self.url)
+ for block in response.data['course_blocks']['blocks'].values():
+ assert block['optional_content']
diff --git a/lms/djangoapps/course_home_api/progress/serializers.py b/lms/djangoapps/course_home_api/progress/serializers.py
index 190cd76bf9b5..98667314674b 100644
--- a/lms/djangoapps/course_home_api/progress/serializers.py
+++ b/lms/djangoapps/course_home_api/progress/serializers.py
@@ -134,6 +134,7 @@ class ProgressTabSerializer(VerifiedModeSerializer):
access_expiration = serializers.DictField()
certificate_data = CertificateDataSerializer()
completion_summary = serializers.DictField()
+ optional_completion_summary = serializers.DictField()
course_grade = CourseGradeSerializer()
credit_course_requirements = serializers.DictField()
end = serializers.DateTimeField()
diff --git a/lms/djangoapps/course_home_api/progress/tests/test_views.py b/lms/djangoapps/course_home_api/progress/tests/test_views.py
index d13ebec29c21..253d9a544e97 100644
--- a/lms/djangoapps/course_home_api/progress/tests/test_views.py
+++ b/lms/djangoapps/course_home_api/progress/tests/test_views.py
@@ -314,3 +314,13 @@ def test_course_grade_considers_subsection_grade_visibility(self, is_staff, expe
assert response.status_code == 200
assert response.data['course_grade']['percent'] == expected_percent
assert response.data['course_grade']['is_passing'] == (expected_percent >= 0.5)
+
+ def test_optional_content(self):
+ CourseEnrollment.enroll(self.user, self.course.id)
+ response = self.client.get(self.url)
+ assert response.status_code == 200
+ assert response.data['optional_completion_summary'] == {
+ 'complete_count': 0,
+ 'incomplete_count': 0,
+ 'locked_count': 0,
+ }
diff --git a/lms/djangoapps/course_home_api/progress/views.py b/lms/djangoapps/course_home_api/progress/views.py
index dc0ea63525f7..ad1bbd4ef5ce 100644
--- a/lms/djangoapps/course_home_api/progress/views.py
+++ b/lms/djangoapps/course_home_api/progress/views.py
@@ -246,6 +246,7 @@ def get(self, request, *args, **kwargs):
'access_expiration': access_expiration,
'certificate_data': get_cert_data(student, course, enrollment_mode, course_grade),
'completion_summary': get_course_blocks_completion_summary(course_key, student),
+ 'optional_completion_summary': get_course_blocks_completion_summary(course_key, student, optional=True),
'course_grade': course_grade,
'credit_course_requirements': credit_course_requirements(course_key, student),
'end': course.end,
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 6ded860329fb..7c992bc7cfe2 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -552,7 +552,7 @@ 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, optional=False):
"""
Returns an object 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
@@ -566,10 +566,18 @@ def get_course_blocks_completion_summary(course_key, user):
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)
+ def _is_optional(*keys):
+ for key in keys:
+ if block_data.get_xblock_field(key, 'optional_content', False):
+ return True
+ return False
+
complete_count, incomplete_count, locked_count = 0, 0, 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):
+ if optional != _is_optional(section_key, subsection_key, unit_key):
+ continue
complete = block_data.get_xblock_field(unit_key, 'complete', False)
contains_gated_content = block_data.get_xblock_field(unit_key, 'contains_gated_content', False)
if contains_gated_content:
diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py
index d58b54f6139f..092f6b41405e 100644
--- a/openedx/features/course_experience/utils.py
+++ b/openedx/features/course_experience/utils.py
@@ -115,6 +115,7 @@ def recurse_mark_auth_denial(block):
'completion',
'complete',
'resume_block',
+ 'optional_content',
],
allow_start_dates_in_future=allow_start_dates_in_future,
)
diff --git a/xmodule/modulestore/inheritance.py b/xmodule/modulestore/inheritance.py
index e75b9ab083c4..46948870266d 100644
--- a/xmodule/modulestore/inheritance.py
+++ b/xmodule/modulestore/inheritance.py
@@ -238,6 +238,17 @@ class InheritanceMixin(XBlockMixin):
scope=Scope.settings
)
+ optional_content = Boolean(
+ display_name=_('Optional'),
+ help=_(
+ 'Set this to true to mark this block as optional.'
+ 'Progress in this block won\'t count towards course completion progress'
+ 'and will count as optional progress instead.'
+ ),
+ default=False,
+ scope=Scope.settings,
+ )
+
@property
def close_date(self):
"""
From 1a4994b6107cbb3cb8931f1fe70869be8c2e7839 Mon Sep 17 00:00:00 2001
From: Daniel Valenzuela
Date: Thu, 29 Feb 2024 03:15:24 -0300
Subject: [PATCH 02/10] feat: address PR comments
---
.../contentstore/tests/test_utils.py | 54 +++++++++++++++++++
cms/djangoapps/contentstore/utils.py | 14 +++++
cms/djangoapps/contentstore/views/block.py | 8 ++-
.../models/settings/course_metadata.py | 2 +-
cms/static/js/models/xblock_info.js | 4 ++
.../js/views/modals/course_outline_modals.js | 28 +++++-----
cms/static/sass/elements/_modal-window.scss | 20 ++++---
cms/templates/course_outline.html | 2 +-
cms/templates/js/course-outline.underscore | 2 +-
.../js/optional-completion-editor.underscore | 26 +++++++++
.../js/optional-content-editor.underscore | 5 --
.../course_api/blocks/serializers.py | 2 +-
.../blocks/transformers/block_completion.py | 4 +-
.../course_home_api/outline/serializers.py | 2 +-
.../outline/tests/test_view.py | 12 ++---
.../progress/tests/test_views.py | 2 +-
lms/djangoapps/courseware/courses.py | 2 +-
openedx/features/course_experience/utils.py | 2 +-
xmodule/modulestore/inheritance.py | 2 +-
19 files changed, 151 insertions(+), 42 deletions(-)
create mode 100644 cms/templates/js/optional-completion-editor.underscore
delete mode 100644 cms/templates/js/optional-content-editor.underscore
diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py
index 5b4f4338b2b6..8c39ed7a5ff5 100644
--- a/cms/djangoapps/contentstore/tests/test_utils.py
+++ b/cms/djangoapps/contentstore/tests/test_utils.py
@@ -336,6 +336,60 @@ def test_no_inheritance_for_orphan(self):
self.assertFalse(utils.ancestor_has_staff_lock(self.orphan))
+class InheritedOptionalCompletionTest(CourseTestCase):
+ """Tests for determining if an xblock inherits optional completion."""
+
+ def setUp(self):
+ super().setUp()
+ chapter = BlockFactory.create(category='chapter', parent=self.course)
+ sequential = BlockFactory.create(category='sequential', parent=chapter)
+ vertical = BlockFactory.create(category='vertical', parent=sequential)
+ html = BlockFactory.create(category='html', parent=vertical)
+ problem = BlockFactory.create(
+ category='problem', parent=vertical, data=""
+ )
+ self.chapter = self.store.get_item(chapter.location)
+ self.sequential = self.store.get_item(sequential.location)
+ self.vertical = self.store.get_item(vertical.location)
+ self.html = self.store.get_item(html.location)
+ self.problem = self.store.get_item(problem.location)
+
+ def set_optional_completion(self, xblock, value):
+ """ Sets optional_completion to specified value and calls update_item to persist the change. """
+ xblock.optional_completion = value
+ self.store.update_item(xblock, self.user.id)
+
+ def update_optional_completions(self, chapter, sequential, vertical):
+ self.set_optional_completion(self.chapter, chapter)
+ self.set_optional_completion(self.sequential, sequential)
+ self.set_optional_completion(self.vertical, vertical)
+
+ def test_no_inheritance(self):
+ """Tests that vertical with no optional ancestors does not have an inherited optional completion"""
+ self.update_optional_completions(False, False, False)
+ self.assertFalse(utils.ancestor_has_optional_completion(self.vertical))
+ self.update_optional_completions(False, False, True)
+ self.assertFalse(utils.ancestor_has_optional_completion(self.vertical))
+
+ def test_inheritance_in_optional_section(self):
+ """Tests that a vertical in an optional section has an inherited optional completion"""
+ self.update_optional_completions(True, False, False)
+ self.assertTrue(utils.ancestor_has_optional_completion(self.vertical))
+ self.update_optional_completions(True, False, True)
+ self.assertTrue(utils.ancestor_has_optional_completion(self.vertical))
+
+ def test_inheritance_in_optional_subsection(self):
+ """Tests that a vertical in an optional subsection has an inherited optional completion"""
+ self.update_optional_completions(False, True, False)
+ self.assertTrue(utils.ancestor_has_optional_completion(self.vertical))
+ self.update_optional_completions(False, True, True)
+ self.assertTrue(utils.ancestor_has_optional_completion(self.vertical))
+
+ def test_no_inheritance_for_orphan(self):
+ """Tests that an orphaned xblock does not inherit optional completion"""
+ self.assertFalse(utils.ancestor_has_optional_completion(self.orphan))
+
+
class GroupVisibilityTest(CourseTestCase):
"""
Test content group access rules.
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 94f689d2cb02..a92d479d4fd0 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -354,6 +354,20 @@ def ancestor_has_staff_lock(xblock, parent_xblock=None):
return parent_xblock.visible_to_staff_only
+def ancestor_has_optional_completion(xblock, parent_xblock=None):
+ """
+ Returns True iff 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.optional_completion
+
+
def reverse_url(handler_name, key_name=None, key_value=None, kwargs=None):
"""
Creates the URL for the given handler.
diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py
index 8b9680c2a3a3..92d3397a3955 100644
--- a/cms/djangoapps/contentstore/views/block.py
+++ b/cms/djangoapps/contentstore/views/block.py
@@ -60,6 +60,7 @@
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, STUDIO_VIEW # lint-amnesty, pylint: disable=wrong-import-order
from ..utils import (
+ ancestor_has_optional_completion,
ancestor_has_staff_lock,
find_release_date_source,
find_staff_lock_source,
@@ -1321,7 +1322,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'group_access': xblock.group_access,
'user_partitions': user_partitions,
'show_correctness': xblock.show_correctness,
- 'optional_content': xblock.optional_content,
+ 'optional_completion': xblock.optional_completion,
})
if xblock.category == 'sequential':
@@ -1406,6 +1407,11 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
xblock_info['ancestor_has_staff_lock'] = False
if course_outline:
+ if xblock_info['optional_completion']:
+ xblock_info['ancestor_has_optional_completion'] = ancestor_has_optional_completion(xblock, parent_xblock)
+ else:
+ xblock_info['ancestor_has_optional_completion'] = False
+
if xblock_info['has_explicit_staff_lock']:
xblock_info['staff_only_message'] = True
elif child_info and child_info['children']:
diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py
index 4be641f82546..3fde376bb781 100644
--- a/cms/djangoapps/models/settings/course_metadata.py
+++ b/cms/djangoapps/models/settings/course_metadata.py
@@ -78,7 +78,7 @@ class CourseMetadata:
'highlights_enabled_for_messaging',
'is_onboarding_exam',
'discussions_settings',
- 'optional',
+ 'optional_completion',
]
@classmethod
diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js
index 983edc4e5648..7c46a3e201c9 100644
--- a/cms/static/js/models/xblock_info.js
+++ b/cms/static/js/models/xblock_info.js
@@ -120,6 +120,10 @@ define(
*/
ancestor_has_staff_lock: null,
/**
+ * True if this any of this xblock's ancestors are optional for 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
diff --git a/cms/static/js/views/modals/course_outline_modals.js b/cms/static/js/views/modals/course_outline_modals.js
index 1197af63e65a..098e55d51079 100644
--- a/cms/static/js/views/modals/course_outline_modals.js
+++ b/cms/static/js/views/modals/course_outline_modals.js
@@ -18,7 +18,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
ReleaseDateEditor, DueDateEditor, SelfPacedDueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor,
StaffLockEditor, UnitAccessEditor, ContentVisibilityEditor, TimedExaminationPreferenceEditor,
AccessEditor, ShowCorrectnessEditor, HighlightsEditor, HighlightsEnableXBlockModal, HighlightsEnableEditor,
- DiscussionEditor, OptionalContentEditor;
+ DiscussionEditor, OptionalCompletionEditor;
CourseOutlineXBlockModal = BaseModal.extend({
events: _.extend({}, BaseModal.prototype.events, {
@@ -1201,26 +1201,26 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
});
- OptionalContentEditor = AbstractEditor.extend(
+ OptionalCompletionEditor = AbstractEditor.extend(
{
- templateName: 'optional-content-editor',
- className: 'edit-optional-content',
+ templateName: 'optional-completion-editor',
+ className: 'edit-optional-completion',
afterRender: function() {
AbstractEditor.prototype.afterRender.call(this);
- this.setValue(this.model.get("optional_content"));
+ this.setValue(this.model.get("optional_completion"));
},
setValue: function(value) {
- this.$('input[name=optional_content]').prop('checked', value);
+ this.$('input[name=optional_completion]').prop('checked', value);
},
currentValue: function() {
- return this.$('input[name=optional_content]').is(':checked');
+ return this.$('input[name=optional_completion]').is(':checked');
},
hasChanges: function() {
- return this.model.get('optional_content') !== this.currentValue();
+ return this.model.get('optional_completion') !== this.currentValue();
},
getRequestData: function() {
@@ -1228,16 +1228,18 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
return {
publish: 'republish',
metadata: {
- optional_content: this.currentValue()
+ optional_completion: this.currentValue()
}
};
} else {
return {};
}
},
+
getContext: function() {
return {
- optional_content: this.model.get('optional_content')
+ optional_completion: this.model.get('optional_completion'),
+ optionalAncestor: this.model.get('ancestor_has_optional_completion')
};
},
})
@@ -1265,7 +1267,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
editors: []
};
if (xblockInfo.isVertical()) {
- editors = [StaffLockEditor, UnitAccessEditor, DiscussionEditor];
+ editors = [StaffLockEditor, UnitAccessEditor, DiscussionEditor, OptionalCompletionEditor];
} else {
tabs = [
{
@@ -1280,10 +1282,10 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
];
if (xblockInfo.isChapter()) {
- tabs[0].editors = [ReleaseDateEditor, OptionalContentEditor];
+ tabs[0].editors = [ReleaseDateEditor, OptionalCompletionEditor];
tabs[1].editors = [StaffLockEditor];
} else if (xblockInfo.isSequential()) {
- tabs[0].editors = [ReleaseDateEditor, GradingEditor, DueDateEditor, OptionalContentEditor];
+ tabs[0].editors = [ReleaseDateEditor, GradingEditor, DueDateEditor, OptionalCompletionEditor];
tabs[1].editors = [ContentVisibilityEditor, ShowCorrectnessEditor];
if (course.get('self_paced') && course.get('is_custom_relative_dates_active')) {
tabs[0].editors.push(SelfPacedDueDateEditor);
diff --git a/cms/static/sass/elements/_modal-window.scss b/cms/static/sass/elements/_modal-window.scss
index 4a91aced1316..92ccfe2f1876 100644
--- a/cms/static/sass/elements/_modal-window.scss
+++ b/cms/static/sass/elements/_modal-window.scss
@@ -747,7 +747,7 @@
.edit-discussion,
.edit-staff-lock,
- .edit-optional-content,
+ .edit-optional-completion,
.edit-content-visibility,
.edit-unit-access {
margin-bottom: $baseline;
@@ -761,20 +761,20 @@
// UI: staff lock and discussion
.edit-discussion,
.edit-staff-lock,
- .edit-optional-content,
+ .edit-optional-completion,
.edit-settings-timed-examination,
.edit-unit-access {
.checkbox-cosmetic .input-checkbox {
@extend %cont-text-sr;
// CASE: unchecked
- ~.tip-warning {
+ ~ .tip-warning {
display: block;
}
// CASE: checked
&:checked {
- ~.tip-warning {
+ ~ .tip-warning {
display: none;
}
}
@@ -832,9 +832,17 @@
}
}
+ .edit-optional-completion {
+ .field-message {
+ @extend %t-copy-sub1;
+ color: $gray-d1;
+ margin-bottom: ($baseline/4);
+ }
+ }
+
.edit-discussion,
.edit-unit-access,
- .edit-optional-content,
+ .edit-optional-completion,
.edit-staff-lock {
.modal-section-content {
@include font-size(16);
@@ -877,7 +885,7 @@
.edit-discussion,
.edit-unit-access,
- .edit-optional-content,
+ .edit-optional-completion,
.edit-staff-lock {
.modal-section-content {
@include font-size(16);
diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html
index f24a574be9fe..e61999553bd5 100644
--- a/cms/templates/course_outline.html
+++ b/cms/templates/course_outline.html
@@ -29,7 +29,7 @@
<%block name="header_extras">
-% 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', 'optional-content-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', 'optional-completion-editor']:
diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore
index 636d37bd131a..6cecb3d7a584 100644
--- a/cms/templates/js/course-outline.underscore
+++ b/cms/templates/js/course-outline.underscore
@@ -225,7 +225,7 @@ if (is_proctored_exam) {
<% if (xblockInfo.get('release_date')) { %>
<%- xblockInfo.get('release_date') %>
<% } %>
- <% if (xblockInfo.get('optional_content')) { %>
+ <% if (xblockInfo.get('optional_completion')) { %>
- <%- gettext('Optional') %>
<% } %>
<% } %>
diff --git a/cms/templates/js/optional-completion-editor.underscore b/cms/templates/js/optional-completion-editor.underscore
new file mode 100644
index 000000000000..e6ae730124ff
--- /dev/null
+++ b/cms/templates/js/optional-completion-editor.underscore
@@ -0,0 +1,26 @@
+
diff --git a/cms/templates/js/optional-content-editor.underscore b/cms/templates/js/optional-content-editor.underscore
deleted file mode 100644
index 8b6101488cc3..000000000000
--- a/cms/templates/js/optional-content-editor.underscore
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- <%- gettext('Mark as optional') %>
-
diff --git a/lms/djangoapps/course_api/blocks/serializers.py b/lms/djangoapps/course_api/blocks/serializers.py
index 68c4ab0f1840..1fe137fad9fc 100644
--- a/lms/djangoapps/course_api/blocks/serializers.py
+++ b/lms/djangoapps/course_api/blocks/serializers.py
@@ -56,7 +56,7 @@ def __init__(
SupportedFieldType('has_score'),
SupportedFieldType('has_scheduled_content'),
SupportedFieldType('weight'),
- SupportedFieldType('optional_content'),
+ SupportedFieldType('optional_completion'),
SupportedFieldType('show_correctness'),
# 'student_view_data'
SupportedFieldType(StudentViewTransformer.STUDENT_VIEW_DATA, StudentViewTransformer),
diff --git a/lms/djangoapps/course_api/blocks/transformers/block_completion.py b/lms/djangoapps/course_api/blocks/transformers/block_completion.py
index 3825137e1abc..85eeef1e2162 100644
--- a/lms/djangoapps/course_api/blocks/transformers/block_completion.py
+++ b/lms/djangoapps/course_api/blocks/transformers/block_completion.py
@@ -14,7 +14,7 @@ class BlockCompletionTransformer(BlockStructureTransformer):
Keep track of the completion of each block within the block structure.
"""
READ_VERSION = 1
- WRITE_VERSION = 1
+ WRITE_VERSION = 2
COMPLETION = 'completion'
COMPLETE = 'complete'
RESUME_BLOCK = 'resume_block'
@@ -43,7 +43,7 @@ def get_block_completion(cls, block_structure, block_key):
@classmethod
def collect(cls, block_structure):
- block_structure.request_xblock_fields('completion_mode', 'optional_content')
+ block_structure.request_xblock_fields('completion_mode', 'optional_completion')
@staticmethod
def _is_block_excluded(block_structure, block_key):
diff --git a/lms/djangoapps/course_home_api/outline/serializers.py b/lms/djangoapps/course_home_api/outline/serializers.py
index d337130fda18..82739aeb0dcf 100644
--- a/lms/djangoapps/course_home_api/outline/serializers.py
+++ b/lms/djangoapps/course_home_api/outline/serializers.py
@@ -54,7 +54,7 @@ def get_blocks(self, block): # pylint: disable=missing-function-docstring
'resume_block': block.get('resume_block', False),
'type': block_type,
'has_scheduled_content': block.get('has_scheduled_content'),
- 'optional_content': block.get('optional_content'),
+ 'optional_completion': block.get('optional_completion', False),
},
}
for child in children:
diff --git a/lms/djangoapps/course_home_api/outline/tests/test_view.py b/lms/djangoapps/course_home_api/outline/tests/test_view.py
index 79a037118b45..ba4c0887096e 100644
--- a/lms/djangoapps/course_home_api/outline/tests/test_view.py
+++ b/lms/djangoapps/course_home_api/outline/tests/test_view.py
@@ -438,17 +438,17 @@ def test_cannot_enroll_if_full(self):
CourseEnrollment.enroll(UserFactory(), self.course.id) # grr, some rando took our spot!
self.assert_can_enroll(False)
- def test_optional_content(self):
+ def test_optional_completion(self):
CourseEnrollment.enroll(self.user, self.course.id)
- assert not self.course.optional_content
+ assert not self.course.optional_completion
response = self.client.get(self.url)
for block in response.data['course_blocks']['blocks'].values():
- assert not block['optional_content']
+ assert not block['optional_completion']
- def test_optional_content_true(self):
- self.course.optional_content = True
+ def test_optional_completion_true(self):
+ self.course.optional_completion = True
self.update_course_and_overview()
CourseEnrollment.enroll(self.user, self.course.id)
response = self.client.get(self.url)
for block in response.data['course_blocks']['blocks'].values():
- assert block['optional_content']
+ assert block['optional_completion']
diff --git a/lms/djangoapps/course_home_api/progress/tests/test_views.py b/lms/djangoapps/course_home_api/progress/tests/test_views.py
index 253d9a544e97..60c648a5e94b 100644
--- a/lms/djangoapps/course_home_api/progress/tests/test_views.py
+++ b/lms/djangoapps/course_home_api/progress/tests/test_views.py
@@ -315,7 +315,7 @@ def test_course_grade_considers_subsection_grade_visibility(self, is_staff, expe
assert response.data['course_grade']['percent'] == expected_percent
assert response.data['course_grade']['is_passing'] == (expected_percent >= 0.5)
- def test_optional_content(self):
+ def test_optional_completion(self):
CourseEnrollment.enroll(self.user, self.course.id)
response = self.client.get(self.url)
assert response.status_code == 200
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 7c992bc7cfe2..4963a5cc5db3 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -568,7 +568,7 @@ def get_course_blocks_completion_summary(course_key, user, optional=False):
def _is_optional(*keys):
for key in keys:
- if block_data.get_xblock_field(key, 'optional_content', False):
+ if block_data.get_xblock_field(key, 'optional_completion', False):
return True
return False
diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py
index 092f6b41405e..8dc2f5d248be 100644
--- a/openedx/features/course_experience/utils.py
+++ b/openedx/features/course_experience/utils.py
@@ -115,7 +115,7 @@ def recurse_mark_auth_denial(block):
'completion',
'complete',
'resume_block',
- 'optional_content',
+ 'optional_completion',
],
allow_start_dates_in_future=allow_start_dates_in_future,
)
diff --git a/xmodule/modulestore/inheritance.py b/xmodule/modulestore/inheritance.py
index 46948870266d..21a30861892d 100644
--- a/xmodule/modulestore/inheritance.py
+++ b/xmodule/modulestore/inheritance.py
@@ -238,7 +238,7 @@ class InheritanceMixin(XBlockMixin):
scope=Scope.settings
)
- optional_content = Boolean(
+ optional_completion = Boolean(
display_name=_('Optional'),
help=_(
'Set this to true to mark this block as optional.'
From 96750c6d96e06e0dc31600bb91b01bb2d7d0c215 Mon Sep 17 00:00:00 2001
From: Daniel Valenzuela
Date: Sat, 9 Mar 2024 19:57:55 -0300
Subject: [PATCH 03/10] fix: use completion tracking flag and small docstring
changes
---
cms/djangoapps/contentstore/utils.py | 4 ++--
cms/djangoapps/contentstore/views/block.py | 5 +----
cms/static/js/views/modals/course_outline_modals.js | 12 ++++++++----
cms/templates/base.html | 2 ++
.../js/optional-completion-editor.underscore | 4 ++--
.../blocks/transformers/block_completion.py | 2 +-
.../course_home_api/outline/tests/test_view.py | 4 ++--
lms/djangoapps/courseware/courses.py | 8 +++++++-
8 files changed, 25 insertions(+), 16 deletions(-)
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index a92d479d4fd0..9045eda25806 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -342,7 +342,7 @@ def find_staff_lock_source(xblock):
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.
"""
if parent_xblock is None:
@@ -356,7 +356,7 @@ def ancestor_has_staff_lock(xblock, parent_xblock=None):
def ancestor_has_optional_completion(xblock, parent_xblock=None):
"""
- Returns True iff one of xblock's ancestors has optional completion.
+ 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:
diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py
index 92d3397a3955..55dbffa124ca 100644
--- a/cms/djangoapps/contentstore/views/block.py
+++ b/cms/djangoapps/contentstore/views/block.py
@@ -1407,10 +1407,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
xblock_info['ancestor_has_staff_lock'] = False
if course_outline:
- if xblock_info['optional_completion']:
- xblock_info['ancestor_has_optional_completion'] = ancestor_has_optional_completion(xblock, parent_xblock)
- else:
- xblock_info['ancestor_has_optional_completion'] = False
+ xblock_info['ancestor_has_optional_completion'] = ancestor_has_optional_completion(xblock, parent_xblock)
if xblock_info['has_explicit_staff_lock']:
xblock_info['staff_only_message'] = True
diff --git a/cms/static/js/views/modals/course_outline_modals.js b/cms/static/js/views/modals/course_outline_modals.js
index 098e55d51079..fdfa38b62221 100644
--- a/cms/static/js/views/modals/course_outline_modals.js
+++ b/cms/static/js/views/modals/course_outline_modals.js
@@ -1239,7 +1239,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
getContext: function() {
return {
optional_completion: this.model.get('optional_completion'),
- optionalAncestor: this.model.get('ancestor_has_optional_completion')
+ optional_ancestor: this.model.get('ancestor_has_optional_completion')
};
},
})
@@ -1267,7 +1267,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
editors: []
};
if (xblockInfo.isVertical()) {
- editors = [StaffLockEditor, UnitAccessEditor, DiscussionEditor, OptionalCompletionEditor];
+ editors = [StaffLockEditor, UnitAccessEditor, DiscussionEditor];
} else {
tabs = [
{
@@ -1282,10 +1282,10 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
];
if (xblockInfo.isChapter()) {
- tabs[0].editors = [ReleaseDateEditor, OptionalCompletionEditor];
+ tabs[0].editors = [ReleaseDateEditor];
tabs[1].editors = [StaffLockEditor];
} else if (xblockInfo.isSequential()) {
- tabs[0].editors = [ReleaseDateEditor, GradingEditor, DueDateEditor, OptionalCompletionEditor];
+ tabs[0].editors = [ReleaseDateEditor, GradingEditor, DueDateEditor];
tabs[1].editors = [ContentVisibilityEditor, ShowCorrectnessEditor];
if (course.get('self_paced') && course.get('is_custom_relative_dates_active')) {
tabs[0].editors.push(SelfPacedDueDateEditor);
@@ -1305,6 +1305,10 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
}
+ if (course.get('completion_tracking_enabled')) {
+ tabs[0].editors.push(OptionalCompletionEditor)
+ }
+
/* globals course */
if (course.get('self_paced')) {
editors = _.without(editors, ReleaseDateEditor, DueDateEditor);
diff --git a/cms/templates/base.html b/cms/templates/base.html
index 1e88505c7fde..a606038c59e3 100644
--- a/cms/templates/base.html
+++ b/cms/templates/base.html
@@ -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
@@ -158,6 +159,7 @@
revision: "${context_course.location.branch | n, js_escaped_string}",
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},
+ completion_tracking_enabled: ${ENABLE_COMPLETION_TRACKING_SWITCH.is_enabled() | 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}
});
diff --git a/cms/templates/js/optional-completion-editor.underscore b/cms/templates/js/optional-completion-editor.underscore
index e6ae730124ff..9a7d55fe847a 100644
--- a/cms/templates/js/optional-completion-editor.underscore
+++ b/cms/templates/js/optional-completion-editor.underscore
@@ -4,7 +4,7 @@
diff --git a/lms/djangoapps/course_api/blocks/transformers/block_completion.py b/lms/djangoapps/course_api/blocks/transformers/block_completion.py
index 85eeef1e2162..8790463d5908 100644
--- a/lms/djangoapps/course_api/blocks/transformers/block_completion.py
+++ b/lms/djangoapps/course_api/blocks/transformers/block_completion.py
@@ -14,7 +14,7 @@ class BlockCompletionTransformer(BlockStructureTransformer):
Keep track of the completion of each block within the block structure.
"""
READ_VERSION = 1
- WRITE_VERSION = 2
+ WRITE_VERSION = 1
COMPLETION = 'completion'
COMPLETE = 'complete'
RESUME_BLOCK = 'resume_block'
diff --git a/lms/djangoapps/course_home_api/outline/tests/test_view.py b/lms/djangoapps/course_home_api/outline/tests/test_view.py
index ba4c0887096e..b7bd34968a80 100644
--- a/lms/djangoapps/course_home_api/outline/tests/test_view.py
+++ b/lms/djangoapps/course_home_api/outline/tests/test_view.py
@@ -438,14 +438,14 @@ def test_cannot_enroll_if_full(self):
CourseEnrollment.enroll(UserFactory(), self.course.id) # grr, some rando took our spot!
self.assert_can_enroll(False)
- def test_optional_completion(self):
+ def test_optional_completion_off_by_default(self):
CourseEnrollment.enroll(self.user, self.course.id)
assert not self.course.optional_completion
response = self.client.get(self.url)
for block in response.data['course_blocks']['blocks'].values():
assert not block['optional_completion']
- def test_optional_completion_true(self):
+ def test_optional_completion_on_is_inherited(self):
self.course.optional_completion = True
self.update_course_and_overview()
CourseEnrollment.enroll(self.user, self.course.id)
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 4963a5cc5db3..c4a03c639450 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -559,7 +559,13 @@ def get_course_blocks_completion_summary(course_key, user, optional=False):
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
- """
+
+ Args:
+ course_key (CourseKey): the course key object.
+ user (User): student user object.
+ optional (bool): if true will only count optional blocks towards summary, else it will exclude optional
+ blocks from summary.
+ """
if not user.id:
return []
store = modulestore()
From 956363335d0fee6a17460848ac9c2e535d82e90e Mon Sep 17 00:00:00 2001
From: Agrendalath
Date: Mon, 18 Mar 2024 18:35:24 +0100
Subject: [PATCH 04/10] fix: use correct field for displaying the optional
completion editor in units
---
cms/static/js/views/modals/course_outline_modals.js | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/cms/static/js/views/modals/course_outline_modals.js b/cms/static/js/views/modals/course_outline_modals.js
index fdfa38b62221..29c38977ac33 100644
--- a/cms/static/js/views/modals/course_outline_modals.js
+++ b/cms/static/js/views/modals/course_outline_modals.js
@@ -1214,7 +1214,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
setValue: function(value) {
this.$('input[name=optional_completion]').prop('checked', value);
},
-
+
currentValue: function() {
return this.$('input[name=optional_completion]').is(':checked');
},
@@ -1306,7 +1306,11 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
if (course.get('completion_tracking_enabled')) {
- tabs[0].editors.push(OptionalCompletionEditor)
+ if (tabs.length > 0) {
+ tabs[0].editors.push(OptionalCompletionEditor);
+ } else {
+ editors.push(OptionalCompletionEditor);
+ }
}
/* globals course */
From 33d374bb750f5c7576bbd2618cc29ec3387c0af3 Mon Sep 17 00:00:00 2001
From: Agrendalath
Date: Mon, 18 Mar 2024 18:37:24 +0100
Subject: [PATCH 05/10] feat: display optional completion information in
sections, subsections, units
---
cms/templates/js/course-outline.underscore | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore
index 6cecb3d7a584..39058ef6f6c0 100644
--- a/cms/templates/js/course-outline.underscore
+++ b/cms/templates/js/course-outline.underscore
@@ -22,6 +22,8 @@ var addStatusMessage = function (statusType, message) {
statusIconClass = 'fa-lock';
} else if (statusType === 'partition-groups') {
statusIconClass = 'fa-eye';
+ } else if (statusType === 'optional-completion') {
+ statusIconClass = 'fa-lightbulb-o';
}
statusMessages.push({iconClass: statusIconClass, text: message});
@@ -81,6 +83,11 @@ var gradingType = gettext('Ungraded');
if (xblockInfo.get('graded')) {
gradingType = xblockInfo.get('format')
}
+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');
@@ -225,9 +232,6 @@ if (is_proctored_exam) {
<% if (xblockInfo.get('release_date')) { %>
<%- xblockInfo.get('release_date') %>
<% } %>
- <% if (xblockInfo.get('optional_completion')) { %>
- - <%- gettext('Optional') %>
- <% } %>
<% } %>
From 63d04fcabd88bd64156b1260d24ba5fae78b67ea Mon Sep 17 00:00:00 2001
From: Agrendalath
Date: Mon, 18 Mar 2024 19:46:44 +0100
Subject: [PATCH 06/10] fix: rely on the inheritance mechanism for optional
completion
---
cms/static/js/views/modals/course_outline_modals.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/cms/static/js/views/modals/course_outline_modals.js b/cms/static/js/views/modals/course_outline_modals.js
index 29c38977ac33..e626d241b2da 100644
--- a/cms/static/js/views/modals/course_outline_modals.js
+++ b/cms/static/js/views/modals/course_outline_modals.js
@@ -1228,7 +1228,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
return {
publish: 'republish',
metadata: {
- optional_completion: this.currentValue()
+ // 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 {
From 8f020f5665a5e5758b2a7ca45399e43d78c1553f Mon Sep 17 00:00:00 2001
From: Daniel Valenzuela
Date: Thu, 21 Mar 2024 02:22:05 -0300
Subject: [PATCH 07/10] test: add tests for outline modals
---
.../contentstore/tests/test_utils.py | 8 +--
.../contentstore/views/tests/test_block.py | 2 +-
.../spec/views/pages/course_outline_spec.js | 59 +++++++++++++++++--
3 files changed, 57 insertions(+), 12 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py
index 8c39ed7a5ff5..32437a19bcfc 100644
--- a/cms/djangoapps/contentstore/tests/test_utils.py
+++ b/cms/djangoapps/contentstore/tests/test_utils.py
@@ -353,6 +353,7 @@ def setUp(self):
self.vertical = self.store.get_item(vertical.location)
self.html = self.store.get_item(html.location)
self.problem = self.store.get_item(problem.location)
+ self.orphan = BlockFactory.create(category='vertical', parent_location=self.sequential.location)
def set_optional_completion(self, xblock, value):
""" Sets optional_completion to specified value and calls update_item to persist the change. """
@@ -371,13 +372,6 @@ def test_no_inheritance(self):
self.update_optional_completions(False, False, True)
self.assertFalse(utils.ancestor_has_optional_completion(self.vertical))
- def test_inheritance_in_optional_section(self):
- """Tests that a vertical in an optional section has an inherited optional completion"""
- self.update_optional_completions(True, False, False)
- self.assertTrue(utils.ancestor_has_optional_completion(self.vertical))
- self.update_optional_completions(True, False, True)
- self.assertTrue(utils.ancestor_has_optional_completion(self.vertical))
-
def test_inheritance_in_optional_subsection(self):
"""Tests that a vertical in an optional subsection has an inherited optional completion"""
self.update_optional_completions(False, True, False)
diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py
index 716b0d9a70b5..addb385c5797 100644
--- a/cms/djangoapps/contentstore/views/tests/test_block.py
+++ b/cms/djangoapps/contentstore/views/tests/test_block.py
@@ -2630,7 +2630,7 @@ def test_json_responses(self):
@ddt.data(
(ModuleStoreEnum.Type.split, 3, 3),
- (ModuleStoreEnum.Type.mongo, 8, 12),
+ (ModuleStoreEnum.Type.mongo, 10, 14),
)
@ddt.unpack
def test_xblock_outline_handler_mongo_calls(self, store_type, chapter_queries, chapter_queries_1):
diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js
index 074a77c47324..1c1079f7a8f1 100644
--- a/cms/static/js/spec/views/pages/course_outline_spec.js
+++ b/cms/static/js/spec/views/pages/course_outline_spec.js
@@ -15,6 +15,7 @@ describe('CourseOutlinePage', function() {
createMockSubsectionJSON, verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON, setSelfPaced, setSelfPacedCustomPLS,
mockSingleSectionCourseJSON, createMockVerticalJSON, createMockIndexJSON, mockCourseEntranceExamJSON,
selectOnboardingExam, createMockCourseJSONWithReviewRules,mockCourseJSONWithReviewRules,
+ expectOptionalCompletion, mockCourseWithoutCompletionJSON, expectOptionalCompletionDisabled,
mockOutlinePage = readFixtures('templates/mock/mock-course-outline-page.underscore'),
mockRerunNotification = readFixtures('templates/mock/mock-course-rerun-notification.underscore');
@@ -42,7 +43,8 @@ describe('CourseOutlinePage', function() {
user_partition_info: {},
highlights_enabled: true,
highlights_enabled_for_messaging: false,
- show_delete_button: true
+ show_delete_button: true,
+ completion_tracking_enabled: true,
}, options, {child_info: {children: children}});
};
@@ -70,6 +72,7 @@ describe('CourseOutlinePage', function() {
user_partition_info: {},
highlights_enabled: true,
highlights_enabled_for_messaging: false,
+ completion_tracking_enabled: true,
show_delete_button: true
}, options, {child_info: {children: children}});
};
@@ -96,6 +99,7 @@ describe('CourseOutlinePage', function() {
user_partition_info: {},
highlights: [],
highlights_enabled: true,
+ completion_tracking_enabled: true,
show_delete_button: true
}, options, {child_info: {children: children}});
};
@@ -127,6 +131,7 @@ describe('CourseOutlinePage', function() {
user_partitions: [],
group_access: {},
user_partition_info: {},
+ completion_tracking_enabled: true,
show_delete_button: true
}, options, {child_info: {children: children}});
};
@@ -146,6 +151,7 @@ describe('CourseOutlinePage', function() {
user_partitions: [],
group_access: {},
user_partition_info: {},
+ completion_tracking_enabled: true,
show_delete_button: true
}, options);
};
@@ -220,6 +226,7 @@ describe('CourseOutlinePage', function() {
createCourseOutlinePage = function(test, courseJSON, createOnly) {
requests = AjaxHelpers.requests(test);
model = new XBlockOutlineInfo(courseJSON, {parse: true});
+ console.warn("course:", model)
outlinePage = new CourseOutlinePage({
model: model,
el: $('#content')
@@ -313,10 +320,10 @@ describe('CourseOutlinePage', function() {
'staff-lock-editor', 'unit-access-editor', 'discussion-editor', 'content-visibility-editor',
'settings-modal-tabs', 'timed-examination-preference-editor', 'access-editor',
'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor',
- 'course-highlights-enable'
+ 'course-highlights-enable', 'optional-completion-editor'
]);
appendSetFixtures(mockOutlinePage);
- mockCourseJSON = createMockCourseJSON({}, [
+ mockCourseJSON = createMockCourseJSON({completion_tracking_enabled: true}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON()
@@ -341,6 +348,22 @@ describe('CourseOutlinePage', function() {
])
])
]);
+ mockCourseWithoutCompletionJSON = createMockCourseJSON({completion_tracking_enabled: false}, [
+ createMockSectionJSON({}, [
+ createMockSubsectionJSON({}, [
+ createMockVerticalJSON()
+ ])
+ ])
+ ]);
+
+ expectOptionalCompletion = function(exists) {
+ expect($('#optional_completion').length).toBeGreaterThanOrEqual(exists, `optional completion existence should be ${exists}`);
+ };
+
+ expectOptionalCompletionDisabled = function(disabled) {
+ expect($('#optional_completion').is(':disabled')).toBe(disabled)
+ };
+
// Create a mock Course object as the JS now expects it.
window.course = new Course({
@@ -944,6 +967,7 @@ describe('CourseOutlinePage', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.section-header-actions .configure-button').click();
$('#start_date').val('1/2/2015');
+
// Section release date can't be cleared.
expect($('.wrapper-modal-window .action-clear')).not.toExist();
@@ -1021,6 +1045,33 @@ describe('CourseOutlinePage', function() {
);
expect($modalWindow.find('.outline-subsection').length).toBe(2);
});
+
+ it('hides optional completion checkbox when completion tracking is disabled', function() {
+ createCourseOutlinePage(this, mockCourseWithoutCompletionJSON, false);
+ outlinePage.$('.section-header-actions .configure-button').click();
+ expect($('.edit-optional-completion').length).toBe(0);
+ });
+
+ describe('Optional Completion', function () {
+ beforeEach(function() {
+ createMockCourseJSON({}, [
+ createMockSectionJSON({optional_completion: true}, [
+ createMockSubsectionJSON({ancestor_has_optional_completion: true}, [])
+ ])
+ ]);
+ });
+
+ it('displays optional completion message and disables children optional completion checkboxes', function() {
+ expect($('.status-message-copy')).toExist()
+ outlinePage.$('.section-header-actions .configure-button').click();
+ expectOptionalCompletion(1);
+ $('.wrapper-modal-window .action-cancel').click();
+ outlinePage.$('.subsection-header-actions .configure-button').click();
+ expectOptionalCompletion(1);
+ expectOptionalCompletionDisabled(true);
+ expect($('.tip-warning')).toExist();
+ });
+ });
});
describe('Subsection', function() {
@@ -2423,7 +2474,7 @@ describe('CourseOutlinePage', function() {
it('hides discussion settings if unit level discussions are disabled', function() {
getUnitStatus({}, {unit_level_discussions: false});
outlinePage.$('.outline-unit .configure-button').click();
- expect($('.modal-section .edit-discussion')).not.toExist();
+ expect($('.modal-section .edit-discussion').length).toBe(0);
});
});
From aa841e7ca0f35d46a97d0448db8f4a1e8dd43dde Mon Sep 17 00:00:00 2001
From: Agrendalath
Date: Wed, 3 Apr 2024 18:39:21 +0200
Subject: [PATCH 08/10] test: fix JS tests
---
.../spec/views/pages/course_outline_spec.js | 136 +++++++++++++-----
1 file changed, 102 insertions(+), 34 deletions(-)
diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js
index 1c1079f7a8f1..5a77aaee548f 100644
--- a/cms/static/js/spec/views/pages/course_outline_spec.js
+++ b/cms/static/js/spec/views/pages/course_outline_spec.js
@@ -15,7 +15,6 @@ describe('CourseOutlinePage', function() {
createMockSubsectionJSON, verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON, setSelfPaced, setSelfPacedCustomPLS,
mockSingleSectionCourseJSON, createMockVerticalJSON, createMockIndexJSON, mockCourseEntranceExamJSON,
selectOnboardingExam, createMockCourseJSONWithReviewRules,mockCourseJSONWithReviewRules,
- expectOptionalCompletion, mockCourseWithoutCompletionJSON, expectOptionalCompletionDisabled,
mockOutlinePage = readFixtures('templates/mock/mock-course-outline-page.underscore'),
mockRerunNotification = readFixtures('templates/mock/mock-course-rerun-notification.underscore');
@@ -348,22 +347,6 @@ describe('CourseOutlinePage', function() {
])
])
]);
- mockCourseWithoutCompletionJSON = createMockCourseJSON({completion_tracking_enabled: false}, [
- createMockSectionJSON({}, [
- createMockSubsectionJSON({}, [
- createMockVerticalJSON()
- ])
- ])
- ]);
-
- expectOptionalCompletion = function(exists) {
- expect($('#optional_completion').length).toBeGreaterThanOrEqual(exists, `optional completion existence should be ${exists}`);
- };
-
- expectOptionalCompletionDisabled = function(disabled) {
- expect($('#optional_completion').is(':disabled')).toBe(disabled)
- };
-
// Create a mock Course object as the JS now expects it.
window.course = new Course({
@@ -1046,30 +1029,41 @@ describe('CourseOutlinePage', function() {
expect($modalWindow.find('.outline-subsection').length).toBe(2);
});
- it('hides optional completion checkbox when completion tracking is disabled', function() {
- createCourseOutlinePage(this, mockCourseWithoutCompletionJSON, false);
+ it('hides optional completion checkbox by default', function() {
+ createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.section-header-actions .configure-button').click();
- expect($('.edit-optional-completion').length).toBe(0);
+ expect($('.edit-optional-completion')).not.toExist();
});
- describe('Optional Completion', function () {
+ describe('supports optional completion and', function () {
beforeEach(function() {
- createMockCourseJSON({}, [
- createMockSectionJSON({optional_completion: true}, [
- createMockSubsectionJSON({ancestor_has_optional_completion: true}, [])
- ])
- ]);
+ window.course.attributes.completion_tracking_enabled = true;
+ });
+
+ it('shows optional completion checkbox unchecked by default', function() {
+ createCourseOutlinePage(this, mockCourseJSON, false);
+ outlinePage.$('.section-header-actions .configure-button').click();
+ expect($('.edit-optional-completion')).toExist();
+ expect($('#optional_completion').is(':checked')).toBe(false);
});
- it('displays optional completion message and disables children optional completion checkboxes', function() {
- expect($('.status-message-copy')).toExist()
+ it('shows optional completion checkbox checked', function() {
+ var mockCourseJSON = createMockCourseJSON({}, [
+ createMockSectionJSON({optional_completion: true})
+ ]);
+ createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.section-header-actions .configure-button').click();
- expectOptionalCompletion(1);
- $('.wrapper-modal-window .action-cancel').click();
- outlinePage.$('.subsection-header-actions .configure-button').click();
- expectOptionalCompletion(1);
- expectOptionalCompletionDisabled(true);
- expect($('.tip-warning')).toExist();
+ expect($('#optional_completion').is(':disabled')).toBe(false);
+ expect($('#optional_completion').is(':checked')).toBe(true);
+ });
+
+ it('disables optional completion checkbox when the parent uses optional completion', function() {
+ var mockCourseJSON = createMockCourseJSON({}, [
+ createMockSectionJSON({ancestor_has_optional_completion: true})
+ ]);
+ createCourseOutlinePage(this, mockCourseJSON, false);
+ outlinePage.$('.section-header-actions .configure-button').click();
+ expect($('#optional_completion').is(':disabled')).toBe(true);
});
});
});
@@ -2372,6 +2366,48 @@ describe('CourseOutlinePage', function() {
);
});
})
+
+ it('hides optional completion checkbox by default', function() {
+ createCourseOutlinePage(this, mockCourseJSON, false);
+ outlinePage.$('.outline-subsection .configure-button').click();
+ expect($('.edit-optional-completion')).not.toExist();
+ });
+
+ describe('supports optional completion and', function () {
+ beforeEach(function() {
+ window.course.attributes.completion_tracking_enabled = true;
+ });
+
+ it('shows optional completion checkbox unchecked by default', function() {
+ createCourseOutlinePage(this, mockCourseJSON, false);
+ outlinePage.$('.outline-subsection .configure-button').click();
+ expect($('.edit-optional-completion')).toExist();
+ expect($('#optional_completion').is(':checked')).toBe(false);
+ });
+
+ it('shows optional completion checkbox checked', function() {
+ var mockCourseJSON = createMockCourseJSON({}, [
+ createMockSectionJSON({}, [
+ createMockSubsectionJSON({optional_completion: true}, [])
+ ])
+ ]);
+ createCourseOutlinePage(this, mockCourseJSON, false);
+ outlinePage.$('.outline-subsection .configure-button').click();
+ expect($('#optional_completion').is(':disabled')).toBe(false);
+ expect($('#optional_completion').is(':checked')).toBe(true);
+ });
+
+ it('disables optional completion checkbox when the parent uses optional completion', function() {
+ var mockCourseJSON = createMockCourseJSON({}, [
+ createMockSectionJSON({}, [
+ createMockSubsectionJSON({ancestor_has_optional_completion: true}, [])
+ ])
+ ]);
+ createCourseOutlinePage(this, mockCourseJSON, false);
+ outlinePage.$('.outline-subsection .configure-button').click();
+ expect($('#optional_completion').is(':disabled')).toBe(true);
+ });
+ });
});
// Note: most tests for units can be found in Bok Choy
@@ -2488,6 +2524,38 @@ describe('CourseOutlinePage', function() {
])
]);
});
+
+ it('hides optional completion checkbox by default', function() {
+ getUnitStatus({}, {});
+ outlinePage.$('.outline-unit .configure-button').click();
+ expect($('.edit-optional-completion')).not.toExist();
+ });
+
+ describe('supports optional completion and', function () {
+ beforeEach(function() {
+ window.course.attributes.completion_tracking_enabled = true;
+ });
+
+ it('shows optional completion checkbox unchecked by default', function() {
+ getUnitStatus({}, {});
+ outlinePage.$('.outline-unit .configure-button').click();
+ expect($('.edit-optional-completion')).toExist();
+ expect($('#optional_completion').is(':checked')).toBe(false);
+ });
+
+ it('shows optional completion checkbox checked', function() {
+ getUnitStatus({optional_completion: true}, {});
+ outlinePage.$('.outline-unit .configure-button').click();
+ expect($('#optional_completion').is(':disabled')).toBe(false);
+ expect($('#optional_completion').is(':checked')).toBe(true);
+ });
+
+ it('disables optional completion checkbox when the parent uses optional completion', function() {
+ getUnitStatus({ancestor_has_optional_completion: true}, {});
+ outlinePage.$('.outline-unit .configure-button').click();
+ expect($('#optional_completion').is(':disabled')).toBe(true);
+ });
+ });
});
describe('Date and Time picker', function() {
From 40d5a3391c76354bf4e4007e430534de2f4c00b9 Mon Sep 17 00:00:00 2001
From: Daniel Valenzuela
Date: Thu, 4 Apr 2024 01:44:22 -0300
Subject: [PATCH 09/10] test: add sets to null unittest
---
.../spec/views/pages/course_outline_spec.js | 86 ++++++++++++++++---
1 file changed, 74 insertions(+), 12 deletions(-)
diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js
index 5a77aaee548f..4dabc3e8cd1a 100644
--- a/cms/static/js/spec/views/pages/course_outline_spec.js
+++ b/cms/static/js/spec/views/pages/course_outline_spec.js
@@ -1065,6 +1065,24 @@ describe('CourseOutlinePage', function() {
outlinePage.$('.section-header-actions .configure-button').click();
expect($('#optional_completion').is(':disabled')).toBe(true);
});
+
+ it('sets optional completion to null instead of false', function() {
+ var mockCourseJSON = createMockCourseJSON({}, [
+ createMockSectionJSON({optional_completion: true})
+ ]);
+ createCourseOutlinePage(this, mockCourseJSON, false);
+ outlinePage.$('.section-header-actions .configure-button').click();
+ expect($('#optional_completion').is(':checked')).toBe(true);
+ $('#optional_completion').click()
+ expect($('#optional_completion').is(':checked')).toBe(false);
+ $('.wrapper-modal-window .action-save').click();
+ AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
+ publish: 'republish',
+ metadata: {
+ optional_completion: null
+ }
+ });
+ });
});
});
@@ -2386,7 +2404,7 @@ describe('CourseOutlinePage', function() {
});
it('shows optional completion checkbox checked', function() {
- var mockCourseJSON = createMockCourseJSON({}, [
+ var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({optional_completion: true}, [])
])
@@ -2398,14 +2416,42 @@ describe('CourseOutlinePage', function() {
});
it('disables optional completion checkbox when the parent uses optional completion', function() {
- var mockCourseJSON = createMockCourseJSON({}, [
- createMockSectionJSON({}, [
- createMockSubsectionJSON({ancestor_has_optional_completion: true}, [])
- ])
- ]);
- createCourseOutlinePage(this, mockCourseJSON, false);
- outlinePage.$('.outline-subsection .configure-button').click();
- expect($('#optional_completion').is(':disabled')).toBe(true);
+ var mockCourseJSON = createMockCourseJSON({}, [
+ createMockSectionJSON({}, [
+ createMockSubsectionJSON({ancestor_has_optional_completion: true}, [])
+ ])
+ ]);
+ createCourseOutlinePage(this, mockCourseJSON, false);
+ outlinePage.$('.outline-subsection .configure-button').click();
+ expect($('#optional_completion').is(':disabled')).toBe(true);
+ });
+
+ it('sets optional completion to null instead of false', function() {
+ var mockCourseJSON = createMockCourseJSON({}, [
+ createMockSectionJSON({}, [
+ createMockSubsectionJSON({optional_completion: true}, [])
+ ])
+ ]);
+ createCourseOutlinePage(this, mockCourseJSON, false);
+ outlinePage.$('.outline-subsection .configure-button').click();
+ expect($('#optional_completion').is(':checked')).toBe(true);
+ $('#optional_completion').click()
+ expect($('#optional_completion').is(':checked')).toBe(false);
+ $('.wrapper-modal-window .action-save').click();
+ AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
+ publish: 'republish',
+ graderType: 'notgraded',
+ isPrereq: false,
+ metadata: {
+ optional_completion: null,
+ due: null,
+ is_practice_exam: false,
+ is_time_limited: false,
+ is_proctored_enabled: false,
+ default_time_limit_minutes: null,
+ is_onboarding_exam: false,
+ }
+ });
});
});
});
@@ -2551,9 +2597,25 @@ describe('CourseOutlinePage', function() {
});
it('disables optional completion checkbox when the parent uses optional completion', function() {
- getUnitStatus({ancestor_has_optional_completion: true}, {});
- outlinePage.$('.outline-unit .configure-button').click();
- expect($('#optional_completion').is(':disabled')).toBe(true);
+ getUnitStatus({ancestor_has_optional_completion: true}, {});
+ outlinePage.$('.outline-unit .configure-button').click();
+ expect($('#optional_completion').is(':disabled')).toBe(true);
+ });
+
+ it('sets optional completion to null instead of false', function() {
+ getUnitStatus({optional_completion: true}, {});
+ outlinePage.$('.outline-unit .configure-button').click();
+ expect($('#optional_completion').is(':checked')).toBe(true);
+ $('#optional_completion').click()
+ expect($('#optional_completion').is(':checked')).toBe(false);
+ $('.wrapper-modal-window .action-save').click();
+ AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-unit', {
+ publish: 'republish',
+ metadata: {
+ visible_to_staff_only: null,
+ optional_completion: null
+ }
+ });
});
});
});
From e57c355623275698d7d6e950c61a680d2ee58b45 Mon Sep 17 00:00:00 2001
From: Agrendalath
Date: Thu, 4 Apr 2024 15:45:14 +0200
Subject: [PATCH 10/10] test: remove redundant changes
---
.../js/spec/views/pages/course_outline_spec.js | 13 +++----------
1 file changed, 3 insertions(+), 10 deletions(-)
diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js
index 4dabc3e8cd1a..85b80e45a4a7 100644
--- a/cms/static/js/spec/views/pages/course_outline_spec.js
+++ b/cms/static/js/spec/views/pages/course_outline_spec.js
@@ -42,8 +42,7 @@ describe('CourseOutlinePage', function() {
user_partition_info: {},
highlights_enabled: true,
highlights_enabled_for_messaging: false,
- show_delete_button: true,
- completion_tracking_enabled: true,
+ show_delete_button: true
}, options, {child_info: {children: children}});
};
@@ -71,7 +70,6 @@ describe('CourseOutlinePage', function() {
user_partition_info: {},
highlights_enabled: true,
highlights_enabled_for_messaging: false,
- completion_tracking_enabled: true,
show_delete_button: true
}, options, {child_info: {children: children}});
};
@@ -98,7 +96,6 @@ describe('CourseOutlinePage', function() {
user_partition_info: {},
highlights: [],
highlights_enabled: true,
- completion_tracking_enabled: true,
show_delete_button: true
}, options, {child_info: {children: children}});
};
@@ -130,7 +127,6 @@ describe('CourseOutlinePage', function() {
user_partitions: [],
group_access: {},
user_partition_info: {},
- completion_tracking_enabled: true,
show_delete_button: true
}, options, {child_info: {children: children}});
};
@@ -150,7 +146,6 @@ describe('CourseOutlinePage', function() {
user_partitions: [],
group_access: {},
user_partition_info: {},
- completion_tracking_enabled: true,
show_delete_button: true
}, options);
};
@@ -225,7 +220,6 @@ describe('CourseOutlinePage', function() {
createCourseOutlinePage = function(test, courseJSON, createOnly) {
requests = AjaxHelpers.requests(test);
model = new XBlockOutlineInfo(courseJSON, {parse: true});
- console.warn("course:", model)
outlinePage = new CourseOutlinePage({
model: model,
el: $('#content')
@@ -322,7 +316,7 @@ describe('CourseOutlinePage', function() {
'course-highlights-enable', 'optional-completion-editor'
]);
appendSetFixtures(mockOutlinePage);
- mockCourseJSON = createMockCourseJSON({completion_tracking_enabled: true}, [
+ mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON()
@@ -950,7 +944,6 @@ describe('CourseOutlinePage', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.section-header-actions .configure-button').click();
$('#start_date').val('1/2/2015');
-
// Section release date can't be cleared.
expect($('.wrapper-modal-window .action-clear')).not.toExist();
@@ -2556,7 +2549,7 @@ describe('CourseOutlinePage', function() {
it('hides discussion settings if unit level discussions are disabled', function() {
getUnitStatus({}, {unit_level_discussions: false});
outlinePage.$('.outline-unit .configure-button').click();
- expect($('.modal-section .edit-discussion').length).toBe(0);
+ expect($('.modal-section .edit-discussion')).not.toExist();
});
});