Merge pull request #28175 from edx/syoon/AA-885

feat: AA-885 show offsets in studio self paced course outline for a subsection
This commit is contained in:
sofiayoon
2021-07-27 10:08:00 -04:00
committed by GitHub
13 changed files with 236 additions and 116 deletions

View File

@@ -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__,)

View File

@@ -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,

View File

@@ -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'

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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}
});
</script>
% endif

View File

@@ -246,6 +246,50 @@ if (is_proctored_exam) {
<% } %>
</p>
</div>
<% if (course.get('self_paced') && course.get('is_custom_relative_dates_active') && xblockInfo.get('relative_weeks_due')) { %>
<div class="status-grading">
<p>
<span class="icon fa fa-calendar" aria-hidden="true"></span>
<span class="status-custom-grading-date">
<%- 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')
}
)
%>
</span>
<p>
</div>
<% } %>
<% } else if (course.get('self_paced') && course.get('is_custom_relative_dates_active') && xblockInfo.get('relative_weeks_due')) { %>
<div class="status-grading">
<p>
<span class="sr status-grading-label"> <%- gettext('Graded as:') %> </span>
<span class="icon fa fa-check" aria-hidden="true"></span>
<span class="status-grading-value"> <%- gradingType %> </span>
</p>
</div>
<div class="status-grading">
<p>
<span class="icon fa fa-calendar" aria-hidden="true"></span>
<span class="status-custom-grading-date">
<%- 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')
}
)
%>
</span>
<p>
</div>
<% } %>
<div class="status-hide-after-due">
<p>

View File

@@ -1,24 +1,30 @@
<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>
<div id="relative_date_input">
<ul class="list-fields list-input date-setter">
<li class="field field-text field-due-in">
<!-- Translators: Please use a generic pluralization that makes sense in most contexts for the second half of the sentence "weeks from learner enrollment date" which is preceded by "Due in: [input entry]" -->
<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" style="width:20%"/>
<%- gettext('weeks from learner enrollment date')%>
</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="relative_weeks_due_projected" class="message">
<%= edx.HtmlUtils.interpolateHtml(
gettext('If a learner starts on {startDate}, this subsection will be due on {projectedDueIn}.'),
{
startDate: edx.HtmlUtils.HTML('<span id="relative_weeks_due_start_date"></span>'),
projectedDueIn: edx.HtmlUtils.HTML('<span id="relative_weeks_due_projected_due_in"></span>')
})
%>
</div>
<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 id="relative_weeks_due_warning_max" class="message-status error">
<%- gettext('The maximum number of weeks this subsection can be due in is 18 weeks from the learner enrollment date.') %>
</div>
<div id="relative_weeks_due_warning_min" class="message-status error">
<%- gettext('The minimum number of weeks this subsection can be due in is 1 week from the learner enrollment date.') %>
</div>
</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>

View File

@@ -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(

View File

@@ -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, ['*'])

View File

@@ -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,
)

View File

@@ -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 = []

View File

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