Commit 1622216b by Sarina Canelake

Merge pull request #3084 from edx/sarina/improve-bulk-beta-add

Improve the batch beta tester add feature
parents 7e3d661f bddae213
...@@ -121,7 +121,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -121,7 +121,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
# Endpoints that only Staff or Instructors can access # Endpoints that only Staff or Instructors can access
self.staff_level_endpoints = [ self.staff_level_endpoints = [
('students_update_enrollment', {'emails': 'foo@example.org', 'action': 'enroll'}), ('students_update_enrollment', {'identifiers': 'foo@example.org', 'action': 'enroll'}),
('get_grading_config', {}), ('get_grading_config', {}),
('get_students_features', {}), ('get_students_features', {}),
('get_distribution', {}), ('get_distribution', {}),
...@@ -138,7 +138,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -138,7 +138,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
] ]
# Endpoints that only Instructors can access # Endpoints that only Instructors can access
self.instructor_level_endpoints = [ self.instructor_level_endpoints = [
('bulk_beta_modify_access', {'emails': 'foo@example.org', 'action': 'add'}), ('bulk_beta_modify_access', {'identifiers': 'foo@example.org', 'action': 'add'}),
('modify_access', {'unique_student_identifier': self.user.email, 'rolename': 'beta', 'action': 'allow'}), ('modify_access', {'unique_student_identifier': self.user.email, 'rolename': 'beta', 'action': 'allow'}),
('list_course_role_members', {'rolename': 'beta'}), ('list_course_role_members', {'rolename': 'beta'}),
('rescore_problem', {'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}), ('rescore_problem', {'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}),
...@@ -291,13 +291,52 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -291,13 +291,52 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
""" Test with an invalid action. """ """ Test with an invalid action. """
action = 'robot-not-an-action' action = 'robot-not-an-action'
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.enrolled_student.email, 'action': action}) response = self.client.get(url, {'identifiers': self.enrolled_student.email, 'action': action})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_invalid_email(self):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'identifiers': 'percivaloctavius@', 'action': 'enroll', 'email_students': False})
self.assertEqual(response.status_code, 200)
# test the response data
expected = {
"action": "enroll",
'auto_enroll': False,
"results": [
{
"identifier": 'percivaloctavius@',
"invalidIdentifier": True,
}
]
}
res_json = json.loads(response.content)
self.assertEqual(res_json, expected)
def test_invalid_username(self):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'identifiers': 'percivaloctavius', 'action': 'enroll', 'email_students': False})
self.assertEqual(response.status_code, 200)
# test the response data
expected = {
"action": "enroll",
'auto_enroll': False,
"results": [
{
"identifier": 'percivaloctavius',
"invalidIdentifier": True,
}
]
}
res_json = json.loads(response.content)
self.assertEqual(res_json, expected)
def test_enroll_with_username(self): def test_enroll_with_username(self):
# Test with an invalid email address (eg, a username).
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False}) response = self.client.get(url, {'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# test the response data # test the response data
...@@ -306,9 +345,19 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -306,9 +345,19 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
'auto_enroll': False, 'auto_enroll': False,
"results": [ "results": [
{ {
"email": self.notenrolled_student.username, "identifier": self.notenrolled_student.username,
"error": True, "before": {
"invalidEmail": True "enrollment": False,
"auto_enroll": False,
"user": True,
"allowed": False,
},
"after": {
"enrollment": True,
"auto_enroll": False,
"user": True,
"allowed": False,
}
} }
] ]
} }
...@@ -318,7 +367,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -318,7 +367,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_enroll_without_email(self): def test_enroll_without_email(self):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.notenrolled_student.email, 'action': 'enroll', 'email_students': False}) response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'enroll', 'email_students': False})
print "type(self.notenrolled_student.email): {}".format(type(self.notenrolled_student.email)) print "type(self.notenrolled_student.email): {}".format(type(self.notenrolled_student.email))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -332,7 +381,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -332,7 +381,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
"auto_enroll": False, "auto_enroll": False,
"results": [ "results": [
{ {
"email": self.notenrolled_student.email, "identifier": self.notenrolled_student.email,
"before": { "before": {
"enrollment": False, "enrollment": False,
"auto_enroll": False, "auto_enroll": False,
...@@ -357,7 +406,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -357,7 +406,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_enroll_with_email(self): def test_enroll_with_email(self):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.notenrolled_student.email, 'action': 'enroll', 'email_students': True}) response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'enroll', 'email_students': True})
print "type(self.notenrolled_student.email): {}".format(type(self.notenrolled_student.email)) print "type(self.notenrolled_student.email): {}".format(type(self.notenrolled_student.email))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -371,7 +420,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -371,7 +420,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
"auto_enroll": False, "auto_enroll": False,
"results": [ "results": [
{ {
"email": self.notenrolled_student.email, "identifier": self.notenrolled_student.email,
"before": { "before": {
"enrollment": False, "enrollment": False,
"auto_enroll": False, "auto_enroll": False,
...@@ -409,7 +458,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -409,7 +458,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_enroll_with_email_not_registered(self): def test_enroll_with_email_not_registered(self):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.notregistered_email, 'action': 'enroll', 'email_students': True}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Check the outbox # Check the outbox
...@@ -432,7 +481,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -432,7 +481,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
# Try with marketing site enabled # Try with marketing site enabled
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
response = self.client.get(url, {'emails': self.notregistered_email, 'action': 'enroll', 'email_students': True}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual( self.assertEqual(
...@@ -446,7 +495,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -446,7 +495,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_enroll_with_email_not_registered_autoenroll(self): def test_enroll_with_email_not_registered_autoenroll(self):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.notregistered_email, 'action': 'enroll', 'email_students': True, 'auto_enroll': True}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True, 'auto_enroll': True})
print "type(self.notregistered_email): {}".format(type(self.notregistered_email)) print "type(self.notregistered_email): {}".format(type(self.notregistered_email))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -467,7 +516,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -467,7 +516,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_unenroll_without_email(self): def test_unenroll_without_email(self):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.enrolled_student.email, 'action': 'unenroll', 'email_students': False}) response = self.client.get(url, {'identifiers': self.enrolled_student.email, 'action': 'unenroll', 'email_students': False})
print "type(self.enrolled_student.email): {}".format(type(self.enrolled_student.email)) print "type(self.enrolled_student.email): {}".format(type(self.enrolled_student.email))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -481,7 +530,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -481,7 +530,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
"auto_enroll": False, "auto_enroll": False,
"results": [ "results": [
{ {
"email": self.enrolled_student.email, "identifier": self.enrolled_student.email,
"before": { "before": {
"enrollment": True, "enrollment": True,
"auto_enroll": False, "auto_enroll": False,
...@@ -506,7 +555,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -506,7 +555,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_unenroll_with_email(self): def test_unenroll_with_email(self):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.enrolled_student.email, 'action': 'unenroll', 'email_students': True}) response = self.client.get(url, {'identifiers': self.enrolled_student.email, 'action': 'unenroll', 'email_students': True})
print "type(self.enrolled_student.email): {}".format(type(self.enrolled_student.email)) print "type(self.enrolled_student.email): {}".format(type(self.enrolled_student.email))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -520,7 +569,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -520,7 +569,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
"auto_enroll": False, "auto_enroll": False,
"results": [ "results": [
{ {
"email": self.enrolled_student.email, "identifier": self.enrolled_student.email,
"before": { "before": {
"enrollment": True, "enrollment": True,
"auto_enroll": False, "auto_enroll": False,
...@@ -557,7 +606,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -557,7 +606,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_unenroll_with_email_allowed_student(self): def test_unenroll_with_email_allowed_student(self):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.allowed_email, 'action': 'unenroll', 'email_students': True}) response = self.client.get(url, {'identifiers': self.allowed_email, 'action': 'unenroll', 'email_students': True})
print "type(self.allowed_email): {}".format(type(self.allowed_email)) print "type(self.allowed_email): {}".format(type(self.allowed_email))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -567,7 +616,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -567,7 +616,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
"auto_enroll": False, "auto_enroll": False,
"results": [ "results": [
{ {
"email": self.allowed_email, "identifier": self.allowed_email,
"before": { "before": {
"enrollment": False, "enrollment": False,
"auto_enroll": False, "auto_enroll": False,
...@@ -605,7 +654,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -605,7 +654,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
mock_uses_shib.return_value = True mock_uses_shib.return_value = True
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.notregistered_email, 'action': 'enroll', 'email_students': True}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Check the outbox # Check the outbox
...@@ -629,7 +678,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -629,7 +678,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
# Try with marketing site enabled # Try with marketing site enabled
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
response = self.client.get(url, {'emails': self.notregistered_email, 'action': 'enroll', 'email_students': True}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual( self.assertEqual(
...@@ -644,7 +693,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -644,7 +693,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
mock_uses_shib.return_value = True mock_uses_shib.return_value = True
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id}) url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.notregistered_email, 'action': 'enroll', 'email_students': True, 'auto_enroll': True}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'enroll', 'email_students': True, 'auto_enroll': True})
print "type(self.notregistered_email): {}".format(type(self.notregistered_email)) print "type(self.notregistered_email): {}".format(type(self.notregistered_email))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -699,21 +748,28 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe ...@@ -699,21 +748,28 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
""" Test with an invalid action. """ """ Test with an invalid action. """
action = 'robot-not-an-action' action = 'robot-not-an-action'
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.beta_tester.email, 'action': action}) response = self.client.get(url, {'identifiers': self.beta_tester.email, 'action': action})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_add_notenrolled(self): def add_notenrolled(self, response, identifier):
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) """
response = self.client.get(url, {'emails': self.notenrolled_student.email, 'action': 'add', 'email_students': False}) Test Helper Method (not a test, called by other tests)
self.assertEqual(response.status_code, 200)
Takes a client response from a call to bulk_beta_modify_access with 'email_students': False,
and the student identifier (email or username) given as 'identifiers' in the request.
Asserts the reponse returns cleanly, that the student was added as a beta tester, and the
response properly contains their identifier, 'error': False, and 'userDoesNotExist': False.
Additionally asserts no email was sent.
"""
self.assertEqual(response.status_code, 200)
self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(self.notenrolled_student)) self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(self.notenrolled_student))
# test the response data # test the response data
expected = { expected = {
"action": "add", "action": "add",
"results": [ "results": [
{ {
"email": self.notenrolled_student.email, "identifier": identifier,
"error": False, "error": False,
"userDoesNotExist": False "userDoesNotExist": False
} }
...@@ -726,9 +782,33 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe ...@@ -726,9 +782,33 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
# Check the outbox # Check the outbox
self.assertEqual(len(mail.outbox), 0) self.assertEqual(len(mail.outbox), 0)
def test_add_notenrolled_email(self):
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': False})
self.add_notenrolled(response, self.notenrolled_student.email)
self.assertFalse(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id))
def test_add_notenrolled_email_autoenroll(self):
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': False, 'auto_enroll': True})
self.add_notenrolled(response, self.notenrolled_student.email)
self.assertTrue(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id))
def test_add_notenrolled_username(self):
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'identifiers': self.notenrolled_student.username, 'action': 'add', 'email_students': False})
self.add_notenrolled(response, self.notenrolled_student.username)
self.assertFalse(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id))
def test_add_notenrolled_username_autoenroll(self):
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'identifiers': self.notenrolled_student.username, 'action': 'add', 'email_students': False, 'auto_enroll': True})
self.add_notenrolled(response, self.notenrolled_student.username)
self.assertTrue(CourseEnrollment.is_enrolled(self.notenrolled_student, self.course.id))
def test_add_notenrolled_with_email(self): def test_add_notenrolled_with_email(self):
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.notenrolled_student.email, 'action': 'add', 'email_students': True}) response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': True})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(self.notenrolled_student)) self.assertTrue(CourseBetaTesterRole(self.course.location).has_user(self.notenrolled_student))
...@@ -737,7 +817,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe ...@@ -737,7 +817,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
"action": "add", "action": "add",
"results": [ "results": [
{ {
"email": self.notenrolled_student.email, "identifier": self.notenrolled_student.email,
"error": False, "error": False,
"userDoesNotExist": False "userDoesNotExist": False
} }
...@@ -769,7 +849,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe ...@@ -769,7 +849,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
# Try with marketing site enabled # Try with marketing site enabled
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): with patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
response = self.client.get(url, {'emails': self.notenrolled_student.email, 'action': 'add', 'email_students': True}) response = self.client.get(url, {'identifiers': self.notenrolled_student.email, 'action': 'add', 'email_students': True})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual( self.assertEqual(
...@@ -786,14 +866,14 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe ...@@ -786,14 +866,14 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
def test_enroll_with_email_not_registered(self): def test_enroll_with_email_not_registered(self):
# User doesn't exist # User doesn't exist
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.notregistered_email, 'action': 'add', 'email_students': True}) response = self.client.get(url, {'identifiers': self.notregistered_email, 'action': 'add', 'email_students': True})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# test the response data # test the response data
expected = { expected = {
"action": "add", "action": "add",
"results": [ "results": [
{ {
"email": self.notregistered_email, "identifier": self.notregistered_email,
"error": True, "error": True,
"userDoesNotExist": True "userDoesNotExist": True
} }
...@@ -807,7 +887,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe ...@@ -807,7 +887,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
def test_remove_without_email(self): def test_remove_without_email(self):
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.beta_tester.email, 'action': 'remove', 'email_students': False}) response = self.client.get(url, {'identifiers': self.beta_tester.email, 'action': 'remove', 'email_students': False})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertFalse(CourseBetaTesterRole(self.course.location).has_user(self.beta_tester)) self.assertFalse(CourseBetaTesterRole(self.course.location).has_user(self.beta_tester))
...@@ -817,7 +897,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe ...@@ -817,7 +897,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
"action": "remove", "action": "remove",
"results": [ "results": [
{ {
"email": self.beta_tester.email, "identifier": self.beta_tester.email,
"error": False, "error": False,
"userDoesNotExist": False "userDoesNotExist": False
} }
...@@ -831,7 +911,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe ...@@ -831,7 +911,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
def test_remove_with_email(self): def test_remove_with_email(self):
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id}) url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
response = self.client.get(url, {'emails': self.beta_tester.email, 'action': 'remove', 'email_students': True}) response = self.client.get(url, {'identifiers': self.beta_tester.email, 'action': 'remove', 'email_students': True})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertFalse(CourseBetaTesterRole(self.course.location).has_user(self.beta_tester)) self.assertFalse(CourseBetaTesterRole(self.course.location).has_user(self.beta_tester))
...@@ -841,7 +921,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe ...@@ -841,7 +921,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
"action": "remove", "action": "remove",
"results": [ "results": [
{ {
"email": self.beta_tester.email, "identifier": self.beta_tester.email,
"error": False, "error": False,
"userDoesNotExist": False "userDoesNotExist": False
} }
......
...@@ -33,7 +33,7 @@ from django_comment_common.models import ( ...@@ -33,7 +33,7 @@ from django_comment_common.models import (
) )
from courseware.models import StudentModule from courseware.models import StudentModule
from student.models import unique_id_for_user from student.models import unique_id_for_user, CourseEnrollment
import instructor_task.api import instructor_task.api
from instructor_task.api_helper import AlreadyRunningError from instructor_task.api_helper import AlreadyRunningError
from instructor_task.views import get_task_completion_info from instructor_task.views import get_task_completion_info
...@@ -204,7 +204,7 @@ def require_level(level): ...@@ -204,7 +204,7 @@ def require_level(level):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff') @require_level('staff')
@require_query_params(action="enroll or unenroll", emails="stringified list of emails") @require_query_params(action="enroll or unenroll", identifiers="stringified list of emails and/or usernames")
def students_update_enrollment(request, course_id): def students_update_enrollment(request, course_id):
""" """
Enroll or unenroll students by email. Enroll or unenroll students by email.
...@@ -212,7 +212,7 @@ def students_update_enrollment(request, course_id): ...@@ -212,7 +212,7 @@ def students_update_enrollment(request, course_id):
Query Parameters: Query Parameters:
- action in ['enroll', 'unenroll'] - action in ['enroll', 'unenroll']
- emails is string containing a list of emails separated by anything split_input_list can handle. - identifiers is string containing a list of emails and/or usernames separated by anything split_input_list can handle.
- auto_enroll is a boolean (defaults to false) - auto_enroll is a boolean (defaults to false)
If auto_enroll is false, students will be allowed to enroll. If auto_enroll is false, students will be allowed to enroll.
If auto_enroll is true, students will be enrolled as soon as they register. If auto_enroll is true, students will be enrolled as soon as they register.
...@@ -244,8 +244,8 @@ def students_update_enrollment(request, course_id): ...@@ -244,8 +244,8 @@ def students_update_enrollment(request, course_id):
""" """
action = request.GET.get('action') action = request.GET.get('action')
emails_raw = request.GET.get('emails') identifiers_raw = request.GET.get('identifiers')
emails = _split_input_list(emails_raw) identifiers = _split_input_list(identifiers_raw)
auto_enroll = request.GET.get('auto_enroll') in ['true', 'True', True] auto_enroll = request.GET.get('auto_enroll') in ['true', 'True', True]
email_students = request.GET.get('email_students') in ['true', 'True', True] email_students = request.GET.get('email_students') in ['true', 'True', True]
...@@ -255,23 +255,23 @@ def students_update_enrollment(request, course_id): ...@@ -255,23 +255,23 @@ def students_update_enrollment(request, course_id):
email_params = get_email_params(course, auto_enroll) email_params = get_email_params(course, auto_enroll)
results = [] results = []
for email in emails: for identifier in identifiers:
# First try to get a user object from the identifer
user = None
email = None
try:
user = get_student_from_identifier(identifier)
except User.DoesNotExist:
email = identifier
else:
email = user.email
try: try:
# Use django.core.validators.validate_email to check email address # Use django.core.validators.validate_email to check email address
# validity (obviously, cannot check if email actually /exists/, # validity (obviously, cannot check if email actually /exists/,
# simply that it is plausibly valid) # simply that it is plausibly valid)
validate_email(email) validate_email(email) # Raises ValidationError if invalid
except ValidationError:
# Flag this email as an error if invalid, but continue checking
# the remaining in the list
results.append({
'email': email,
'error': True,
'invalidEmail': True,
})
continue
try:
if action == 'enroll': if action == 'enroll':
before, after = enroll_email(course_id, email, auto_enroll, email_students, email_params) before, after = enroll_email(course_id, email, auto_enroll, email_students, email_params)
elif action == 'unenroll': elif action == 'unenroll':
...@@ -281,20 +281,29 @@ def students_update_enrollment(request, course_id): ...@@ -281,20 +281,29 @@ def students_update_enrollment(request, course_id):
"Unrecognized action '{}'".format(action) "Unrecognized action '{}'".format(action)
)) ))
except ValidationError:
# Flag this email as an error if invalid, but continue checking
# the remaining in the list
results.append({ results.append({
'email': email, 'identifier': identifier,
'before': before.to_dict(), 'invalidIdentifier': True,
'after': after.to_dict(),
}) })
# catch and log any exceptions
# so that one error doesn't cause a 500.
except Exception as exc: # pylint: disable=W0703 except Exception as exc: # pylint: disable=W0703
# catch and log any exceptions
# so that one error doesn't cause a 500.
log.exception("Error while #{}ing student") log.exception("Error while #{}ing student")
log.exception(exc) log.exception(exc)
results.append({ results.append({
'email': email, 'identifier': identifier,
'error': True, 'error': True,
'invalidEmail': False, })
else:
results.append({
'identifier': identifier,
'before': before.to_dict(),
'after': after.to_dict(),
}) })
response_payload = { response_payload = {
...@@ -310,7 +319,7 @@ def students_update_enrollment(request, course_id): ...@@ -310,7 +319,7 @@ def students_update_enrollment(request, course_id):
@require_level('instructor') @require_level('instructor')
@common_exceptions_400 @common_exceptions_400
@require_query_params( @require_query_params(
emails="stringified list of emails", identifiers="stringified list of emails and/or usernames",
action="add or remove", action="add or remove",
) )
def bulk_beta_modify_access(request, course_id): def bulk_beta_modify_access(request, course_id):
...@@ -318,13 +327,15 @@ def bulk_beta_modify_access(request, course_id): ...@@ -318,13 +327,15 @@ def bulk_beta_modify_access(request, course_id):
Enroll or unenroll users in beta testing program. Enroll or unenroll users in beta testing program.
Query parameters: Query parameters:
- emails is string containing a list of emails separated by anything split_input_list can handle. - identifiers is string containing a list of emails and/or usernames separated by
anything split_input_list can handle.
- action is one of ['add', 'remove'] - action is one of ['add', 'remove']
""" """
action = request.GET.get('action') action = request.GET.get('action')
emails_raw = request.GET.get('emails') identifiers_raw = request.GET.get('identifiers')
emails = _split_input_list(emails_raw) identifiers = _split_input_list(identifiers_raw)
email_students = request.GET.get('email_students') in ['true', 'True', True] email_students = request.GET.get('email_students') in ['true', 'True', True]
auto_enroll = request.GET.get('auto_enroll') in ['true', 'True', True]
results = [] results = []
rolename = 'beta' rolename = 'beta'
course = get_course_by_id(course_id) course = get_course_by_id(course_id)
...@@ -333,11 +344,11 @@ def bulk_beta_modify_access(request, course_id): ...@@ -333,11 +344,11 @@ def bulk_beta_modify_access(request, course_id):
if email_students: if email_students:
email_params = get_email_params(course, auto_enroll=False) email_params = get_email_params(course, auto_enroll=False)
for email in emails: for identifier in identifiers:
try: try:
error = False error = False
user_does_not_exist = False user_does_not_exist = False
user = User.objects.get(email=email) user = get_student_from_identifier(identifier)
if action == 'add': if action == 'add':
allow_access(course, user, rolename) allow_access(course, user, rolename)
...@@ -360,10 +371,16 @@ def bulk_beta_modify_access(request, course_id): ...@@ -360,10 +371,16 @@ def bulk_beta_modify_access(request, course_id):
# If no exception thrown, see if we should send an email # If no exception thrown, see if we should send an email
if email_students: if email_students:
send_beta_role_email(action, user, email_params) send_beta_role_email(action, user, email_params)
# See if we should autoenroll the student
if auto_enroll:
# Check if student is already enrolled
if not CourseEnrollment.is_enrolled(user, course_id):
CourseEnrollment.enroll(user, course_id)
finally: finally:
# Tabulate the action result of this email address # Tabulate the action result of this email address
results.append({ results.append({
'email': email, 'identifier': identifier,
'error': error, 'error': error,
'userDoesNotExist': user_does_not_exist 'userDoesNotExist': user_does_not_exist
}) })
...@@ -543,8 +560,9 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06 ...@@ -543,8 +560,9 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06
student_data = analytics.basic.enrolled_students_features(course_id, query_features) student_data = analytics.basic.enrolled_students_features(course_id, query_features)
# Scrape the query features for i18n - can't translate here because it breaks further queries # Provide human-friendly and translatable names for these features. These names
# and how the coffeescript works. The actual translation will be done in data_download.coffee # will be displayed in the table generated in data_download.coffee. It is not (yet)
# used as the header row in the CSV, but could be in the future.
query_features_names = { query_features_names = {
'username': _('Username'), 'username': _('Username'),
'name': _('Name'), 'name': _('Name'),
...@@ -1086,7 +1104,7 @@ def update_forum_role_membership(request, course_id): ...@@ -1086,7 +1104,7 @@ def update_forum_role_membership(request, course_id):
target_is_instructor = has_access(user, course, 'instructor') target_is_instructor = has_access(user, course, 'instructor')
# cannot revoke instructor # cannot revoke instructor
if target_is_instructor and action == 'revoke' and rolename == FORUM_ROLE_ADMINISTRATOR: if target_is_instructor and action == 'revoke' and rolename == FORUM_ROLE_ADMINISTRATOR:
return HttpResponseBadRequest("Cannot revoke instructor forum admin privelages.") return HttpResponseBadRequest("Cannot revoke instructor forum admin privileges.")
try: try:
update_forum_role(course_id, user, rolename, action) update_forum_role(course_id, user, rolename, action)
......
...@@ -178,19 +178,22 @@ class AuthListWidget extends MemberListWidget ...@@ -178,19 +178,22 @@ class AuthListWidget extends MemberListWidget
class BetaTesterBulkAddition class BetaTesterBulkAddition
constructor: (@$container) -> constructor: (@$container) ->
# gather elements # gather elements
@$emails_input = @$container.find("textarea[name='student-emails-for-beta']") @$identifier_input = @$container.find("textarea[name='student-ids-for-beta']")
@$btn_beta_testers = @$container.find("input[name='beta-testers']") @$btn_beta_testers = @$container.find("input[name='beta-testers']")
@$checkbox_emailstudents = @$container.find("input[name='email-students']") @$checkbox_emailstudents = @$container.find("input[name='email-students']")
@$checkbox_autoenroll = @$container.find("input[name='auto-enroll']")
@$task_response = @$container.find(".request-response") @$task_response = @$container.find(".request-response")
@$request_response_error = @$container.find(".request-response-error") @$request_response_error = @$container.find(".request-response-error")
# click handlers # click handlers
@$btn_beta_testers.click => @$btn_beta_testers.click =>
emailStudents = @$checkbox_emailstudents.is(':checked') emailStudents = @$checkbox_emailstudents.is(':checked')
autoEnroll = @$checkbox_autoenroll.is(':checked')
send_data = send_data =
action: $(event.target).data('action') # 'add' or 'remove' action: $(event.target).data('action') # 'add' or 'remove'
emails: @$emails_input.val() identifiers: @$identifier_input.val()
email_students: emailStudents email_students: emailStudents
auto_enroll: autoEnroll
$.ajax $.ajax
dataType: 'json' dataType: 'json'
...@@ -199,13 +202,22 @@ class BetaTesterBulkAddition ...@@ -199,13 +202,22 @@ class BetaTesterBulkAddition
success: (data) => @display_response data success: (data) => @display_response data
error: std_ajax_err => @fail_with_error gettext "Error adding/removing users as beta testers." error: std_ajax_err => @fail_with_error gettext "Error adding/removing users as beta testers."
# clear the input text field
clear_input: ->
@$identifier_input.val ''
# default for the checkboxes should be checked
@$checkbox_emailstudents.attr('checked', true)
@$checkbox_autoenroll.attr('checked', true)
fail_with_error: (msg) -> fail_with_error: (msg) ->
console.warn msg console.warn msg
@clear_input()
@$task_response.empty() @$task_response.empty()
@$request_response_error.empty() @$request_response_error.empty()
@$request_response_error.text msg @$request_response_error.text msg
display_response: (data_from_server) -> display_response: (data_from_server) ->
@clear_input()
@$task_response.empty() @$task_response.empty()
@$request_response_error.empty() @$request_response_error.empty()
errors = [] errors = []
...@@ -219,39 +231,37 @@ class BetaTesterBulkAddition ...@@ -219,39 +231,37 @@ class BetaTesterBulkAddition
else else
successes.push student_results successes.push student_results
console.log(sr.email for sr in successes) render_list = (label, ids) =>
render_list = (label, emails) =>
task_res_section = $ '<div/>', class: 'request-res-section' task_res_section = $ '<div/>', class: 'request-res-section'
task_res_section.append $ '<h3/>', text: label task_res_section.append $ '<h3/>', text: label
email_list = $ '<ul/>' ids_list = $ '<ul/>'
task_res_section.append email_list task_res_section.append ids_list
for email in emails for identifier in ids
email_list.append $ '<li/>', text: email ids_list.append $ '<li/>', text: identifier
@$task_response.append task_res_section @$task_response.append task_res_section
if successes.length and data_from_server.action is 'add' if successes.length and data_from_server.action is 'add'
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("These users were successfully added as beta testers:"), (sr.email for sr in successes) render_list gettext("These users were successfully added as beta testers:"), (sr.identifier for sr in successes)
if successes.length and data_from_server.action is 'remove' if successes.length and data_from_server.action is 'remove'
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("These users were successfully removed as beta testers:"), (sr.email for sr in successes) render_list gettext("These users were successfully removed as beta testers:"), (sr.identifier for sr in successes)
if errors.length and data_from_server.action is 'add' if errors.length and data_from_server.action is 'add'
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("These users were not added as beta testers:"), (sr.email for sr in errors) render_list gettext("These users were not added as beta testers:"), (sr.identifier for sr in errors)
if errors.length and data_from_server.action is 'remove' if errors.length and data_from_server.action is 'remove'
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("These users were not removed as beta testers:"), (sr.email for sr in errors) render_list gettext("These users were not removed as beta testers:"), (sr.identifier for sr in errors)
if no_users.length if no_users.length
no_users.push gettext("Users must create and activate their account before they can be promoted to beta tester.") no_users.push $ gettext("Users must create and activate their account before they can be promoted to beta tester.")
`// Translators: A list of email addresses appears after this sentence` `// Translators: A list of identifiers (which are email addresses and/or usernames) appears after this sentence`
render_list gettext("Could not find users associated with the following email addresses:"), (sr.email for sr in no_users) render_list gettext("Could not find users associated with the following identifiers:"), (sr.identifier for sr in no_users)
# Wrapper for the batch enrollment subsection. # Wrapper for the batch enrollment subsection.
# This object handles buttons, success and failure reporting, # This object handles buttons, success and failure reporting,
...@@ -259,7 +269,7 @@ class BetaTesterBulkAddition ...@@ -259,7 +269,7 @@ class BetaTesterBulkAddition
class BatchEnrollment class BatchEnrollment
constructor: (@$container) -> constructor: (@$container) ->
# gather elements # gather elements
@$emails_input = @$container.find("textarea[name='student-emails']") @$identifier_input = @$container.find("textarea[name='student-ids']")
@$enrollment_button = @$container.find(".enrollment-button") @$enrollment_button = @$container.find(".enrollment-button")
@$checkbox_autoenroll = @$container.find("input[name='auto-enroll']") @$checkbox_autoenroll = @$container.find("input[name='auto-enroll']")
@$checkbox_emailstudents = @$container.find("input[name='email-students']") @$checkbox_emailstudents = @$container.find("input[name='email-students']")
...@@ -271,7 +281,7 @@ class BatchEnrollment ...@@ -271,7 +281,7 @@ class BatchEnrollment
emailStudents: @$checkbox_emailstudents.is(':checked') emailStudents: @$checkbox_emailstudents.is(':checked')
send_data = send_data =
action: $(event.target).data('action') # 'enroll' or 'unenroll' action: $(event.target).data('action') # 'enroll' or 'unenroll'
emails: @$emails_input.val() identifiers: @$identifier_input.val()
auto_enroll: @$checkbox_autoenroll.is(':checked') auto_enroll: @$checkbox_autoenroll.is(':checked')
email_students: emailStudents email_students: emailStudents
...@@ -283,21 +293,30 @@ class BatchEnrollment ...@@ -283,21 +293,30 @@ class BatchEnrollment
error: std_ajax_err => @fail_with_error gettext "Error enrolling/unenrolling users." error: std_ajax_err => @fail_with_error gettext "Error enrolling/unenrolling users."
# clear the input text field
clear_input: ->
@$identifier_input.val ''
# default for the checkboxes should be checked
@$checkbox_emailstudents.attr('checked', true)
@$checkbox_autoenroll.attr('checked', true)
fail_with_error: (msg) -> fail_with_error: (msg) ->
console.warn msg console.warn msg
@clear_input()
@$task_response.empty() @$task_response.empty()
@$request_response_error.empty() @$request_response_error.empty()
@$request_response_error.text msg @$request_response_error.text msg
display_response: (data_from_server) -> display_response: (data_from_server) ->
@clear_input()
@$task_response.empty() @$task_response.empty()
@$request_response_error.empty() @$request_response_error.empty()
# these results arrays contain student_results # these results arrays contain student_results
# only populated arrays will be rendered # only populated arrays will be rendered
# #
# invalid email addresses # invalid identifiers
invalid_email = [] invalid_identifier = []
# students for which there was an error during the action # students for which there was an error during the action
errors = [] errors = []
# students who are now enrolled in the course # students who are now enrolled in the course
...@@ -315,7 +334,7 @@ class BatchEnrollment ...@@ -315,7 +334,7 @@ class BatchEnrollment
for student_results in data_from_server.results for student_results in data_from_server.results
# for a successful action. # for a successful action.
# student_results is of the form { # student_results is of the form {
# "email": "jd405@edx.org", # "identifier": "jd405@edx.org",
# "before": { # "before": {
# "enrollment": true, # "enrollment": true,
# "auto_enroll": false, # "auto_enroll": false,
...@@ -332,13 +351,14 @@ class BatchEnrollment ...@@ -332,13 +351,14 @@ class BatchEnrollment
# #
# for an action error. # for an action error.
# student_results is of the form { # student_results is of the form {
# 'email': email, # 'identifier': identifier,
# # then one of:
# 'error': True, # 'error': True,
# 'invalidEmail': True, # if email doesn't match "[^@]+@[^@]+\.[^@]+" # 'invalidIdentifier': True # if identifier can't find a valid User object and doesn't pass validate_email
# } # }
if student_results.invalidEmail if student_results.invalidIdentifier
invalid_email.push student_results invalid_identifier.push student_results
else if student_results.error else if student_results.error
errors.push student_results errors.push student_results
...@@ -364,19 +384,19 @@ class BatchEnrollment ...@@ -364,19 +384,19 @@ class BatchEnrollment
console.warn student_results console.warn student_results
# render populated result arrays # render populated result arrays
render_list = (label, emails) => render_list = (label, ids) =>
task_res_section = $ '<div/>', class: 'request-res-section' task_res_section = $ '<div/>', class: 'request-res-section'
task_res_section.append $ '<h3/>', text: label task_res_section.append $ '<h3/>', text: label
email_list = $ '<ul/>' ids_list = $ '<ul/>'
task_res_section.append email_list task_res_section.append ids_list
for email in emails for identifier in ids
email_list.append $ '<li/>', text: email ids_list.append $ '<li/>', text: identifier
@$task_response.append task_res_section @$task_response.append task_res_section
if invalid_email.length if invalid_identifier.length
render_list gettext("The following email addresses are invalid:"), (sr.email for sr in invalid_email) render_list gettext("The following email addresses and/or usernames are invalid:"), (sr.identifier for sr in invalid_identifier)
if errors.length if errors.length
errors_label = do -> errors_label = do ->
...@@ -389,53 +409,53 @@ class BatchEnrollment ...@@ -389,53 +409,53 @@ class BatchEnrollment
"There was an error processing:" "There was an error processing:"
for student_results in errors for student_results in errors
render_list errors_label, (sr.email for sr in errors) render_list errors_label, (sr.identifier for sr in errors)
if enrolled.length and emailStudents if enrolled.length and emailStudents
render_list gettext("Successfully enrolled and sent email to the following users:"), (sr.email for sr in enrolled) render_list gettext("Successfully enrolled and sent email to the following users:"), (sr.identifier for sr in enrolled)
if enrolled.length and not emailStudents if enrolled.length and not emailStudents
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("Successfully enrolled the following users:"), (sr.email for sr in enrolled) render_list gettext("Successfully enrolled the following users:"), (sr.identifier for sr in enrolled)
# Student hasn't registered so we allow them to enroll # Student hasn't registered so we allow them to enroll
if allowed.length and emailStudents if allowed.length and emailStudents
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("Successfully sent enrollment emails to the following users. They will be allowed to enroll once they register:"), render_list gettext("Successfully sent enrollment emails to the following users. They will be allowed to enroll once they register:"),
(sr.email for sr in allowed) (sr.identifier for sr in allowed)
# Student hasn't registered so we allow them to enroll # Student hasn't registered so we allow them to enroll
if allowed.length and not emailStudents if allowed.length and not emailStudents
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("These users will be allowed to enroll once they register:"), render_list gettext("These users will be allowed to enroll once they register:"),
(sr.email for sr in allowed) (sr.identifier for sr in allowed)
# Student hasn't registered so we allow them to enroll with autoenroll # Student hasn't registered so we allow them to enroll with autoenroll
if autoenrolled.length and emailStudents if autoenrolled.length and emailStudents
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("Successfully sent enrollment emails to the following users. They will be enrolled once they register:"), render_list gettext("Successfully sent enrollment emails to the following users. They will be enrolled once they register:"),
(sr.email for sr in autoenrolled) (sr.identifier for sr in autoenrolled)
# Student hasn't registered so we allow them to enroll with autoenroll # Student hasn't registered so we allow them to enroll with autoenroll
if autoenrolled.length and not emailStudents if autoenrolled.length and not emailStudents
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("These users will be enrolled once they register:"), render_list gettext("These users will be enrolled once they register:"),
(sr.email for sr in autoenrolled) (sr.identifier for sr in autoenrolled)
if notenrolled.length and emailStudents if notenrolled.length and emailStudents
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("Emails successfully sent. The following users are no longer enrolled in the course:"), render_list gettext("Emails successfully sent. The following users are no longer enrolled in the course:"),
(sr.email for sr in notenrolled) (sr.identifier for sr in notenrolled)
if notenrolled.length and not emailStudents if notenrolled.length and not emailStudents
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("The following users are no longer enrolled in the course:"), render_list gettext("The following users are no longer enrolled in the course:"),
(sr.email for sr in notenrolled) (sr.identifier for sr in notenrolled)
if notunenrolled.length if notunenrolled.length
`// Translators: A list of users appears after this sentence` `// Translators: A list of users appears after this sentence`
render_list gettext("These users were not affiliated with the course so could not be unenrolled:"), render_list gettext("These users were not affiliated with the course so could not be unenrolled:"),
(sr.email for sr in notunenrolled) (sr.identifier for sr in notunenrolled)
# Wrapper for auth list subsection. # Wrapper for auth list subsection.
# manages a list of users who have special access. # manages a list of users who have special access.
......
...@@ -358,6 +358,11 @@ section.instructor-dashboard-content-2 { ...@@ -358,6 +358,11 @@ section.instructor-dashboard-content-2 {
display: block; display: block;
} }
label[for="auto-enroll-beta"]:hover + .auto-enroll-beta-hint {
width: 30%;
display: block;
}
label[for="email-students"]:hover + .email-students-hint { label[for="email-students"]:hover + .email-students-hint {
display: block; display: block;
......
...@@ -30,9 +30,10 @@ ...@@ -30,9 +30,10 @@
<div class="batch-enrollment"> <div class="batch-enrollment">
<h2> ${_("Batch Enrollment")} </h2> <h2> ${_("Batch Enrollment")} </h2>
<p> <p>
<label for="student-emails">${_("Enter email addresses separated by new lines or commas.")} <label for="student-ids">
${_("Enter email addresses and/or usernames separated by new lines or commas.")}
${_("You will not get notification for emails that bounce, so please double-check spelling.")} </label> ${_("You will not get notification for emails that bounce, so please double-check spelling.")} </label>
<textarea rows="6" name="student-emails" placeholder="${_("Email Addresses")}" spellcheck="false"></textarea> <textarea rows="6" name="student-ids" placeholder="${_("Email Addresses/Usernames")}" spellcheck="false"></textarea>
</p> </p>
<div class="enroll-option"> <div class="enroll-option">
...@@ -70,17 +71,30 @@ ...@@ -70,17 +71,30 @@
%if section_data['access']['instructor']: %if section_data['access']['instructor']:
<div class="batch-beta-testers"> <div class="batch-beta-testers">
<h2> ${_("Batch Beta Testers")} </h2> <h2> ${_("Batch Beta Tester Addition")} </h2>
<p> <p>
<label for="student-emails-for-beta"> <label for="student-ids-for-beta">
${_("Enter email addresses separated by new lines or commas.")}<br/> ${_("Enter email addresses and/or usernames separated by new lines or commas.")}<br/>
${_("Note: Users must have an activated {platform_name} account before they can be enrolled as a beta tester.").format(platform_name=settings.PLATFORM_NAME)} ${_("Note: Users must have an activated {platform_name} account before they can be enrolled as a beta tester.").format(platform_name=settings.PLATFORM_NAME)}
</label> </label>
<textarea rows="6" cols="50" name="student-emails-for-beta" placeholder="${_("Email Addresses")}" spellcheck="false"></textarea> <textarea rows="6" cols="50" name="student-ids-for-beta" placeholder="${_("Email Addresses/Usernames")}" spellcheck="false"></textarea>
</p> </p>
<div class="enroll-option"> <div class="enroll-option">
<input type="checkbox" name="auto-enroll" value="Auto-Enroll" checked="yes">
<label for="auto-enroll-beta">${_("Auto Enroll")}</label>
<div class="hint auto-enroll-beta-hint">
<span class="hint-caret"></span>
<p>
${_("If this option is <em>checked</em>, users who have not enrolled in your course will be automatically enrolled.")}
<br /><br />
${_("Checking this box has no effect if 'Remove beta testers' is selected.")}
</p>
</div>
</div>
<div class="enroll-option">
<input type="checkbox" name="email-students" value="Notify-students-by-email" checked="yes"> <input type="checkbox" name="email-students" value="Notify-students-by-email" checked="yes">
<label for="email-students-beta">${_("Notify users by email")}</label> <label for="email-students-beta">${_("Notify users by email")}</label>
<div class="hint email-students-beta-hint"> <div class="hint email-students-beta-hint">
......
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