From 898684287a71d4253c4c21819caf9e23ce7259f9 Mon Sep 17 00:00:00 2001
From: Sofia Yoon
Date: Wed, 23 Jun 2021 16:04:07 -0400
Subject: [PATCH] feat: AA-885 show offsets in studio self paced course outline
for a subsection
feat: AA-883 basic prototype for custom pacing pls in studio
refactor: merge with basic prototype for self paced courses from AA-844
feat: add due date estimate message in self paced courses studio modal
refactor: merge with main that has up to date self paced custom pls editor and tests
fix: only display projected date if start date exists
fix: tests to check grading date in outline
fix: only one warning message show at a time
fix: do not show projected date when it is before the start date
---
cms/djangoapps/contentstore/config/waffle.py | 10 ++-
cms/djangoapps/contentstore/views/item.py | 2 +-
.../spec/views/pages/course_outline_spec.js | 68 ++++++++++++-----
.../js/views/modals/course_outline_modals.js | 76 ++++++++++++++-----
cms/static/sass/elements/_modules.scss | 3 +-
cms/templates/base.html | 4 +-
cms/templates/js/course-outline.underscore | 44 +++++++++++
.../js/self-paced-due-date-editor.underscore | 46 ++++++-----
.../xmodule/modulestore/inheritance.py | 8 +-
.../xmodule/modulestore/xml_importer.py | 2 +-
common/lib/xmodule/xmodule/seq_module.py | 8 +-
.../course_date_signals/handlers.py | 17 +++--
.../djangoapps/course_date_signals/tests.py | 64 ++++++++--------
13 files changed, 236 insertions(+), 116 deletions(-)
diff --git a/cms/djangoapps/contentstore/config/waffle.py b/cms/djangoapps/contentstore/config/waffle.py
index fcc1e536be..3dc567a14f 100644
--- a/cms/djangoapps/contentstore/config/waffle.py
+++ b/cms/djangoapps/contentstore/config/waffle.py
@@ -69,13 +69,15 @@ REDIRECT_TO_LIBRARY_AUTHORING_MICROFRONTEND = LegacyWaffleFlag(
)
-# .. toggle_name: studio.custom_pls
+# .. toggle_name: studio.custom_relative_dates
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
-# .. toggle_description: Waffle flag to enable custom pacing for PLS
+# .. toggle_description: Waffle flag to enable custom pacing input for Personalized Learner Schedule (PLS).
+# .. This flag guards an input in Studio for a self paced course, where the user can enter date offsets
+# .. for a subsection.
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2021-07-12
# .. toggle_target_removal_date: 2021-12-31
-# .. toggle_warnings: For this flag to be active, add flag 'studio.custom_pls' in Django Admin
+# .. toggle_warnings: Flag course_experience.relative_dates should also be active for relative dates functionalities to work.
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-844
-CUSTOM_PLS = CourseWaffleFlag(WAFFLE_NAMESPACE, 'custom_pls', module_name=__name__,)
+CUSTOM_RELATIVE_DATES = CourseWaffleFlag(WAFFLE_NAMESPACE, 'custom_relative_dates', module_name=__name__,)
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index 3dcc3e4cec..8feaf96042 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -1230,7 +1230,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'graded': xblock.graded,
'due_date': get_default_time_display(xblock.due),
'due': xblock.fields['due'].to_json(xblock.due),
- 'due_num_weeks': xblock.due_num_weeks,
+ 'relative_weeks_due': xblock.relative_weeks_due,
'format': xblock.format,
'course_graders': [grader.get('type') for grader in graders],
'has_changes': has_changes,
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 bdc73ce382..cd962f9f18 100644
--- a/cms/static/js/spec/views/pages/course_outline_spec.js
+++ b/cms/static/js/spec/views/pages/course_outline_spec.js
@@ -204,7 +204,7 @@ describe('CourseOutlinePage', function() {
setSelfPacedCustomPLS = function() {
setSelfPaced();
- course.set('is_custom_pls_active', true);
+ course.set('is_custom_relative_dates_active', true);
}
createCourseOutlinePage = function(test, courseJSON, createOnly) {
@@ -1009,7 +1009,7 @@ describe('CourseOutlinePage', function() {
describe('Subsection', function() {
var getDisplayNameWrapper, setEditModalValues, setEditModalValuesForCustomPacing, setContentVisibility, mockServerValuesJson,
mockCustomPacingServerValuesJson, selectDisableSpecialExams, selectTimedExam, selectProctoredExam, selectPracticeExam,
- selectPrerequisite, selectLastPrerequisiteSubsection, selectDueNumWeeksSubsection, checkOptionFieldVisibility,
+ selectPrerequisite, selectLastPrerequisiteSubsection, selectRelativeWeeksSubsection, checkOptionFieldVisibility,
defaultModalSettings, modalSettingsWithExamReviewRules, getMockNoPrereqOrExamsCourseJSON, expectShowCorrectness;
getDisplayNameWrapper = function() {
@@ -2134,12 +2134,12 @@ describe('CourseOutlinePage', function() {
setSelfPacedCustomPLS();
});
- setEditModalValuesForCustomPacing = function(due_in, grading_type) {
- $('#due_in').val(due_in);
+ setEditModalValuesForCustomPacing = function(grading_type, due_in) {
$('#grading_type').val(grading_type);
+ $('#due_in').val(due_in);
};
- selectDueNumWeeksSubsection = function(weeks) {
+ selectRelativeWeeksSubsection = function(weeks) {
$('#due_in').val(weeks).trigger('keyup');
}
@@ -2148,7 +2148,8 @@ describe('CourseOutlinePage', function() {
}, [
createMockSubsectionJSON({
graded: true,
- due_num_weeks: 3,
+ relative_weeks_due: 3,
+ start: '2014-07-09T00:00:00Z',
format: 'Lab',
has_explicit_staff_lock: true,
staff_only_message: true,
@@ -2178,14 +2179,14 @@ describe('CourseOutlinePage', function() {
it('can be edited when custom pacing for self paced course is active', function() {
outlinePage.$('.outline-subsection .configure-button').click();
- setEditModalValuesForCustomPacing('3', 'Lab');
- selectAdvancedSettings();
+ setEditModalValuesForCustomPacing('Lab', '3');
$('.wrapper-modal-window .action-save').click();
+
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
graderType: 'Lab',
isPrereq: false,
metadata: {
- due_num_weeks: 3,
+ relative_weeks_due: 3,
is_time_limited: false,
is_practice_exam: false,
is_proctored_enabled: false,
@@ -2200,6 +2201,10 @@ describe('CourseOutlinePage', function() {
AjaxHelpers.respondWithJson(requests, mockCustomPacingServerValuesJson);
AjaxHelpers.expectNoRequests(requests);
+ expect($('.outline-subsection .status-custom-grading-date').text().trim()).toEqual(
+ 'Custom due date: 3 weeks from enrollment'
+ );
+
expect($('.outline-subsection .status-grading-value')).toContainText(
'Lab'
);
@@ -2209,6 +2214,9 @@ describe('CourseOutlinePage', function() {
expect($('.outline-item .outline-subsection .status-grading-value')).toContainText('Lab');
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
+
+ expect($('#relative_date_input').css('display')).not.toBe('none');
+ expect($('#relative_weeks_due_projected.message').text().trim()).toEqual('If a learner starts on Jul 09, 2014, this subsection will be due on Jul 30, 2014.');
expect($('#due_in').val()).toBe('3');
expect($('#grading_type').val()).toBe('Lab');
expect($('input[name=content-visibility][value=staff_only]').is(':checked')).toBe(true);
@@ -2219,32 +2227,45 @@ describe('CourseOutlinePage', function() {
expectShowCorrectness('never');
});
- it('shows validation error on due number of weeks', function() {
+ it ('does not show relative date input when assignment is not graded', function() {
+ outlinePage.$('.outline-subsection .configure-button').click();
+ $('#grading_type').val('Lab').trigger('change');
+ $('#due_in').val('').trigger('change');
+ expect($('#relative_date_input').css('display')).not.toBe('none');
+
+ $('#grading_type').val('notgraded').trigger('change');
+ $('#due_in').val('').trigger('change');
+ expect($('#relative_date_input').css('display')).toBe('none');
+ })
+
+ it('shows validation error on relative date', function() {
outlinePage.$('.outline-subsection .configure-button').click();
// when due number of weeks goes over 18
- selectDueNumWeeksSubsection('19');
- expect($('#due-num-weeks-warning-max').css('display')).not.toBe('none');
+ selectRelativeWeeksSubsection('19');
+ expect($('#relative_weeks_due_warning_max').css('display')).not.toBe('none');
+ expect($('#relative_weeks_due_warning_max')).toContainText('The maximum number of weeks this subsection can be due in is 18 weeks from the learner enrollment date.');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
// when due number of weeks is less than 1
- selectDueNumWeeksSubsection('-1');
- expect($('#due-num-weeks-warning-min').css('display')).not.toBe('none');
+ selectRelativeWeeksSubsection('-1');
+ expect($('#relative_weeks_due_warning_min').css('display')).not.toBe('none');
+ expect($('#relative_weeks_due_warning_min')).toContainText('The minimum number of weeks this subsection can be due in is 1 week from the learner enrollment date.');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
// when no validation error should show up
- selectDueNumWeeksSubsection('10');
- expect($('#due-num-weeks-warning-max').css('display')).toBe('none');
- expect($('#due-num-weeks-warning-min').css('display')).toBe('none');
+ selectRelativeWeeksSubsection('10');
+ expect($('#relative_weeks_due_warning_max').css('display')).toBe('none');
+ expect($('#relative_weeks_due_warning_min').css('display')).toBe('none');
expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(false);
expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(false);
});
- it('due num weeks (due_in) can be cleared.', function() {
+ it('outline with assignment type and date are cleared when relative date input is cleared.', function() {
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
- setEditModalValuesForCustomPacing('3', 'Lab');
+ setEditModalValuesForCustomPacing('Lab', '3');
setContentVisibility('staff_only');
$('.wrapper-modal-window .action-save').click();
@@ -2253,6 +2274,10 @@ describe('CourseOutlinePage', function() {
// This is the response for the subsequent fetch operation.
AjaxHelpers.respondWithJson(requests, mockCustomPacingServerValuesJson);
+ expect($('.outline-subsection .status-custom-grading-date').text().trim()).toEqual(
+ 'Custom due date: 3 weeks from enrollment'
+ );
+
expect($('.outline-subsection .status-grading-value')).toContainText(
'Lab'
);
@@ -2261,12 +2286,12 @@ describe('CourseOutlinePage', function() {
);
outlinePage.$('.outline-subsection .configure-button').click();
+ expect($('#relative_weeks_due_projected.message').text().trim()).toEqual('If a learner starts on Jul 09, 2014, this subsection will be due on Jul 30, 2014.');
expect($('#due_in').val()).toBe('3');
expect($('#grading_type').val()).toBe('Lab');
expect($('input[name=content-visibility][value=staff_only]').is(':checked')).toBe(true);
- $('.wrapper-modal-window .due-date-input .action-clear').click();
- expect($('#due_in').val()).toBe('');
+ $('#due_in').val('');
$('#grading_type').val('notgraded');
setContentVisibility('visible');
@@ -2280,6 +2305,7 @@ describe('CourseOutlinePage', function() {
createMockSectionJSON({}, [createMockSubsectionJSON()])
);
+ expect($('.outline-subsection .status-custom-grading-date')).not.toExist();
expect($('.outline-subsection .status-grading-value')).not.toExist();
expect($('.outline-subsection .status-message-copy')).not.toContainText(
'Contains staff only content'
diff --git a/cms/static/js/views/modals/course_outline_modals.js b/cms/static/js/views/modals/course_outline_modals.js
index 650a5779c1..020fa384fa 100644
--- a/cms/static/js/views/modals/course_outline_modals.js
+++ b/cms/static/js/views/modals/course_outline_modals.js
@@ -390,12 +390,11 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
});
SelfPacedDueDateEditor = AbstractEditor.extend({
- fieldName: 'due_num_weeks',
+ fieldName: 'relative_weeks_due',
templateName: 'self-paced-due-date-editor',
className: 'modal-section-content has-actions due-date-input grading-due-date',
-
events: {
- 'click .clear-date': 'clearValue',
+ 'change #due_in': 'validateDueIn',
'keyup #due_in': 'validateDueIn',
'blur #due_in': 'validateDueIn',
},
@@ -404,44 +403,72 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
return parseInt(this.$('#due_in').val());
},
+ showProjectedDate: function() {
+ if (!this.getValue()) return;
+ var startDate = new Date(this.model.get('start'));
+ // The value returned by toUTCString() is a string in the form Www, dd Mmm yyyy hh:mm:ss GMT
+ var startDateList = startDate.toUTCString().split(' ')
+ // This text will look like Mmm dd, yyyy (i.e. Jul 26, 2021)
+ this.$("#relative_weeks_due_start_date").text(startDateList[2] + ' ' + startDateList[1] + ', ' + startDateList[3]);
+ var projectedDate = new Date(startDate)
+ projectedDate.setDate(projectedDate.getDate() + this.getValue()*7);
+ var projectedDateList = projectedDate.toUTCString().split(' ');
+ this.$("#relative_weeks_due_projected_due_in").text(projectedDateList[2] + ' ' + projectedDateList[1] + ', ' + projectedDateList[3]);
+ this.$('#relative_weeks_due_projected').show();
+ },
+
validateDueIn: function() {
+ this.$('#relative_weeks_due_projected').hide();
if (this.getValue() > 18){
- this.$('#due-num-weeks-warning-max').show();
+ this.$('#relative_weeks_due_warning_max').show();
BaseModal.prototype.disableActionButton.call(this.parent, 'save');
}
else if (this.getValue() < 1){
- this.$('#due-num-weeks-warning-min').show()
+ this.$('#relative_weeks_due_warning_min').show()
BaseModal.prototype.disableActionButton.call(this.parent, 'save');
}
else {
- this.$('#due-num-weeks-warning-max').hide();
- this.$('#due-num-weeks-warning-min').hide();
+ this.$('#relative_weeks_due_warning_max').hide();
+ this.$('#relative_weeks_due_warning_min').hide();
+ if (this.model.get('start')){
+ this.showProjectedDate();
+ }
BaseModal.prototype.enableActionButton.call(this.parent, 'save');
}
},
- clearValue: function(event) {
- event.preventDefault();
- this.$('#due_in').val('');
- },
-
afterRender: function() {
AbstractEditor.prototype.afterRender.call(this);
- this.$('.field-due-in input').val(this.model.get('due_num_weeks'));
+ if (this.model.get('graded')) {
+ this.$('#relative_date_input').show()
+ }
+ else {
+ this.$('#relative_date_input').hide()
+ }
+ this.$('.field-due-in input').val(this.model.get('relative_weeks_due'));
+ this.$('#relative_weeks_due_projected').hide();
+ if (this.getValue() && this.model.get('start')){
+ this.showProjectedDate();
+ }
},
getRequestData: function() {
- if (this.getValue() < 19 && this.getValue() > 0) {
+ if (this.getValue() < 19 && this.getValue() > 0 && $('#grading_type').val() !== 'notgraded') {
return {
metadata: {
- due_num_weeks: this.getValue()
+ relative_weeks_due: this.getValue()
}
};
+ } else {
+ return {
+ metadata: {
+ relative_weeks_due: null
+ }
+ }
}
- }
+ },
});
-
ReleaseDateEditor = BaseDateEditor.extend({
fieldName: 'start',
templateName: 'release-date-editor',
@@ -692,6 +719,18 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
GradingEditor = AbstractEditor.extend({
templateName: 'grading-editor',
className: 'edit-settings-grading',
+ events: {
+ 'change #grading_type': 'handleGradingSelect',
+ },
+
+ handleGradingSelect: function(event) {
+ event.preventDefault();
+ if (this.$('#grading_type').val() !== 'notgraded' && course.get('self_paced') && course.get('is_custom_relative_dates_active')) {
+ $('#relative_date_input').show();
+ } else {
+ $('#relative_date_input').hide();
+ }
+ },
afterRender: function() {
AbstractEditor.prototype.afterRender.call(this);
@@ -1130,10 +1169,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
} else if (xblockInfo.isSequential()) {
tabs[0].editors = [ReleaseDateEditor, GradingEditor, DueDateEditor];
tabs[1].editors = [ContentVisibilityEditor, ShowCorrectnessEditor];
- if (course.get('self_paced') && course.get('is_custom_pls_active')) {
+ if (course.get('self_paced') && course.get('is_custom_relative_dates_active')) {
tabs[0].editors.push(SelfPacedDueDateEditor);
}
-
if (options.enable_proctored_exams || options.enable_timed_exams) {
advancedTab.editors.push(TimedExaminationPreferenceEditor);
}
diff --git a/cms/static/sass/elements/_modules.scss b/cms/static/sass/elements/_modules.scss
index ecb239d223..9859da0b2d 100644
--- a/cms/static/sass/elements/_modules.scss
+++ b/cms/static/sass/elements/_modules.scss
@@ -652,7 +652,8 @@ $outline-indent-width: $baseline;
}
.status-grading-value,
- .status-proctored-exam-value {
+ .status-proctored-exam-value,
+ .status-custom-grading-date {
display: inline-block;
vertical-align: middle;
}
diff --git a/cms/templates/base.html b/cms/templates/base.html
index 53da173858..47cfe5dd11 100644
--- a/cms/templates/base.html
+++ b/cms/templates/base.html
@@ -10,7 +10,7 @@
<%!
from django.utils.translation import ugettext as _
-from cms.djangoapps.contentstore.config.waffle import CUSTOM_PLS
+from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
from lms.djangoapps.branding import api as branding_api
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from openedx.core.djangolib.js_utils import (
@@ -157,7 +157,7 @@ from openedx.core.release import RELEASE_LINE
display_course_number: "${context_course.display_coursenumber | n, js_escaped_string}",
revision: "${context_course.location.branch | n, js_escaped_string}",
self_paced: ${ context_course.self_paced | n, dump_js_escaped_json },
- is_custom_pls_active: ${CUSTOM_PLS.is_enabled(context_course.id) | n, dump_js_escaped_json}
+ is_custom_relative_dates_active: ${CUSTOM_RELATIVE_DATES.is_enabled(context_course.id) | n, dump_js_escaped_json}
});
% endif
diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore
index 9110793fc5..7267eaf640 100644
--- a/cms/templates/js/course-outline.underscore
+++ b/cms/templates/js/course-outline.underscore
@@ -246,6 +246,50 @@ if (is_proctored_exam) {
<% } %>
+ <% if (course.get('self_paced') && course.get('is_custom_relative_dates_active') && xblockInfo.get('relative_weeks_due')) { %>
+
+
+
+
+ <%- edx.StringUtils.interpolate(
+ ngettext(
+ 'Custom due date: {relativeWeeks} week from enrollment',
+ 'Custom due date: {relativeWeeks} weeks from enrollment',
+ xblockInfo.get('relative_weeks_due')),
+ {
+ relativeWeeks: xblockInfo.get('relative_weeks_due')
+ }
+ )
+ %>
+
+
+
+ <% } %>
+ <% } else if (course.get('self_paced') && course.get('is_custom_relative_dates_active') && xblockInfo.get('relative_weeks_due')) { %>
+
+
+ <%- gettext('Graded as:') %>
+
+ <%- gradingType %>
+
+
+
+
+
+
+ <%- edx.StringUtils.interpolate(
+ ngettext(
+ 'Custom due date: {relativeWeeks} week from enrollment',
+ 'Custom due date: {relativeWeeks} weeks from enrollment',
+ xblockInfo.get('relative_weeks_due')),
+ {
+ relativeWeeks: xblockInfo.get('relative_weeks_due')
+ }
+ )
+ %>
+
+
+
<% } %>
diff --git a/cms/templates/js/self-paced-due-date-editor.underscore b/cms/templates/js/self-paced-due-date-editor.underscore
index 4669056555..ceaf5e6873 100644
--- a/cms/templates/js/self-paced-due-date-editor.underscore
+++ b/cms/templates/js/self-paced-due-date-editor.underscore
@@ -1,24 +1,30 @@
-
+
+
-
+
+ <%= edx.HtmlUtils.interpolateHtml(
+ gettext('If a learner starts on {startDate}, this subsection will be due on {projectedDueIn}.'),
+ {
+ startDate: edx.HtmlUtils.HTML(''),
+ projectedDueIn: edx.HtmlUtils.HTML('')
+ })
+ %>
+
-
- <%- gettext('The maximum number of weeks this subsection can be due in is 18 weeks.') %>
+
+ <%- gettext('The maximum number of weeks this subsection can be due in is 18 weeks from the learner enrollment date.') %>
+
+
+
+ <%- gettext('The minimum number of weeks this subsection can be due in is 1 week from the learner enrollment date.') %>
+
-
- <%- gettext('The minimum number of weeks this subsection can be due in is 1 week.') %>
-
diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py
index 81757341a5..bf05a192ec 100644
--- a/common/lib/xmodule/xmodule/modulestore/inheritance.py
+++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py
@@ -46,10 +46,10 @@ class InheritanceMixin(XBlockMixin):
help=_("Enter the default date by which problems are due."),
scope=Scope.settings,
)
- # This attribute is for custom pacing in self paced courses for Studio if CUSTOM_PLS flag is active
- due_num_weeks = Integer(
- display_name=_("Number of Weeks Due By"),
- help=_("Enter the number of weeks the problems are due by relative to the learner's start date"),
+ # This attribute is for custom pacing in self paced courses for Studio if CUSTOM_RELATIVE_DATES flag is active
+ relative_weeks_due = Integer(
+ display_name=_("Number of Relative Weeks Due By"),
+ help=_("Enter the number of weeks the problems are due by relative to the learner's enrollment date"),
scope=Scope.settings,
)
visible_to_staff_only = Boolean(
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
index 57db76be52..4134676e61 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
@@ -1051,7 +1051,7 @@ def allowed_metadata_by_category(category):
return {
'vertical': [],
'chapter': ['start'],
- 'sequential': ['due', 'due_num_weeks', 'format', 'start', 'graded']
+ 'sequential': ['due', 'relative_weeks_due', 'format', 'start', 'graded']
}.get(category, ['*'])
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index 6938ed5911..e916833f9e 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -78,10 +78,10 @@ class SequenceFields: # lint-amnesty, pylint: disable=missing-class-docstring
help=_("Enter the date by which problems are due."),
scope=Scope.settings,
)
- # This attribute is for custom pacing in self paced courses for Studio if CUSTOM_PLS flag is active
- due_num_weeks = Integer(
- display_name=_("Number of Weeks Due By"),
- help=_("Enter the number of weeks the problems are due by relative to the learner's start date"),
+ # This attribute is for custom pacing in self paced courses for Studio if CUSTOM_RELATIVE_DATES flag is active
+ relative_weeks_due = Integer(
+ display_name=_("Number of Relative Weeks Due By"),
+ help=_("Enter the number of weeks the problems are due by relative to the learner's enrollment date"),
scope=Scope.settings,
)
diff --git a/openedx/core/djangoapps/course_date_signals/handlers.py b/openedx/core/djangoapps/course_date_signals/handlers.py
index 6cb58de305..92abefec8f 100644
--- a/openedx/core/djangoapps/course_date_signals/handlers.py
+++ b/openedx/core/djangoapps/course_date_signals/handlers.py
@@ -8,7 +8,7 @@ from django.dispatch import receiver
from edx_when.api import FIELDS_TO_EXTRACT, set_dates_for_course
from xblock.fields import Scope
-from cms.djangoapps.contentstore.config.waffle import CUSTOM_PLS
+from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
from openedx.core.lib.graph_traversals import get_children, leaf_filter, traverse_pre_order
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import SignalHandler, modulestore
@@ -111,19 +111,22 @@ def extract_dates_from_course(course):
# unless that item already has a relative date set
for _, section, weeks_to_complete in spaced_out_sections(course):
section_date_items = []
+ # section_due_date will end up being the max of all due dates of its subsections
+ section_due_date = timedelta(weeks=1)
for subsection in section.get_children():
# If custom pacing is set on a subsection, apply the set relative
# date to all the content inside the subsection. Otherwise
# apply the default Personalized Learner Schedules (PLS)
# logic for self paced courses.
- due_num_weeks = subsection.fields['due_num_weeks'].read_from(subsection)
- if (CUSTOM_PLS.is_enabled(course.id) and due_num_weeks):
- section_date_items.extend(_get_custom_pacing_children(subsection, due_num_weeks))
+ relative_weeks_due = subsection.fields['relative_weeks_due'].read_from(subsection)
+ if (CUSTOM_RELATIVE_DATES.is_enabled(course.id) and relative_weeks_due):
+ section_due_date = max(section_due_date, timedelta(weeks=relative_weeks_due))
+ section_date_items.extend(_get_custom_pacing_children(subsection, relative_weeks_due))
else:
+ section_due_date = max(section_due_date, weeks_to_complete)
section_date_items.extend(_gather_graded_items(subsection, weeks_to_complete))
- # if custom pls is active, we will allow due dates to be set for ungraded items as well
- if section_date_items and (section.graded or CUSTOM_PLS.is_enabled(course.id)):
- date_items.append((section.location, weeks_to_complete))
+ if section_date_items and (section.graded or CUSTOM_RELATIVE_DATES.is_enabled(course.id)):
+ date_items.append((section.location, {'due': section_due_date}))
date_items.extend(section_date_items)
else:
date_items = []
diff --git a/openedx/core/djangoapps/course_date_signals/tests.py b/openedx/core/djangoapps/course_date_signals/tests.py
index a36d33f249..9c04d1f935 100644
--- a/openedx/core/djangoapps/course_date_signals/tests.py
+++ b/openedx/core/djangoapps/course_date_signals/tests.py
@@ -2,7 +2,7 @@
from datetime import timedelta
from unittest.mock import patch # lint-amnesty, pylint: disable=wrong-import-order
-from cms.djangoapps.contentstore.config.waffle import CUSTOM_PLS
+from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
from edx_toggles.toggles.testutils import override_waffle_flag
from openedx.core.djangoapps.course_date_signals.handlers import (
_gather_graded_items,
@@ -125,11 +125,11 @@ class SelfPacedDueDatesTests(ModuleStoreTestCase): # lint-amnesty, pylint: disa
def test_get_custom_pacing_children(self):
"""
_get_custom_pacing_items should return a list of (block item location, field metadata dictionary)
- where the due dates are set from due_num_weeks
+ where the due dates are set from relative_weeks_due
"""
# A subsection with multiple units but no problems. Units should inherit due date.
with self.store.bulk_operations(self.course.id):
- sequence = ItemFactory(parent=self.course, category='sequential', due_num_weeks=2)
+ sequence = ItemFactory(parent=self.course, category='sequential', relative_weeks_due=2)
vertical1 = ItemFactory(parent=sequence, category='vertical')
vertical2 = ItemFactory(parent=sequence, category='vertical')
vertical3 = ItemFactory(parent=sequence, category='vertical')
@@ -175,7 +175,7 @@ class SelfPacedCustomDueDateTests(ModuleStoreTestCase):
self.course = CourseFactory.create(self_paced=True)
self.chapter = ItemFactory.create(category='chapter', parent=self.course)
- @override_waffle_flag(CUSTOM_PLS, active=True)
+ @override_waffle_flag(CUSTOM_RELATIVE_DATES, active=True)
def test_extract_dates_from_course_inheritance(self):
"""
extract_dates_from_course should return a list of (block item location, field metadata dictionary)
@@ -183,12 +183,12 @@ class SelfPacedCustomDueDateTests(ModuleStoreTestCase):
(ex. If a subsection is assigned a due date, its children should also have the same due date)
"""
with self.store.bulk_operations(self.course.id):
- sequential = ItemFactory.create(category='sequential', parent=self.chapter, due_num_weeks=3)
+ sequential = ItemFactory.create(category='sequential', parent=self.chapter, relative_weeks_due=3)
vertical = ItemFactory.create(category='vertical', parent=sequential)
problem = ItemFactory.create(category='problem', parent=vertical)
expected_dates = [
(self.course.location, {}),
- (self.chapter.location, timedelta(days=28)),
+ (self.chapter.location, {'due': timedelta(days=21)}),
(sequential.location, {'due': timedelta(days=21)}),
(vertical.location, {'due': timedelta(days=21)}),
(problem.location, {'due': timedelta(days=21)})
@@ -196,38 +196,38 @@ class SelfPacedCustomDueDateTests(ModuleStoreTestCase):
course = self.store.get_item(self.course.location)
self.assertCountEqual(extract_dates_from_course(course), expected_dates)
- @override_waffle_flag(CUSTOM_PLS, active=True)
+ @override_waffle_flag(CUSTOM_RELATIVE_DATES, active=True)
def test_extract_dates_from_course_custom_and_default_pls_one_subsection(self):
"""
- due_num_weeks in one of the subsections. Only one of them should have a set due date.
+ relative_weeks_due in one of the subsections. Only one of them should have a set due date.
The other subsections do not have due dates because they are not graded
and default PLS do not assign due dates to non graded assignments.
If custom PLS is not set, the subsection will fall back to the default
PLS logic of evenly spaced sections.
"""
with self.store.bulk_operations(self.course.id):
- sequential = ItemFactory.create(category='sequential', parent=self.chapter, due_num_weeks=3)
+ sequential = ItemFactory.create(category='sequential', parent=self.chapter, relative_weeks_due=3)
ItemFactory.create(category='sequential', parent=self.chapter)
ItemFactory.create(category='sequential', parent=self.chapter)
expected_dates = [
(self.course.location, {}),
- (self.chapter.location, timedelta(days=28)),
+ (self.chapter.location, {'due': timedelta(days=28)}),
(sequential.location, {'due': timedelta(days=21)})
]
course = self.store.get_item(self.course.location)
self.assertCountEqual(extract_dates_from_course(course), expected_dates)
- @override_waffle_flag(CUSTOM_PLS, active=True)
+ @override_waffle_flag(CUSTOM_RELATIVE_DATES, active=True)
def test_extract_dates_from_course_custom_and_default_pls_one_subsection_graded(self):
"""
- A section with a subsection that has due_num_weeks and
- a subsection without due_num_weeks that has graded content.
- Default PLS should apply for the subsection without due_num_weeks that has graded content.
+ A section with a subsection that has relative_weeks_due and
+ a subsection without relative_weeks_due that has graded content.
+ Default PLS should apply for the subsection without relative_weeks_due that has graded content.
If custom PLS is not set, the subsection will fall back to the default
PLS logic of evenly spaced sections.
"""
with self.store.bulk_operations(self.course.id):
- sequential1 = ItemFactory.create(category='sequential', parent=self.chapter, due_num_weeks=2)
+ sequential1 = ItemFactory.create(category='sequential', parent=self.chapter, relative_weeks_due=2)
vertical1 = ItemFactory.create(category='vertical', parent=sequential1)
problem1 = ItemFactory.create(category='problem', parent=vertical1)
@@ -238,11 +238,11 @@ class SelfPacedCustomDueDateTests(ModuleStoreTestCase):
expected_dates = [
(self.course.location, {}),
- (self.chapter.location, timedelta(days=21)),
+ (self.chapter.location, {'due': timedelta(days=14)}),
(sequential1.location, {'due': timedelta(days=14)}),
(vertical1.location, {'due': timedelta(days=14)}),
(problem1.location, {'due': timedelta(days=14)}),
- (chapter2.location, timedelta(days=42)),
+ (chapter2.location, {'due': timedelta(days=42)}),
(sequential2.location, {'due': timedelta(days=42)}),
(vertical2.location, {'due': timedelta(days=42)}),
(problem2.location, {'due': timedelta(days=42)})
@@ -251,23 +251,23 @@ class SelfPacedCustomDueDateTests(ModuleStoreTestCase):
with patch.object(utils, 'get_expected_duration', return_value=timedelta(weeks=6)):
self.assertCountEqual(extract_dates_from_course(course), expected_dates)
- @override_waffle_flag(CUSTOM_PLS, active=True)
+ @override_waffle_flag(CUSTOM_RELATIVE_DATES, active=True)
def test_extract_dates_from_course_custom_and_default_pls_multiple_subsections_graded(self):
"""
- A section with a subsection that has due_num_weeks and multiple sections without
- due_num_weeks that has graded content. Default PLS should apply for the subsections
- without due_num_weeks that has graded content.
+ A section with a subsection that has relative_weeks_due and multiple sections without
+ relative_weeks_due that has graded content. Default PLS should apply for the subsections
+ without relative_weeks_due that has graded content.
If custom PLS is not set, the subsection will fall back to the default
PLS logic of evenly spaced sections.
"""
with self.store.bulk_operations(self.course.id):
- sequential1 = ItemFactory.create(category='sequential', parent=self.chapter, due_num_weeks=4)
+ sequential1 = ItemFactory.create(category='sequential', parent=self.chapter, relative_weeks_due=4)
vertical1 = ItemFactory.create(category='vertical', parent=sequential1)
problem1 = ItemFactory.create(category='problem', parent=vertical1)
expected_dates = [
(self.course.location, {}),
- (self.chapter.location, timedelta(days=14)),
+ (self.chapter.location, {'due': timedelta(days=28)}),
(sequential1.location, {'due': timedelta(days=28)}),
(vertical1.location, {'due': timedelta(days=28)}),
(problem1.location, {'due': timedelta(days=28)})
@@ -280,7 +280,7 @@ class SelfPacedCustomDueDateTests(ModuleStoreTestCase):
problem = ItemFactory.create(category='problem', parent=vertical)
num_days = i * 14 + 28
expected_dates.extend([
- (chapter.location, timedelta(days=num_days)),
+ (chapter.location, {'due': timedelta(days=num_days)}),
(sequential.location, {'due': timedelta(days=num_days)}),
(vertical.location, {'due': timedelta(days=num_days)}),
(problem.location, {'due': timedelta(days=num_days)}),
@@ -289,19 +289,19 @@ class SelfPacedCustomDueDateTests(ModuleStoreTestCase):
with patch.object(utils, 'get_expected_duration', return_value=timedelta(weeks=8)):
self.assertCountEqual(extract_dates_from_course(course), expected_dates)
- @override_waffle_flag(CUSTOM_PLS, active=True)
+ @override_waffle_flag(CUSTOM_RELATIVE_DATES, active=True)
def test_extract_dates_from_course_all_subsections(self):
"""
- With due_num_weeks on all subsections. All subsections should
+ With relative_weeks_due on all subsections. All subsections should
have their corresponding due dates.
"""
with self.store.bulk_operations(self.course.id):
- sequential1 = ItemFactory.create(category='sequential', parent=self.chapter, due_num_weeks=3)
- sequential2 = ItemFactory.create(category='sequential', parent=self.chapter, due_num_weeks=4)
- sequential3 = ItemFactory.create(category='sequential', parent=self.chapter, due_num_weeks=5)
+ sequential1 = ItemFactory.create(category='sequential', parent=self.chapter, relative_weeks_due=3)
+ sequential2 = ItemFactory.create(category='sequential', parent=self.chapter, relative_weeks_due=4)
+ sequential3 = ItemFactory.create(category='sequential', parent=self.chapter, relative_weeks_due=5)
expected_dates = [
(self.course.location, {}),
- (self.chapter.location, timedelta(days=28)),
+ (self.chapter.location, {'due': timedelta(days=35)}),
(sequential1.location, {'due': timedelta(days=21)}),
(sequential2.location, {'due': timedelta(days=28)}),
(sequential3.location, {'due': timedelta(days=35)})
@@ -309,10 +309,10 @@ class SelfPacedCustomDueDateTests(ModuleStoreTestCase):
course = self.store.get_item(self.course.location)
self.assertCountEqual(extract_dates_from_course(course), expected_dates)
- @override_waffle_flag(CUSTOM_PLS, active=True)
+ @override_waffle_flag(CUSTOM_RELATIVE_DATES, active=True)
def test_extract_dates_from_course_no_subsections(self):
"""
- Without due_num_weeks on all subsections. None of the subsections should
+ Without relative_weeks_due on all subsections. None of the subsections should
have due dates.
"""
with self.store.bulk_operations(self.course.id):