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):
# Endpoints that only Staff or Instructors can access
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_students_features', {}),
('get_distribution', {}),
......@@ -138,7 +138,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
]
# Endpoints that only Instructors can access
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'}),
('list_course_role_members', {'rolename': 'beta'}),
('rescore_problem', {'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}),
......@@ -291,13 +291,52 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
""" Test with an invalid action. """
action = 'robot-not-an-action'
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)
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):
# Test with an invalid email address (eg, a username).
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)
# test the response data
......@@ -306,9 +345,19 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
'auto_enroll': False,
"results": [
{
"email": self.notenrolled_student.username,
"error": True,
"invalidEmail": True
"identifier": self.notenrolled_student.username,
"before": {
"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):
def test_enroll_without_email(self):
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))
self.assertEqual(response.status_code, 200)
......@@ -332,7 +381,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
"auto_enroll": False,
"results": [
{
"email": self.notenrolled_student.email,
"identifier": self.notenrolled_student.email,
"before": {
"enrollment": False,
"auto_enroll": False,
......@@ -357,7 +406,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_enroll_with_email(self):
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))
self.assertEqual(response.status_code, 200)
......@@ -371,7 +420,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
"auto_enroll": False,
"results": [
{
"email": self.notenrolled_student.email,
"identifier": self.notenrolled_student.email,
"before": {
"enrollment": False,
"auto_enroll": False,
......@@ -409,7 +458,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_enroll_with_email_not_registered(self):
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)
# Check the outbox
......@@ -432,7 +481,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
# Try with marketing site enabled
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(
......@@ -446,7 +495,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_enroll_with_email_not_registered_autoenroll(self):
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))
self.assertEqual(response.status_code, 200)
......@@ -467,7 +516,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_unenroll_without_email(self):
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))
self.assertEqual(response.status_code, 200)
......@@ -481,7 +530,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
"auto_enroll": False,
"results": [
{
"email": self.enrolled_student.email,
"identifier": self.enrolled_student.email,
"before": {
"enrollment": True,
"auto_enroll": False,
......@@ -506,7 +555,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_unenroll_with_email(self):
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))
self.assertEqual(response.status_code, 200)
......@@ -520,7 +569,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
"auto_enroll": False,
"results": [
{
"email": self.enrolled_student.email,
"identifier": self.enrolled_student.email,
"before": {
"enrollment": True,
"auto_enroll": False,
......@@ -557,7 +606,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
def test_unenroll_with_email_allowed_student(self):
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))
self.assertEqual(response.status_code, 200)
......@@ -567,7 +616,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
"auto_enroll": False,
"results": [
{
"email": self.allowed_email,
"identifier": self.allowed_email,
"before": {
"enrollment": False,
"auto_enroll": False,
......@@ -605,7 +654,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
mock_uses_shib.return_value = True
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)
# Check the outbox
......@@ -629,7 +678,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
url = reverse('students_update_enrollment', kwargs={'course_id': self.course.id})
# Try with marketing site enabled
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(
......@@ -644,7 +693,7 @@ class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
mock_uses_shib.return_value = True
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))
self.assertEqual(response.status_code, 200)
......@@ -699,21 +748,28 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
""" Test with an invalid action. """
action = 'robot-not-an-action'
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)
def test_add_notenrolled(self):
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})
self.assertEqual(response.status_code, 200)
def add_notenrolled(self, response, identifier):
"""
Test Helper Method (not a test, called by other tests)
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))
# test the response data
expected = {
"action": "add",
"results": [
{
"email": self.notenrolled_student.email,
"identifier": identifier,
"error": False,
"userDoesNotExist": False
}
......@@ -726,9 +782,33 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
# Check the outbox
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):
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.assertTrue(CourseBetaTesterRole(self.course.location).has_user(self.notenrolled_student))
......@@ -737,7 +817,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
"action": "add",
"results": [
{
"email": self.notenrolled_student.email,
"identifier": self.notenrolled_student.email,
"error": False,
"userDoesNotExist": False
}
......@@ -769,7 +849,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
url = reverse('bulk_beta_modify_access', kwargs={'course_id': self.course.id})
# Try with marketing site enabled
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(
......@@ -786,14 +866,14 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
def test_enroll_with_email_not_registered(self):
# User doesn't exist
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)
# test the response data
expected = {
"action": "add",
"results": [
{
"email": self.notregistered_email,
"identifier": self.notregistered_email,
"error": True,
"userDoesNotExist": True
}
......@@ -807,7 +887,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
def test_remove_without_email(self):
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.assertFalse(CourseBetaTesterRole(self.course.location).has_user(self.beta_tester))
......@@ -817,7 +897,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
"action": "remove",
"results": [
{
"email": self.beta_tester.email,
"identifier": self.beta_tester.email,
"error": False,
"userDoesNotExist": False
}
......@@ -831,7 +911,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
def test_remove_with_email(self):
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.assertFalse(CourseBetaTesterRole(self.course.location).has_user(self.beta_tester))
......@@ -841,7 +921,7 @@ class TestInstructorAPIBulkBetaEnrollment(ModuleStoreTestCase, LoginEnrollmentTe
"action": "remove",
"results": [
{
"email": self.beta_tester.email,
"identifier": self.beta_tester.email,
"error": False,
"userDoesNotExist": False
}
......
......@@ -33,7 +33,7 @@ from django_comment_common.models import (
)
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
from instructor_task.api_helper import AlreadyRunningError
from instructor_task.views import get_task_completion_info
......@@ -204,7 +204,7 @@ def require_level(level):
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@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):
"""
Enroll or unenroll students by email.
......@@ -212,7 +212,7 @@ def students_update_enrollment(request, course_id):
Query Parameters:
- 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)
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.
......@@ -244,8 +244,8 @@ def students_update_enrollment(request, course_id):
"""
action = request.GET.get('action')
emails_raw = request.GET.get('emails')
emails = _split_input_list(emails_raw)
identifiers_raw = request.GET.get('identifiers')
identifiers = _split_input_list(identifiers_raw)
auto_enroll = request.GET.get('auto_enroll') 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):
email_params = get_email_params(course, auto_enroll)
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:
# Use django.core.validators.validate_email to check email address
# validity (obviously, cannot check if email actually /exists/,
# simply that it is plausibly valid)
validate_email(email)
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
validate_email(email) # Raises ValidationError if invalid
try:
if action == 'enroll':
before, after = enroll_email(course_id, email, auto_enroll, email_students, email_params)
elif action == 'unenroll':
......@@ -281,20 +281,29 @@ def students_update_enrollment(request, course_id):
"Unrecognized action '{}'".format(action)
))
except ValidationError:
# Flag this email as an error if invalid, but continue checking
# the remaining in the list
results.append({
'email': email,
'before': before.to_dict(),
'after': after.to_dict(),
'identifier': identifier,
'invalidIdentifier': True,
})
except Exception as exc: # pylint: disable=W0703
# catch and log any exceptions
# so that one error doesn't cause a 500.
except Exception as exc: # pylint: disable=W0703
log.exception("Error while #{}ing student")
log.exception(exc)
results.append({
'email': email,
'identifier': identifier,
'error': True,
'invalidEmail': False,
})
else:
results.append({
'identifier': identifier,
'before': before.to_dict(),
'after': after.to_dict(),
})
response_payload = {
......@@ -310,7 +319,7 @@ def students_update_enrollment(request, course_id):
@require_level('instructor')
@common_exceptions_400
@require_query_params(
emails="stringified list of emails",
identifiers="stringified list of emails and/or usernames",
action="add or remove",
)
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.
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 = request.GET.get('action')
emails_raw = request.GET.get('emails')
emails = _split_input_list(emails_raw)
identifiers_raw = request.GET.get('identifiers')
identifiers = _split_input_list(identifiers_raw)
email_students = request.GET.get('email_students') in ['true', 'True', True]
auto_enroll = request.GET.get('auto_enroll') in ['true', 'True', True]
results = []
rolename = 'beta'
course = get_course_by_id(course_id)
......@@ -333,11 +344,11 @@ def bulk_beta_modify_access(request, course_id):
if email_students:
email_params = get_email_params(course, auto_enroll=False)
for email in emails:
for identifier in identifiers:
try:
error = False
user_does_not_exist = False
user = User.objects.get(email=email)
user = get_student_from_identifier(identifier)
if action == 'add':
allow_access(course, user, rolename)
......@@ -360,10 +371,16 @@ def bulk_beta_modify_access(request, course_id):
# If no exception thrown, see if we should send an email
if email_students:
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:
# Tabulate the action result of this email address
results.append({
'email': email,
'identifier': identifier,
'error': error,
'userDoesNotExist': user_does_not_exist
})
......@@ -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)
# Scrape the query features for i18n - can't translate here because it breaks further queries
# and how the coffeescript works. The actual translation will be done in data_download.coffee
# Provide human-friendly and translatable names for these features. These names
# 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 = {
'username': _('Username'),
'name': _('Name'),
......@@ -1086,7 +1104,7 @@ def update_forum_role_membership(request, course_id):
target_is_instructor = has_access(user, course, 'instructor')
# cannot revoke instructor
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:
update_forum_role(course_id, user, rolename, action)
......
......@@ -178,19 +178,22 @@ class AuthListWidget extends MemberListWidget
class BetaTesterBulkAddition
constructor: (@$container) ->
# 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']")
@$checkbox_emailstudents = @$container.find("input[name='email-students']")
@$checkbox_autoenroll = @$container.find("input[name='auto-enroll']")
@$task_response = @$container.find(".request-response")
@$request_response_error = @$container.find(".request-response-error")
# click handlers
@$btn_beta_testers.click =>
emailStudents = @$checkbox_emailstudents.is(':checked')
autoEnroll = @$checkbox_autoenroll.is(':checked')
send_data =
action: $(event.target).data('action') # 'add' or 'remove'
emails: @$emails_input.val()
identifiers: @$identifier_input.val()
email_students: emailStudents
auto_enroll: autoEnroll
$.ajax
dataType: 'json'
......@@ -199,13 +202,22 @@ class BetaTesterBulkAddition
success: (data) => @display_response data
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) ->
console.warn msg
@clear_input()
@$task_response.empty()
@$request_response_error.empty()
@$request_response_error.text msg
display_response: (data_from_server) ->
@clear_input()
@$task_response.empty()
@$request_response_error.empty()
errors = []
......@@ -219,39 +231,37 @@ class BetaTesterBulkAddition
else
successes.push student_results
console.log(sr.email for sr in successes)
render_list = (label, emails) =>
render_list = (label, ids) =>
task_res_section = $ '<div/>', class: 'request-res-section'
task_res_section.append $ '<h3/>', text: label
email_list = $ '<ul/>'
task_res_section.append email_list
ids_list = $ '<ul/>'
task_res_section.append ids_list
for email in emails
email_list.append $ '<li/>', text: email
for identifier in ids
ids_list.append $ '<li/>', text: identifier
@$task_response.append task_res_section
if successes.length and data_from_server.action is 'add'
`// 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'
`// 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'
`// 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'
`// 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
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`
render_list gettext("Could not find users associated with the following email addresses:"), (sr.email for sr in no_users)
no_users.push $ gettext("Users must create and activate their account before they can be promoted to beta tester.")
`// 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 identifiers:"), (sr.identifier for sr in no_users)
# Wrapper for the batch enrollment subsection.
# This object handles buttons, success and failure reporting,
......@@ -259,7 +269,7 @@ class BetaTesterBulkAddition
class BatchEnrollment
constructor: (@$container) ->
# gather elements
@$emails_input = @$container.find("textarea[name='student-emails']")
@$identifier_input = @$container.find("textarea[name='student-ids']")
@$enrollment_button = @$container.find(".enrollment-button")
@$checkbox_autoenroll = @$container.find("input[name='auto-enroll']")
@$checkbox_emailstudents = @$container.find("input[name='email-students']")
......@@ -271,7 +281,7 @@ class BatchEnrollment
emailStudents: @$checkbox_emailstudents.is(':checked')
send_data =
action: $(event.target).data('action') # 'enroll' or 'unenroll'
emails: @$emails_input.val()
identifiers: @$identifier_input.val()
auto_enroll: @$checkbox_autoenroll.is(':checked')
email_students: emailStudents
......@@ -283,21 +293,30 @@ class BatchEnrollment
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) ->
console.warn msg
@clear_input()
@$task_response.empty()
@$request_response_error.empty()
@$request_response_error.text msg
display_response: (data_from_server) ->
@clear_input()
@$task_response.empty()
@$request_response_error.empty()
# these results arrays contain student_results
# only populated arrays will be rendered
#
# invalid email addresses
invalid_email = []
# invalid identifiers
invalid_identifier = []
# students for which there was an error during the action
errors = []
# students who are now enrolled in the course
......@@ -315,7 +334,7 @@ class BatchEnrollment
for student_results in data_from_server.results
# for a successful action.
# student_results is of the form {
# "email": "jd405@edx.org",
# "identifier": "jd405@edx.org",
# "before": {
# "enrollment": true,
# "auto_enroll": false,
......@@ -332,13 +351,14 @@ class BatchEnrollment
#
# for an action error.
# student_results is of the form {
# 'email': email,
# 'identifier': identifier,
# # then one of:
# '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
invalid_email.push student_results
if student_results.invalidIdentifier
invalid_identifier.push student_results
else if student_results.error
errors.push student_results
......@@ -364,19 +384,19 @@ class BatchEnrollment
console.warn student_results
# render populated result arrays
render_list = (label, emails) =>
render_list = (label, ids) =>
task_res_section = $ '<div/>', class: 'request-res-section'
task_res_section.append $ '<h3/>', text: label
email_list = $ '<ul/>'
task_res_section.append email_list
ids_list = $ '<ul/>'
task_res_section.append ids_list
for email in emails
email_list.append $ '<li/>', text: email
for identifier in ids
ids_list.append $ '<li/>', text: identifier
@$task_response.append task_res_section
if invalid_email.length
render_list gettext("The following email addresses are invalid:"), (sr.email for sr in invalid_email)
if invalid_identifier.length
render_list gettext("The following email addresses and/or usernames are invalid:"), (sr.identifier for sr in invalid_identifier)
if errors.length
errors_label = do ->
......@@ -389,53 +409,53 @@ class BatchEnrollment
"There was an error processing:"
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
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
`// 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
if allowed.length and emailStudents
`// 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:"),
(sr.email for sr in allowed)
(sr.identifier for sr in allowed)
# Student hasn't registered so we allow them to enroll
if allowed.length and not emailStudents
`// Translators: A list of users appears after this sentence`
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
if autoenrolled.length and emailStudents
`// 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:"),
(sr.email for sr in autoenrolled)
(sr.identifier for sr in autoenrolled)
# Student hasn't registered so we allow them to enroll with autoenroll
if autoenrolled.length and not emailStudents
`// Translators: A list of users appears after this sentence`
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
`// 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:"),
(sr.email for sr in notenrolled)
(sr.identifier for sr in notenrolled)
if notenrolled.length and not emailStudents
`// Translators: A list of users appears after this sentence`
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
`// 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:"),
(sr.email for sr in notunenrolled)
(sr.identifier for sr in notunenrolled)
# Wrapper for auth list subsection.
# manages a list of users who have special access.
......
......@@ -358,6 +358,11 @@ section.instructor-dashboard-content-2 {
display: block;
}
label[for="auto-enroll-beta"]:hover + .auto-enroll-beta-hint {
width: 30%;
display: block;
}
label[for="email-students"]:hover + .email-students-hint {
display: block;
......
......@@ -30,9 +30,10 @@
<div class="batch-enrollment">
<h2> ${_("Batch Enrollment")} </h2>
<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>
<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>
<div class="enroll-option">
......@@ -70,17 +71,30 @@
%if section_data['access']['instructor']:
<div class="batch-beta-testers">
<h2> ${_("Batch Beta Testers")} </h2>
<h2> ${_("Batch Beta Tester Addition")} </h2>
<p>
<label for="student-emails-for-beta">
${_("Enter email addresses separated by new lines or commas.")}<br/>
<label for="student-ids-for-beta">
${_("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)}
</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>
<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">
<label for="email-students-beta">${_("Notify users by email")}</label>
<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