diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index c03b164e37..85c4db1a43 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -333,7 +333,9 @@ class CourseMode(models.Model): @classmethod @request_cached(CACHE_NAMESPACE) - def modes_for_course(cls, course_id=None, include_expired=False, only_selectable=True, course=None): + def modes_for_course( + cls, course_id=None, include_expired=False, only_selectable=True, course=None, exclude_credit=True + ): """ Returns a list of the non-expired modes for a given course id @@ -384,7 +386,7 @@ class CourseMode(models.Model): if only_selectable: if course is not None and hasattr(course, 'selectable_modes'): found_course_modes = course.selectable_modes - else: + elif exclude_credit: found_course_modes = found_course_modes.exclude(mode_slug__in=cls.CREDIT_MODES) modes = ([mode.to_tuple() for mode in found_course_modes]) diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py index 1ec44bd791..092b0db596 100644 --- a/lms/djangoapps/support/tests/test_views.py +++ b/lms/djangoapps/support/tests/test_views.py @@ -15,13 +15,14 @@ import six from django.contrib.auth.models import User from django.db.models import signals from django.urls import reverse +from mock import patch from pytz import UTC from common.test.utils import disable_signal 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 ENROLLED_TO_ENROLLED, CourseEnrollment, ManualEnrollmentAudit +from student.models import ENROLLED_TO_ENROLLED, CourseEnrollment, CourseEnrollmentAttribute, ManualEnrollmentAudit from student.roles import GlobalStaff, SupportStaffRole from student.tests.factories import CourseEnrollmentFactory, UserFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase @@ -257,7 +258,8 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase }, data[0]) self.assertEqual( {CourseMode.VERIFIED, CourseMode.AUDIT, CourseMode.HONOR, - CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.PROFESSIONAL}, + CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.PROFESSIONAL, + CourseMode.CREDIT_MODE}, {mode['slug'] for mode in data[0]['course_modes']} ) @@ -329,10 +331,9 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase self.assertIsNone(ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email)) @disable_signal(signals, 'post_save') - @ddt.data('honor', 'audit', 'verified', 'professional', 'no-id-professional') + @ddt.data('honor', 'audit', 'verified', 'professional', 'no-id-professional', 'credit') def test_update_enrollment_for_all_modes(self, new_mode): - """ Verify support can changed the enrollment to all available modes - except credit. """ + """ Verify support can changed the enrollment to all available modes""" self.assert_update_enrollment('username', new_mode) @disable_signal(signals, 'post_save') @@ -342,10 +343,6 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase self.set_course_end_date_and_expiry() self.assert_update_enrollment('username', new_mode) - def test_update_enrollment_with_credit_mode_throws_error(self): - """ Verify that enrollment cannot be changed to credit mode. """ - self.assert_update_enrollment('username', CourseMode.CREDIT_MODE) - @ddt.data('username', 'email') def test_get_enrollments_with_expired_mode(self, search_string_type): """ Verify that page can get the all modes with archived course. """ @@ -367,7 +364,7 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase def _assert_generated_modes(self, response): """Dry method to generate course modes dict and test with response data.""" - modes = CourseMode.modes_for_course(self.course.id, include_expired=True) + modes = CourseMode.modes_for_course(self.course.id, include_expired=True, exclude_credit=False) modes_data = [] for mode in modes: expiry = mode.expiration_datetime.strftime('%Y-%m-%dT%H:%M:%SZ') if mode.expiration_datetime else None @@ -394,7 +391,7 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase self.assertEqual( {CourseMode.VERIFIED, CourseMode.AUDIT, CourseMode.NO_ID_PROFESSIONAL_MODE, - CourseMode.PROFESSIONAL, CourseMode.HONOR}, + CourseMode.PROFESSIONAL, CourseMode.HONOR, CourseMode.CREDIT_MODE}, {mode['slug'] for mode in data[0]['course_modes']} ) @@ -405,19 +402,25 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase 'support:enrollment_list', kwargs={'username_or_email': getattr(self.student, search_string_type)} ) - response = self.client.post(url, data={ - 'course_id': six.text_type(self.course.id), - 'old_mode': CourseMode.AUDIT, - 'new_mode': new_mode, - 'reason': 'Financial Assistance' - }) - # Enrollment cannot be changed to credit mode. - if new_mode == CourseMode.CREDIT_MODE: - self.assertEqual(response.status_code, 400) - else: - self.assertEqual(response.status_code, 200) - self.assertIsNotNone(ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email)) - self.assert_enrollment(new_mode) + + with patch('support.views.enrollments.get_credit_provider_attribute_values') as mock_method: + credit_provider = ( + [u'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={ + 'course_id': six.text_type(self.course.id), + 'old_mode': CourseMode.AUDIT, + 'new_mode': new_mode, + 'reason': 'Financial Assistance' + }) + + self.assertEqual(response.status_code, 200) + self.assertIsNotNone(ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email)) + self.assert_enrollment(new_mode) + if new_mode == 'credit': + enrollment_attr = CourseEnrollmentAttribute.objects.first() + self.assertEqual(enrollment_attr.value, unicode(credit_provider[0])) def set_course_end_date_and_expiry(self): """ Set the course-end date and expire its verified mode.""" diff --git a/lms/djangoapps/support/views/enrollments.py b/lms/djangoapps/support/views/enrollments.py index 54cec1e0f2..3c7d7aa092 100644 --- a/lms/djangoapps/support/views/enrollments.py +++ b/lms/djangoapps/support/views/enrollments.py @@ -21,10 +21,11 @@ from edxmako.shortcuts import render_to_response from lms.djangoapps.support.decorators import require_support_permission from lms.djangoapps.support.serializers import ManualEnrollmentSerializer from lms.djangoapps.verify_student.models import VerificationDeadline +from openedx.core.djangoapps.credit.email_utils import get_credit_provider_attribute_values from openedx.core.djangoapps.enrollments.api import get_enrollments, update_enrollment from openedx.core.djangoapps.enrollments.errors import CourseModeNotFoundError from openedx.core.djangoapps.enrollments.serializers import ModeSerializer -from student.models import ENROLLED_TO_ENROLLED, CourseEnrollment, ManualEnrollmentAudit +from student.models import ENROLLED_TO_ENROLLED, CourseEnrollment, CourseEnrollmentAttribute, ManualEnrollmentAudit from util.json_request import JsonResponse @@ -94,8 +95,6 @@ class EnrollmentSupportListView(GenericAPIView): username=user.username, old_mode=old_mode )) - if new_mode == CourseMode.CREDIT_MODE: - return HttpResponseBadRequest(u'Enrollment cannot be changed to credit mode.') except KeyError as err: return HttpResponseBadRequest(u'The field {} is required.'.format(text_type(err))) except InvalidKeyError: @@ -119,6 +118,16 @@ class EnrollmentSupportListView(GenericAPIView): reason=reason, enrollment=enrollment ) + if new_mode == CourseMode.CREDIT_MODE: + provider_ids = get_credit_provider_attribute_values(course_key, 'id') + credit_provider_attr = { + 'namespace': 'credit', + 'name': 'provider_id', + 'value': provider_ids[0], + } + CourseEnrollmentAttribute.add_enrollment_attr( + enrollment=enrollment, data_list=[credit_provider_attr] + ) return JsonResponse(ManualEnrollmentSerializer(instance=manual_enrollment).data) except CourseModeNotFoundError as err: return HttpResponseBadRequest(text_type(err)) @@ -178,7 +187,8 @@ class EnrollmentSupportListView(GenericAPIView): """ course_modes = CourseMode.modes_for_course( course_key, - include_expired=True + include_expired=True, + exclude_credit=False ) return [ ModeSerializer(mode).data