Merge pull request #15298 from edx/sofiya/preregister-cohorts

Assign users to cohorts before registration
This commit is contained in:
Sofiya Semenova
2017-06-20 11:59:23 -04:00
committed by GitHub
14 changed files with 557 additions and 165 deletions

View File

@@ -1,7 +1,8 @@
/* globals _, NotificationModel, NotificationView, interpolate_text */
(function(define) {
'use strict';
define(['backbone', 'underscore', 'jquery', 'gettext', 'js/groups/views/cohort_form', 'string_utils',
'js/models/notification', 'js/views/notification'],
'js/models/notification', 'js/views/notification'],
function(Backbone, _, $, gettext, CohortFormView) {
var CohortEditorView = Backbone.View.extend({
@@ -24,6 +25,8 @@
errorNotifications: null,
// Any confirmation messages that are currently being displayed (for example, number of students added).
confirmationNotifications: null,
// Any messages about preassigned email addresses currently being displayed to the instructor.
preassignedNotifications: null,
render: function() {
this.$el.html(this.template({
@@ -47,13 +50,18 @@
},
selectTab: function(event) {
var tabElement = $(event.currentTarget),
tabName = tabElement.data('tab');
var $tabElement = $(event.currentTarget),
tabName = $tabElement.data('tab');
event.preventDefault();
this.$('.wrapper-tabs .tab').removeClass('is-selected');
this.$('.wrapper-tabs .tab').find('span.sr').remove();
tabElement.addClass('is-selected');
tabElement.find('a').prepend('<span class="sr">' + gettext('Selected tab') + ' </span>');
$tabElement.addClass('is-selected');
edx.HtmlUtils.prepend(
$($tabElement.find('a')),
edx.HtmlUtils.interpolateHtml('<span class="sr"> {selectedTab} </span>',
{selectedTab: gettext('Selected tab')}
)
);
this.$('.tab-content').addClass('is-hidden');
this.$('.tab-content-' + tabName).removeClass('is-hidden').focus();
},
@@ -108,7 +116,7 @@
}
});
}).fail(function() {
self.showErrorMessage(gettext('Error adding students.'), true);
self.showErrorMessage(gettext('Error adding learners.'), true);
});
} else {
self.showErrorMessage(gettext('Enter a username or email.'), true);
@@ -151,18 +159,20 @@
},
addNotifications: function(modifiedUsers) {
var oldCohort, title, details, numPresent, numUsersAdded, numErrors,
createErrorDetails, errorActionCallback, errorModel,
var oldCohort, title, details, numPresent, numUsersAdded, numPreassigned,
numErrors, createErrorDetails, errorActionCallback, errorModel, i,
errorLimit = 5;
// Show confirmation messages.
this.undelegateViewEvents(this.confirmationNotifications);
numUsersAdded = modifiedUsers.added.length + modifiedUsers.changed.length;
numPresent = modifiedUsers.present.length;
numPreassigned = modifiedUsers.preassigned.length;
title = '';
if (numUsersAdded > 0 || numPresent > 0) {
title = interpolate_text(
ngettext('{numUsersAdded} student has been added to this cohort',
'{numUsersAdded} students have been added to this cohort', numUsersAdded),
title += interpolate_text(
ngettext('{numUsersAdded} learner has been added to this cohort. ',
'{numUsersAdded} learners have been added to this cohort. ', numUsersAdded),
{numUsersAdded: numUsersAdded}
);
@@ -171,27 +181,28 @@
oldCohort = changedInfo.previous_cohort;
if (oldCohort in movedByCohort) {
movedByCohort[oldCohort] = movedByCohort[oldCohort] + 1;
}
else {
} else {
movedByCohort[oldCohort] = 1;
}
});
details = [];
for (oldCohort in movedByCohort) {
_.each(movedByCohort, function(numMoved, prevCohort) {
details.push(
interpolate_text(
ngettext('{numMoved} student was removed from {oldCohort}',
'{numMoved} students were removed from {oldCohort}', movedByCohort[oldCohort]),
{numMoved: movedByCohort[oldCohort], oldCohort: oldCohort}
ngettext('{numMoved} learner was moved from {prevCohort}',
'{numMoved} learners were moved from {prevCohort}', numMoved),
{numMoved: numMoved, prevCohort: prevCohort}
)
);
}
});
if (numPresent > 0) {
details.push(
interpolate_text(
ngettext('{numPresent} student was already in the cohort',
'{numPresent} students were already in the cohort', numPresent),
ngettext('{numPresent} learner was already in the cohort',
'{numPresent} learners were already in the cohort', numPresent),
{numPresent: numPresent}
)
);
@@ -206,35 +217,81 @@
})
});
this.confirmationNotifications.render();
}
else if (this.confirmationNotifications) {
} else if (this.confirmationNotifications) {
this.confirmationNotifications.$el.html('');
this.confirmationNotifications = null;
}
// Show preassigned email addresses.
this.undelegateViewEvents(this.preassignedNotifications);
if (numPreassigned > 0) {
details = [];
for (i = 0; i < modifiedUsers.preassigned.length; i++) {
details.push(interpolate_text(gettext('{email}'),
{email: modifiedUsers.preassigned[i]}));
}
title = (
interpolate_text(
ngettext('{numPreassigned} learner was pre-assigned for this cohort. ' +
'This learner will automatically be added to the cohort when ' +
'they enroll in the course.',
'{numPreassigned} learners were pre-assigned for this cohort. ' +
'These learners will automatically be added to the cohort when ' +
'they enroll in the course.',
numPreassigned),
{numPreassigned: numPreassigned}
)
);
this.preassignedNotifications = new NotificationView({
el: this.$('.cohort-preassigned'),
model: new NotificationModel({
type: 'warning',
title: title,
details: details
})
});
this.preassignedNotifications.render();
} else if (this.preassignedNotifications) {
this.preassignedNotifications.$el.html('');
this.preassignedNotifications = null;
}
// Show error messages.
this.undelegateViewEvents(this.errorNotifications);
numErrors = modifiedUsers.unknown.length;
numErrors = modifiedUsers.unknown.length + modifiedUsers.invalid.length;
if (numErrors > 0) {
createErrorDetails = function(unknownUsers, showAllErrors) {
var numErrors = unknownUsers.length, details = [];
createErrorDetails = function(unknownUsers, invalidEmails, showAllErrors) {
var unknownErrorsShown = showAllErrors ? unknownUsers.length :
Math.min(errorLimit, unknownUsers.length);
var invalidErrorsShown = showAllErrors ? invalidEmails.length :
Math.min(errorLimit - unknownUsers.length, invalidEmails.length);
details = [];
for (var i = 0; i < (showAllErrors ? numErrors : Math.min(errorLimit, numErrors)); i++) {
details.push(interpolate_text(gettext('Unknown user: {user}'), {user: unknownUsers[i]}));
for (i = 0; i < unknownErrorsShown; i++) {
details.push(interpolate_text(gettext('Unknown username: {user}'),
{user: unknownUsers[i]}));
}
for (i = 0; i < invalidErrorsShown; i++) {
details.push(interpolate_text(gettext('Invalid email address: {email}'),
{email: invalidEmails[i]}));
}
return details;
};
title = interpolate_text(
ngettext('There was an error when trying to add students:',
'There were {numErrors} errors when trying to add students:', numErrors),
ngettext('There was an error when trying to add learners:',
'{numErrors} learners could not be added to this cohort:', numErrors),
{numErrors: numErrors}
);
details = createErrorDetails(modifiedUsers.unknown, false);
details = createErrorDetails(modifiedUsers.unknown, modifiedUsers.invalid, false);
errorActionCallback = function(view) {
view.model.set('actionText', null);
view.model.set('details', createErrorDetails(modifiedUsers.unknown, true));
view.model.set('details',
createErrorDetails(modifiedUsers.unknown, modifiedUsers.invalid, true));
view.render();
};

View File

@@ -1,3 +1,5 @@
/* globals _ */
define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers',
'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/content_group',
@@ -10,11 +12,11 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
describe('Cohorts View', function() {
var catLoversInitialCount = 123, dogLoversInitialCount = 456, unknownUserMessage,
createMockCohort, createMockCohorts, createMockContentGroups, createMockCohortSettingsJson,
createMockVerifiedTrackCohortsJson, flushVerifiedTrackCohortRequests, createCohortsView,
cohortsView, requests, respondToRefresh, verifyMessage, verifyNoMessage, verifyDetailedMessage,
verifyHeader, verifyVerifiedTrackMessage, verifyVerifiedTrackUIUpdates, expectCohortAddRequest,
getAddModal, selectContentGroup, clearContentGroup,
invalidEmailMessage, createMockCohort, createMockCohorts, createMockContentGroups,
createMockCohortSettingsJson, createMockVerifiedTrackCohortsJson, flushVerifiedTrackCohortRequests,
createCohortsView, cohortsView, requests, respondToRefresh, verifyMessage, verifyNoMessage,
verifyDetailedMessage, verifyHeader, verifyVerifiedTrackMessage, verifyVerifiedTrackUIUpdates,
expectCohortAddRequest, getAddModal, selectContentGroup, clearContentGroup,
saveFormAndExpectErrors, createMockCohortSettings, MOCK_COHORTED_USER_PARTITION_ID,
MOCK_UPLOAD_COHORTS_CSV_URL, MOCK_STUDIO_ADVANCED_SETTINGS_URL, MOCK_STUDIO_GROUP_CONFIGURATIONS_URL,
MOCK_VERIFIED_TRACK_COHORTING_URL, MOCK_MANUAL_ASSIGNMENT, MOCK_RANDOM_ASSIGNMENT;
@@ -249,7 +251,11 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
};
unknownUserMessage = function(name) {
return 'Unknown user: ' + name;
return 'Unknown username: ' + name;
};
invalidEmailMessage = function(name) {
return 'Invalid email address: ' + name;
};
beforeEach(function() {
@@ -299,7 +305,7 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
expect(cohortsView.$(fileUploadFormCss).length).toBe(0);
uploadCsvToggle = cohortsView.$('.toggle-cohort-management-secondary');
expect(uploadCsvToggle.text()).
toContain('Assign students to cohorts by uploading a CSV file');
toContain('Assign learners to cohorts by uploading a CSV file');
uploadCsvToggle.click();
// After toggle is clicked, it should be hidden.
expect(uploadCsvToggle).toHaveClass('hidden');
@@ -690,7 +696,8 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
respondToAdd = function(result) {
AjaxHelpers.respondWithJson(
requests,
_.extend({unknown: [], added: [], present: [], changed: [], success: true}, result)
_.extend({unknown: [], added: [], present: [], changed: [],
success: true, preassigned: [], invalid: []}, result)
);
};
@@ -709,27 +716,57 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
respondToAdd({added: ['student@sample.com']});
respondToRefresh(catLoversUpdatedCount, dogLoversInitialCount);
verifyHeader(1, 'Cat Lovers', catLoversUpdatedCount);
verifyMessage('1 student has been added to this cohort', 'confirmation');
verifyMessage('1 learner has been added to this cohort.', 'confirmation');
expect(getStudentInput().val()).toBe('');
});
it('shows an error when adding a student that does not exist', function() {
it('preassigns an email address if it is not associated with a user', function() {
createCohortsView(this, {selectCohort: 1});
addStudents('unknown@sample.com');
AjaxHelpers.expectRequest(
requests, 'POST', '/mock_service/cohorts/1/add', 'users=unknown%40sample.com'
);
respondToAdd({unknown: ['unknown@sample.com']});
respondToAdd({preassigned: ['unknown@sample.com']});
respondToRefresh(catLoversInitialCount, dogLoversInitialCount);
verifyHeader(1, 'Cat Lovers', catLoversInitialCount);
verifyDetailedMessage('There was an error when trying to add students:', 'error',
[unknownUserMessage('unknown@sample.com')]
);
expect(getStudentInput().val()).toBe('unknown@sample.com');
verifyDetailedMessage('1 learner was pre-assigned for this cohort. ' +
'This learner will automatically be added to the cohort when they enroll in the course.',
'warning',
['unknown@sample.com']);
expect(getStudentInput().val()).toBe('');
});
it('shows an error when adding an invalid email address', function() {
createCohortsView(this, {selectCohort: 1});
addStudents('unknown@');
AjaxHelpers.expectRequest(
requests, 'POST', '/mock_service/cohorts/1/add', 'users=unknown%40'
);
respondToAdd({invalid: ['unknown@']});
respondToRefresh(catLoversInitialCount, dogLoversInitialCount);
verifyHeader(1, 'Cat Lovers', catLoversInitialCount);
verifyDetailedMessage('There was an error when trying to add learners:', 'error',
[invalidEmailMessage('unknown@')]
);
});
it('shows an error when adding an unknown user', function() {
createCohortsView(this, {selectCohort: 1});
addStudents('unknown');
AjaxHelpers.expectRequest(
requests, 'POST', '/mock_service/cohorts/1/add', 'users=unknown'
);
respondToAdd({unknown: ['unknown']});
respondToRefresh(catLoversInitialCount, dogLoversInitialCount);
verifyHeader(1, 'Cat Lovers', catLoversInitialCount);
verifyDetailedMessage('There was an error when trying to add learners:', 'error',
[unknownUserMessage('unknown')]
);
});
it('shows a "view all" button when more than 5 students do not exist', function() {
var sixUsers = 'unknown1@sample.com, unknown2@sample.com, unknown3@sample.com, unknown4@sample.com, unknown5@sample.com, unknown6@sample.com';
var sixUsers = 'unknown1, unknown2, unknown3, unknown4, unknown5, unknown6';
createCohortsView(this, {selectCohort: 1});
addStudents(sixUsers);
@@ -738,30 +775,30 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
'users=' + sixUsers.replace(/@/g, '%40').replace(/, /g, '%2C+')
);
respondToAdd({unknown: [
'unknown1@sample.com',
'unknown2@sample.com',
'unknown3@sample.com',
'unknown4@sample.com',
'unknown5@sample.com',
'unknown6@sample.com']
'unknown1',
'unknown2',
'unknown3',
'unknown4',
'unknown5',
'unknown6']
});
respondToRefresh(catLoversInitialCount + 6, dogLoversInitialCount);
verifyDetailedMessage('There were 6 errors when trying to add students:', 'error',
verifyDetailedMessage('6 learners could not be added to this cohort:', 'error',
[
unknownUserMessage('unknown1@sample.com'), unknownUserMessage('unknown2@sample.com'),
unknownUserMessage('unknown3@sample.com'), unknownUserMessage('unknown4@sample.com'),
unknownUserMessage('unknown5@sample.com')
unknownUserMessage('unknown1'), unknownUserMessage('unknown2'),
unknownUserMessage('unknown3'), unknownUserMessage('unknown4'),
unknownUserMessage('unknown5')
],
'View all errors'
);
expect(getStudentInput().val()).toBe(sixUsers);
// Click "View all"
cohortsView.$('.action-expand').click();
verifyDetailedMessage('There were 6 errors when trying to add students:', 'error',
verifyDetailedMessage('6 learners could not be added to this cohort:', 'error',
[
unknownUserMessage('unknown1@sample.com'), unknownUserMessage('unknown2@sample.com'),
unknownUserMessage('unknown3@sample.com'), unknownUserMessage('unknown4@sample.com'),
unknownUserMessage('unknown5@sample.com'), unknownUserMessage('unknown6@sample.com')
unknownUserMessage('unknown1'), unknownUserMessage('unknown2'),
unknownUserMessage('unknown3'), unknownUserMessage('unknown4'),
unknownUserMessage('unknown5'), unknownUserMessage('unknown6')
]
);
});
@@ -784,11 +821,11 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
});
respondToRefresh();
verifyDetailedMessage('3 students have been added to this cohort', 'confirmation',
verifyDetailedMessage('3 learners have been added to this cohort.', 'confirmation',
[
'2 students were removed from cohort 2',
'1 student was removed from cohort 3',
'1 student was already in the cohort'
'2 learners were moved from cohort 2',
'1 learner was moved from cohort 3',
'1 learner was already in the cohort'
]
);
expect(getStudentInput().val()).toBe('');
@@ -798,7 +835,7 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
createCohortsView(this, {selectCohort: 1});
addStudents('student@sample.com');
AjaxHelpers.respondWithError(requests);
verifyMessage('Error adding students.', 'error');
verifyMessage('Error adding learners.', 'error');
expect(getStudentInput().val()).toBe('student@sample.com');
});
@@ -808,13 +845,13 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
// First verify that an error is shown
addStudents('student@sample.com');
AjaxHelpers.respondWithError(requests);
verifyMessage('Error adding students.', 'error');
verifyMessage('Error adding learners.', 'error');
// Now verify that the error is removed on a subsequent add
addStudents('student@sample.com');
respondToAdd({added: ['student@sample.com']});
respondToRefresh(catLoversInitialCount + 1, dogLoversInitialCount);
verifyMessage('1 student has been added to this cohort', 'confirmation');
verifyMessage('1 learner has been added to this cohort.', 'confirmation');
});
});