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):