Commit 1e82c542 by chrisndodge

Merge pull request #5670 from edx/cdodge/WL-98

WL-98
parents b6083026 1035d67a
......@@ -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
\ No newline at end of file
......@@ -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;
......
<%! 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)}
<%! 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
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