Support tool for changing enrollments.

Allows support staff or global staff to view a list of a learner's
enrollments, and change enrollment modes. We generate a
ManualEnrollmentAudit record for these enrollment changes in order to
track updates. Additionally, enrollment changes are handled through
the enrollment API, which handles bookkeeping such as granting refunds
where appropriate.

ECOM-2825
This commit is contained in:
Peter Fogg
2015-12-11 17:10:49 -05:00
parent 98f73c062b
commit 30f25105e9
24 changed files with 1053 additions and 17 deletions

View File

@@ -1,13 +1,26 @@
# coding: UTF-8
"""
Tests for support views.
"""
from datetime import datetime, timedelta
import itertools
import json
import re
import ddt
from django.test import TestCase
from django.core.urlresolvers import reverse
from pytz import UTC
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from lms.djangoapps.verify_student.models import VerificationDeadline
from student.models import CourseEnrollment, ManualEnrollmentAudit, ENROLLED_TO_ENROLLED
from student.roles import GlobalStaff, SupportStaffRole
from student.tests.factories import UserFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
class SupportViewTestCase(TestCase):
@@ -33,17 +46,21 @@ class SupportViewAccessTests(SupportViewTestCase):
Tests for access control of support views.
"""
@ddt.data(
("support:index", GlobalStaff, True),
("support:index", SupportStaffRole, True),
("support:index", None, False),
("support:certificates", GlobalStaff, True),
("support:certificates", SupportStaffRole, True),
("support:certificates", None, False),
("support:refund", GlobalStaff, True),
("support:refund", SupportStaffRole, True),
("support:refund", None, False),
)
@ddt.data(*(
(url_name, role, has_access)
for (url_name, (role, has_access))
in itertools.product((
'support:index',
'support:certificates',
'support:refund',
'support:enrollment',
'support:enrollment_list'
), (
(GlobalStaff, True),
(SupportStaffRole, True),
(None, False)
))
))
@ddt.unpack
def test_access(self, url_name, role, has_access):
if role is not None:
@@ -57,7 +74,13 @@ class SupportViewAccessTests(SupportViewTestCase):
else:
self.assertEqual(response.status_code, 403)
@ddt.data("support:index", "support:certificates", "support:refund")
@ddt.data(
"support:index",
"support:certificates",
"support:refund",
"support:enrollment",
"support:enrollment_list"
)
def test_require_login(self, url_name):
url = reverse(url_name)
@@ -116,3 +139,114 @@ class SupportViewCertificatesTests(SupportViewTestCase):
url = reverse("support:certificates") + "?query=student@example.com"
response = self.client.get(url)
self.assertContains(response, "userQuery: 'student@example.com'")
@ddt.ddt
class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase):
"""Tests for the enrollment support view."""
def setUp(self):
super(SupportViewEnrollmentsTests, self).setUp()
SupportStaffRole().add_users(self.user)
self.course = CourseFactory(display_name=u'te')
self.student = UserFactory.create(username='student', email='test@example.com', password='test')
for mode in (CourseMode.AUDIT, CourseMode.VERIFIED):
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) # pylint: disable=no-member
self.verification_deadline = VerificationDeadline(
course_key=self.course.id, # pylint: disable=no-member
deadline=datetime.now(UTC) + timedelta(days=365)
)
self.verification_deadline.save()
CourseEnrollmentFactory.create(mode=CourseMode.AUDIT, user=self.student, course_id=self.course.id) # pylint: disable=no-member
self.url = reverse('support:enrollment_list', kwargs={'username': self.student.username})
def assert_enrollment(self, mode):
"""
Assert that the student's enrollment has the correct mode.
"""
enrollment = CourseEnrollment.get_enrollment(self.student, self.course.id) # pylint: disable=no-member
self.assertEqual(enrollment.mode, mode)
def test_get_enrollments(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertEqual(len(data), 1)
self.assertDictContainsSubset({
'mode': CourseMode.AUDIT,
'manual_enrollment': {},
'user': self.student.username,
'course_id': unicode(self.course.id), # pylint: disable=no-member
'is_active': True,
'verified_upgrade_deadline': None,
}, data[0])
self.assertEqual(
{CourseMode.VERIFIED, CourseMode.AUDIT},
{mode['slug'] for mode in data[0]['course_modes']}
)
def test_get_manual_enrollment_history(self):
ManualEnrollmentAudit.create_manual_enrollment_audit(
self.user,
self.student.email,
ENROLLED_TO_ENROLLED,
'Financial Assistance',
CourseEnrollment.objects.get(course_id=self.course.id, user=self.student) # pylint: disable=no-member
)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset({
'enrolled_by': self.user.email,
'reason': 'Financial Assistance',
}, json.loads(response.content)[0]['manual_enrollment'])
def test_change_enrollment(self):
self.assertIsNone(ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email))
response = self.client.post(self.url, data={
'course_id': unicode(self.course.id), # pylint: disable=no-member
'old_mode': CourseMode.AUDIT,
'new_mode': CourseMode.VERIFIED,
'reason': 'Financial Assistance'
})
self.assertEqual(response.status_code, 200)
self.assertIsNotNone(ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email))
self.assert_enrollment(CourseMode.VERIFIED)
@ddt.data(
({}, r"The field '\w+' is required."),
({'course_id': 'bad course key'}, 'Could not parse course key.'),
({
'course_id': 'course-v1:TestX+T101+2015',
'old_mode': CourseMode.AUDIT,
'new_mode': CourseMode.VERIFIED,
'reason': ''
}, 'Could not find enrollment for user'),
({
'course_id': None,
'old_mode': CourseMode.HONOR,
'new_mode': CourseMode.VERIFIED,
'reason': ''
}, r'User \w+ is not enrolled with mode ' + CourseMode.HONOR),
({
'course_id': None,
'old_mode': CourseMode.AUDIT,
'new_mode': CourseMode.CREDIT_MODE,
'reason': ''
}, "Specified course mode '{}' unavailable".format(CourseMode.CREDIT_MODE))
)
@ddt.unpack
def test_change_enrollment_bad_data(self, data, error_message):
# `self` isn't available from within the DDT declaration, so
# assign the course ID here
if 'course_id' in data and data['course_id'] is None:
data['course_id'] = unicode(self.course.id) # pylint: disable=no-member
response = self.client.post(self.url, data)
self.assertEqual(response.status_code, 400)
self.assertIsNotNone(re.match(error_message, response.content))
self.assert_enrollment(CourseMode.AUDIT)
self.assertIsNone(ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email))