Skip to content

Commit

Permalink
feat: optional xblocks
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielVZ96 committed Feb 22, 2024
1 parent c270b4f commit b1f6a46
Show file tree
Hide file tree
Showing 17 changed files with 111 additions and 8 deletions.
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/views/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,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':
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 @@ -78,6 +78,7 @@ class CourseMetadata:
'highlights_enabled_for_messaging',
'is_onboarding_exam',
'discussions_settings',
'optional',
]

@classmethod
Expand Down
46 changes: 43 additions & 3 deletions cms/static/js/views/modals/course_outline_modals.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -1206,6 +1206,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') {
Expand Down Expand Up @@ -1245,10 +1285,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);
Expand Down
8 changes: 6 additions & 2 deletions cms/static/sass/elements/_modal-window.scss
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@

.edit-discussion,
.edit-staff-lock,
.edit-optional-content,
.edit-content-visibility,
.edit-unit-access {
margin-bottom: $baseline;
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -832,6 +834,7 @@

.edit-discussion,
.edit-unit-access,
.edit-optional-content,
.edit-staff-lock {
.modal-section-content {
@include font-size(16);
Expand Down Expand Up @@ -874,6 +877,7 @@

.edit-discussion,
.edit-unit-access,
.edit-optional-content,
.edit-staff-lock {
.modal-section-content {
@include font-size(16);
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']:
% 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']:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
Expand Down
3 changes: 3 additions & 0 deletions cms/templates/js/course-outline.underscore
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ if (is_proctored_exam) {
<% if (xblockInfo.get('release_date')) { %>
<%- xblockInfo.get('release_date') %>
<% } %>
<% if (xblockInfo.get('optional_content')) { %>
- <%- gettext('Optional') %>
<% } %>
<% } %>
</span>
</p>
Expand Down
5 changes: 5 additions & 0 deletions cms/templates/js/optional-content-editor.underscore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<h3 class="modal-section-title">
<input type="checkbox" id="optional_content" name="optional_content"
class="input input-checkbox" />
<%- gettext('Mark as optional') %>
</h3>
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 @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
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 @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions lms/djangoapps/course_home_api/outline/tests/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
1 change: 1 addition & 0 deletions lms/djangoapps/course_home_api/progress/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
10 changes: 10 additions & 0 deletions lms/djangoapps/course_home_api/progress/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
1 change: 1 addition & 0 deletions lms/djangoapps/course_home_api/progress/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion lms/djangoapps/courseware/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
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 @@ -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,
)
Expand Down
11 changes: 11 additions & 0 deletions xmodule/modulestore/inheritance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down

0 comments on commit b1f6a46

Please sign in to comment.