Merge pull request #28016 from edx/syoon/AA-844

feat: AA-883 Basic prototype for self paced due dates in Studio
This commit is contained in:
sofiayoon
2021-07-09 09:23:05 -04:00
committed by GitHub
13 changed files with 540 additions and 23 deletions

View File

@@ -67,3 +67,15 @@ REDIRECT_TO_LIBRARY_AUTHORING_MICROFRONTEND = LegacyWaffleFlag(
flag_name='library_authoring_mfe',
module_name=__name__,
)
# .. toggle_name: studio.custom_pls
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to enable custom pacing for PLS
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2021-07-08
# .. toggle_target_removal_date: 2021-12-31
# .. toggle_warnings: For this flag to be active, add flag 'studio.custom_pls' in Django Admin
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-844
CUSTOM_PLS = CourseWaffleFlag(WAFFLE_NAMESPACE, 'custom_pls', module_name=__name__,)

View File

@@ -9,6 +9,4 @@ def should_show_checklists_quality(course_key):
Determine if the ENABLE_CHECKLISTS_QUALITY waffle flag is set
and if the user is able to see it
"""
if ENABLE_CHECKLISTS_QUALITY.is_enabled(course_key):
return True
return False
return ENABLE_CHECKLISTS_QUALITY.is_enabled(course_key)

View File

@@ -1230,6 +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,
'format': xblock.format,
'course_graders': [grader.get('type') for grader in graders],
'has_changes': has_changes,

View File

@@ -12,7 +12,7 @@ describe('CourseOutlinePage', function() {
var createCourseOutlinePage, displayNameInput, model, outlinePage, requests, getItemsOfType, getItemHeaders,
verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState, selectBasicSettings,
selectVisibilitySettings, selectAdvancedSettings, createMockCourseJSON, createMockSectionJSON,
createMockSubsectionJSON, verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON, setSelfPaced,
createMockSubsectionJSON, verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON, setSelfPaced,setSelfPacedCustomPLS,
mockSingleSectionCourseJSON, createMockVerticalJSON, createMockIndexJSON, mockCourseEntranceExamJSON,
selectOnboardingExam, createMockCourseJSONWithReviewRules,mockCourseJSONWithReviewRules,
mockOutlinePage = readFixtures('templates/mock/mock-course-outline-page.underscore'),
@@ -202,6 +202,11 @@ describe('CourseOutlinePage', function() {
course.set('self_paced', true);
};
setSelfPacedCustomPLS = function() {
setSelfPaced();
course.set('is_custom_pls_active', true);
}
createCourseOutlinePage = function(test, courseJSON, createOnly) {
requests = AjaxHelpers.requests(test);
model = new XBlockOutlineInfo(courseJSON, {parse: true});
@@ -294,7 +299,7 @@ describe('CourseOutlinePage', function() {
TemplateHelpers.installTemplates([
'course-outline', 'xblock-string-field-editor', 'modal-button',
'basic-modal', 'course-outline-modal', 'release-date-editor',
'due-date-editor', 'grading-editor', 'publish-editor',
'due-date-editor', 'self-paced-due-date-editor', 'grading-editor', 'publish-editor',
'staff-lock-editor', 'unit-access-editor', 'content-visibility-editor',
'settings-modal-tabs', 'timed-examination-preference-editor', 'access-editor',
'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor',
@@ -1002,9 +1007,9 @@ describe('CourseOutlinePage', function() {
});
describe('Subsection', function() {
var getDisplayNameWrapper, setEditModalValues, setContentVisibility, mockServerValuesJson,
selectDisableSpecialExams, selectTimedExam, selectProctoredExam, selectPracticeExam,
selectPrerequisite, selectLastPrerequisiteSubsection, checkOptionFieldVisibility,
var getDisplayNameWrapper, setEditModalValues, setEditModalValuesForCustomPacing, setContentVisibility, mockServerValuesJson,
mockCustomPacingServerValuesJson, selectDisableSpecialExams, selectTimedExam, selectProctoredExam, selectPracticeExam,
selectPrerequisite, selectLastPrerequisiteSubsection, selectDueNumWeeksSubsection, checkOptionFieldVisibility,
defaultModalSettings, modalSettingsWithExamReviewRules, getMockNoPrereqOrExamsCourseJSON, expectShowCorrectness;
getDisplayNameWrapper = function() {
@@ -2117,6 +2122,170 @@ describe('CourseOutlinePage', function() {
);
expect($modalWindow.find('.outline-subsection')).not.toExist();
});
describe('Self Paced with Custom Personalized Learner Schedules (PLS)', function () {
beforeEach(function() {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [])
])
]);
createCourseOutlinePage(this, mockCourseJSON, false);
setSelfPacedCustomPLS();
});
setEditModalValuesForCustomPacing = function(due_in, grading_type) {
$('#due_in').val(due_in);
$('#grading_type').val(grading_type);
};
selectDueNumWeeksSubsection = function(weeks) {
$('#due_in').val(weeks).trigger('keyup');
}
mockCustomPacingServerValuesJson = createMockSectionJSON({
release_date: 'Jan 01, 2970 at 05:00 UTC'
}, [
createMockSubsectionJSON({
graded: true,
due_num_weeks: 3,
format: 'Lab',
has_explicit_staff_lock: true,
staff_only_message: true,
is_prereq: false,
show_correctness: 'never',
is_time_limited: false,
is_practice_exam: false,
is_proctored_exam: false,
default_time_limit_minutes: null,
}, [
createMockVerticalJSON({
has_changes: true,
published: false
})
])
]);
it('can show correct editors for self_paced course with custom pacing', function (){
outlinePage.$('.outline-subsection .configure-button').click();
expect($('.edit-settings-release').length).toBe(0);
// Due date input exists for custom pacing self paced courses
expect($('.grading-due-date').length).toBe(1);
expect($('.edit-settings-grading').length).toBe(1);
expect($('.edit-content-visibility').length).toBe(1);
expect($('.edit-show-correctness').length).toBe(1);
});
it('can be edited when custom pacing for self paced course is active', function() {
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValuesForCustomPacing('3', 'Lab');
selectAdvancedSettings();
$('.wrapper-modal-window .action-save').click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
graderType: 'Lab',
isPrereq: false,
metadata: {
due_num_weeks: 3,
is_time_limited: false,
is_practice_exam: false,
is_proctored_enabled: false,
default_time_limit_minutes: null,
is_onboarding_exam: false,
}
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
AjaxHelpers.respondWithJson(requests, {});
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
AjaxHelpers.respondWithJson(requests, mockCustomPacingServerValuesJson);
AjaxHelpers.expectNoRequests(requests);
expect($('.outline-subsection .status-grading-value')).toContainText(
'Lab'
);
expect($('.outline-subsection .status-message-copy')).toContainText(
'Contains staff only content'
);
expect($('.outline-item .outline-subsection .status-grading-value')).toContainText('Lab');
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
expect($('#due_in').val()).toBe('3');
expect($('#grading_type').val()).toBe('Lab');
expect($('input[name=content-visibility][value=staff_only]').is(':checked')).toBe(true);
expect($('input.timed_exam').is(':checked')).toBe(false);
expect($('input.proctored_exam').is(':checked')).toBe(false);
expect($('input.no_special_exam').is(':checked')).toBe(true);
expect($('input.practice_exam').is(':checked')).toBe(false);
expectShowCorrectness('never');
});
it('shows validation error on due number of weeks', 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');
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');
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');
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() {
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
setEditModalValuesForCustomPacing('3', 'Lab');
setContentVisibility('staff_only');
$('.wrapper-modal-window .action-save').click();
// This is the response for the change operation.
AjaxHelpers.respondWithJson(requests, {});
// This is the response for the subsequent fetch operation.
AjaxHelpers.respondWithJson(requests, mockCustomPacingServerValuesJson);
expect($('.outline-subsection .status-grading-value')).toContainText(
'Lab'
);
expect($('.outline-subsection .status-message-copy')).toContainText(
'Contains staff only content'
);
outlinePage.$('.outline-subsection .configure-button').click();
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('');
$('#grading_type').val('notgraded');
setContentVisibility('visible');
$('.wrapper-modal-window .action-save').click();
// This is the response for the change operation.
AjaxHelpers.respondWithJson(requests, {});
// This is the response for the subsequent fetch operation.
AjaxHelpers.respondWithJson(requests,
createMockSectionJSON({}, [createMockSubsectionJSON()])
);
expect($('.outline-subsection .status-grading-value')).not.toExist();
expect($('.outline-subsection .status-message-copy')).not.toContainText(
'Contains staff only content'
);
});
})
});
// Note: most tests for units can be found in Bok Choy

View File

@@ -15,7 +15,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
'use strict';
var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, HighlightsXBlockModal,
AbstractEditor, BaseDateEditor,
ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor,
ReleaseDateEditor, DueDateEditor, SelfPacedDueDateEditor, GradingEditor, PublishEditor, AbstractVisibilityEditor,
StaffLockEditor, UnitAccessEditor, ContentVisibilityEditor, TimedExaminationPreferenceEditor,
AccessEditor, ShowCorrectnessEditor, HighlightsEditor, HighlightsEnableXBlockModal, HighlightsEnableEditor;
@@ -389,6 +389,58 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
});
SelfPacedDueDateEditor = AbstractEditor.extend({
fieldName: 'due_num_weeks',
templateName: 'self-paced-due-date-editor',
className: 'modal-section-content has-actions due-date-input grading-due-date',
events: {
'click .clear-date': 'clearValue',
'keyup #due_in': 'validateDueIn',
'blur #due_in': 'validateDueIn',
},
getValue: function() {
return parseInt(this.$('#due_in').val());
},
validateDueIn: function() {
if (this.getValue() > 18){
this.$('#due-num-weeks-warning-max').show();
BaseModal.prototype.disableActionButton.call(this.parent, 'save');
}
else if (this.getValue() < 1){
this.$('#due-num-weeks-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();
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'));
},
getRequestData: function() {
if (this.getValue() < 19 && this.getValue() > 0) {
return {
metadata: {
due_num_weeks: this.getValue()
}
};
}
}
});
ReleaseDateEditor = BaseDateEditor.extend({
fieldName: 'start',
templateName: 'release-date-editor',
@@ -1077,6 +1129,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')) {
tabs[0].editors.push(SelfPacedDueDateEditor);
}
if (options.enable_proctored_exams || options.enable_timed_exams) {
advancedTab.editors.push(TimedExaminationPreferenceEditor);

View File

@@ -10,6 +10,7 @@
<%!
from django.utils.translation import ugettext as _
from cms.djangoapps.contentstore.config.waffle import CUSTOM_PLS
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 (
@@ -155,7 +156,8 @@ from openedx.core.release import RELEASE_LINE
num: "${context_course.location.course | n, js_escaped_string}",
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 }
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}
});
</script>
% endif

View File

@@ -29,7 +29,7 @@ from django.urls import reverse
<%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', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'unit-access-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', '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']:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>

View File

@@ -0,0 +1,25 @@
<ul class="list-fields list-input date-setter">
<li class="field field-text field-due-in">
<label for="due_in"><%- gettext('Due in:') %></label>
<input type="number" id="due_in" name="due_in" value=""
placeholder="" autocomplete="off" min="1" max="18"/> weeks
</li>
</ul>
<ul class="list-actions">
<li class="action-item">
<a href="#" data-tooltip="<%- gettext('Clear Due Date') %>" class="clear-date action-button action-clear">
<span class="icon fa fa-undo" aria-hidden="true"></span>
<span class="sr"><%- gettext('Clear Due Date') %></span>
</a>
</li>
</ul>
<div id="due-num-weeks-warning-max" class="message-status error">
<%- gettext('The maximum number of weeks this subsection can be due in is 18 weeks.') %>
</div>
<div id="due-num-weeks-warning-min" class="message-status error">
<%- gettext('The minimum number of weeks this subsection can be due in is 1 week.') %>
</div>