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') + } + ) + %> + +
+
+ <%- 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') + } + ) + %> + +
+