Commit 8d9d4b85 by Ahsan Ulhaq

LMS: Modification in enrollment API

Changed enrollment api to set enrollment atributes if the course is
credit course

ECOM-1719
parent 07ce76b3
......@@ -5,8 +5,8 @@ course level, such as available course modes.
"""
from django.utils import importlib
import logging
from django.core.cache import cache
from django.conf import settings
from django.core.cache import cache
from enrollment import errors
log = logging.getLogger(__name__)
......@@ -181,7 +181,7 @@ def add_enrollment(user_id, course_id, mode='honor', 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):
def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_attributes=None):
"""Updates the course mode for the enrolled user.
Update a course enrollment for the given user and course.
......@@ -232,6 +232,10 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None):
msg = u"Course Enrollment not found for user {user} in course {course}".format(user=user_id, course=course_id)
log.warn(msg)
raise errors.EnrollmentNotFoundError(msg)
else:
if enrollment_attributes is not None:
set_enrollment_attributes(user_id, course_id, enrollment_attributes)
return enrollment
......@@ -302,6 +306,53 @@ def get_course_enrollment_details(course_id, include_expired=False):
return course_enrollment_details
def set_enrollment_attributes(user_id, course_id, attributes):
"""Set enrollment attributes for the enrollment of given user in the
course provided.
Args:
course_id (str): The Course to set enrollment attributes for.
user_id (str): The User to set enrollment attributes for.
attributes (list): Attributes to be set.
Example:
>>>set_enrollment_attributes(
"Bob",
"course-v1-edX-DemoX-1T2015",
[
{
"namespace": "credit",
"name": "provider_id",
"value": "hogwarts",
},
]
)
"""
_data_api().add_or_update_enrollment_attr(user_id, course_id, attributes)
def get_enrollment_attributes(user_id, course_id):
"""Retrieve enrollment attributes for given user for provided course.
Args:
user_id: The User to get enrollment attributes for
course_id (str): The Course to get enrollment attributes for.
Example:
>>>get_enrollment_attributes("Bob", "course-v1-edX-DemoX-1T2015")
[
{
"namespace": "credit",
"name": "provider_id",
"value": "hogwarts",
},
]
Returns: list
"""
return _data_api().get_enrollment_attributes(user_id, course_id)
def _validate_course_mode(course_id, mode):
"""Checks to see if the specified course mode is valid for the course.
......
......@@ -9,12 +9,12 @@ from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore
from enrollment.errors import (
CourseNotFoundError, CourseEnrollmentClosedError, CourseEnrollmentFullError,
CourseEnrollmentExistsError, UserNotFoundError,
CourseEnrollmentExistsError, UserNotFoundError, InvalidEnrollmentAttribute
)
from enrollment.serializers import CourseEnrollmentSerializer, CourseField
from student.models import (
CourseEnrollment, NonExistentCourseError, EnrollmentClosedError,
CourseFullError, AlreadyEnrolledError,
CourseFullError, AlreadyEnrolledError, CourseEnrollmentAttribute
)
log = logging.getLogger(__name__)
......@@ -136,12 +136,112 @@ def update_course_enrollment(username, course_id, mode=None, is_active=None):
return None
def add_or_update_enrollment_attr(user_id, course_id, attributes):
"""Set enrollment attributes for the enrollment of given user in the
course provided.
Args:
course_id (str): The Course to set enrollment attributes for.
user_id (str): The User to set enrollment attributes for.
attributes (list): Attributes to be set.
Example:
>>>add_or_update_enrollment_attr(
"Bob",
"course-v1-edX-DemoX-1T2015",
[
{
"namespace": "credit",
"name": "provider_id",
"value": "hogwarts",
},
]
)
"""
course_key = CourseKey.from_string(course_id)
user = _get_user(user_id)
enrollment = CourseEnrollment.get_enrollment(user, course_key)
if not _invalid_attribute(attributes) and enrollment is not None:
CourseEnrollmentAttribute.add_enrollment_attr(enrollment, attributes)
def get_enrollment_attributes(user_id, course_id):
"""Retrieve enrollment attributes for given user for provided course.
Args:
user_id: The User to get enrollment attributes for
course_id (str): The Course to get enrollment attributes for.
Example:
>>>get_enrollment_attributes("Bob", "course-v1-edX-DemoX-1T2015")
[
{
"namespace": "credit",
"name": "provider_id",
"value": "hogwarts",
},
]
Returns: list
"""
course_key = CourseKey.from_string(course_id)
user = _get_user(user_id)
enrollment = CourseEnrollment.get_enrollment(user, course_key)
return CourseEnrollmentAttribute.get_enrollment_attributes(enrollment)
def _get_user(user_id):
"""Retrieve user with provided user_id
Args:
user_id(str): username of the user for which object is to retrieve
Returns: obj
"""
try:
return User.objects.get(username=user_id)
except User.DoesNotExist:
msg = u"Not user with username '{username}' found.".format(username=user_id)
log.warn(msg)
raise UserNotFoundError(msg)
def _update_enrollment(enrollment, is_active=None, mode=None):
enrollment.update_enrollment(is_active=is_active, mode=mode)
enrollment.save()
return CourseEnrollmentSerializer(enrollment).data # pylint: disable=no-member
def _invalid_attribute(attributes):
"""Validate enrollment attribute
Args:
attributes(dict): dict of attribute
Return:
list of invalid attributes
"""
invalid_attributes = []
for attribute in attributes:
if "namespace" not in attribute:
msg = u"'namespace' not in enrollment attribute"
log.warn(msg)
invalid_attributes.append("namespace")
raise InvalidEnrollmentAttribute(msg)
if "name" not in attribute:
msg = u"'name' not in enrollment attribute"
log.warn(msg)
invalid_attributes.append("name")
raise InvalidEnrollmentAttribute(msg)
if "value" not in attribute:
msg = u"'value' not in enrollment attribute"
log.warn(msg)
invalid_attributes.append("value")
raise InvalidEnrollmentAttribute(msg)
return invalid_attributes
def get_course_enrollment_info(course_id, include_expired=False):
"""Returns all course enrollment information for the given course.
......
......@@ -50,3 +50,8 @@ class EnrollmentNotFoundError(CourseEnrollmentError):
class EnrollmentApiLoadError(CourseEnrollmentError):
"""The data API could not be loaded."""
pass
class InvalidEnrollmentAttribute(CourseEnrollmentError):
"""Enrollment Attributes could not be validated"""
pass
......@@ -19,6 +19,8 @@ _ENROLLMENTS = []
_COURSES = []
_ENROLLMENT_ATTRIBUTES = []
# pylint: disable=unused-argument
def get_course_enrollments(student_id):
......@@ -78,6 +80,23 @@ def add_enrollment(student_id, course_id, is_active=True, mode='honor'):
return enrollment
# pylint: disable=unused-argument
def add_or_update_enrollment_attr(user_id, course_id, attributes):
"""Add or update enrollment attribute array"""
for attribute in attributes:
_ENROLLMENT_ATTRIBUTES.append({
'namespace': attribute['namespace'],
'name': attribute['name'],
'value': attribute['value']
})
# pylint: disable=unused-argument
def get_enrollment_attributes(user_id, course_id):
"""Retrieve enrollment attribute array"""
return _ENROLLMENT_ATTRIBUTES
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 = {
......
......@@ -143,6 +143,29 @@ class EnrollmentTest(TestCase):
result = api.update_enrollment(self.USERNAME, self.COURSE_ID, mode='verified')
self.assertEquals('verified', result['mode'])
def test_update_enrollment_attributes(self):
# Add fake course enrollment information to the fake data API
fake_data_api.add_course(self.COURSE_ID, course_modes=['honor', 'verified', 'audit', 'credit'])
# Enroll in the course and verify the URL we get sent to
result = api.add_enrollment(self.USERNAME, self.COURSE_ID, mode='audit')
get_result = api.get_enrollment(self.USERNAME, self.COURSE_ID)
self.assertEquals(result, get_result)
enrollment_attributes = [
{
"namespace": "credit",
"name": "provider_id",
"value": "hogwarts",
}
]
result = api.update_enrollment(
self.USERNAME, self.COURSE_ID, mode='credit', enrollment_attributes=enrollment_attributes
)
self.assertEquals('credit', result['mode'])
attributes = api.get_enrollment_attributes(self.USERNAME, self.COURSE_ID)
self.assertEquals(enrollment_attributes[0], attributes[0])
def test_get_course_details(self):
# Add a fake course enrollment information to the fake data API
fake_data_api.add_course(self.COURSE_ID, course_modes=['honor', 'verified', 'audit'])
......
......@@ -170,6 +170,45 @@ class EnrollmentDataTest(ModuleStoreTestCase):
self.assertEqual(self.user.username, result['user'])
self.assertEqual(enrollment, result)
@ddt.data(
# Default (no course modes in the database)
# Expect that users are automatically enrolled as "honor".
([], 'credit'),
# Audit / Verified / Honor
# We should always go to the "choose your course" page.
# We should also be enrolled as "honor" by default.
(['honor', 'verified', 'audit', 'credit'], 'credit'),
)
@ddt.unpack
def test_add_or_update_enrollment_attr(self, course_modes, enrollment_mode):
# Create the course modes (if any) required for this test case
self._create_course_modes(course_modes)
data.create_course_enrollment(self.user.username, unicode(self.course.id), enrollment_mode, True)
enrollment_attributes = [
{
"namespace": "credit",
"name": "provider_id",
"value": "hogwarts",
}
]
data.add_or_update_enrollment_attr(self.user.username, unicode(self.course.id), enrollment_attributes)
enrollment_attr = data.get_enrollment_attributes(self.user.username, unicode(self.course.id))
self.assertEqual(enrollment_attr[0], enrollment_attributes[0])
enrollment_attributes = [
{
"namespace": "credit",
"name": "provider_id",
"value": "ASU",
}
]
data.add_or_update_enrollment_attr(self.user.username, unicode(self.course.id), enrollment_attributes)
enrollment_attr = data.get_enrollment_attributes(self.user.username, unicode(self.course.id))
self.assertEqual(enrollment_attr[0], enrollment_attributes[0])
@raises(CourseNotFoundError)
def test_non_existent_course(self):
data.get_course_enrollment_info("this/is/bananas")
......
......@@ -46,6 +46,7 @@ class EnrollmentTestMixin(object):
as_server=False,
mode=CourseMode.HONOR,
is_active=None,
enrollment_attributes=None,
):
"""
Enroll in the course and verify the response's status code. If the expected status is 200, also validates
......@@ -62,7 +63,8 @@ class EnrollmentTestMixin(object):
'course_details': {
'course_id': course_id
},
'user': username
'user': username,
'enrollment_attributes': enrollment_attributes
}
if is_active is not None:
......@@ -547,6 +549,78 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
self.assertTrue(is_active)
self.assertEqual(course_mode, CourseMode.VERIFIED)
def test_enrollment_with_credit_mode(self):
"""With the right API key, update an existing enrollment with credit
mode and set enrollment attributes.
"""
for mode in [CourseMode.HONOR, CourseMode.CREDIT_MODE]:
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 honor.
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.HONOR)
# Check that the enrollment upgraded to credit.
enrollment_attributes = [{
"namespace": "credit",
"name": "provider_id",
"value": "hogwarts",
}]
self.assert_enrollment_status(
as_server=True,
mode=CourseMode.CREDIT_MODE,
expected_status=status.HTTP_200_OK,
enrollment_attributes=enrollment_attributes
)
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
self.assertTrue(is_active)
self.assertEqual(course_mode, CourseMode.CREDIT_MODE)
def test_enrollment_with_invalid_attr(self):
"""Check response status is bad request when invalid enrollment
attributes are passed
"""
for mode in [CourseMode.HONOR, CourseMode.CREDIT_MODE]:
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 honor.
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.HONOR)
# Check that the enrollment upgraded to credit.
enrollment_attributes = [{
"namespace": "credit",
"name": "invalid",
"value": "hogwarts",
}]
self.assert_enrollment_status(
as_server=True,
mode=CourseMode.CREDIT_MODE,
expected_status=status.HTTP_400_BAD_REQUEST,
enrollment_attributes=enrollment_attributes
)
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
self.assertTrue(is_active)
self.assertEqual(course_mode, CourseMode.HONOR)
def test_downgrade_enrollment_with_mode(self):
"""With the right API key, downgrade an existing enrollment with a new mode. """
# Create an honor and verified mode for a course. This allows an update.
......
......@@ -5,7 +5,6 @@ consist primarily of authentication, request validation, and serialization.
"""
import logging
from ipware.ip import get_ip
from django.core.exceptions import ObjectDoesNotExist
from django.utils.decorators import method_decorator
from opaque_keys import InvalidKeyError
......@@ -33,7 +32,11 @@ from enrollment.errors import (
)
from student.models import User
log = logging.getLogger(__name__)
REQUIRED_ATTRIBUTES = {
"credit": ["credit:provider_id"],
}
class EnrollmentCrossDomainSessionAuth(SessionAuthenticationAllowInactiveUser, SessionAuthenticationCrossDomainCsrf):
......@@ -264,9 +267,13 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
If honor mode is not supported for the course, the request fails and returns the available modes.
A server-to-server call can be used by this command to enroll a user in other modes, such as "verified"
or "professional". If the mode is not supported for the course, the request will fail and return the
available modes.
A server-to-server call can be used by this command to enroll a user in other modes, such as "verified",
"professional" or "credit". If the mode is not supported for the course, the request will fail and
return the available modes.
You can include other parameters as enrollment attributes for specific course mode as needed. For
example, for credit mode, you can include parameters namespace:'credit', name:'provider_id',
value:'UniversityX' to specify credit provider attribute.
**Example Requests**:
......@@ -274,6 +281,12 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
POST /api/enrollment/v1/enrollment{"mode": "honor", "course_details":{"course_id": "edX/DemoX/Demo_Course"}}
POST /api/enrollment/v1/enrollment{
"mode": "credit",
"course_details":{"course_id": "edX/DemoX/Demo_Course"},
"enrollment_attributes":[{"namespace": "credit","name": "provider_id","value": "hogwarts",},]
}
**Post Parameters**
* user: The username of the currently logged in user. Optional.
......@@ -292,6 +305,12 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
* email_opt_in: A Boolean indicating whether the user
wishes to opt into email from the organization running this course. Optional.
* enrollment_attributes: A list of dictionary that contains:
* namespace: Namespace of the attribute
* name: Name of the attribute
* value: Value of the attribute
**Response Values**
A collection of course enrollments for the user, or for the newly created enrollment.
......@@ -335,7 +354,6 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
* user: The username of the user.
"""
authentication_classes = OAuth2AuthenticationAllowInactiveUser, EnrollmentCrossDomainSessionAuth
permission_classes = ApiKeyHeaderPermissionIsAuthenticated,
throttle_classes = EnrollmentUserThrottle,
......@@ -370,6 +388,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
go through `add_enrollment()`, which allows creation of new and reactivation of old enrollments.
"""
# Get the User, Course ID, and Mode from the request.
username = request.DATA.get('user', request.user.username)
course_id = request.DATA.get('course_details', {}).get('course_id')
......@@ -438,9 +457,17 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
}
)
enrollment_attributes = request.DATA.get('enrollment_attributes')
enrollment = api.get_enrollment(username, unicode(course_id))
mode_changed = enrollment and mode is not None and enrollment['mode'] != mode
active_changed = enrollment and is_active is not None and enrollment['is_active'] != is_active
missing_attrs = []
if enrollment_attributes:
actual_attrs = [
u"{namespace}:{name}".format(**attr)
for attr in enrollment_attributes
]
missing_attrs = set(REQUIRED_ATTRIBUTES.get(mode, [])) - set(actual_attrs)
if has_api_key_permissions and (mode_changed or active_changed):
if mode_changed and active_changed and not is_active:
# if the requester wanted to deactivate but specified the wrong mode, fail
......@@ -451,7 +478,21 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
)
log.warning(msg)
return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg})
response = api.update_enrollment(username, unicode(course_id), mode=mode, is_active=is_active)
if len(missing_attrs) > 0:
msg = u"Missing enrollment attributes: requested mode={} required attributes={}".format(
mode, REQUIRED_ATTRIBUTES.get(mode)
)
log.warning(msg)
return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg})
response = api.update_enrollment(
username,
unicode(course_id),
mode=mode,
is_active=is_active,
enrollment_attributes=enrollment_attributes
)
else:
# Will reactivate inactive enrollments.
response = api.add_enrollment(username, unicode(course_id), mode=mode, is_active=is_active)
......
......@@ -1854,3 +1854,47 @@ class CourseEnrollmentAttribute(models.Model):
name=self.name,
value=self.value,
)
@classmethod
def add_enrollment_attr(cls, enrollment, data_list):
"""Delete all the enrollment attributes for the given enrollment and
add new attributes.
Args:
enrollment(CourseEnrollment): 'CourseEnrollment' for which attribute is to be added
data(list): list of dictionaries containing data to save
"""
cls.objects.filter(enrollment=enrollment).delete()
attributes = [
cls(enrollment=enrollment, namespace=data['namespace'], name=data['name'], value=data['value'])
for data in data_list
]
cls.objects.bulk_create(attributes)
@classmethod
def get_enrollment_attributes(cls, enrollment):
"""Retrieve list of all enrollment attributes.
Args:
enrollment(CourseEnrollment): 'CourseEnrollment' for which list is to retrieve
Returns: list
Example:
>>> CourseEnrollmentAttribute.get_enrollment_attributes(CourseEnrollment)
[
{
"namespace": "credit",
"name": "provider_id",
"value": "hogwarts",
},
]
"""
return [
{
"namespace": attribute.namespace,
"name": attribute.name,
"value": attribute.value,
}
for attribute in cls.objects.filter(enrollment=enrollment)
]
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