Commit 31d99e8d by cahrens Committed by Sofiya Semenova

ED-549 Assign learner to a cohort before registration.

parent 84aa4a26
...@@ -149,8 +149,8 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin ...@@ -149,8 +149,8 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin
confirmation_messages = self.cohort_management_page.get_cohort_confirmation_messages() confirmation_messages = self.cohort_management_page.get_cohort_confirmation_messages()
self.assertEqual( self.assertEqual(
[ [
"2 students have been added to this cohort", "2 learners have been added to this cohort.",
"1 student was removed from " + self.manual_cohort_name "1 learner was moved from " + self.manual_cohort_name
], ],
confirmation_messages confirmation_messages
) )
...@@ -217,16 +217,16 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin ...@@ -217,16 +217,16 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin
self.assertEqual( self.assertEqual(
[ [
"0 students have been added to this cohort", "0 learners have been added to this cohort.",
"1 student was already in the cohort" "1 learner was already in the cohort"
], ],
self.cohort_management_page.get_cohort_confirmation_messages() self.cohort_management_page.get_cohort_confirmation_messages()
) )
self.assertEqual( self.assertEqual(
[ [
"There was an error when trying to add students:", "There was an error when trying to add learners:",
"Unknown user: unknown_user" "Unknown username: unknown_user"
], ],
self.cohort_management_page.get_cohort_error_messages() self.cohort_management_page.get_cohort_error_messages()
) )
......
...@@ -10,6 +10,7 @@ from time import time ...@@ -10,6 +10,7 @@ from time import time
import unicodecsv import unicodecsv
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.files.storage import DefaultStorage from django.core.files.storage import DefaultStorage
from openassessment.data import OraAggregateData from openassessment.data import OraAggregateData
from pytz import UTC from pytz import UTC
...@@ -137,9 +138,9 @@ def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, tas ...@@ -137,9 +138,9 @@ def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, tas
# cohorts_status is a mapping from cohort_name to metadata about # cohorts_status is a mapping from cohort_name to metadata about
# that cohort. The metadata will include information about users # that cohort. The metadata will include information about users
# successfully added to the cohort, users not found, and a cached # successfully added to the cohort, users not found, Preassigned
# reference to the corresponding cohort object to prevent # users, and a cached reference to the corresponding cohort object
# redundant cohort queries. # to prevent redundant cohort queries.
cohorts_status = {} cohorts_status = {}
with DefaultStorage().open(task_input['file_name']) as f: with DefaultStorage().open(task_input['file_name']) as f:
...@@ -152,8 +153,10 @@ def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, tas ...@@ -152,8 +153,10 @@ def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, tas
if not cohorts_status.get(cohort_name): if not cohorts_status.get(cohort_name):
cohorts_status[cohort_name] = { cohorts_status[cohort_name] = {
'Cohort Name': cohort_name, 'Cohort Name': cohort_name,
'Students Added': 0, 'Learners Added': 0,
'Students Not Found': set() 'Learners Not Found': set(),
'Invalid Email Addresses': set(),
'Preassigned Learners': set()
} }
try: try:
cohorts_status[cohort_name]['cohort'] = CourseUserGroup.objects.get( cohorts_status[cohort_name]['cohort'] = CourseUserGroup.objects.get(
...@@ -170,11 +173,25 @@ def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, tas ...@@ -170,11 +173,25 @@ def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, tas
continue continue
try: try:
add_user_to_cohort(cohorts_status[cohort_name]['cohort'], username_or_email) # If add_user_to_cohort successfully adds a user, a user object is returned.
cohorts_status[cohort_name]['Students Added'] += 1 # If a user is preassigned to a cohort, no user object is returned (we already have the email address).
task_progress.succeeded += 1 (user, previous_cohort, preassigned) = add_user_to_cohort(cohorts_status[cohort_name]['cohort'], username_or_email)
if preassigned:
cohorts_status[cohort_name]['Preassigned Learners'].add(username_or_email)
task_progress.preassigned += 1
else:
cohorts_status[cohort_name]['Learners Added'] += 1
task_progress.succeeded += 1
except User.DoesNotExist: except User.DoesNotExist:
cohorts_status[cohort_name]['Students Not Found'].add(username_or_email) # Raised when a user with the username could not be found, and the email is not valid
cohorts_status[cohort_name]['Learners Not Found'].add(username_or_email)
task_progress.failed += 1
except ValidationError:
# Raised when a user with the username could not be found, and the email is not valid,
# but the entered string contains an "@"
# Since there is no way to know if the entered string is an invalid username or an invalid email,
# assume that a string with the "@" symbol in it is an attempt at entering an email
cohorts_status[cohort_name]['Invalid Email Addresses'].add(username_or_email)
task_progress.failed += 1 task_progress.failed += 1
except ValueError: except ValueError:
# Raised when the user is already in the given cohort # Raised when the user is already in the given cohort
...@@ -186,10 +203,12 @@ def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, tas ...@@ -186,10 +203,12 @@ def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, tas
task_progress.update_task_state(extra_meta=current_step) task_progress.update_task_state(extra_meta=current_step)
# Filter the output of `add_users_to_cohorts` in order to upload the result. # Filter the output of `add_users_to_cohorts` in order to upload the result.
output_header = ['Cohort Name', 'Exists', 'Students Added', 'Students Not Found'] output_header = ['Cohort Name', 'Exists', 'Learners Added', 'Learners Not Found', 'Invalid Email Addresses', 'Preassigned Learners']
output_rows = [ output_rows = [
[ [
','.join(status_dict.get(column_name, '')) if column_name == 'Students Not Found' ','.join(status_dict.get(column_name, '')) if (column_name == 'Learners Not Found'
or column_name == 'Invalid Email Addresses'
or column_name == 'Preassigned Learners')
else status_dict[column_name] else status_dict[column_name]
for column_name in output_header for column_name in output_header
] ]
......
...@@ -26,6 +26,7 @@ class TaskProgress(object): ...@@ -26,6 +26,7 @@ class TaskProgress(object):
self.succeeded = 0 self.succeeded = 0
self.skipped = 0 self.skipped = 0
self.failed = 0 self.failed = 0
self.preassigned = 0
def update_task_state(self, extra_meta=None): def update_task_state(self, extra_meta=None):
""" """
...@@ -47,6 +48,7 @@ class TaskProgress(object): ...@@ -47,6 +48,7 @@ class TaskProgress(object):
'skipped': self.skipped, 'skipped': self.skipped,
'failed': self.failed, 'failed': self.failed,
'total': self.total, 'total': self.total,
'preassigned': self.preassigned,
'duration_ms': int((time() - self.start_time) * 1000), 'duration_ms': int((time() - self.start_time) * 1000),
} }
if extra_meta is not None: if extra_meta is not None:
......
...@@ -1464,7 +1464,7 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1464,7 +1464,7 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.cohort_2 = CohortFactory(course_id=self.course.id, name='Cohort 2') self.cohort_2 = CohortFactory(course_id=self.course.id, name='Cohort 2')
self.student_1 = self.create_student(username=u'student_1\xec', email='student_1@example.com') self.student_1 = self.create_student(username=u'student_1\xec', email='student_1@example.com')
self.student_2 = self.create_student(username='student_2', email='student_2@example.com') self.student_2 = self.create_student(username='student_2', email='student_2@example.com')
self.csv_header_row = ['Cohort Name', 'Exists', 'Students Added', 'Students Not Found'] self.csv_header_row = ['Cohort Name', 'Exists', 'Learners Added', 'Learners Not Found', 'Invalid Email Addresses', 'Preassigned Learners']
def _cohort_students_and_upload(self, csv_data): def _cohort_students_and_upload(self, csv_data):
""" """
...@@ -1485,8 +1485,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1485,8 +1485,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result) self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result)
self.verify_rows_in_csv( self.verify_rows_in_csv(
[ [
dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', '', '', ''])),
dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', '', '', ''])),
], ],
verify_order=False verify_order=False
) )
...@@ -1500,8 +1500,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1500,8 +1500,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result) self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result)
self.verify_rows_in_csv( self.verify_rows_in_csv(
[ [
dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', '', '', ''])),
dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', '', '', ''])),
], ],
verify_order=False verify_order=False
) )
...@@ -1515,8 +1515,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1515,8 +1515,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result) self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result)
self.verify_rows_in_csv( self.verify_rows_in_csv(
[ [
dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', '', '', ''])),
dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', '', '', ''])),
], ],
verify_order=False verify_order=False
) )
...@@ -1536,8 +1536,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1536,8 +1536,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result) self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result)
self.verify_rows_in_csv( self.verify_rows_in_csv(
[ [
dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', '', '', ''])),
dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', '', '', ''])),
], ],
verify_order=False verify_order=False
) )
...@@ -1546,13 +1546,11 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1546,13 +1546,11 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
result = self._cohort_students_and_upload( result = self._cohort_students_and_upload(
'username,email,cohort\n' 'username,email,cohort\n'
'Invalid,,Cohort 1\n' 'Invalid,,Cohort 1\n'
'student_2,also_fake@bad.com,Cohort 2'
) )
self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 0, 'failed': 2}, result) self.assertDictContainsSubset({'total': 1, 'attempted': 1, 'succeeded': 0, 'failed': 1}, result)
self.verify_rows_in_csv( self.verify_rows_in_csv(
[ [
dict(zip(self.csv_header_row, ['Cohort 1', 'True', '0', 'Invalid'])), dict(zip(self.csv_header_row, ['Cohort 1', 'True', '0', 'Invalid', '', ''])),
dict(zip(self.csv_header_row, ['Cohort 2', 'True', '0', 'also_fake@bad.com'])),
], ],
verify_order=False verify_order=False
) )
...@@ -1566,8 +1564,35 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1566,8 +1564,35 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 1, 'failed': 1}, result) self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 1, 'failed': 1}, result)
self.verify_rows_in_csv( self.verify_rows_in_csv(
[ [
dict(zip(self.csv_header_row, ['Does Not Exist', 'False', '0', ''])), dict(zip(self.csv_header_row, ['Does Not Exist', 'False', '0', '', '', ''])),
dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', '', '', ''])),
],
verify_order=False
)
def test_preassigned_user(self):
result = self._cohort_students_and_upload(
'username,email,cohort\n'
',example_email@example.com,Cohort 1'
)
self.assertDictContainsSubset({'total': 1, 'attempted': 1, 'succeeded': 0, 'failed': 0},
result)
self.verify_rows_in_csv(
[
dict(zip(self.csv_header_row, ['Cohort 1', 'True', '0', '', '', 'example_email@example.com'])),
],
verify_order=False
)
def test_invalid_email(self):
result = self._cohort_students_and_upload(
'username,email,cohort\n'
',student_1@,Cohort 1\n'
)
self.assertDictContainsSubset({'total': 1, 'attempted': 1, 'succeeded': 0, 'failed': 1}, result)
self.verify_rows_in_csv(
[
dict(zip(self.csv_header_row, ['Cohort 1', 'True', '0', '', 'student_1@', ''])),
], ],
verify_order=False verify_order=False
) )
...@@ -1592,7 +1617,7 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1592,7 +1617,7 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 0, 'failed': 2}, result) self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 0, 'failed': 2}, result)
self.verify_rows_in_csv( self.verify_rows_in_csv(
[ [
dict(zip(self.csv_header_row, ['', 'False', '0', ''])), dict(zip(self.csv_header_row, ['', 'False', '0', '', '', ''])),
], ],
verify_order=False verify_order=False
) )
...@@ -1616,8 +1641,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1616,8 +1641,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result) self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result)
self.verify_rows_in_csv( self.verify_rows_in_csv(
[ [
dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', '', '', ''])),
dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', '', '', ''])),
], ],
verify_order=False verify_order=False
) )
...@@ -1634,8 +1659,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1634,8 +1659,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result) self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result)
self.verify_rows_in_csv( self.verify_rows_in_csv(
[ [
dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', '', '', ''])),
dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', '', '', ''])),
], ],
verify_order=False verify_order=False
) )
...@@ -1654,8 +1679,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1654,8 +1679,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result) self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'succeeded': 2, 'failed': 0}, result)
self.verify_rows_in_csv( self.verify_rows_in_csv(
[ [
dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 1', 'True', '1', '', '', ''])),
dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', ''])), dict(zip(self.csv_header_row, ['Cohort 2', 'True', '1', '', '', ''])),
], ],
verify_order=False verify_order=False
) )
...@@ -1674,8 +1699,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase): ...@@ -1674,8 +1699,8 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'skipped': 2, 'failed': 0}, result) self.assertDictContainsSubset({'total': 2, 'attempted': 2, 'skipped': 2, 'failed': 0}, result)
self.verify_rows_in_csv( self.verify_rows_in_csv(
[ [
dict(zip(self.csv_header_row, ['Cohort 1', 'True', '0', ''])), dict(zip(self.csv_header_row, ['Cohort 1', 'True', '0', '', '', ''])),
dict(zip(self.csv_header_row, ['Cohort 2', 'True', '0', ''])), dict(zip(self.csv_header_row, ['Cohort 2', 'True', '0', '', '', ''])),
], ],
verify_order=False verify_order=False
) )
......
/* globals _, NotificationModel, NotificationView, interpolate_text */
(function(define) { (function(define) {
'use strict'; 'use strict';
define(['backbone', 'underscore', 'jquery', 'gettext', 'js/groups/views/cohort_form', 'string_utils', 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) { function(Backbone, _, $, gettext, CohortFormView) {
var CohortEditorView = Backbone.View.extend({ var CohortEditorView = Backbone.View.extend({
...@@ -24,6 +25,8 @@ ...@@ -24,6 +25,8 @@
errorNotifications: null, errorNotifications: null,
// Any confirmation messages that are currently being displayed (for example, number of students added). // Any confirmation messages that are currently being displayed (for example, number of students added).
confirmationNotifications: null, confirmationNotifications: null,
// Any messages about preassigned email addresses currently being displayed to the instructor.
preassignedNotifications: null,
render: function() { render: function() {
this.$el.html(this.template({ this.$el.html(this.template({
...@@ -47,13 +50,18 @@ ...@@ -47,13 +50,18 @@
}, },
selectTab: function(event) { selectTab: function(event) {
var tabElement = $(event.currentTarget), var $tabElement = $(event.currentTarget),
tabName = tabElement.data('tab'); tabName = $tabElement.data('tab');
event.preventDefault(); event.preventDefault();
this.$('.wrapper-tabs .tab').removeClass('is-selected'); this.$('.wrapper-tabs .tab').removeClass('is-selected');
this.$('.wrapper-tabs .tab').find('span.sr').remove(); this.$('.wrapper-tabs .tab').find('span.sr').remove();
tabElement.addClass('is-selected'); $tabElement.addClass('is-selected');
tabElement.find('a').prepend('<span class="sr">' + gettext('Selected tab') + ' </span>'); 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').addClass('is-hidden');
this.$('.tab-content-' + tabName).removeClass('is-hidden').focus(); this.$('.tab-content-' + tabName).removeClass('is-hidden').focus();
}, },
...@@ -108,7 +116,7 @@ ...@@ -108,7 +116,7 @@
} }
}); });
}).fail(function() { }).fail(function() {
self.showErrorMessage(gettext('Error adding students.'), true); self.showErrorMessage(gettext('Error adding learners.'), true);
}); });
} else { } else {
self.showErrorMessage(gettext('Enter a username or email.'), true); self.showErrorMessage(gettext('Enter a username or email.'), true);
...@@ -151,18 +159,20 @@ ...@@ -151,18 +159,20 @@
}, },
addNotifications: function(modifiedUsers) { addNotifications: function(modifiedUsers) {
var oldCohort, title, details, numPresent, numUsersAdded, numErrors, var oldCohort, title, details, numPresent, numUsersAdded, numPreassigned,
createErrorDetails, errorActionCallback, errorModel, numErrors, createErrorDetails, errorActionCallback, errorModel, i,
errorLimit = 5; errorLimit = 5;
// Show confirmation messages. // Show confirmation messages.
this.undelegateViewEvents(this.confirmationNotifications); this.undelegateViewEvents(this.confirmationNotifications);
numUsersAdded = modifiedUsers.added.length + modifiedUsers.changed.length; numUsersAdded = modifiedUsers.added.length + modifiedUsers.changed.length;
numPresent = modifiedUsers.present.length; numPresent = modifiedUsers.present.length;
numPreassigned = modifiedUsers.preassigned.length;
title = '';
if (numUsersAdded > 0 || numPresent > 0) { if (numUsersAdded > 0 || numPresent > 0) {
title = interpolate_text( title += interpolate_text(
ngettext('{numUsersAdded} student has been added to this cohort', ngettext('{numUsersAdded} learner has been added to this cohort. ',
'{numUsersAdded} students have been added to this cohort', numUsersAdded), '{numUsersAdded} learners have been added to this cohort. ', numUsersAdded),
{numUsersAdded: numUsersAdded} {numUsersAdded: numUsersAdded}
); );
...@@ -171,27 +181,28 @@ ...@@ -171,27 +181,28 @@
oldCohort = changedInfo.previous_cohort; oldCohort = changedInfo.previous_cohort;
if (oldCohort in movedByCohort) { if (oldCohort in movedByCohort) {
movedByCohort[oldCohort] = movedByCohort[oldCohort] + 1; movedByCohort[oldCohort] = movedByCohort[oldCohort] + 1;
} } else {
else {
movedByCohort[oldCohort] = 1; movedByCohort[oldCohort] = 1;
} }
}); });
details = []; details = [];
for (oldCohort in movedByCohort) {
_.each(movedByCohort, function(numMoved, prevCohort) {
details.push( details.push(
interpolate_text( interpolate_text(
ngettext('{numMoved} student was removed from {oldCohort}', ngettext('{numMoved} learner was moved from {prevCohort}',
'{numMoved} students were removed from {oldCohort}', movedByCohort[oldCohort]), '{numMoved} learners were moved from {prevCohort}', numMoved),
{numMoved: movedByCohort[oldCohort], oldCohort: oldCohort} {numMoved: numMoved, prevCohort: prevCohort}
) )
); );
} });
if (numPresent > 0) { if (numPresent > 0) {
details.push( details.push(
interpolate_text( interpolate_text(
ngettext('{numPresent} student was already in the cohort', ngettext('{numPresent} learner was already in the cohort',
'{numPresent} students were already in the cohort', numPresent), '{numPresent} learners were already in the cohort', numPresent),
{numPresent: numPresent} {numPresent: numPresent}
) )
); );
...@@ -206,35 +217,81 @@ ...@@ -206,35 +217,81 @@
}) })
}); });
this.confirmationNotifications.render(); this.confirmationNotifications.render();
} } else if (this.confirmationNotifications) {
else if (this.confirmationNotifications) {
this.confirmationNotifications.$el.html(''); this.confirmationNotifications.$el.html('');
this.confirmationNotifications = null; 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. // Show error messages.
this.undelegateViewEvents(this.errorNotifications); this.undelegateViewEvents(this.errorNotifications);
numErrors = modifiedUsers.unknown.length; numErrors = modifiedUsers.unknown.length + modifiedUsers.invalid.length;
if (numErrors > 0) { if (numErrors > 0) {
createErrorDetails = function(unknownUsers, showAllErrors) { createErrorDetails = function(unknownUsers, invalidEmails, showAllErrors) {
var numErrors = unknownUsers.length, details = []; 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; return details;
}; };
title = interpolate_text( title = interpolate_text(
ngettext('There was an error when trying to add students:', ngettext('There was an error when trying to add learners:',
'There were {numErrors} errors when trying to add students:', numErrors), '{numErrors} learners could not be added to this cohort:', numErrors),
{numErrors: numErrors} {numErrors: numErrors}
); );
details = createErrorDetails(modifiedUsers.unknown, false); details = createErrorDetails(modifiedUsers.unknown, modifiedUsers.invalid, false);
errorActionCallback = function(view) { errorActionCallback = function(view) {
view.model.set('actionText', null); 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.render();
}; };
......
/* globals _ */
define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers', 'common/js/spec_helpers/template_helpers',
'js/groups/views/cohorts', 'js/groups/collections/cohort', 'js/groups/models/content_group', '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 ...@@ -10,11 +12,11 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
describe('Cohorts View', function() { describe('Cohorts View', function() {
var catLoversInitialCount = 123, dogLoversInitialCount = 456, unknownUserMessage, var catLoversInitialCount = 123, dogLoversInitialCount = 456, unknownUserMessage,
createMockCohort, createMockCohorts, createMockContentGroups, createMockCohortSettingsJson, invalidEmailMessage, createMockCohort, createMockCohorts, createMockContentGroups,
createMockVerifiedTrackCohortsJson, flushVerifiedTrackCohortRequests, createCohortsView, createMockCohortSettingsJson, createMockVerifiedTrackCohortsJson, flushVerifiedTrackCohortRequests,
cohortsView, requests, respondToRefresh, verifyMessage, verifyNoMessage, verifyDetailedMessage, createCohortsView, cohortsView, requests, respondToRefresh, verifyMessage, verifyNoMessage,
verifyHeader, verifyVerifiedTrackMessage, verifyVerifiedTrackUIUpdates, expectCohortAddRequest, verifyDetailedMessage, verifyHeader, verifyVerifiedTrackMessage, verifyVerifiedTrackUIUpdates,
getAddModal, selectContentGroup, clearContentGroup, expectCohortAddRequest, getAddModal, selectContentGroup, clearContentGroup,
saveFormAndExpectErrors, createMockCohortSettings, MOCK_COHORTED_USER_PARTITION_ID, saveFormAndExpectErrors, createMockCohortSettings, MOCK_COHORTED_USER_PARTITION_ID,
MOCK_UPLOAD_COHORTS_CSV_URL, MOCK_STUDIO_ADVANCED_SETTINGS_URL, MOCK_STUDIO_GROUP_CONFIGURATIONS_URL, 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; 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 ...@@ -249,7 +251,11 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
}; };
unknownUserMessage = function(name) { unknownUserMessage = function(name) {
return 'Unknown user: ' + name; return 'Unknown username: ' + name;
};
invalidEmailMessage = function(name) {
return 'Invalid email address: ' + name;
}; };
beforeEach(function() { beforeEach(function() {
...@@ -299,7 +305,7 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers ...@@ -299,7 +305,7 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
expect(cohortsView.$(fileUploadFormCss).length).toBe(0); expect(cohortsView.$(fileUploadFormCss).length).toBe(0);
uploadCsvToggle = cohortsView.$('.toggle-cohort-management-secondary'); uploadCsvToggle = cohortsView.$('.toggle-cohort-management-secondary');
expect(uploadCsvToggle.text()). 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(); uploadCsvToggle.click();
// After toggle is clicked, it should be hidden. // After toggle is clicked, it should be hidden.
expect(uploadCsvToggle).toHaveClass('hidden'); expect(uploadCsvToggle).toHaveClass('hidden');
...@@ -690,7 +696,8 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers ...@@ -690,7 +696,8 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
respondToAdd = function(result) { respondToAdd = function(result) {
AjaxHelpers.respondWithJson( AjaxHelpers.respondWithJson(
requests, 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 ...@@ -709,27 +716,57 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
respondToAdd({added: ['student@sample.com']}); respondToAdd({added: ['student@sample.com']});
respondToRefresh(catLoversUpdatedCount, dogLoversInitialCount); respondToRefresh(catLoversUpdatedCount, dogLoversInitialCount);
verifyHeader(1, 'Cat Lovers', catLoversUpdatedCount); 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(''); 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}); createCohortsView(this, {selectCohort: 1});
addStudents('unknown@sample.com'); addStudents('unknown@sample.com');
AjaxHelpers.expectRequest( AjaxHelpers.expectRequest(
requests, 'POST', '/mock_service/cohorts/1/add', 'users=unknown%40sample.com' requests, 'POST', '/mock_service/cohorts/1/add', 'users=unknown%40sample.com'
); );
respondToAdd({unknown: ['unknown@sample.com']}); respondToAdd({preassigned: ['unknown@sample.com']});
respondToRefresh(catLoversInitialCount, dogLoversInitialCount); respondToRefresh(catLoversInitialCount, dogLoversInitialCount);
verifyHeader(1, 'Cat Lovers', catLoversInitialCount); verifyHeader(1, 'Cat Lovers', catLoversInitialCount);
verifyDetailedMessage('There was an error when trying to add students:', 'error', verifyDetailedMessage('1 learner was pre-assigned for this cohort. ' +
[unknownUserMessage('unknown@sample.com')] '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')]
); );
expect(getStudentInput().val()).toBe('unknown@sample.com');
}); });
it('shows a "view all" button when more than 5 students do not exist', function() { 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}); createCohortsView(this, {selectCohort: 1});
addStudents(sixUsers); addStudents(sixUsers);
...@@ -738,30 +775,30 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers ...@@ -738,30 +775,30 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
'users=' + sixUsers.replace(/@/g, '%40').replace(/, /g, '%2C+') 'users=' + sixUsers.replace(/@/g, '%40').replace(/, /g, '%2C+')
); );
respondToAdd({unknown: [ respondToAdd({unknown: [
'unknown1@sample.com', 'unknown1',
'unknown2@sample.com', 'unknown2',
'unknown3@sample.com', 'unknown3',
'unknown4@sample.com', 'unknown4',
'unknown5@sample.com', 'unknown5',
'unknown6@sample.com'] 'unknown6']
}); });
respondToRefresh(catLoversInitialCount + 6, dogLoversInitialCount); 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('unknown1'), unknownUserMessage('unknown2'),
unknownUserMessage('unknown3@sample.com'), unknownUserMessage('unknown4@sample.com'), unknownUserMessage('unknown3'), unknownUserMessage('unknown4'),
unknownUserMessage('unknown5@sample.com') unknownUserMessage('unknown5')
], ],
'View all errors' 'View all errors'
); );
expect(getStudentInput().val()).toBe(sixUsers); expect(getStudentInput().val()).toBe(sixUsers);
// Click "View all" // Click "View all"
cohortsView.$('.action-expand').click(); 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('unknown1'), unknownUserMessage('unknown2'),
unknownUserMessage('unknown3@sample.com'), unknownUserMessage('unknown4@sample.com'), unknownUserMessage('unknown3'), unknownUserMessage('unknown4'),
unknownUserMessage('unknown5@sample.com'), unknownUserMessage('unknown6@sample.com') unknownUserMessage('unknown5'), unknownUserMessage('unknown6')
] ]
); );
}); });
...@@ -784,11 +821,11 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers ...@@ -784,11 +821,11 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
}); });
respondToRefresh(); 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', '2 learners were moved from cohort 2',
'1 student was removed from cohort 3', '1 learner was moved from cohort 3',
'1 student was already in the cohort' '1 learner was already in the cohort'
] ]
); );
expect(getStudentInput().val()).toBe(''); expect(getStudentInput().val()).toBe('');
...@@ -798,7 +835,7 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers ...@@ -798,7 +835,7 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
createCohortsView(this, {selectCohort: 1}); createCohortsView(this, {selectCohort: 1});
addStudents('student@sample.com'); addStudents('student@sample.com');
AjaxHelpers.respondWithError(requests); AjaxHelpers.respondWithError(requests);
verifyMessage('Error adding students.', 'error'); verifyMessage('Error adding learners.', 'error');
expect(getStudentInput().val()).toBe('student@sample.com'); expect(getStudentInput().val()).toBe('student@sample.com');
}); });
...@@ -808,13 +845,13 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers ...@@ -808,13 +845,13 @@ define(['backbone', 'jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers
// First verify that an error is shown // First verify that an error is shown
addStudents('student@sample.com'); addStudents('student@sample.com');
AjaxHelpers.respondWithError(requests); AjaxHelpers.respondWithError(requests);
verifyMessage('Error adding students.', 'error'); verifyMessage('Error adding learners.', 'error');
// Now verify that the error is removed on a subsequent add // Now verify that the error is removed on a subsequent add
addStudents('student@sample.com'); addStudents('student@sample.com');
respondToAdd({added: ['student@sample.com']}); respondToAdd({added: ['student@sample.com']});
respondToRefresh(catLoversInitialCount + 1, dogLoversInitialCount); respondToRefresh(catLoversInitialCount + 1, dogLoversInitialCount);
verifyMessage('1 student has been added to this cohort', 'confirmation'); verifyMessage('1 learner has been added to this cohort.', 'confirmation');
}); });
}); });
......
...@@ -2,26 +2,27 @@ ...@@ -2,26 +2,27 @@
<header class="cohort-management-group-header"></header> <header class="cohort-management-group-header"></header>
<ul class="wrapper-tabs"> <ul class="wrapper-tabs">
<li class="tab tab-manage_students is-selected" data-tab="manage_students"><button type="button" class="toggle-button"><span class="sr"><%- gettext('Selected tab') %> </span><%- gettext("Manage Students") %></button></li> <li class="tab tab-manage_students is-selected" data-tab="manage_students"><button type="button" class="toggle-button"><span class="sr"><%- gettext('Selected tab') %> </span><%- gettext("Manage Learners") %></button></li>
<li class="tab tab-settings" data-tab="settings"><button type="button" class="toggle-button"><%- gettext("Settings") %></button></li> <li class="tab tab-settings" data-tab="settings"><button type="button" class="toggle-button"><%- gettext("Settings") %></button></li>
</ul> </ul>
<div class="cohort-management-group-add tab-content tab-content-manage_students" tabindex="-1"> <div class="cohort-management-group-add tab-content tab-content-manage_students" tabindex="-1">
<form action="" method="post" id="cohort-management-group-add-form" class="cohort-management-group-add-form"> <form action="" method="post" id="cohort-management-group-add-form" class="cohort-management-group-add-form">
<h4 class="hd hd-3 form-title"><%- gettext('Add students to this cohort') %></h4> <h4 class="hd hd-3 form-title"><%- gettext('Add learners to this cohort') %></h4>
<div class="form-introduction"> <div class="form-introduction">
<p><%- gettext('Note: Students can be in only one cohort. Adding students to this group overrides any previous group assignment.') %></p> <p><%- gettext('Note: Learners can be in only one cohort. Adding learners to this group overrides any previous group assignment.') %></p>
</div> </div>
<div class="cohort-confirmations" aria-live="polite" tabindex="-1"></div> <div class="cohort-confirmations" aria-live="polite" tabindex="-1"></div>
<div class="cohort-preassigned" aria-live="polite" tabindex="-1"></div>
<div class="cohort-errors" aria-live="polite" tabindex="-1"></div> <div class="cohort-errors" aria-live="polite" tabindex="-1"></div>
<div class="form-fields"> <div class="form-fields">
<div class="field field-textarea is-required"> <div class="field field-textarea is-required">
<label for="cohort-management-group-add-students" class="label"> <label for="cohort-management-group-add-students" class="label">
<%- gettext('Enter email addresses and/or usernames, separated by new lines or commas, for the students you want to add. *') %> <%- gettext('Enter email addresses and/or usernames, separated by new lines or commas, for the learners you want to add. *') %>
<span class="sr"><%- gettext('(Required Field)') %></span> <span class="sr"><%- gettext('(Required Field)') %></span>
</label> </label>
<textarea name="cohort-management-group-add-students" id="cohort-management-group-add-students" <textarea name="cohort-management-group-add-students" id="cohort-management-group-add-students"
...@@ -35,7 +36,7 @@ ...@@ -35,7 +36,7 @@
<div class="form-actions"> <div class="form-actions">
<button class="form-submit button action-primary action-view"> <button class="form-submit button action-primary action-view">
<span class="button-icon icon fa fa-plus" aria-hidden="true"></span> <%- gettext('Add Students') %> <span class="button-icon icon fa fa-plus" aria-hidden="true"></span> <%- gettext('Add Learners') %>
</button> </button>
</div> </div>
</form> </form>
......
...@@ -34,14 +34,14 @@ ...@@ -34,14 +34,14 @@
<hr class="divider divider-lv1" /> <hr class="divider divider-lv1" />
<!-- Uploading a CSV file of cohort assignments. --> <!-- Uploading a CSV file of cohort assignments. -->
<button class="toggle-cohort-management-secondary" data-href="#cohort-management-file-upload"><%- gettext('Assign students to cohorts by uploading a CSV file') %></button> <button class="toggle-cohort-management-secondary" data-href="#cohort-management-file-upload"><%- gettext('Assign learners to cohorts by uploading a CSV file') %></button>
<div class="cohort-management-file-upload csv-upload hidden" id="cohort-management-file-upload" tabindex="-1"></div> <div class="cohort-management-file-upload csv-upload hidden" id="cohort-management-file-upload" tabindex="-1"></div>
<div class="cohort-management-supplemental"> <div class="cohort-management-supplemental">
<p class=""> <p class="">
<span class="icon fa fa-info-circle" aria-hidden="true"></span> <span class="icon fa fa-info-circle" aria-hidden="true"></span>
<%= HtmlUtils.interpolateHtml( <%= HtmlUtils.interpolateHtml(
gettext('To review student cohort assignments or see the results of uploading a CSV file, download course profile information or cohort results on the {link_start}Data Download{link_end} page.'), gettext('To review learner cohort assignments or see the results of uploading a CSV file, download course profile information or cohort results on the {link_start}Data Download{link_end} page.'),
{ {
link_start: HtmlUtils.HTML('<button type="button" class="btn-link link-cross-reference" data-section="data_download">'), link_start: HtmlUtils.HTML('<button type="button" class="btn-link link-cross-reference" data-section="data_download">'),
link_end: HtmlUtils.HTML('</button>') link_end: HtmlUtils.HTML('</button>')
......
...@@ -6,14 +6,16 @@ forums, and to the cohort admin views. ...@@ -6,14 +6,16 @@ forums, and to the cohort admin views.
import logging import logging
import random import random
import request_cache
from courseware import courses
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.db.models.signals import m2m_changed, post_save from django.db.models.signals import m2m_changed, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.http import Http404 from django.http import Http404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import request_cache
from courseware import courses
from eventtracking import tracker from eventtracking import tracker
from request_cache.middleware import request_cached from request_cache.middleware import request_cached
from student.models import get_user_by_username_or_email from student.models import get_user_by_username_or_email
...@@ -23,7 +25,8 @@ from .models import ( ...@@ -23,7 +25,8 @@ from .models import (
CourseCohort, CourseCohort,
CourseCohortsSettings, CourseCohortsSettings,
CourseUserGroup, CourseUserGroup,
CourseUserGroupPartitionGroup CourseUserGroupPartitionGroup,
UnregisteredLearnerCohortAssignments
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -241,10 +244,22 @@ def get_cohort(user, course_key, assign=True, use_cached=False): ...@@ -241,10 +244,22 @@ def get_cohort(user, course_key, assign=True, use_cached=False):
# Otherwise assign the user a cohort. # Otherwise assign the user a cohort.
try: try:
with transaction.atomic(): with transaction.atomic():
# If learner has been pre-registered in a cohort, get that cohort. Otherwise assign to a random cohort.
course_user_group = None
for assignment in UnregisteredLearnerCohortAssignments.objects.filter(email=user.email, course_id=course_key):
course_user_group = assignment.course_user_group
unregistered_learner = assignment
if course_user_group:
unregistered_learner.delete()
else:
course_user_group = get_random_cohort(course_key)
membership = CohortMembership.objects.create( membership = CohortMembership.objects.create(
user=user, user=user,
course_user_group=get_random_cohort(course_key) course_user_group=course_user_group,
) )
return cache.setdefault(cache_key, membership.course_user_group) return cache.setdefault(cache_key, membership.course_user_group)
except IntegrityError as integrity_error: except IntegrityError as integrity_error:
# An IntegrityError is raised when multiple workers attempt to # An IntegrityError is raised when multiple workers attempt to
...@@ -423,28 +438,65 @@ def add_user_to_cohort(cohort, username_or_email): ...@@ -423,28 +438,65 @@ def add_user_to_cohort(cohort, username_or_email):
username_or_email: string. Treated as email if has '@' username_or_email: string. Treated as email if has '@'
Returns: Returns:
Tuple of User object and string (or None) indicating previous cohort User object (or None if the email address is preassigned),
string (or None) indicating previous cohort,
and whether the user is a preassigned user or not
Raises: Raises:
User.DoesNotExist if can't find user. User.DoesNotExist if can't find user. However, if a valid email is provided for the user, it is stored
in a database so that the user can be added to the cohort if they eventually enroll in the course.
ValueError if user already present in this cohort. ValueError if user already present in this cohort.
ValidationError if an invalid email address is entered.
User.DoesNotExist if a user could not be found.
""" """
user = get_user_by_username_or_email(username_or_email) try:
user = get_user_by_username_or_email(username_or_email)
membership = CohortMembership(course_user_group=cohort, user=user) membership = CohortMembership(course_user_group=cohort, user=user)
membership.save() # This will handle both cases, creation and updating, of a CohortMembership for this user. membership.save() # This will handle both cases, creation and updating, of a CohortMembership for this user.
tracker.emit( tracker.emit(
"edx.cohort.user_add_requested", "edx.cohort.user_add_requested",
{ {
"user_id": user.id, "user_id": user.id,
"cohort_id": cohort.id, "cohort_id": cohort.id,
"cohort_name": cohort.name, "cohort_name": cohort.name,
"previous_cohort_id": membership.previous_cohort_id, "previous_cohort_id": membership.previous_cohort_id,
"previous_cohort_name": membership.previous_cohort_name, "previous_cohort_name": membership.previous_cohort_name,
} }
) )
return (user, membership.previous_cohort_name) return (user, membership.previous_cohort_name, False)
except User.DoesNotExist as ex:
# If username_or_email is an email address, store in database.
try:
validate_email(username_or_email)
try:
assignment = UnregisteredLearnerCohortAssignments.objects.get(
email=username_or_email, course_id=cohort.course_id
)
assignment.course_user_group = cohort
assignment.save()
except UnregisteredLearnerCohortAssignments.DoesNotExist:
assignment = UnregisteredLearnerCohortAssignments.objects.create(
course_user_group=cohort, email=username_or_email, course_id=cohort.course_id
)
tracker.emit(
"edx.cohort.email_address_preassigned",
{
"user_email": assignment.email,
"cohort_id": cohort.id,
"cohort_name": cohort.name,
}
)
return (None, None, True)
except ValidationError as invalid:
if "@" in username_or_email:
raise invalid
else:
raise ex
def get_group_info_for_cohort(cohort, use_cached=False): def get_group_info_for_cohort(cohort, use_cached=False):
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import openedx.core.djangoapps.xmodule_django.models
class Migration(migrations.Migration):
dependencies = [
('course_groups', '0002_change_inline_default_cohort_value'),
]
operations = [
migrations.CreateModel(
name='UnregisteredLearnerCohortAssignments',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('email', models.CharField(db_index=True, max_length=255, blank=True)),
('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255)),
('course_user_group', models.ForeignKey(to='course_groups.CourseUserGroup')),
],
),
migrations.AlterUniqueTogether(
name='unregisteredlearnercohortassignments',
unique_together=set([('course_id', 'email')]),
),
]
...@@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError ...@@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError
from django.db import models, transaction from django.db import models, transaction
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
from util.db import outer_atomic from util.db import outer_atomic
...@@ -236,3 +235,13 @@ class CourseCohort(models.Model): ...@@ -236,3 +235,13 @@ class CourseCohort(models.Model):
) )
return course_cohort return course_cohort
class UnregisteredLearnerCohortAssignments(models.Model):
class Meta(object):
unique_together = (('course_id', 'email'), )
course_user_group = models.ForeignKey(CourseUserGroup)
email = models.CharField(blank=True, max_length=255, db_index=True)
course_id = CourseKeyField(max_length=255)
...@@ -258,6 +258,64 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -258,6 +258,64 @@ class TestCohorts(ModuleStoreTestCase):
"other_user should be assigned to the default cohort" "other_user should be assigned to the default cohort"
) )
def test_get_cohort_preassigned_user(self):
"""
When an email address is added to a cohort and a user signs up for the course with that email address,
the user should automatically be added to that cohort and not a random cohort.
"""
course = modulestore().get_course(self.toy_course_key)
cohort = CohortFactory(course_id=course.id, name="TestCohort", users=[])
cohort2 = CohortFactory(course_id=course.id, name="RandomCohort", users=[])
config_course_cohorts(course, is_cohorted=True)
# Add email address to the cohort
(user, previous_cohort, prereg) = cohorts.add_user_to_cohort(cohort, "email@example.com")
self.assertEquals(
(user, previous_cohort, prereg),
(None, None, True)
)
# Create user with this email address
user = UserFactory(username="test", email="email@example.com")
self.assertEquals(
cohorts.get_cohort(user, course.id).id,
cohort.id,
"User should be assigned to the right cohort"
)
def test_get_cohort_multiple_preassignments(self):
"""
When an email address is added to multiple cohorts, the last cohort assignment should be respected.
Then, when a user signs up for the course with that email address,
the user should automatically be added to that cohort and not a random cohort.
"""
course = modulestore().get_course(self.toy_course_key)
cohort = CohortFactory(course_id=course.id, name="TestCohort", users=[])
cohort2 = CohortFactory(course_id=course.id, name="RandomCohort", users=[])
config_course_cohorts(course, is_cohorted=True)
# Add email address to the first cohort
(user, previous_cohort, prereg) = cohorts.add_user_to_cohort(cohort, "email@example.com")
self.assertEquals(
(user, previous_cohort, prereg),
(None, None, True)
)
# Add email address to the second cohort
(user, previous_cohort, prereg) = cohorts.add_user_to_cohort(cohort2, "email@example.com")
self.assertEquals(
(user, previous_cohort, prereg),
(None, None, True)
)
# Create user with this email address
user = UserFactory(username="test", email="email@example.com")
self.assertEquals(
cohorts.get_cohort(user, course.id).id,
cohort2.id,
"User should be assigned to the right cohort"
)
@ddt.data( @ddt.data(
(True, 2), (True, 2),
(False, 6), (False, 6),
...@@ -549,7 +607,7 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -549,7 +607,7 @@ class TestCohorts(ModuleStoreTestCase):
# We shouldn't get back a previous cohort, since the user wasn't in one # We shouldn't get back a previous cohort, since the user wasn't in one
self.assertEqual( self.assertEqual(
cohorts.add_user_to_cohort(first_cohort, "Username"), cohorts.add_user_to_cohort(first_cohort, "Username"),
(course_user, None) (course_user, None, False)
) )
mock_tracker.emit.assert_any_call( mock_tracker.emit.assert_any_call(
"edx.cohort.user_add_requested", "edx.cohort.user_add_requested",
...@@ -565,7 +623,7 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -565,7 +623,7 @@ class TestCohorts(ModuleStoreTestCase):
# another # another
self.assertEqual( self.assertEqual(
cohorts.add_user_to_cohort(second_cohort, "Username"), cohorts.add_user_to_cohort(second_cohort, "Username"),
(course_user, "FirstCohort") (course_user, "FirstCohort", False)
) )
mock_tracker.emit.assert_any_call( mock_tracker.emit.assert_any_call(
"edx.cohort.user_add_requested", "edx.cohort.user_add_requested",
...@@ -577,6 +635,21 @@ class TestCohorts(ModuleStoreTestCase): ...@@ -577,6 +635,21 @@ class TestCohorts(ModuleStoreTestCase):
"previous_cohort_name": first_cohort.name, "previous_cohort_name": first_cohort.name,
} }
) )
# Should preregister email address for a cohort if an email address
# not associated with a user is added
(user, previous_cohort, prereg) = cohorts.add_user_to_cohort(first_cohort, "new_email@example.com")
self.assertEqual(
(user, previous_cohort, prereg),
(None, None, True)
)
mock_tracker.emit.assert_any_call(
"edx.cohort.email_address_preassigned",
{
"user_email": "new_email@example.com",
"cohort_id": first_cohort.id,
"cohort_name": first_cohort.name,
}
)
# Error cases # Error cases
# Should get ValueError if user already in cohort # Should get ValueError if user already in cohort
self.assertRaises( self.assertRaises(
......
...@@ -747,7 +747,7 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -747,7 +747,7 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
return json.loads(response.content) return json.loads(response.content)
def verify_added_users_to_cohort(self, response_dict, cohort, course, expected_added, expected_changed, def verify_added_users_to_cohort(self, response_dict, cohort, course, expected_added, expected_changed,
expected_present, expected_unknown): expected_present, expected_unknown, expected_preassigned, expected_invalid):
""" """
Check that add_users_to_cohort returned the expected response and has Check that add_users_to_cohort returned the expected response and has
the expected side effects. the expected side effects.
...@@ -757,6 +757,8 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -757,6 +757,8 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
`expected_present` is a list of (user, email/username) tuples where `expected_present` is a list of (user, email/username) tuples where
email/username corresponds to the input email/username corresponds to the input
`expected_unknown` is a list of strings corresponding to the input `expected_unknown` is a list of strings corresponding to the input
'expected_preassigned' is a list of email addresses
'expected_invalid' is a list of email addresses
""" """
self.assertTrue(response_dict.get("success")) self.assertTrue(response_dict.get("success"))
self.assertEqual( self.assertEqual(
...@@ -782,6 +784,8 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -782,6 +784,8 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
[username_or_email for (_, username_or_email) in expected_present] [username_or_email for (_, username_or_email) in expected_present]
) )
self.assertEqual(response_dict.get("unknown"), expected_unknown) self.assertEqual(response_dict.get("unknown"), expected_unknown)
self.assertEqual(response_dict.get("invalid"), expected_invalid)
self.assertEqual(response_dict.get("preassigned"), expected_preassigned)
for user in expected_added + [user for (user, _) in expected_changed + expected_present]: for user in expected_added + [user for (user, _) in expected_changed + expected_present]:
self.assertEqual( self.assertEqual(
CourseUserGroup.objects.get( CourseUserGroup.objects.get(
...@@ -815,7 +819,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -815,7 +819,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
expected_added=[], expected_added=[],
expected_changed=[], expected_changed=[],
expected_present=[], expected_present=[],
expected_unknown=[] expected_preassigned=[],
expected_unknown=[],
expected_invalid=[]
) )
def test_only_added(self): def test_only_added(self):
...@@ -834,7 +840,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -834,7 +840,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
expected_added=self.cohortless_users, expected_added=self.cohortless_users,
expected_changed=[], expected_changed=[],
expected_present=[], expected_present=[],
expected_unknown=[] expected_preassigned=[],
expected_unknown=[],
expected_invalid=[]
) )
def test_only_changed(self): def test_only_changed(self):
...@@ -856,7 +864,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -856,7 +864,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
[(user, self.cohort3.name) for user in self.cohort3_users] [(user, self.cohort3.name) for user in self.cohort3_users]
), ),
expected_present=[], expected_present=[],
expected_unknown=[] expected_preassigned=[],
expected_unknown=[],
expected_invalid=[]
) )
def test_only_present(self): def test_only_present(self):
...@@ -876,7 +886,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -876,7 +886,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
expected_added=[], expected_added=[],
expected_changed=[], expected_changed=[],
expected_present=[(user, user.username) for user in self.cohort1_users], expected_present=[(user, user.username) for user in self.cohort1_users],
expected_unknown=[] expected_preassigned=[],
expected_unknown=[],
expected_invalid=[]
) )
def test_only_unknown(self): def test_only_unknown(self):
...@@ -896,7 +908,54 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -896,7 +908,54 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
expected_added=[], expected_added=[],
expected_changed=[], expected_changed=[],
expected_present=[], expected_present=[],
expected_unknown=usernames expected_preassigned=[],
expected_unknown=usernames,
expected_invalid=[]
)
def test_preassigned_users(self):
"""
Verify that email addresses can be preassigned for a cohort if the user associated with that email
address does not yet exist.
"""
email_addresses = ["email@example.com", "email2@example.com", "email3@example.com"]
response_dict = self.request_add_users_to_cohort(
",".join(email_addresses),
self.cohort1,
self.course
)
self.verify_added_users_to_cohort(
response_dict,
self.cohort1,
self.course,
expected_added=[],
expected_changed=[],
expected_present=[],
expected_preassigned=email_addresses,
expected_unknown=[],
expected_invalid=[]
)
def test_invalid_email_addresses(self):
"""
Verify that invalid email addresses return an error.
"""
email_addresses = ["email@", "@email", "invalid@email."]
response_dict = self.request_add_users_to_cohort(
",".join(email_addresses),
self.cohort1,
self.course
)
self.verify_added_users_to_cohort(
response_dict,
self.cohort1,
self.course,
expected_added=[],
expected_changed=[],
expected_present=[],
expected_preassigned=[],
expected_unknown=[],
expected_invalid=email_addresses
) )
def check_user_count(self, expected_count, cohort): def check_user_count(self, expected_count, cohort):
...@@ -915,10 +974,12 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -915,10 +974,12 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
Test all adding conditions together. Test all adding conditions together.
""" """
unknowns = ["unknown_user{}".format(i) for i in range(3)] unknowns = ["unknown_user{}".format(i) for i in range(3)]
valid_emails = ["email@example.com", "email2@example.com", "email3@example.com"]
new_users = self.cohortless_users + self.cohort1_users + self.cohort2_users + self.cohort3_users new_users = self.cohortless_users + self.cohort1_users + self.cohort2_users + self.cohort3_users
response_dict = self.request_add_users_to_cohort( response_dict = self.request_add_users_to_cohort(
",".join( ",".join(
unknowns + unknowns +
valid_emails +
[ [
user.username user.username
for user in new_users for user in new_users
...@@ -940,20 +1001,26 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -940,20 +1001,26 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
[(user, self.cohort3.name) for user in self.cohort3_users] [(user, self.cohort3.name) for user in self.cohort3_users]
), ),
expected_present=[(user, user.username) for user in self.cohort1_users], expected_present=[(user, user.username) for user in self.cohort1_users],
expected_unknown=unknowns expected_preassigned=valid_emails,
expected_unknown=unknowns,
expected_invalid=[]
) )
def test_emails(self): def test_emails(self):
""" """
Verify that we can use emails to identify users. Verify that we can use emails to identify users.
Expect unknown email address not associated with an account to be preassigned.
Expect unknown user (neither an email address nor a username) to not be added.
""" """
unknown = "unknown_user@example.com" valid_email_no_account = "unknown_user@example.com"
unknown_user = "unknown"
response_dict = self.request_add_users_to_cohort( response_dict = self.request_add_users_to_cohort(
",".join([ ",".join([
self.cohort1_users[0].email, self.cohort1_users[0].email,
self.cohort2_users[0].email, self.cohort2_users[0].email,
self.cohortless_users[0].email, self.cohortless_users[0].email,
unknown valid_email_no_account,
unknown_user
]), ]),
self.cohort1, self.cohort1,
self.course self.course
...@@ -965,7 +1032,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -965,7 +1032,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
expected_added=[self.cohortless_users[0]], expected_added=[self.cohortless_users[0]],
expected_changed=[(self.cohort2_users[0], self.cohort2.name)], expected_changed=[(self.cohort2_users[0], self.cohort2.name)],
expected_present=[(self.cohort1_users[0], self.cohort1_users[0].email)], expected_present=[(self.cohort1_users[0], self.cohort1_users[0].email)],
expected_unknown=[unknown] expected_preassigned=[valid_email_no_account],
expected_unknown=[unknown_user],
expected_invalid=[]
) )
def test_delimiters(self): def test_delimiters(self):
...@@ -991,7 +1060,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -991,7 +1060,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
expected_added=[self.cohortless_users[0]], expected_added=[self.cohortless_users[0]],
expected_changed=[(self.cohort2_users[0], self.cohort2.name)], expected_changed=[(self.cohort2_users[0], self.cohort2.name)],
expected_present=[(self.cohort1_users[0], self.cohort1_users[0].username)], expected_present=[(self.cohort1_users[0], self.cohort1_users[0].username)],
expected_unknown=[unknown] expected_preassigned=[],
expected_unknown=[unknown],
expected_invalid=[]
) )
def test_can_cohort_unenrolled_users(self): def test_can_cohort_unenrolled_users(self):
...@@ -1018,7 +1089,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase): ...@@ -1018,7 +1089,9 @@ class AddUsersToCohortTestCase(CohortViewsTestCase):
expected_added=self.unenrolled_users, expected_added=self.unenrolled_users,
expected_changed=[], expected_changed=[],
expected_present=[], expected_present=[],
expected_unknown=[] expected_preassigned=[],
expected_unknown=[],
expected_invalid=[]
) )
def test_non_existent_cohort(self): def test_non_existent_cohort(self):
......
...@@ -5,9 +5,9 @@ Views related to course groups functionality. ...@@ -5,9 +5,9 @@ Views related to course groups functionality.
import logging import logging
import re import re
from courseware.courses import get_course_with_access
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.paginator import EmptyPage, Paginator from django.core.paginator import EmptyPage, Paginator
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
...@@ -15,9 +15,13 @@ from django.http import Http404, HttpResponseBadRequest ...@@ -15,9 +15,13 @@ from django.http import Http404, HttpResponseBadRequest
from django.utils.translation import ugettext from django.utils.translation import ugettext
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods, require_POST from django.views.decorators.http import require_http_methods, require_POST
from edxmako.shortcuts import render_to_response
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.courses import get_course_with_access
from edxmako.shortcuts import render_to_response
from lms.djangoapps.django_comment_client.constants import TYPE_ENTRY
from lms.djangoapps.django_comment_client.utils import get_discussion_categories_ids, get_discussion_category_map
from util.json_request import JsonResponse, expect_json from util.json_request import JsonResponse, expect_json
from . import cohorts from . import cohorts
...@@ -267,7 +271,9 @@ def add_users_to_cohort(request, course_key_string, cohort_id): ...@@ -267,7 +271,9 @@ def add_users_to_cohort(request, course_key_string, cohort_id):
'email': ..., 'email': ...,
'previous_cohort': ...}, ...], 'previous_cohort': ...}, ...],
'present': [str1, str2, ...], # already there 'present': [str1, str2, ...], # already there
'unknown': [str1, str2, ...]} 'unknown': [str1, str2, ...],
'preassigned': [str1, str2, ...],
'invalid': [str1, str2, ...]}
Raises Http404 if the cohort cannot be found for the given course. Raises Http404 if the cohort cannot be found for the given course.
""" """
...@@ -288,31 +294,41 @@ def add_users_to_cohort(request, course_key_string, cohort_id): ...@@ -288,31 +294,41 @@ def add_users_to_cohort(request, course_key_string, cohort_id):
changed = [] changed = []
present = [] present = []
unknown = [] unknown = []
preassigned = []
invalid = []
for username_or_email in split_by_comma_and_whitespace(users): for username_or_email in split_by_comma_and_whitespace(users):
if not username_or_email: if not username_or_email:
continue continue
try: try:
(user, previous_cohort) = cohorts.add_user_to_cohort(cohort, username_or_email) # A user object is only returned by add_user_to_cohort if the user already exists.
info = { (user, previous_cohort, preassignedCohort) = cohorts.add_user_to_cohort(cohort, username_or_email)
'username': user.username,
'email': user.email, if preassignedCohort:
} preassigned.append(username_or_email)
if previous_cohort: elif previous_cohort:
info['previous_cohort'] = previous_cohort info = {'email': user.email,
'previous_cohort': previous_cohort,
'username': user.username}
changed.append(info) changed.append(info)
else: else:
info = {'username': user.username,
'email': user.email}
added.append(info) added.append(info)
except ValueError:
present.append(username_or_email)
except User.DoesNotExist: except User.DoesNotExist:
unknown.append(username_or_email) unknown.append(username_or_email)
except ValidationError:
invalid.append(username_or_email)
except ValueError:
present.append(username_or_email)
return json_http_response({'success': True, return json_http_response({'success': True,
'added': added, 'added': added,
'changed': changed, 'changed': changed,
'present': present, 'present': present,
'unknown': unknown}) 'unknown': unknown,
'preassigned': preassigned,
'invalid': invalid})
@ensure_csrf_cookie @ensure_csrf_cookie
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment