From 0d1d9190e5b822acebb8fc274000504eb5776637 Mon Sep 17 00:00:00 2001 From: Ali-D-Akbar Date: Thu, 3 Jun 2021 01:01:08 +0500 Subject: [PATCH] feat: add a create enrollment endpoint for support tools --- .../static/support/js/models/enrollment.js | 2 +- .../support/js/spec/models/enrollment_spec.js | 2 +- .../js/spec/views/enrollment_modal_spec.js | 2 +- lms/djangoapps/support/tests/test_views.py | 61 ++++++++++++++++--- lms/djangoapps/support/views/enrollments.py | 53 ++++++++++++++++ 5 files changed, 110 insertions(+), 10 deletions(-) diff --git a/lms/djangoapps/support/static/support/js/models/enrollment.js b/lms/djangoapps/support/static/support/js/models/enrollment.js index 5bfa2f7d1b..fc4db98b33 100644 --- a/lms/djangoapps/support/static/support/js/models/enrollment.js +++ b/lms/djangoapps/support/static/support/js/models/enrollment.js @@ -5,7 +5,7 @@ updateEnrollment: function(new_mode, reason) { return $.ajax({ url: this.url(), - type: 'POST', + type: 'PATCH', contentType: 'application/json', data: JSON.stringify({ course_id: this.get('course_id'), diff --git a/lms/djangoapps/support/static/support/js/spec/models/enrollment_spec.js b/lms/djangoapps/support/static/support/js/spec/models/enrollment_spec.js index ccd2cb934c..6beaf69dd2 100644 --- a/lms/djangoapps/support/static/support/js/spec/models/enrollment_spec.js +++ b/lms/djangoapps/support/static/support/js/spec/models/enrollment_spec.js @@ -22,7 +22,7 @@ define([ reason: 'Financial Assistance' }; enrollment.updateEnrollment('verified', 'Financial Assistance'); - AjaxHelpers.expectJsonRequest(requests, 'POST', '/support/enrollment/test-user', { + AjaxHelpers.expectJsonRequest(requests, 'PATCH', '/support/enrollment/test-user', { course_id: EnrollmentHelpers.TEST_COURSE, new_mode: 'verified', old_mode: 'audit', diff --git a/lms/djangoapps/support/static/support/js/spec/views/enrollment_modal_spec.js b/lms/djangoapps/support/static/support/js/spec/views/enrollment_modal_spec.js index be9c9972a3..c22b2ac2e5 100644 --- a/lms/djangoapps/support/static/support/js/spec/views/enrollment_modal_spec.js +++ b/lms/djangoapps/support/static/support/js/spec/views/enrollment_modal_spec.js @@ -73,7 +73,7 @@ define([ $('.enrollment-new-mode').val('verified'); $('.enrollment-reason').val('Financial Assistance'); $('.enrollment-change-submit').click(); - AjaxHelpers.expectJsonRequest(requests, 'POST', '/support/enrollment/test-user', { + AjaxHelpers.expectJsonRequest(requests, 'PATCH', '/support/enrollment/test-user', { course_id: EnrollmentHelpers.TEST_COURSE, new_mode: 'verified', old_mode: 'audit', diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py index 684b760862..180c1dd89f 100644 --- a/lms/djangoapps/support/tests/test_views.py +++ b/lms/djangoapps/support/tests/test_views.py @@ -24,6 +24,7 @@ from common.djangoapps.course_modes.tests.factories import CourseModeFactory from common.djangoapps.entitlements.tests.factories import CourseEntitlementFactory from common.djangoapps.student.models import ( ENROLLED_TO_ENROLLED, + UNENROLLED_TO_ENROLLED, CourseEnrollment, CourseEnrollmentAttribute, ManualEnrollmentAudit @@ -329,18 +330,64 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase @disable_signal(signals, 'post_save') @ddt.data('username', 'email') - def test_change_enrollment(self, search_string_type): + def test_create_new_enrollment(self, search_string_type): + """ + Assert that a new enrollment is created through post request endpoint. + """ + test_user = UserFactory.create(username='newStudent', email='test2@example.com', password='test') + assert ManualEnrollmentAudit.get_manual_enrollment_by_email(test_user.email) is None + url = reverse( + 'support:enrollment_list', + kwargs={'username_or_email': getattr(test_user, search_string_type)} + ) + response = self.client.post(url, data={ + 'course_id': str(self.course.id), + 'mode': CourseMode.AUDIT, + 'reason': 'Financial Assistance' + }) + assert response.status_code == 200 + manual_enrollment = ManualEnrollmentAudit.get_manual_enrollment_by_email(test_user.email) + assert manual_enrollment is not None + assert manual_enrollment.reason == response.json()['reason'] + assert manual_enrollment.enrolled_email == 'test2@example.com' + assert manual_enrollment.state_transition == UNENROLLED_TO_ENROLLED + + @disable_signal(signals, 'post_save') + @ddt.data('username', 'email') + def test_create_existing_enrollment(self, search_string_type): + """ + Assert that a new enrollment is not created when an enrollment already exist for that course. + """ assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is None url = reverse( 'support:enrollment_list', kwargs={'username_or_email': getattr(self.student, search_string_type)} ) response = self.client.post(url, data={ + 'course_id': str(self.course.id), + 'mode': CourseMode.AUDIT, + 'reason': 'Financial Assistance' + }) + assert response.status_code == 400 + assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is None + + @disable_signal(signals, 'post_save') + @ddt.data('username', 'email') + def test_change_enrollment(self, search_string_type): + """ + Assert changing mode for an enrollment. + """ + assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is None + url = reverse( + 'support:enrollment_list', + kwargs={'username_or_email': getattr(self.student, search_string_type)} + ) + response = self.client.patch(url, data={ 'course_id': str(self.course.id), 'old_mode': CourseMode.AUDIT, 'new_mode': CourseMode.VERIFIED, 'reason': 'Financial Assistance' - }) + }, content_type='application/json') assert response.status_code == 200 assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is not None self.assert_enrollment(CourseMode.VERIFIED) @@ -365,12 +412,12 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase 'support:enrollment_list', kwargs={'username_or_email': getattr(self.student, search_string_type)} ) - response = self.client.post(url, data={ + response = self.client.patch(url, data={ 'course_id': str(self.course.id), 'old_mode': CourseMode.AUDIT, 'new_mode': CourseMode.VERIFIED, 'reason': 'Financial Assistance' - }) + }, content_type='application/json') entitlement.refresh_from_db() assert response.status_code == 200 assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is not None @@ -406,7 +453,7 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase # assign the course ID here if 'course_id' in data and data['course_id'] is None: data['course_id'] = str(self.course.id) - response = self.client.post(self.url, data) + response = self.client.patch(self.url, data, content_type='application/json') assert response.status_code == 400 assert re.match(error_message, response.content.decode('utf-8').replace("'", '').replace('"', '')) is not None @@ -485,12 +532,12 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase ['Arizona State University'], 'You are now eligible for credit from Arizona State University' ) mock_method.return_value = credit_provider - response = self.client.post(url, data={ + response = self.client.patch(url, data={ 'course_id': str(self.course.id), 'old_mode': CourseMode.AUDIT, 'new_mode': new_mode, 'reason': 'Financial Assistance' - }) + }, content_type='application/json') assert response.status_code == 200 assert ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email) is not None diff --git a/lms/djangoapps/support/views/enrollments.py b/lms/djangoapps/support/views/enrollments.py index ff314e4b84..e52858e223 100644 --- a/lms/djangoapps/support/views/enrollments.py +++ b/lms/djangoapps/support/views/enrollments.py @@ -17,6 +17,7 @@ from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.student.models import ( ENROLLED_TO_ENROLLED, + UNENROLLED_TO_ENROLLED, CourseEnrollment, CourseEnrollmentAttribute, ManualEnrollmentAudit @@ -84,6 +85,58 @@ class EnrollmentSupportListView(GenericAPIView): @method_decorator(require_support_permission) def post(self, request, username_or_email): + """ + Allows support staff to create a user's enrollment. + """ + try: + course_id = request.data['course_id'] + course_key = CourseKey.from_string(course_id) + mode = request.data['mode'] + reason = request.data['reason'] + user = User.objects.get(Q(username=username_or_email) | Q(email=username_or_email)) + except KeyError as err: + return HttpResponseBadRequest(f'The field {str(err)} is required.') + except InvalidKeyError: + return HttpResponseBadRequest('Could not parse course key.') + except User.DoesNotExist: + return HttpResponseBadRequest( + 'Could not find user {username}.'.format( + username=username_or_email + ) + ) + + enrollment = CourseEnrollment.get_enrollment(user=user, course_key=course_key) + if enrollment is not None: + return HttpResponseBadRequest( + f'The user {str(username_or_email)} is already enrolled in {str(course_id)}.' + ) + + enrollment_modes = [ + enrollment_mode['slug'] + for enrollment_mode in self.get_course_modes(course_key) + ] + if mode not in enrollment_modes: + return HttpResponseBadRequest( + f'{str(mode)} is not a valid mode for {str(course_id)}. ' + f'Possible valid modes are {str(enrollment_modes)}' + ) + + enrollment = CourseEnrollment.enroll(user=user, course_key=course_key, mode=mode) + + # Wrapped in a transaction so that we can be sure the + # ManualEnrollmentAudit record is always created correctly. + with transaction.atomic(): + manual_enrollment = ManualEnrollmentAudit.create_manual_enrollment_audit( + request.user, + enrollment.user.email, + UNENROLLED_TO_ENROLLED, + reason=reason, + enrollment=enrollment + ) + return JsonResponse(ManualEnrollmentSerializer(instance=manual_enrollment).data) + + @method_decorator(require_support_permission) + def patch(self, request, username_or_email): """Allows support staff to alter a user's enrollment.""" try: user = User.objects.get(Q(username=username_or_email) | Q(email=username_or_email))