Commit c9933a58 by Matt Drayer

Merge pull request #11209 from edx/saleem-latif/MAYN-167

MAYN-167: Bulk uploads (CSV) of manual enrollments on white labels should be performed as 'honor' modes
parents f1802d98 a856fec5
...@@ -9,7 +9,6 @@ import random ...@@ -9,7 +9,6 @@ import random
import pytz import pytz
import io import io
import json import json
import requests
import shutil import shutil
import tempfile import tempfile
from urllib import quote from urllib import quote
...@@ -51,8 +50,6 @@ from student.models import ( ...@@ -51,8 +50,6 @@ from student.models import (
) )
from student.tests.factories import UserFactory, CourseModeFactory, AdminFactory from student.tests.factories import UserFactory, CourseModeFactory, AdminFactory
from student.roles import CourseBetaTesterRole, CourseSalesAdminRole, CourseFinanceAdminRole, CourseInstructorRole from student.roles import CourseBetaTesterRole, CourseSalesAdminRole, CourseFinanceAdminRole, CourseInstructorRole
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.fields import Date from xmodule.fields import Date
...@@ -394,13 +391,39 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas ...@@ -394,13 +391,39 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas
def setUpClass(cls): def setUpClass(cls):
super(TestInstructorAPIBulkAccountCreationAndEnrollment, cls).setUpClass() super(TestInstructorAPIBulkAccountCreationAndEnrollment, cls).setUpClass()
cls.course = CourseFactory.create() cls.course = CourseFactory.create()
cls.url = reverse('register_and_enroll_students', kwargs={'course_id': cls.course.id.to_deprecated_string()})
# Create a course with mode 'audit'
cls.audit_course = CourseFactory.create()
CourseModeFactory(course_id=cls.audit_course.id, mode_slug=CourseMode.AUDIT)
cls.url = reverse(
'register_and_enroll_students', kwargs={'course_id': unicode(cls.course.id)}
)
cls.audit_course_url = reverse(
'register_and_enroll_students', kwargs={'course_id': unicode(cls.audit_course.id)}
)
def setUp(self): def setUp(self):
super(TestInstructorAPIBulkAccountCreationAndEnrollment, self).setUp() super(TestInstructorAPIBulkAccountCreationAndEnrollment, self).setUp()
# Create a course with mode 'honor' and with price
self.white_label_course = CourseFactory.create()
self.white_label_course_mode = CourseModeFactory(
course_id=self.white_label_course.id,
mode_slug=CourseMode.HONOR,
min_price=10,
suggested_prices='10',
)
self.white_label_course_url = reverse(
'register_and_enroll_students', kwargs={'course_id': unicode(self.white_label_course.id)}
)
self.request = RequestFactory().request() self.request = RequestFactory().request()
self.instructor = InstructorFactory(course_key=self.course.id) self.instructor = InstructorFactory(course_key=self.course.id)
self.audit_course_instructor = InstructorFactory(course_key=self.audit_course.id)
self.white_label_course_instructor = InstructorFactory(course_key=self.white_label_course.id)
self.client.login(username=self.instructor.username, password='test') self.client.login(username=self.instructor.username, password='test')
self.not_enrolled_student = UserFactory( self.not_enrolled_student = UserFactory(
...@@ -627,7 +650,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas ...@@ -627,7 +650,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas
"test_student2@example.com,test_student_1,tester2,US" "test_student2@example.com,test_student_1,tester2,US"
uploaded_file = SimpleUploadedFile("temp.csv", csv_content) uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
with patch('instructor.views.api.create_and_enroll_user') as mock: with patch('instructor.views.api.create_manual_course_enrollment') as mock:
mock.side_effect = NonExistentCourseError() mock.side_effect = NonExistentCourseError()
response = self.client.post(self.url, {'students_list': uploaded_file}) response = self.client.post(self.url, {'students_list': uploaded_file})
...@@ -687,6 +710,89 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas ...@@ -687,6 +710,89 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas
manual_enrollments = ManualEnrollmentAudit.objects.all() manual_enrollments = ManualEnrollmentAudit.objects.all()
self.assertEqual(manual_enrollments.count(), 0) self.assertEqual(manual_enrollments.count(), 0)
@patch.dict(settings.FEATURES, {'ALLOW_AUTOMATED_SIGNUPS': True})
def test_audit_enrollment_mode(self):
"""
Test that enrollment mode for audit courses (paid courses) is 'audit'.
"""
# Login Audit Course instructor
self.client.login(username=self.audit_course_instructor.username, password='test')
csv_content = "test_student_wl@example.com,test_student_wl,Test Student,USA"
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
response = self.client.post(self.audit_course_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)
manual_enrollments = ManualEnrollmentAudit.objects.all()
self.assertEqual(manual_enrollments.count(), 1)
self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ENROLLED)
# Verify enrollment modes to be 'audit'
for enrollment in manual_enrollments:
self.assertEqual(enrollment.enrollment.mode, CourseMode.AUDIT)
@patch.dict(settings.FEATURES, {'ALLOW_AUTOMATED_SIGNUPS': True})
def test_honor_enrollment_mode(self):
"""
Test that enrollment mode for unpaid honor courses is 'honor'.
"""
# Remove white label course price
self.white_label_course_mode.min_price = 0
self.white_label_course_mode.suggested_prices = ''
self.white_label_course_mode.save() # pylint: disable=no-member
# Login Audit Course instructor
self.client.login(username=self.white_label_course_instructor.username, password='test')
csv_content = "test_student_wl@example.com,test_student_wl,Test Student,USA"
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
response = self.client.post(self.white_label_course_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)
manual_enrollments = ManualEnrollmentAudit.objects.all()
self.assertEqual(manual_enrollments.count(), 1)
self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ENROLLED)
# Verify enrollment modes to be 'honor'
for enrollment in manual_enrollments:
self.assertEqual(enrollment.enrollment.mode, CourseMode.HONOR)
@patch.dict(settings.FEATURES, {'ALLOW_AUTOMATED_SIGNUPS': True})
def test_default_shopping_cart_enrollment_mode_for_white_label(self):
"""
Test that enrollment mode for white label courses (paid courses) is DEFAULT_SHOPPINGCART_MODE_SLUG.
"""
# Login white label course instructor
self.client.login(username=self.white_label_course_instructor.username, password='test')
csv_content = "test_student_wl@example.com,test_student_wl,Test Student,USA"
uploaded_file = SimpleUploadedFile("temp.csv", csv_content)
response = self.client.post(self.white_label_course_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)
manual_enrollments = ManualEnrollmentAudit.objects.all()
self.assertEqual(manual_enrollments.count(), 1)
self.assertEqual(manual_enrollments[0].state_transition, UNENROLLED_TO_ENROLLED)
# Verify enrollment modes to be CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG
for enrollment in manual_enrollments:
self.assertEqual(enrollment.enrollment.mode, CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG)
@attr('shard_1') @attr('shard_1')
@ddt.ddt @ddt.ddt
......
...@@ -343,6 +343,13 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man ...@@ -343,6 +343,13 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man
row_errors = [] row_errors = []
general_errors = [] general_errors = []
# for white labels we use 'shopping cart' which uses CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG as
# course mode for creating course enrollments.
if CourseMode.is_white_label(course_id):
course_mode = CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG
else:
course_mode = None
if 'students_list' in request.FILES: if 'students_list' in request.FILES:
students = [] students = []
...@@ -416,17 +423,16 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man ...@@ -416,17 +423,16 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man
email email
) )
# make sure user is enrolled in course # enroll a user if it is not already enrolled.
if not CourseEnrollment.is_enrolled(user, course_id): if not CourseEnrollment.is_enrolled(user, course_id):
enrollment_obj = CourseEnrollment.enroll(user, course_id) # Enroll user to the course and add manual enrollment audit trail
reason = 'Enrolling via csv upload' create_manual_course_enrollment(
ManualEnrollmentAudit.create_manual_enrollment_audit( user=user,
request.user, email, UNENROLLED_TO_ENROLLED, reason, enrollment_obj course_id=course_id,
) mode=course_mode,
log.info( enrolled_by=request.user,
u'user %s enrolled in the course %s', reason='Enrolling via csv upload',
username, state_transition=UNENROLLED_TO_ENROLLED,
course.id,
) )
enroll_email(course_id=course_id, student_email=email, auto_enroll=True, email_students=True, email_params=email_params) enroll_email(course_id=course_id, student_email=email, auto_enroll=True, email_students=True, email_params=email_params)
else: else:
...@@ -434,29 +440,10 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man ...@@ -434,29 +440,10 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man
# If username already exists in the database, then create_and_enroll_user # If username already exists in the database, then create_and_enroll_user
# will raise an IntegrityError exception. # will raise an IntegrityError exception.
password = generate_unique_password(generated_passwords) password = generate_unique_password(generated_passwords)
errors = create_and_enroll_user(
try: email, username, name, country, password, course_id, course_mode, request.user, email_params
with transaction.atomic(): )
enrollment_obj = create_and_enroll_user(email, username, name, country, password, course_id) row_errors.extend(errors)
reason = 'Enrolling via csv upload'
ManualEnrollmentAudit.create_manual_enrollment_audit(
request.user, email, UNENROLLED_TO_ENROLLED, reason, enrollment_obj
)
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(u'email sent to new created user at %s', email)
else: else:
general_errors.append({ general_errors.append({
...@@ -497,9 +484,18 @@ def generate_unique_password(generated_passwords, password_length=12): ...@@ -497,9 +484,18 @@ def generate_unique_password(generated_passwords, password_length=12):
return password return password
def create_and_enroll_user(email, username, name, country, password, course_id): def create_user_and_user_profile(email, username, name, country, password):
""" Creates a user and enroll him/her in the course""" """
Create a new user, add a new Registration instance for letting user verify its identity and create a user profile.
:param email: user's email address
:param username: user's username
:param name: user's name
:param country: user's country
:param password: user's password
:return: User instance of the new user.
"""
user = User.objects.create_user(username, email, password) user = User.objects.create_user(username, email, password)
reg = Registration() reg = Registration()
reg.register(user) reg.register(user)
...@@ -509,8 +505,102 @@ def create_and_enroll_user(email, username, name, country, password, course_id): ...@@ -509,8 +505,102 @@ def create_and_enroll_user(email, username, name, country, password, course_id):
profile.country = country profile.country = country
profile.save() profile.save()
# try to enroll the user in this course return user
return CourseEnrollment.enroll(user, course_id)
def create_manual_course_enrollment(user, course_id, mode, enrolled_by, reason, state_transition):
"""
Create course enrollment for the given student and create manual enrollment audit trail.
:param user: User who is to enroll in course
:param course_id: course identifier of the course in which to enroll the user.
:param mode: mode for user enrollment, e.g. 'honor', 'audit' etc.
:param enrolled_by: User who made the manual enrollment entry (usually instructor or support)
:param reason: Reason behind manual enrollment
:param state_transition: state transition denoting whether student enrolled from un-enrolled,
un-enrolled from enrolled etc.
:return CourseEnrollment instance.
"""
enrollment_obj = CourseEnrollment.enroll(user, course_id, mode=mode)
ManualEnrollmentAudit.create_manual_enrollment_audit(
enrolled_by, user.email, state_transition, reason, enrollment_obj
)
log.info(u'user %s enrolled in the course %s', user.username, course_id)
return enrollment_obj
def create_and_enroll_user(email, username, name, country, password, course_id, course_mode, enrolled_by, email_params):
"""
Create a new user and enroll him/her to the given course, return list of errors in the following format
Error format:
each error is key-value pait dict with following key-value pairs.
1. username: username of the user to enroll
1. email: email of the user to enroll
1. response: readable error message
:param email: user's email address
:param username: user's username
:param name: user's name
:param country: user's country
:param password: user's password
:param course_id: course identifier of the course in which to enroll the user.
:param course_mode: mode for user enrollment, e.g. 'honor', 'audit' etc.
:param enrolled_by: User who made the manual enrollment entry (usually instructor or support)
:param email_params: information to send to the user via email
:return: list of errors
"""
errors = list()
try:
with transaction.atomic():
# Create a new user
user = create_user_and_user_profile(email, username, name, country, password)
# Enroll user to the course and add manual enrollment audit trail
create_manual_course_enrollment(
user=user,
course_id=course_id,
mode=course_mode,
enrolled_by=enrolled_by,
reason='Enrolling via csv upload',
state_transition=UNENROLLED_TO_ENROLLED,
)
except IntegrityError:
errors.append({
'username': username, 'email': email, 'response': _('Username {user} already exists.').format(user=username)
})
except Exception as ex: # pylint: disable=broad-except
log.exception(type(ex).__name__)
errors.append({
'username': username, 'email': email, 'response': type(ex).__name__,
})
else:
try:
# It's a new user, an email will be sent to each newly created user.
email_params.update({
'message': 'account_creation_and_enrollment',
'email_address': email,
'password': password,
'platform_name': microsite.get_value('platform_name', settings.PLATFORM_NAME),
})
send_mail_to_student(email, email_params)
except Exception as ex: # pylint: disable=broad-except
log.exception(
"Exception '{exception}' raised while sending email to new user.".format(exception=type(ex).__name__)
)
errors.append({
'username': username,
'email': email,
'response':
_("Error '{error}' while sending email to new user (user email={email}). "
"Without the email student would not be able to login. "
"Please contact support for further information.").format(error=type(ex).__name__, email=email),
})
else:
log.info(u'email sent to new created user at %s', email)
return errors
@ensure_csrf_cookie @ensure_csrf_cookie
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment