@@ -331,6 +331,10 @@ def send_mail_to_student(student, param_dict):
|
||||
'emails/remove_beta_tester_email_subject.txt',
|
||||
'emails/remove_beta_tester_email_message.txt'
|
||||
),
|
||||
'account_creation_and_enrollment': (
|
||||
'emails/enroll_email_enrolledsubject.txt',
|
||||
'emails/account_creation_and_enroll_emailMessage.txt'
|
||||
),
|
||||
}
|
||||
|
||||
subject_template, message_template = email_template_dict.get(message_type, (None, None))
|
||||
|
||||
@@ -9,6 +9,7 @@ import requests
|
||||
import datetime
|
||||
import ddt
|
||||
import random
|
||||
import io
|
||||
from urllib import quote
|
||||
from django.test import TestCase
|
||||
from nose.tools import raises
|
||||
@@ -40,6 +41,7 @@ from courseware.models import StudentModule
|
||||
# modules which are mocked in test cases.
|
||||
import instructor_task.api
|
||||
import instructor.views.api
|
||||
from instructor.views.api import generate_unique_password
|
||||
from instructor.views.api import _split_input_list, common_exceptions_400
|
||||
from instructor_task.api_helper import AlreadyRunningError
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
@@ -48,6 +50,8 @@ from shoppingcart.models import (
|
||||
PaidCourseRegistration, Coupon, Invoice, CourseRegistrationCode
|
||||
)
|
||||
from course_modes.models import CourseMode
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from student.models import NonExistentCourseError
|
||||
|
||||
from .test_tools import msk_from_problem_urlname
|
||||
from ..views.tools import get_extended_due
|
||||
@@ -285,6 +289,242 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
@patch.dict(settings.FEATURES, {'ALLOW_AUTOMATED_SIGNUPS': True})
|
||||
class TestInstructorAPIBulkAccountCreationAndEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
Test Bulk account creation and enrollment from csv file
|
||||
"""
|
||||
def setUp(self):
|
||||
self.request = RequestFactory().request()
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.url = reverse('register_and_enroll_students', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
self.not_enrolled_student = UserFactory(
|
||||
username='NotEnrolledStudent',
|
||||
email='nonenrolled@test.com',
|
||||
first_name='NotEnrolled',
|
||||
last_name='Student'
|
||||
)
|
||||
|
||||
@patch('instructor.views.api.log.info')
|
||||
def test_account_creation_and_enrollment_with_csv(self, info_log):
|
||||
"""
|
||||
Happy path test to create a single new user
|
||||
"""
|
||||
csv_content = "test_student@example.com,test_student_1,tester1,USA"
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content)
|
||||
self.assertEquals(len(data['row_errors']), 0)
|
||||
self.assertEquals(len(data['warnings']), 0)
|
||||
self.assertEquals(len(data['general_errors']), 0)
|
||||
|
||||
# test the log for email that's send to new created user.
|
||||
info_log.assert_called_with('email sent to new created user at test_student@example.com')
|
||||
|
||||
@patch('instructor.views.api.log.info')
|
||||
def test_account_creation_and_enrollment_with_csv_with_blank_lines(self, info_log):
|
||||
"""
|
||||
Happy path test to create a single new user
|
||||
"""
|
||||
csv_content = "\ntest_student@example.com,test_student_1,tester1,USA\n\n"
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content)
|
||||
self.assertEquals(len(data['row_errors']), 0)
|
||||
self.assertEquals(len(data['warnings']), 0)
|
||||
self.assertEquals(len(data['general_errors']), 0)
|
||||
|
||||
# test the log for email that's send to new created user.
|
||||
info_log.assert_called_with('email sent to new created user at test_student@example.com')
|
||||
|
||||
@patch('instructor.views.api.log.info')
|
||||
def test_email_and_username_already_exist(self, info_log):
|
||||
"""
|
||||
If the email address and username already exists
|
||||
and the user is enrolled in the course, do nothing (including no email gets sent out)
|
||||
"""
|
||||
csv_content = "test_student@example.com,test_student_1,tester1,USA\n" \
|
||||
"test_student@example.com,test_student_1,tester2,US"
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content)
|
||||
self.assertEquals(len(data['row_errors']), 0)
|
||||
self.assertEquals(len(data['warnings']), 0)
|
||||
self.assertEquals(len(data['general_errors']), 0)
|
||||
|
||||
# test the log for email that's send to new created user.
|
||||
info_log.assert_called_with("user already exists with username '{username}' and email '{email}'".format(username='test_student_1', email='test_student@example.com'))
|
||||
|
||||
def test_bad_file_upload_type(self):
|
||||
"""
|
||||
Try uploading some non-CSV file and verify that it is rejected
|
||||
"""
|
||||
uploaded_file = SimpleUploadedFile("temp.jpg", io.BytesIO(b"some initial binary data: \x00\x01").read())
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content)
|
||||
self.assertNotEquals(len(data['general_errors']), 0)
|
||||
self.assertEquals(data['general_errors'][0]['response'], 'Could not read uploaded file.')
|
||||
|
||||
def test_insufficient_data(self):
|
||||
"""
|
||||
Try uploading a CSV file which does not have the exact four columns of data
|
||||
"""
|
||||
csv_content = "test_student@example.com,test_student_1\n"
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content)
|
||||
self.assertEquals(len(data['row_errors']), 0)
|
||||
self.assertEquals(len(data['warnings']), 0)
|
||||
self.assertEquals(len(data['general_errors']), 1)
|
||||
self.assertEquals(data['general_errors'][0]['response'], 'Data in row #1 must have exactly four columns: email, username, full name, and country')
|
||||
|
||||
def test_invalid_email_in_csv(self):
|
||||
"""
|
||||
Test failure case of a poorly formatted email field
|
||||
"""
|
||||
csv_content = "test_student.example.com,test_student_1,tester1,USA"
|
||||
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
data = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotEquals(len(data['row_errors']), 0)
|
||||
self.assertEquals(len(data['warnings']), 0)
|
||||
self.assertEquals(len(data['general_errors']), 0)
|
||||
self.assertEquals(data['row_errors'][0]['response'], 'Invalid email {0}.'.format('test_student.example.com'))
|
||||
|
||||
@patch('instructor.views.api.log.info')
|
||||
def test_csv_user_exist_and_not_enrolled(self, info_log):
|
||||
"""
|
||||
If the email address and username already exists
|
||||
and the user is not enrolled in the course, enrolled him/her and iterate to next one.
|
||||
"""
|
||||
csv_content = "nonenrolled@test.com,NotEnrolledStudent,tester1,USA"
|
||||
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
info_log.assert_called_with('user {username} enrolled in the course {course}'.format(username='NotEnrolledStudent', course=self.course.id))
|
||||
|
||||
def test_user_with_already_existing_email_in_csv(self):
|
||||
"""
|
||||
If the email address already exists, but the username is different,
|
||||
assume it is the correct user and just register the user in the course.
|
||||
"""
|
||||
csv_content = "test_student@example.com,test_student_1,tester1,USA\n" \
|
||||
"test_student@example.com,test_student_2,tester2,US"
|
||||
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content)
|
||||
warning_message = 'An account with email {email} exists but the provided username {username} ' \
|
||||
'is different. Enrolling anyway with {email}.'.format(email='test_student@example.com', username='test_student_2')
|
||||
self.assertNotEquals(len(data['warnings']), 0)
|
||||
self.assertEquals(data['warnings'][0]['response'], warning_message)
|
||||
user = User.objects.get(email='test_student@example.com')
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(user, self.course.id))
|
||||
|
||||
def test_user_with_already_existing_username_in_csv(self):
|
||||
"""
|
||||
If the username already exists (but not the email),
|
||||
assume it is a different user and fail to create the new account.
|
||||
"""
|
||||
csv_content = "test_student1@example.com,test_student_1,tester1,USA\n" \
|
||||
"test_student2@example.com,test_student_1,tester2,US"
|
||||
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content)
|
||||
self.assertNotEquals(len(data['row_errors']), 0)
|
||||
self.assertEquals(data['row_errors'][0]['response'], 'Username {user} already exists.'.format(user='test_student_1'))
|
||||
|
||||
def test_csv_file_not_attached(self):
|
||||
"""
|
||||
Test when the user does not attach a file
|
||||
"""
|
||||
csv_content = "test_student1@example.com,test_student_1,tester1,USA\n" \
|
||||
"test_student2@example.com,test_student_1,tester2,US"
|
||||
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
|
||||
response = self.client.post(self.url, {'file_not_found': uploaded_file})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content)
|
||||
self.assertNotEquals(len(data['general_errors']), 0)
|
||||
self.assertEquals(data['general_errors'][0]['response'], 'File is not attached.')
|
||||
|
||||
def test_raising_exception_in_auto_registration_and_enrollment_case(self):
|
||||
"""
|
||||
Test that exceptions are handled well
|
||||
"""
|
||||
csv_content = "test_student1@example.com,test_student_1,tester1,USA\n" \
|
||||
"test_student2@example.com,test_student_1,tester2,US"
|
||||
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
with patch('instructor.views.api.create_and_enroll_user') as mock:
|
||||
mock.side_effect = NonExistentCourseError()
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content)
|
||||
self.assertNotEquals(len(data['row_errors']), 0)
|
||||
self.assertEquals(data['row_errors'][0]['response'], 'NonExistentCourseError')
|
||||
|
||||
def test_generate_unique_password(self):
|
||||
"""
|
||||
generate_unique_password should generate a unique password string that excludes certain characters.
|
||||
"""
|
||||
password = generate_unique_password([], 12)
|
||||
self.assertEquals(len(password), 12)
|
||||
for letter in password:
|
||||
self.assertNotIn(letter, 'aAeEiIoOuU1l')
|
||||
|
||||
def test_users_created_and_enrolled_successfully_if_others_fail(self):
|
||||
|
||||
csv_content = "test_student1@example.com,test_student_1,tester1,USA\n" \
|
||||
"test_student3@example.com,test_student_1,tester3,CA\n" \
|
||||
"test_student2@example.com,test_student_2,tester2,USA"
|
||||
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = json.loads(response.content)
|
||||
self.assertNotEquals(len(data['row_errors']), 0)
|
||||
self.assertEquals(data['row_errors'][0]['response'], 'Username {user} already exists.'.format(user='test_student_1'))
|
||||
self.assertTrue(User.objects.filter(username='test_student_1', email='test_student1@example.com').exists())
|
||||
self.assertTrue(User.objects.filter(username='test_student_2', email='test_student2@example.com').exists())
|
||||
self.assertFalse(User.objects.filter(email='test_student3@example.com').exists())
|
||||
|
||||
@patch.object(instructor.views.api, 'generate_random_string',
|
||||
Mock(side_effect=['first', 'first', 'second']))
|
||||
def test_generate_unique_password_no_reuse(self):
|
||||
"""
|
||||
generate_unique_password should generate a unique password string that hasn't been generated before.
|
||||
"""
|
||||
generated_password = ['first']
|
||||
password = generate_unique_password(generated_password, 12)
|
||||
self.assertNotEquals(password, 'first')
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ALLOW_AUTOMATED_SIGNUPS': False})
|
||||
def test_allow_automated_signups_flag_not_set(self):
|
||||
csv_content = "test_student1@example.com,test_student_1,tester1,USA"
|
||||
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
|
||||
response = self.client.post(self.url, {'students_list': uploaded_file})
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class TestInstructorAPIEnrollment(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
|
||||
@@ -50,6 +50,7 @@ from instructor_task.models import ReportStore
|
||||
import instructor.enrollment as enrollment
|
||||
from instructor.enrollment import (
|
||||
enroll_email,
|
||||
send_mail_to_student,
|
||||
get_email_params,
|
||||
send_beta_role_email,
|
||||
unenroll_email
|
||||
@@ -83,6 +84,7 @@ from .tools import (
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
from student.models import UserProfile, Registration
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -216,6 +218,187 @@ def require_level(level):
|
||||
return decorator
|
||||
|
||||
|
||||
EMAIL_INDEX = 0
|
||||
USERNAME_INDEX = 1
|
||||
NAME_INDEX = 2
|
||||
COUNTRY_INDEX = 3
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
def register_and_enroll_students(request, course_id): # pylint: disable=R0915
|
||||
"""
|
||||
Create new account and Enroll students in this course.
|
||||
Passing a csv file that contains a list of students.
|
||||
Order in csv should be the following email = 0; username = 1; name = 2; country = 3.
|
||||
Requires staff access.
|
||||
|
||||
-If the email address and username already exists and the user is enrolled in the course,
|
||||
do nothing (including no email gets sent out)
|
||||
|
||||
-If the email address already exists, but the username is different,
|
||||
match on the email address only and continue to enroll the user in the course using the email address
|
||||
as the matching criteria. Note the change of username as a warning message (but not a failure). Send a standard enrollment email
|
||||
which is the same as the existing manual enrollment
|
||||
|
||||
-If the username already exists (but not the email), assume it is a different user and fail to create the new account.
|
||||
The failure will be messaged in a response in the browser.
|
||||
"""
|
||||
|
||||
if not microsite.get_value('ALLOW_AUTOMATED_SIGNUPS', settings.FEATURES.get('ALLOW_AUTOMATED_SIGNUPS', False)):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
warnings = []
|
||||
row_errors = []
|
||||
general_errors = []
|
||||
|
||||
if 'students_list' in request.FILES:
|
||||
students = []
|
||||
|
||||
try:
|
||||
upload_file = request.FILES.get('students_list')
|
||||
students = [row for row in csv.reader(upload_file.read().splitlines())]
|
||||
except Exception: # pylint: disable=W0703
|
||||
general_errors.append({
|
||||
'username': '', 'email': '', 'response': _('Could not read uploaded file.')
|
||||
})
|
||||
finally:
|
||||
upload_file.close()
|
||||
|
||||
generated_passwords = []
|
||||
course = get_course_by_id(course_id)
|
||||
row_num = 0
|
||||
for student in students:
|
||||
row_num = row_num + 1
|
||||
|
||||
# verify that we have exactly four columns in every row but allow for blank lines
|
||||
if len(student) != 4:
|
||||
if len(student) > 0:
|
||||
general_errors.append({
|
||||
'username': '',
|
||||
'email': '',
|
||||
'response': _('Data in row #{row_num} must have exactly four columns: email, username, full name, and country').format(row_num=row_num)
|
||||
})
|
||||
continue
|
||||
|
||||
# Iterate each student in the uploaded csv file.
|
||||
email = student[EMAIL_INDEX]
|
||||
username = student[USERNAME_INDEX]
|
||||
name = student[NAME_INDEX]
|
||||
country = student[COUNTRY_INDEX][:2]
|
||||
|
||||
email_params = get_email_params(course, True, secure=request.is_secure())
|
||||
try:
|
||||
validate_email(email) # Raises ValidationError if invalid
|
||||
except ValidationError:
|
||||
row_errors.append({
|
||||
'username': username, 'email': email, 'response': _('Invalid email {email_address}.').format(email_address=email)})
|
||||
else:
|
||||
if User.objects.filter(email=email).exists():
|
||||
# Email address already exists. assume it is the correct user
|
||||
# and just register the user in the course and send an enrollment email.
|
||||
user = User.objects.get(email=email)
|
||||
|
||||
# see if it is an exact match with email and username
|
||||
# if it's not an exact match then just display a warning message, but continue onwards
|
||||
if not User.objects.filter(email=email, username=username).exists():
|
||||
warning_message = _(
|
||||
'An account with email {email} exists but the provided username {username} '
|
||||
'is different. Enrolling anyway with {email}.'
|
||||
).format(email=email, username=username)
|
||||
|
||||
warnings.append({
|
||||
'username': username, 'email': email, 'response': warning_message})
|
||||
log.warning('email {email} already exist'.format(email=email))
|
||||
else:
|
||||
log.info("user already exists with username '{username}' and email '{email}'".format(email=email, username=username))
|
||||
|
||||
# make sure user is enrolled in course
|
||||
if not CourseEnrollment.is_enrolled(user, course_id):
|
||||
CourseEnrollment.enroll(user, course_id)
|
||||
log.info('user {username} enrolled in the course {course}'.format(username=username, course=course.id))
|
||||
enroll_email(course_id=course_id, student_email=email, auto_enroll=True, email_students=True, email_params=email_params)
|
||||
else:
|
||||
# This email does not yet exist, so we need to create a new account
|
||||
# If username already exists in the database, then create_and_enroll_user
|
||||
# will raise an IntegrityError exception.
|
||||
password = generate_unique_password(generated_passwords)
|
||||
|
||||
try:
|
||||
create_and_enroll_user(email, username, name, country, password, course_id)
|
||||
except IntegrityError:
|
||||
row_errors.append({
|
||||
'username': username, 'email': email, 'response': _('Username {user} already exists.').format(user=username)})
|
||||
except Exception as ex:
|
||||
log.exception(type(ex).__name__)
|
||||
row_errors.append({
|
||||
'username': username, 'email': email, 'response': _(type(ex).__name__)})
|
||||
else:
|
||||
# It's a new user, an email will be sent to each newly created user.
|
||||
email_params['message'] = 'account_creation_and_enrollment'
|
||||
email_params['email_address'] = email
|
||||
email_params['password'] = password
|
||||
email_params['platform_name'] = microsite.get_value('platform_name', settings.PLATFORM_NAME)
|
||||
send_mail_to_student(email, email_params)
|
||||
log.info('email sent to new created user at {email}'.format(email=email))
|
||||
|
||||
else:
|
||||
general_errors.append({
|
||||
'username': '', 'email': '', 'response': _('File is not attached.')
|
||||
})
|
||||
|
||||
results = {
|
||||
'row_errors': row_errors,
|
||||
'general_errors': general_errors,
|
||||
'warnings': warnings
|
||||
}
|
||||
return JsonResponse(results)
|
||||
|
||||
|
||||
def generate_random_string(length):
|
||||
"""
|
||||
Create a string of random characters of specified length
|
||||
"""
|
||||
chars = [
|
||||
char for char in string.ascii_uppercase + string.digits + string.ascii_lowercase
|
||||
if char not in 'aAeEiIoOuU1l'
|
||||
]
|
||||
|
||||
return string.join((random.choice(chars) for __ in range(length)), '')
|
||||
|
||||
|
||||
def generate_unique_password(generated_passwords, password_length=12):
|
||||
"""
|
||||
generate a unique password for each student.
|
||||
"""
|
||||
|
||||
password = generate_random_string(password_length)
|
||||
while password in generated_passwords:
|
||||
password = generate_random_string(password_length)
|
||||
|
||||
generated_passwords.append(password)
|
||||
|
||||
return password
|
||||
|
||||
|
||||
def create_and_enroll_user(email, username, name, country, password, course_id):
|
||||
""" Creates a user and enroll him/her in the course"""
|
||||
|
||||
user = User.objects.create_user(username, email, password)
|
||||
reg = Registration()
|
||||
reg.register(user)
|
||||
|
||||
profile = UserProfile(user=user)
|
||||
profile.name = name
|
||||
profile.country = country
|
||||
profile.save()
|
||||
|
||||
# try to enroll the user in this course
|
||||
CourseEnrollment.enroll(user, course_id)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@@ -852,13 +1035,8 @@ def random_code_generator():
|
||||
generate a random alphanumeric code of length defined in
|
||||
REGISTRATION_CODE_LENGTH settings
|
||||
"""
|
||||
chars = ''
|
||||
for char in string.ascii_uppercase + string.digits + string.ascii_lowercase:
|
||||
# removing vowel words and specific characters
|
||||
chars += char.strip('aAeEiIoOuU1l')
|
||||
|
||||
code_length = getattr(settings, 'REGISTRATION_CODE_LENGTH', 8)
|
||||
return string.join((random.choice(chars) for _ in range(code_length)), '')
|
||||
return generate_random_string(code_length)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
|
||||
@@ -7,6 +7,8 @@ from django.conf.urls import patterns, url
|
||||
urlpatterns = patterns('', # nopep8
|
||||
url(r'^students_update_enrollment$',
|
||||
'instructor.views.api.students_update_enrollment', name="students_update_enrollment"),
|
||||
url(r'^register_and_enroll_students$',
|
||||
'instructor.views.api.register_and_enroll_students', name="register_and_enroll_students"),
|
||||
url(r'^list_course_role_members$',
|
||||
'instructor.views.api.list_course_role_members', name="list_course_role_members"),
|
||||
url(r'^modify_access$',
|
||||
|
||||
@@ -250,6 +250,7 @@ def _section_membership(course, access):
|
||||
'access': access,
|
||||
'enroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'unenroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'upload_student_csv_button_url': reverse('register_and_enroll_students', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'modify_beta_testers_button_url': reverse('bulk_beta_modify_access', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'list_course_role_members_url': reverse('list_course_role_members', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'modify_access_url': reverse('modify_access', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
|
||||
@@ -287,6 +287,11 @@ FEATURES = {
|
||||
|
||||
# Enable the new dashboard, account, and profile pages
|
||||
'ENABLE_NEW_DASHBOARD': False,
|
||||
|
||||
# Show a section in the membership tab of the instructor dashboard
|
||||
# to allow an upload of a CSV file that contains a list of new accounts to create
|
||||
# and register for course.
|
||||
'ALLOW_AUTOMATED_SIGNUPS': False,
|
||||
}
|
||||
|
||||
# Ignore static asset files on import which match this pattern
|
||||
|
||||
@@ -141,7 +141,7 @@ class AuthListWidget extends MemberListWidget
|
||||
url: @list_endpoint
|
||||
data: rolename: @rolename
|
||||
success: (data) => cb? null, data[@rolename]
|
||||
error: std_ajax_err =>
|
||||
error: std_ajax_err =>
|
||||
`// Translators: A rolename appears this sentence. A rolename is something like "staff" or "beta tester".`
|
||||
cb? gettext("Error fetching list for role") + " '#{@rolename}'"
|
||||
|
||||
@@ -174,6 +174,108 @@ class AuthListWidget extends MemberListWidget
|
||||
else
|
||||
@reload_list()
|
||||
|
||||
class AutoEnrollmentViaCsv
|
||||
constructor: (@$container) ->
|
||||
# Wrapper for the AutoEnrollmentViaCsv subsection.
|
||||
# This object handles buttons, success and failure reporting,
|
||||
# and server communication.
|
||||
@$student_enrollment_form = @$container.find("form#student-auto-enroll-form")
|
||||
@$enrollment_signup_button = @$container.find("[name='enrollment_signup_button']")
|
||||
@$students_list_file = @$container.find("input[name='students_list']")
|
||||
@$csrf_token = @$container.find("input[name='csrfmiddlewaretoken']")
|
||||
@$results = @$container.find("div.results")
|
||||
@$browse_button = @$container.find("#browseBtn")
|
||||
@$browse_file = @$container.find("#browseFile")
|
||||
|
||||
@processing = false
|
||||
|
||||
@$browse_button.on "change", (event) =>
|
||||
if event.currentTarget.files.length == 1
|
||||
@$browse_file.val(event.currentTarget.value.substring(event.currentTarget.value.lastIndexOf("\\") + 1))
|
||||
|
||||
# attach click handler for @$enrollment_signup_button
|
||||
@$enrollment_signup_button.click =>
|
||||
@$student_enrollment_form.submit (event) =>
|
||||
if @processing
|
||||
return false
|
||||
|
||||
@processing = true
|
||||
|
||||
event.preventDefault()
|
||||
data = new FormData(event.currentTarget)
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
type: 'POST'
|
||||
url: event.currentTarget.action
|
||||
data: data
|
||||
processData: false
|
||||
contentType: false
|
||||
success: (data) =>
|
||||
@processing = false
|
||||
@display_response data
|
||||
|
||||
return false
|
||||
|
||||
display_response: (data_from_server) ->
|
||||
@$results.empty()
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
result_from_server_is_success = true
|
||||
|
||||
if data_from_server.general_errors.length
|
||||
result_from_server_is_success = false
|
||||
for general_error in data_from_server.general_errors
|
||||
general_error['is_general_error'] = true
|
||||
errors.push general_error
|
||||
|
||||
if data_from_server.row_errors.length
|
||||
result_from_server_is_success = false
|
||||
for error in data_from_server.row_errors
|
||||
error['is_general_error'] = false
|
||||
errors.push error
|
||||
|
||||
if data_from_server.warnings.length
|
||||
result_from_server_is_success = false
|
||||
for warning in data_from_server.warnings
|
||||
warning['is_general_error'] = false
|
||||
warnings.push warning
|
||||
|
||||
render_response = (label, type, student_results) =>
|
||||
if type is 'success'
|
||||
task_res_section = $ '<div/>', class: 'message message-confirmation'
|
||||
message_title = $ '<h3/>', class: 'message-title', text: label
|
||||
task_res_section.append message_title
|
||||
@$results.append task_res_section
|
||||
return
|
||||
|
||||
if type is 'error'
|
||||
task_res_section = $ '<div/>', class: 'message message-error'
|
||||
if type is 'warning'
|
||||
task_res_section = $ '<div/>', class: 'message message-warning'
|
||||
|
||||
message_title = $ '<h3/>', class: 'message-title', text: label
|
||||
task_res_section. append message_title
|
||||
messages_copy = $ '<div/>', class: 'message-copy'
|
||||
task_res_section. append messages_copy
|
||||
messages_summary = $ '<ul/>', class: 'list-summary summary-items'
|
||||
messages_copy.append messages_summary
|
||||
|
||||
for student_result in student_results
|
||||
if student_result.is_general_error
|
||||
response_message = student_result.response
|
||||
else
|
||||
response_message = student_result.username + ' ('+ student_result.email + '): ' + ' (' + student_result.response + ')'
|
||||
messages_summary.append $ '<li/>', class: 'summary-item', text: response_message
|
||||
|
||||
@$results.append task_res_section
|
||||
|
||||
if errors.length
|
||||
render_response gettext("The following errors were generated:"), 'error', errors
|
||||
if warnings.length
|
||||
render_response gettext("The following warnings were generated:"), 'warning', warnings
|
||||
if result_from_server_is_success
|
||||
render_response gettext("All accounts were created successfully."), 'success', []
|
||||
|
||||
class BetaTesterBulkAddition
|
||||
constructor: (@$container) ->
|
||||
@@ -189,7 +291,7 @@ class BetaTesterBulkAddition
|
||||
@$btn_beta_testers.click (event) =>
|
||||
emailStudents = @$checkbox_emailstudents.is(':checked')
|
||||
autoEnroll = @$checkbox_autoenroll.is(':checked')
|
||||
send_data =
|
||||
send_data =
|
||||
action: $(event.target).data('action') # 'add' or 'remove'
|
||||
identifiers: @$identifier_input.val()
|
||||
email_students: emailStudents
|
||||
@@ -580,7 +682,10 @@ class Membership
|
||||
|
||||
# isolate # initialize BatchEnrollment subsection
|
||||
plantTimeout 0, => new BatchEnrollment @$section.find '.batch-enrollment'
|
||||
|
||||
|
||||
# isolate # initialize AutoEnrollmentViaCsv subsection
|
||||
plantTimeout 0, => new AutoEnrollmentViaCsv @$section.find '.auto_enroll_csv'
|
||||
|
||||
# initialize BetaTesterBulkAddition subsection
|
||||
plantTimeout 0, => new BetaTesterBulkAddition @$section.find '.batch-beta-testers'
|
||||
|
||||
@@ -626,4 +731,4 @@ class Membership
|
||||
_.defaults window, InstructorDashboard: {}
|
||||
_.defaults window.InstructorDashboard, sections: {}
|
||||
_.defaults window.InstructorDashboard.sections,
|
||||
Membership: Membership
|
||||
Membership: Membership
|
||||
@@ -143,6 +143,7 @@ $light-gray: #ddd;
|
||||
// used by descriptor css
|
||||
$lightGrey: #edf1f5;
|
||||
$darkGrey: #8891a1;
|
||||
$lightGrey1: #ccc;
|
||||
$blue-d1: shade($blue,20%);
|
||||
$blue-d2: shade($blue,40%);
|
||||
$blue-d4: shade($blue,80%);
|
||||
|
||||
@@ -684,6 +684,52 @@ section.instructor-dashboard-content-2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Auto Enroll Csv Section
|
||||
.auto_enroll_csv {
|
||||
.results {
|
||||
|
||||
}
|
||||
.enrollment_signup_button {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
// Custom File upload
|
||||
.customBrowseBtn {
|
||||
margin: ($baseline/2) 0;
|
||||
display: inline-block;
|
||||
.file-browse {
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
display: inline;
|
||||
margin-left: -5px;
|
||||
span.browse{
|
||||
@include button(simple, $blue);
|
||||
padding: 6px ($baseline/2);
|
||||
font-size: 12px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
margin-right: ($baseline);
|
||||
}
|
||||
input.file_field {
|
||||
position:absolute;
|
||||
top:0;
|
||||
right:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
cursor:pointer;
|
||||
opacity:0;
|
||||
filter:alpha(opacity=0);
|
||||
}
|
||||
}
|
||||
& > span, & input[disabled]{
|
||||
vertical-align: middle;
|
||||
}
|
||||
input[disabled] {
|
||||
cursor: not-allowed;
|
||||
border: 1px solid $lightGrey1;
|
||||
border-radius: 4px 0 0 4px;
|
||||
padding: 6px 6px 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enroll-option {
|
||||
margin: ($baseline/2) 0;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
${_("Welcome to {course_name}").format(course_name=course.display_name_with_default)}
|
||||
|
||||
${_("To get started, please visit https://{site_name}. The login information for your account follows.").format(site_name=site_name)}
|
||||
|
||||
${_("email: {email}").format(email=email_address)}
|
||||
${_("password: {password}").format(password=password)}
|
||||
|
||||
${_("It is recommended that you change your password.")}
|
||||
|
||||
${_("Sincerely yours,")}
|
||||
|
||||
${_("The {course_name} Team").format(course_name=course.display_name_with_default)}
|
||||
@@ -1,5 +1,6 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%page args="section_data"/>
|
||||
<%! from microsite_configuration import microsite %>
|
||||
|
||||
<script type="text/template" id="member-list-widget-template">
|
||||
<div class="member-list-widget">
|
||||
@@ -68,6 +69,30 @@
|
||||
<div class="request-response-error"></div>
|
||||
</div>
|
||||
|
||||
%if microsite.get_value('ALLOW_AUTOMATED_SIGNUPS', settings.FEATURES.get('ALLOW_AUTOMATED_SIGNUPS', False)):
|
||||
<hr class="divider" />
|
||||
|
||||
<div class="auto_enroll auto_enroll_csv">
|
||||
<h2> ${_("Register/Enroll Students")} </h2>
|
||||
<p>
|
||||
${_("To register and enroll a list of users in this course, choose a CSV file that contains the following columns in this exact order: email, username, name, and country. Please include one student per row and do not include any headers, footers, or blank lines.")}
|
||||
</p>
|
||||
<form id="student-auto-enroll-form" method="post" action="${ section_data['upload_student_csv_button_url'] }" enctype="multipart/form-data">
|
||||
<div class="customBrowseBtn">
|
||||
<input disabled="disabled" id="browseFile" placeholder="choose file" />
|
||||
<div class="file-browse btn btn-primary">
|
||||
<span class="browse"> Browse </span>
|
||||
<input class="file_field" id="browseBtn" name="students_list" type="file" accept=".csv"/>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" name="enrollment_signup_button">${_("Upload CSV")}</button>
|
||||
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
</form>
|
||||
<div class="results"></div>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
<hr class="divider" />
|
||||
|
||||
%if section_data['access']['instructor']:
|
||||
@@ -245,5 +270,6 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</%block>
|
||||
% endif
|
||||
|
||||
Reference in New Issue
Block a user