Commit c6a6cdd7 by Tasawer Committed by Awais

Quality fixes.

ECOM-4214
parent 920fba50
......@@ -192,7 +192,7 @@ def add_enrollment(user_id, course_id, mode=None, is_active=True):
return _data_api().create_course_enrollment(user_id, course_id, mode, is_active)
def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_attributes=None):
def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_attributes=None, include_expired=False):
"""Updates the course mode for the enrolled user.
Update a course enrollment for the given user and course.
......@@ -205,6 +205,7 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_
mode (str): The new course mode for this enrollment.
is_active (bool): Sets whether the enrollment is active or not.
enrollment_attributes (list): Attributes to be set the enrollment.
include_expired (bool): Boolean denoting whether expired course modes should be included.
Returns:
A serializable dictionary representing the updated enrollment.
......@@ -241,7 +242,7 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_
"""
if mode is not None:
_validate_course_mode(course_id, mode, is_active=is_active)
_validate_course_mode(course_id, mode, is_active=is_active, include_expired=include_expired)
enrollment = _data_api().update_course_enrollment(user_id, course_id, mode=mode, is_active=is_active)
if enrollment is None:
msg = u"Course Enrollment not found for user {user} in course {course}".format(user=user_id, course=course_id)
......@@ -393,7 +394,7 @@ def _default_course_mode(course_id):
return CourseMode.DEFAULT_MODE_SLUG
def _validate_course_mode(course_id, mode, is_active=None):
def _validate_course_mode(course_id, mode, is_active=None, include_expired=False):
"""Checks to see if the specified course mode is valid for the course.
If the requested course mode is not available for the course, raise an error with corresponding
......@@ -405,6 +406,7 @@ def _validate_course_mode(course_id, mode, is_active=None):
Keyword Arguments:
is_active (bool): Whether the enrollment is to be activated or deactivated.
include_expired (bool): Boolean denoting whether expired course modes should be included.
Returns:
None
......@@ -414,7 +416,9 @@ def _validate_course_mode(course_id, mode, is_active=None):
"""
# If the client has requested an enrollment deactivation, we want to include expired modes
# in the set of available modes. This allows us to unenroll users from expired modes.
include_expired = not is_active if is_active is not None else False
# If include_expired is set as True we should not redetermine its value.
if not include_expired:
include_expired = not is_active if is_active is not None else False
course_enrollment_info = _data_api().get_course_enrollment_info(course_id, include_expired=include_expired)
course_modes = course_enrollment_info["course_modes"]
......
......@@ -21,6 +21,8 @@ _COURSES = []
_ENROLLMENT_ATTRIBUTES = []
_VERIFIED_MODE_EXPIRED = []
# pylint: disable=unused-argument
def get_course_enrollments(student_id):
......@@ -50,7 +52,7 @@ def update_course_enrollment(student_id, course_id, mode=None, is_active=None):
def get_course_enrollment_info(course_id, include_expired=False):
"""Stubbed out Enrollment data request."""
return _get_fake_course_info(course_id)
return _get_fake_course_info(course_id, include_expired)
def _get_fake_enrollment(student_id, course_id):
......@@ -60,10 +62,14 @@ def _get_fake_enrollment(student_id, course_id):
return enrollment
def _get_fake_course_info(course_id):
def _get_fake_course_info(course_id, include_expired=False):
"""Get a course from the courses array."""
# if verified mode is expired and include expired is false
# then remove the verified mode from the course.
for course in _COURSES:
if course_id == course['course_id']:
if course_id in _VERIFIED_MODE_EXPIRED and not include_expired:
course['course_modes'] = [mode for mode in course['course_modes'] if mode['slug'] != 'verified']
return course
......@@ -97,6 +103,11 @@ def get_enrollment_attributes(user_id, course_id):
return _ENROLLMENT_ATTRIBUTES
def set_expired_mode(course_id):
"""Set course verified mode as expired."""
_VERIFIED_MODE_EXPIRED.append(course_id)
def add_course(course_id, enrollment_start=None, enrollment_end=None, invite_only=False, course_modes=None):
"""Append course to the courses array."""
course_info = {
......@@ -122,3 +133,5 @@ def reset():
_COURSES = []
global _ENROLLMENTS # pylint: disable=global-statement
_ENROLLMENTS = []
global _VERIFIED_MODE_EXPIRED # pylint: disable=global-statement
_VERIFIED_MODE_EXPIRED = []
......@@ -230,3 +230,52 @@ class EnrollmentTest(CacheIsolationTestCase):
# The data matches
self.assertEqual(len(details['course_modes']), 3)
self.assertEqual(details, cached_details)
def test_update_enrollment_expired_mode_with_error(self):
""" Verify that if verified mode is expired and include expire flag is
false then enrollment cannot be updated. """
self.assert_add_modes_with_enrollment('audit')
# On updating enrollment mode to verified it should the raise the error.
with self.assertRaises(CourseModeNotFoundError):
self.assert_update_enrollment(mode='verified', include_expired=False)
def test_update_enrollment_with_expired_mode(self):
""" Verify that if verified mode is expired then enrollment can be
updated if include_expired flag is true."""
self.assert_add_modes_with_enrollment('audit')
# enrollment in verified mode will work fine with include_expired=True
self.assert_update_enrollment(mode='verified', include_expired=True)
@ddt.data(True, False)
def test_unenroll_with_expired_mode(self, include_expired):
""" Verify that un-enroll will work fine for expired courses whether include_expired
is true or false."""
self.assert_add_modes_with_enrollment('verified')
self.assert_update_enrollment(mode='verified', is_active=False, include_expired=include_expired)
def assert_add_modes_with_enrollment(self, enrollment_mode):
""" Dry method for adding fake course enrollment information to fake
data API and enroll the student in the course. """
fake_data_api.add_course(self.COURSE_ID, course_modes=['honor', 'verified', 'audit'])
result = api.add_enrollment(self.USERNAME, self.COURSE_ID, mode=enrollment_mode)
get_result = api.get_enrollment(self.USERNAME, self.COURSE_ID)
self.assertEquals(result, get_result)
# set the course verify mode as expire.
fake_data_api.set_expired_mode(self.COURSE_ID)
def assert_update_enrollment(self, mode, is_active=True, include_expired=False):
""" Dry method for updating enrollment."""
result = api.update_enrollment(
self.USERNAME, self.COURSE_ID, mode=mode, is_active=is_active, include_expired=include_expired
)
self.assertEquals(mode, result['mode'])
self.assertIsNotNone(result)
self.assertEquals(result['student'], self.USERNAME)
self.assertEquals(result['course']['course_id'], self.COURSE_ID)
self.assertEquals(result['mode'], mode)
if is_active:
self.assertTrue(result['is_active'])
else:
self.assertFalse(result['is_active'])
......@@ -2,14 +2,17 @@
Test the Data Aggregation Layer for Course Enrollments.
"""
import datetime
import unittest
import ddt
from mock import patch
from nose.tools import raises
import unittest
from pytz import UTC
from django.conf import settings
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from course_modes.models import CourseMode
from enrollment import data
from enrollment.errors import (
UserNotFoundError, CourseEnrollmentClosedError,
CourseEnrollmentFullError, CourseEnrollmentExistsError,
......@@ -17,7 +20,8 @@ from enrollment.errors import (
from openedx.core.lib.exceptions import CourseNotFoundError
from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment, EnrollmentClosedError, CourseFullError, AlreadyEnrolledError
from enrollment import data
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@ddt.ddt
......@@ -257,3 +261,34 @@ class EnrollmentDataTest(ModuleStoreTestCase):
def test_update_for_non_existent_course(self):
enrollment = data.update_course_enrollment(self.user.username, "some/fake/course", is_active=False)
self.assertIsNone(enrollment)
def test_get_course_with_expired_mode_included(self):
"""Verify that method returns expired modes if include_expired
is true."""
modes = ['honor', 'verified', 'audit']
self._create_course_modes(modes, course=self.course)
self._update_verified_mode_as_expired(self.course.id)
self.assert_enrollment_modes(modes, True)
def test_get_course_without_expired_mode_included(self):
"""Verify that method does not returns expired modes if include_expired
is false."""
self._create_course_modes(['honor', 'verified', 'audit'], course=self.course)
self._update_verified_mode_as_expired(self.course.id)
self.assert_enrollment_modes(['audit', 'honor'], False)
def _update_verified_mode_as_expired(self, course_id):
"""Dry method to change verified mode expiration."""
mode = CourseMode.objects.get(course_id=course_id, mode_slug=CourseMode.VERIFIED)
mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=UTC)
mode.save()
def assert_enrollment_modes(self, expected_modes, include_expired):
"""Get enrollment data and assert response with expected modes."""
result_course = data.get_course_enrollment_info(unicode(self.course.id), include_expired=include_expired)
result_slugs = [mode['slug'] for mode in result_course['course_modes']]
for course_mode in expected_modes:
self.assertIn(course_mode, result_slugs)
if not include_expired:
self.assertNotIn('verified', result_slugs)
......@@ -885,6 +885,37 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
self.assert_enrollment_status(username='fake-user', expected_status=status.HTTP_406_NOT_ACCEPTABLE,
as_server=True)
def test_update_enrollment_with_expired_mode_throws_error(self):
"""Verify that if verified mode is expired than it's enrollment cannot be updated. """
for mode in [CourseMode.DEFAULT_MODE_SLUG, CourseMode.VERIFIED]:
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=mode,
mode_display_name=mode,
)
# Create an enrollment
self.assert_enrollment_status(as_server=True)
# Check that the enrollment is the default.
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
self.assertTrue(is_active)
self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG)
# Change verified mode expiration.
mode = CourseMode.objects.get(course_id=self.course.id, mode_slug=CourseMode.VERIFIED)
mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=pytz.utc)
mode.save()
self.assert_enrollment_status(
as_server=True,
mode=CourseMode.VERIFIED,
expected_status=status.HTTP_400_BAD_REQUEST
)
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
self.assertTrue(is_active)
self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EnrollmentEmbargoTest(EnrollmentTestMixin, UrlResetMixin, ModuleStoreTestCase):
......
......@@ -161,7 +161,10 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase
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):
for mode in (
CourseMode.AUDIT, CourseMode.PROFESSIONAL, CourseMode.CREDIT_MODE,
CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.VERIFIED, CourseMode.HONOR
):
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) # pylint: disable=no-member
self.verification_deadline = VerificationDeadline(
......@@ -200,7 +203,8 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase
'verified_upgrade_deadline': None,
}, data[0])
self.assertEqual(
{CourseMode.VERIFIED, CourseMode.AUDIT},
{CourseMode.VERIFIED, CourseMode.AUDIT, CourseMode.HONOR,
CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.PROFESSIONAL},
{mode['slug'] for mode in data[0]['course_modes']}
)
......@@ -252,11 +256,11 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase
'reason': ''
}, r'User \w+ is not enrolled with mode ' + CourseMode.HONOR),
({
'course_id': None,
'course_id': 'course-v1:TestX+T101+2015',
'old_mode': CourseMode.AUDIT,
'new_mode': CourseMode.CREDIT_MODE,
'reason': ''
}, "Specified course mode '{}' unavailable".format(CourseMode.CREDIT_MODE))
'reason': 'Enrollment cannot be changed to credit mode'
}, '')
)
@ddt.unpack
def test_change_enrollment_bad_data(self, data, error_message):
......@@ -269,3 +273,104 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase
self.assertIsNotNone(re.match(error_message, response.content))
self.assert_enrollment(CourseMode.AUDIT)
self.assertIsNone(ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email))
@ddt.data('honor', 'audit', 'verified', 'professional', 'no-id-professional')
def test_update_enrollment_for_all_modes(self, new_mode):
""" Verify support can changed the enrollment to all available modes
except credit. """
self.assert_update_enrollment('username', new_mode)
@ddt.data('honor', 'audit', 'verified', 'professional', 'no-id-professional')
def test_update_enrollment_for_ended_course(self, new_mode):
""" Verify support can changed the enrollment of archived course. """
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. """
self.set_course_end_date_and_expiry()
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(self.student, search_string_type)}
)
response = self.client.get(url)
self._assert_generated_modes(response)
@ddt.data('username', 'email')
def test_update_enrollments_with_expired_mode(self, search_string_type):
""" Verify that enrollment can be updated to verified mode. """
self.set_course_end_date_and_expiry()
self.assertIsNone(ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email))
self.assert_update_enrollment(search_string_type, CourseMode.VERIFIED)
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) # pylint: disable=no-member
modes_data = []
for mode in modes:
expiry = mode.expiration_datetime.strftime('%Y-%m-%dT%H:%M:%SZ') if mode.expiration_datetime else None
modes_data.append({
'sku': mode.sku,
'expiration_datetime': expiry,
'name': mode.name,
'currency': mode.currency,
'bulk_sku': mode.bulk_sku,
'min_price': mode.min_price,
'suggested_prices': mode.suggested_prices,
'slug': mode.slug,
'description': mode.description
})
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertEqual(len(data), 1)
self.assertEqual(
modes_data,
data[0]['course_modes']
)
self.assertEqual(
{CourseMode.VERIFIED, CourseMode.AUDIT, CourseMode.NO_ID_PROFESSIONAL_MODE,
CourseMode.PROFESSIONAL, CourseMode.HONOR},
{mode['slug'] for mode in data[0]['course_modes']}
)
def assert_update_enrollment(self, search_string_type, new_mode):
""" Dry method to update the enrollment and assert response."""
self.assertIsNone(ManualEnrollmentAudit.get_manual_enrollment_by_email(self.student.email))
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': getattr(self.student, search_string_type)}
)
response = self.client.post(url, data={
'course_id': unicode(self.course.id), # pylint: disable=no-member
'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)
def set_course_end_date_and_expiry(self):
""" Set the course-end date and expire its verified mode."""
self.course.start = datetime(year=1970, month=1, day=1, tzinfo=UTC)
self.course.end = datetime(year=1970, month=1, day=10, tzinfo=UTC)
# change verified mode expiry.
verified_mode = CourseMode.objects.get(
course_id=self.course.id, # pylint: disable=no-member
mode_slug=CourseMode.VERIFIED
)
verified_mode.expiration_datetime = datetime(year=1970, month=1, day=9, tzinfo=UTC)
verified_mode.save()
......@@ -16,6 +16,7 @@ from course_modes.models import CourseMode
from edxmako.shortcuts import render_to_response
from enrollment.api import get_enrollments, update_enrollment
from enrollment.errors import CourseModeNotFoundError
from enrollment.serializers import ModeSerializer
from lms.djangoapps.support.decorators import require_support_permission
from lms.djangoapps.support.serializers import ManualEnrollmentSerializer
from lms.djangoapps.verify_student.models import VerificationDeadline
......@@ -61,6 +62,8 @@ class EnrollmentSupportListView(GenericAPIView):
# Folds the course_details field up into the main JSON object.
enrollment.update(**enrollment.pop('course_details'))
course_key = CourseKey.from_string(enrollment['course_id'])
# get the all courses modes and replace with existing modes.
enrollment['course_modes'] = self.get_course_modes(course_key)
# Add the price of the course's verified mode.
self.include_verified_mode_info(enrollment, course_key)
# Add manual enrollment history, if it exists
......@@ -83,6 +86,8 @@ 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(err.message))
except InvalidKeyError:
......@@ -98,7 +103,7 @@ class EnrollmentSupportListView(GenericAPIView):
# Wrapped in a transaction so that we can be sure the
# ManualEnrollmentAudit record is always created correctly.
with transaction.atomic():
update_enrollment(user.username, course_id, mode=new_mode)
update_enrollment(user.username, course_id, mode=new_mode, include_expired=True)
manual_enrollment = ManualEnrollmentAudit.create_manual_enrollment_audit(
request.user,
enrollment.user.email,
......@@ -150,3 +155,24 @@ class EnrollmentSupportListView(GenericAPIView):
if manual_enrollment_audit is None:
return {}
return ManualEnrollmentSerializer(instance=manual_enrollment_audit).data
@staticmethod
def get_course_modes(course_key):
"""
Returns a list of all modes including expired modes for a given course id
Arguments:
course_id (CourseKey): Search for course modes for this course.
Returns:
list of `Mode`
"""
course_modes = CourseMode.modes_for_course(
course_key,
include_expired=True
)
return [
ModeSerializer(mode).data
for mode in course_modes
]
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