Commit 22a18e35 by stephensanchez

Updating the design of the DRF views for enrollments.

Consolidate PUT and POST on the RESTful Layer.

Change URLs for API

Test cleanup.

Adding a course details URL to the enrollment API.

Change student to user

Change to v1, remove feature flag from API URLs

Updating student to user in tests

Re-ordering redirect urls to be evaluated last.

Adding pagination and testing.

Adding Django REST settings for pagination.

Revert "Re-ordering redirect urls to be evaluated last."

This reverts commit 4c9502daa383e49b46f8abec5456c271e5e24ccb.

Re-ordering redirect urls to be evaluated last.

Conflicts:
	common/djangoapps/enrollment/urls.py

Revert "Adding Django REST settings for pagination."

This reverts commit 9f8a54c41f34caa24818c88f1e75ac59f6ce5259.

Conflicts:
	common/djangoapps/enrollment/urls.py

Revert "Adding pagination and testing."

This reverts commit 0b2d46262abb78f5ad170700205e7fd28b6af942.

Additional testing, logging, and error messages.
parent a07df1a6
...@@ -6,50 +6,24 @@ course level, such as available course modes. ...@@ -6,50 +6,24 @@ course level, such as available course modes.
from django.utils import importlib from django.utils import importlib
import logging import logging
from django.conf import settings from django.conf import settings
from enrollment import errors
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CourseEnrollmentError(Exception):
"""Generic Course Enrollment Error.
Describes any error that may occur when reading or updating enrollment information for a student or a course.
"""
def __init__(self, msg, data=None):
super(CourseEnrollmentError, self).__init__(msg)
# Corresponding information to help resolve the error.
self.data = data
class CourseModeNotFoundError(CourseEnrollmentError):
"""The requested course mode could not be found."""
pass
class EnrollmentNotFoundError(CourseEnrollmentError):
"""The requested enrollment could not be found."""
pass
class EnrollmentApiLoadError(CourseEnrollmentError):
"""The data API could not be loaded."""
pass
DEFAULT_DATA_API = 'enrollment.data' DEFAULT_DATA_API = 'enrollment.data'
def get_enrollments(student_id): def get_enrollments(user_id):
"""Retrieves all the courses a student is enrolled in. """Retrieves all the courses a user is enrolled in.
Takes a student and retrieves all relative enrollments. Includes information regarding how the student is enrolled Takes a user and retrieves all relative enrollments. Includes information regarding how the user is enrolled
in the the course. in the the course.
Args: Args:
student_id (str): The username of the student we want to retrieve course enrollment information for. user_id (str): The username of the user we want to retrieve course enrollment information for.
Returns: Returns:
A list of enrollment information for the given student. A list of enrollment information for the given user.
Examples: Examples:
>>> get_enrollments("Bob") >>> get_enrollments("Bob")
...@@ -58,7 +32,7 @@ def get_enrollments(student_id): ...@@ -58,7 +32,7 @@ def get_enrollments(student_id):
"created": "2014-10-20T20:18:00Z", "created": "2014-10-20T20:18:00Z",
"mode": "honor", "mode": "honor",
"is_active": True, "is_active": True,
"student": "Bob", "user": "Bob",
"course": { "course": {
"course_id": "edX/DemoX/2014T2", "course_id": "edX/DemoX/2014T2",
"enrollment_end": 2014-12-20T20:18:00Z, "enrollment_end": 2014-12-20T20:18:00Z,
...@@ -81,7 +55,7 @@ def get_enrollments(student_id): ...@@ -81,7 +55,7 @@ def get_enrollments(student_id):
"created": "2014-10-25T20:18:00Z", "created": "2014-10-25T20:18:00Z",
"mode": "verified", "mode": "verified",
"is_active": True, "is_active": True,
"student": "Bob", "user": "Bob",
"course": { "course": {
"course_id": "edX/edX-Insider/2014T2", "course_id": "edX/edX-Insider/2014T2",
"enrollment_end": 2014-12-20T20:18:00Z, "enrollment_end": 2014-12-20T20:18:00Z,
...@@ -103,16 +77,16 @@ def get_enrollments(student_id): ...@@ -103,16 +77,16 @@ def get_enrollments(student_id):
] ]
""" """
return _data_api().get_course_enrollments(student_id) return _data_api().get_course_enrollments(user_id)
def get_enrollment(student_id, course_id): def get_enrollment(user_id, course_id):
"""Retrieves all enrollment information for the student in respect to a specific course. """Retrieves all enrollment information for the user in respect to a specific course.
Gets all the course enrollment information specific to a student in a course. Gets all the course enrollment information specific to a user in a course.
Args: Args:
student_id (str): The student to get course enrollment information for. user_id (str): The user to get course enrollment information for.
course_id (str): The course to get enrollment information for. course_id (str): The course to get enrollment information for.
Returns: Returns:
...@@ -124,7 +98,7 @@ def get_enrollment(student_id, course_id): ...@@ -124,7 +98,7 @@ def get_enrollment(student_id, course_id):
"created": "2014-10-20T20:18:00Z", "created": "2014-10-20T20:18:00Z",
"mode": "honor", "mode": "honor",
"is_active": True, "is_active": True,
"student": "Bob", "user": "Bob",
"course": { "course": {
"course_id": "edX/DemoX/2014T2", "course_id": "edX/DemoX/2014T2",
"enrollment_end": 2014-12-20T20:18:00Z, "enrollment_end": 2014-12-20T20:18:00Z,
...@@ -145,17 +119,17 @@ def get_enrollment(student_id, course_id): ...@@ -145,17 +119,17 @@ def get_enrollment(student_id, course_id):
} }
""" """
return _data_api().get_course_enrollment(student_id, course_id) return _data_api().get_course_enrollment(user_id, course_id)
def add_enrollment(student_id, course_id, mode='honor', is_active=True): def add_enrollment(user_id, course_id, mode='honor', is_active=True):
"""Enrolls a student in a course. """Enrolls a user in a course.
Enrolls a student in a course. If the mode is not specified, this will default to 'honor'. Enrolls a user in a course. If the mode is not specified, this will default to 'honor'.
Args: Args:
student_id (str): The student to enroll. user_id (str): The user to enroll.
course_id (str): The course to enroll the student in. course_id (str): The course to enroll the user in.
mode (str): Optional argument for the type of enrollment to create. Ex. 'audit', 'honor', 'verified', mode (str): Optional argument for the type of enrollment to create. Ex. 'audit', 'honor', 'verified',
'professional'. If not specified, this defaults to 'honor'. 'professional'. If not specified, this defaults to 'honor'.
is_active (boolean): Optional argument for making the new enrollment inactive. If not specified, is_active is_active (boolean): Optional argument for making the new enrollment inactive. If not specified, is_active
...@@ -170,7 +144,7 @@ def add_enrollment(student_id, course_id, mode='honor', is_active=True): ...@@ -170,7 +144,7 @@ def add_enrollment(student_id, course_id, mode='honor', is_active=True):
"created": "2014-10-20T20:18:00Z", "created": "2014-10-20T20:18:00Z",
"mode": "honor", "mode": "honor",
"is_active": True, "is_active": True,
"student": "Bob", "user": "Bob",
"course": { "course": {
"course_id": "edX/DemoX/2014T2", "course_id": "edX/DemoX/2014T2",
"enrollment_end": 2014-12-20T20:18:00Z, "enrollment_end": 2014-12-20T20:18:00Z,
...@@ -191,66 +165,19 @@ def add_enrollment(student_id, course_id, mode='honor', is_active=True): ...@@ -191,66 +165,19 @@ def add_enrollment(student_id, course_id, mode='honor', is_active=True):
} }
""" """
_validate_course_mode(course_id, mode) _validate_course_mode(course_id, mode)
return _data_api().update_course_enrollment(student_id, course_id, mode=mode, is_active=is_active) return _data_api().create_course_enrollment(user_id, course_id, mode, is_active)
def deactivate_enrollment(student_id, course_id):
"""Un-enrolls a student in a course
Deactivate the enrollment of a student in a course. We will not remove the enrollment data, but simply flag it
as inactive.
Args:
student_id (str): The student associated with the deactivated enrollment.
course_id (str): The course associated with the deactivated enrollment.
Returns:
A serializable dictionary representing the deactivated course enrollment for the student.
Example:
>>> deactivate_enrollment("Bob", "edX/DemoX/2014T2")
{
"created": "2014-10-20T20:18:00Z",
"mode": "honor",
"is_active": False,
"student": "Bob",
"course": {
"course_id": "edX/DemoX/2014T2",
"enrollment_end": 2014-12-20T20:18:00Z,
"course_modes": [
{
"slug": "honor",
"name": "Honor Code Certificate",
"min_price": 0,
"suggested_prices": "",
"currency": "usd",
"expiration_datetime": null,
"description": null
}
],
"enrollment_start": 2014-10-15T20:18:00Z,
"invite_only": False
}
}
"""
# Check to see if there is an enrollment. We do not want to create a deactivated enrollment.
if not _data_api().get_course_enrollment(student_id, course_id):
raise EnrollmentNotFoundError(
u"No enrollment was found for student {student} in course {course}"
.format(student=student_id, course=course_id)
)
return _data_api().update_course_enrollment(student_id, course_id, is_active=False)
def update_enrollment(student_id, course_id, mode): def update_enrollment(user_id, course_id, mode=None, is_active=None):
"""Updates the course mode for the enrolled user. """Updates the course mode for the enrolled user.
Update a course enrollment for the given student and course. Update a course enrollment for the given user and course.
Args: Args:
student_id (str): The student associated with the updated enrollment. user_id (str): The user associated with the updated enrollment.
course_id (str): The course associated with the updated enrollment. course_id (str): The course associated with the updated enrollment.
mode (str): The new course mode for this enrollment. mode (str): The new course mode for this enrollment.
is_active (bool): Sets whether the enrollment is active or not.
Returns: Returns:
A serializable dictionary representing the updated enrollment. A serializable dictionary representing the updated enrollment.
...@@ -261,7 +188,7 @@ def update_enrollment(student_id, course_id, mode): ...@@ -261,7 +188,7 @@ def update_enrollment(student_id, course_id, mode):
"created": "2014-10-20T20:18:00Z", "created": "2014-10-20T20:18:00Z",
"mode": "honor", "mode": "honor",
"is_active": True, "is_active": True,
"student": "Bob", "user": "Bob",
"course": { "course": {
"course_id": "edX/DemoX/2014T2", "course_id": "edX/DemoX/2014T2",
"enrollment_end": 2014-12-20T20:18:00Z, "enrollment_end": 2014-12-20T20:18:00Z,
...@@ -283,7 +210,12 @@ def update_enrollment(student_id, course_id, mode): ...@@ -283,7 +210,12 @@ def update_enrollment(student_id, course_id, mode):
""" """
_validate_course_mode(course_id, mode) _validate_course_mode(course_id, mode)
return _data_api().update_course_enrollment(student_id, course_id, mode) 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)
log.warn(msg)
raise errors.EnrollmentNotFoundError(msg)
return enrollment
def get_course_enrollment_details(course_id): def get_course_enrollment_details(course_id):
...@@ -354,7 +286,7 @@ def _validate_course_mode(course_id, mode): ...@@ -354,7 +286,7 @@ def _validate_course_mode(course_id, mode):
available=", ".join(available_modes) available=", ".join(available_modes)
) )
log.warn(msg) log.warn(msg)
raise CourseModeNotFoundError(msg, course_enrollment_info) raise errors.CourseModeNotFoundError(msg, course_enrollment_info)
def _data_api(): def _data_api():
...@@ -371,4 +303,4 @@ def _data_api(): ...@@ -371,4 +303,4 @@ def _data_api():
return importlib.import_module(api_path) return importlib.import_module(api_path)
except (ImportError, ValueError): except (ImportError, ValueError):
log.exception(u"Could not load module at '{path}'".format(path=api_path)) log.exception(u"Could not load module at '{path}'".format(path=api_path))
raise EnrollmentApiLoadError(api_path) raise errors.EnrollmentApiLoadError(api_path)
...@@ -7,37 +7,40 @@ import logging ...@@ -7,37 +7,40 @@ import logging
from django.contrib.auth.models import User from django.contrib.auth.models import User
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from enrollment.errors import CourseNotFoundError, CourseEnrollmentClosedError, CourseEnrollmentFullError, \
CourseEnrollmentExistsError, UserNotFoundError
from enrollment.serializers import CourseEnrollmentSerializer, CourseField from enrollment.serializers import CourseEnrollmentSerializer, CourseField
from student.models import CourseEnrollment, NonExistentCourseError from student.models import CourseEnrollment, NonExistentCourseError, CourseEnrollmentException, EnrollmentClosedError, \
CourseFullError, AlreadyEnrolledError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def get_course_enrollments(student_id): def get_course_enrollments(user_id):
"""Retrieve a list representing all aggregated data for a student's course enrollments. """Retrieve a list representing all aggregated data for a user's course enrollments.
Construct a representation of all course enrollment data for a specific student. Construct a representation of all course enrollment data for a specific user.
Args: Args:
student_id (str): The name of the student to retrieve course enrollment information for. user_id (str): The name of the user to retrieve course enrollment information for.
Returns: Returns:
A serializable list of dictionaries of all aggregated enrollment data for a student. A serializable list of dictionaries of all aggregated enrollment data for a user.
""" """
qset = CourseEnrollment.objects.filter( qset = CourseEnrollment.objects.filter(
user__username=student_id, is_active=True user__username=user_id, is_active=True
).order_by('created') ).order_by('created')
return CourseEnrollmentSerializer(qset).data # pylint: disable=no-member return CourseEnrollmentSerializer(qset).data # pylint: disable=no-member
def get_course_enrollment(student_id, course_id): def get_course_enrollment(username, course_id):
"""Retrieve an object representing all aggregated data for a student's course enrollment. """Retrieve an object representing all aggregated data for a user's course enrollment.
Get the course enrollment information for a specific student and course. Get the course enrollment information for a specific user and course.
Args: Args:
student_id (str): The name of the student to retrieve course enrollment information for. username (str): The name of the user to retrieve course enrollment information for.
course_id (str): The course to retrieve course enrollment information for. course_id (str): The course to retrieve course enrollment information for.
Returns: Returns:
...@@ -47,22 +50,65 @@ def get_course_enrollment(student_id, course_id): ...@@ -47,22 +50,65 @@ def get_course_enrollment(student_id, course_id):
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
try: try:
enrollment = CourseEnrollment.objects.get( enrollment = CourseEnrollment.objects.get(
user__username=student_id, course_id=course_key user__username=username, course_id=course_key
) )
return CourseEnrollmentSerializer(enrollment).data # pylint: disable=no-member return CourseEnrollmentSerializer(enrollment).data # pylint: disable=no-member
except CourseEnrollment.DoesNotExist: except CourseEnrollment.DoesNotExist:
return None return None
def update_course_enrollment(student_id, course_id, mode=None, is_active=None): def create_course_enrollment(username, course_id, mode, is_active):
"""Modify a course enrollment for a student. """Create a new course enrollment for the given user.
Creates a new course enrollment for the specified user username.
Args:
username (str): The name of the user to create a new course enrollment for.
course_id (str): The course to create the course enrollment for.
mode (str): (Optional) The mode for the new enrollment.
is_active (boolean): (Optional) Determines if the enrollment is active.
Returns:
A serializable dictionary representing the new course enrollment.
Raises:
CourseNotFoundError
CourseEnrollmentFullError
EnrollmentClosedError
CourseEnrollmentExistsError
"""
course_key = CourseKey.from_string(course_id)
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
msg = u"Not user with username '{username}' found.".format(username=username)
log.warn(msg)
raise UserNotFoundError(msg)
try:
enrollment = CourseEnrollment.enroll(user, course_key, check_access=True)
return _update_enrollment(enrollment, is_active=is_active, mode=mode)
except NonExistentCourseError as err:
raise CourseNotFoundError(err.message)
except EnrollmentClosedError as err:
raise CourseEnrollmentClosedError(err.message)
except CourseFullError as err:
raise CourseEnrollmentFullError(err.message)
except AlreadyEnrolledError as err:
raise CourseEnrollmentExistsError(err.message)
def update_course_enrollment(username, course_id, mode=None, is_active=None):
"""Modify a course enrollment for a user.
Allows updates to a specific course enrollment. Allows updates to a specific course enrollment.
Args: Args:
student_id (str): The name of the student to retrieve course enrollment information for. username (str): The name of the user to retrieve course enrollment information for.
course_id (str): The course to retrieve course enrollment information for. course_id (str): The course to retrieve course enrollment information for.
mode (str): (Optional) The mode for the new enrollment. mode (str): (Optional) If specified, modify the mode for this enrollment.
is_active (boolean): (Optional) Determines if the enrollment is active. is_active (boolean): (Optional) Determines if the enrollment is active.
Returns: Returns:
...@@ -70,12 +116,22 @@ def update_course_enrollment(student_id, course_id, mode=None, is_active=None): ...@@ -70,12 +116,22 @@ def update_course_enrollment(student_id, course_id, mode=None, is_active=None):
""" """
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
student = User.objects.get(username=student_id)
if not CourseEnrollment.is_enrolled(student, course_key):
enrollment = CourseEnrollment.enroll(student, course_key, check_access=True)
else:
enrollment = CourseEnrollment.objects.get(user=student, course_id=course_key)
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
msg = u"Not user with username '{username}' found.".format(username=username)
log.warn(msg)
raise UserNotFoundError(msg)
try:
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_key)
return _update_enrollment(enrollment, is_active=is_active, mode=mode)
except CourseEnrollment.DoesNotExist:
return None
def _update_enrollment(enrollment, is_active=None, mode=None):
enrollment.update_enrollment(is_active=is_active, mode=mode) enrollment.update_enrollment(is_active=is_active, mode=mode)
enrollment.save() enrollment.save()
return CourseEnrollmentSerializer(enrollment).data # pylint: disable=no-member return CourseEnrollmentSerializer(enrollment).data # pylint: disable=no-member
...@@ -92,13 +148,14 @@ def get_course_enrollment_info(course_id): ...@@ -92,13 +148,14 @@ def get_course_enrollment_info(course_id):
Returns: Returns:
A serializable dictionary representing the course's enrollment information. A serializable dictionary representing the course's enrollment information.
Raises:
CourseNotFoundError
""" """
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
course = modulestore().get_course(course_key) course = modulestore().get_course(course_key)
if course is None: if course is None:
log.warning( msg = u"Requested enrollment information for unknown course {course}".format(course=course_id)
u"Requested enrollment information for unknown course {course}" log.warning(msg)
.format(course=course_id) raise CourseNotFoundError(msg)
)
raise NonExistentCourseError
return CourseField().to_native(course) return CourseField().to_native(course)
"""All Error Types pertaining to Enrollment."""
class CourseEnrollmentError(Exception):
"""Generic Course Enrollment Error.
Describes any error that may occur when reading or updating enrollment information for a user or a course.
"""
def __init__(self, msg, data=None):
super(CourseEnrollmentError, self).__init__(msg)
# Corresponding information to help resolve the error.
self.data = data
class CourseNotFoundError(CourseEnrollmentError):
pass
class UserNotFoundError(CourseEnrollmentError):
pass
class CourseEnrollmentClosedError(CourseEnrollmentError):
pass
class CourseEnrollmentFullError(CourseEnrollmentError):
pass
class CourseEnrollmentExistsError(CourseEnrollmentError):
pass
class CourseModeNotFoundError(CourseEnrollmentError):
"""The requested course mode could not be found."""
pass
class EnrollmentNotFoundError(CourseEnrollmentError):
"""The requested enrollment could not be found."""
pass
class EnrollmentApiLoadError(CourseEnrollmentError):
"""The data API could not be loaded."""
pass
...@@ -53,8 +53,12 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer): ...@@ -53,8 +53,12 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
the Course Descriptor and course modes, to give a complete representation of course enrollment. the Course Descriptor and course modes, to give a complete representation of course enrollment.
""" """
course = CourseField() course_details = serializers.SerializerMethodField('get_course_details')
student = serializers.SerializerMethodField('get_username') user = serializers.SerializerMethodField('get_username')
def get_course_details(self, model):
field = CourseField()
return field.to_native(model.course)
def get_username(self, model): def get_username(self, model):
"""Retrieves the username from the associated model.""" """Retrieves the username from the associated model."""
...@@ -62,7 +66,7 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer): ...@@ -62,7 +66,7 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
class Meta: # pylint: disable=missing-docstring class Meta: # pylint: disable=missing-docstring
model = CourseEnrollment model = CourseEnrollment
fields = ('created', 'mode', 'is_active', 'course', 'student') fields = ('created', 'mode', 'is_active', 'course_details', 'user')
lookup_field = 'username' lookup_field = 'username'
......
...@@ -31,14 +31,17 @@ def get_course_enrollment(student_id, course_id): ...@@ -31,14 +31,17 @@ def get_course_enrollment(student_id, course_id):
return _get_fake_enrollment(student_id, course_id) return _get_fake_enrollment(student_id, course_id)
def create_course_enrollment(student_id, course_id, mode='honor', is_active=True):
"""Stubbed out Enrollment creation request. """
return add_enrollment(student_id, course_id, mode=mode, is_active=is_active)
def update_course_enrollment(student_id, course_id, mode=None, is_active=None): def update_course_enrollment(student_id, course_id, mode=None, is_active=None):
"""Stubbed out Enrollment data request.""" """Stubbed out Enrollment data request."""
enrollment = _get_fake_enrollment(student_id, course_id) enrollment = _get_fake_enrollment(student_id, course_id)
if not enrollment: if enrollment and mode is not None:
enrollment = add_enrollment(student_id, course_id)
if mode is not None:
enrollment['mode'] = mode enrollment['mode'] = mode
if is_active is not None: if enrollment and is_active is not None:
enrollment['is_active'] = is_active enrollment['is_active'] = is_active
return enrollment return enrollment
......
...@@ -8,6 +8,7 @@ from django.test import TestCase ...@@ -8,6 +8,7 @@ from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
from enrollment import api from enrollment import api
from enrollment.errors import EnrollmentApiLoadError, EnrollmentNotFoundError, CourseModeNotFoundError
from enrollment.tests import fake_data_api from enrollment.tests import fake_data_api
...@@ -51,7 +52,7 @@ class EnrollmentTest(TestCase): ...@@ -51,7 +52,7 @@ class EnrollmentTest(TestCase):
get_result = api.get_enrollment(self.USERNAME, self.COURSE_ID) get_result = api.get_enrollment(self.USERNAME, self.COURSE_ID)
self.assertEquals(result, get_result) self.assertEquals(result, get_result)
@raises(api.CourseModeNotFoundError) @raises(CourseModeNotFoundError)
def test_prof_ed_enroll(self): def test_prof_ed_enroll(self):
# Add a fake course enrollment information to the fake data API # Add a fake course enrollment information to the fake data API
fake_data_api.add_course(self.COURSE_ID, course_modes=['professional']) fake_data_api.add_course(self.COURSE_ID, course_modes=['professional'])
...@@ -83,18 +84,18 @@ class EnrollmentTest(TestCase): ...@@ -83,18 +84,18 @@ class EnrollmentTest(TestCase):
self.assertEquals(result['mode'], mode) self.assertEquals(result['mode'], mode)
self.assertTrue(result['is_active']) self.assertTrue(result['is_active'])
result = api.deactivate_enrollment(self.USERNAME, self.COURSE_ID) result = api.update_enrollment(self.USERNAME, self.COURSE_ID, mode=mode, is_active=False)
self.assertIsNotNone(result) self.assertIsNotNone(result)
self.assertEquals(result['student'], self.USERNAME) self.assertEquals(result['student'], self.USERNAME)
self.assertEquals(result['course']['course_id'], self.COURSE_ID) self.assertEquals(result['course']['course_id'], self.COURSE_ID)
self.assertEquals(result['mode'], mode) self.assertEquals(result['mode'], mode)
self.assertFalse(result['is_active']) self.assertFalse(result['is_active'])
@raises(api.EnrollmentNotFoundError) @raises(EnrollmentNotFoundError)
def test_unenroll_not_enrolled_in_course(self): def test_unenroll_not_enrolled_in_course(self):
# Add a fake course enrollment information to the fake data API # Add a fake course enrollment information to the fake data API
fake_data_api.add_course(self.COURSE_ID, course_modes=['honor']) fake_data_api.add_course(self.COURSE_ID, course_modes=['honor'])
api.deactivate_enrollment(self.USERNAME, self.COURSE_ID) api.update_enrollment(self.USERNAME, self.COURSE_ID, mode='honor', is_active=False)
@ddt.data( @ddt.data(
# Simple test of honor and verified. # Simple test of honor and verified.
...@@ -145,7 +146,7 @@ class EnrollmentTest(TestCase): ...@@ -145,7 +146,7 @@ class EnrollmentTest(TestCase):
self.assertEquals(3, len(result['course_modes'])) self.assertEquals(3, len(result['course_modes']))
@override_settings(ENROLLMENT_DATA_API='foo.bar.biz.baz') @override_settings(ENROLLMENT_DATA_API='foo.bar.biz.baz')
@raises(api.EnrollmentApiLoadError) @raises(EnrollmentApiLoadError)
def test_data_api_config_error(self): def test_data_api_config_error(self):
# Enroll in the course and verify the URL we get sent to # Enroll in the course and verify the URL we get sent to
api.add_enrollment(self.USERNAME, self.COURSE_ID, mode='audit') api.add_enrollment(self.USERNAME, self.COURSE_ID, mode='audit')
...@@ -3,6 +3,7 @@ Test the Data Aggregation Layer for Course Enrollments. ...@@ -3,6 +3,7 @@ Test the Data Aggregation Layer for Course Enrollments.
""" """
import ddt import ddt
from mock import patch
from nose.tools import raises from nose.tools import raises
import unittest import unittest
...@@ -12,8 +13,10 @@ from xmodule.modulestore.tests.django_utils import ( ...@@ -12,8 +13,10 @@ from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, mixed_store_config ModuleStoreTestCase, mixed_store_config
) )
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from enrollment.errors import CourseNotFoundError, UserNotFoundError, CourseEnrollmentClosedError, \
CourseEnrollmentFullError, CourseEnrollmentExistsError
from student.tests.factories import UserFactory, CourseModeFactory from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment, NonExistentCourseError from student.models import CourseEnrollment, EnrollmentClosedError, CourseFullError, AlreadyEnrolledError
from enrollment import data from enrollment import data
# Since we don't need any XML course fixtures, use a modulestore configuration # Since we don't need any XML course fixtures, use a modulestore configuration
...@@ -54,12 +57,11 @@ class EnrollmentDataTest(ModuleStoreTestCase): ...@@ -54,12 +57,11 @@ class EnrollmentDataTest(ModuleStoreTestCase):
def test_enroll(self, course_modes, enrollment_mode): def test_enroll(self, course_modes, enrollment_mode):
# Create the course modes (if any) required for this test case # Create the course modes (if any) required for this test case
self._create_course_modes(course_modes) self._create_course_modes(course_modes)
enrollment = data.create_course_enrollment(
enrollment = data.update_course_enrollment(
self.user.username, self.user.username,
unicode(self.course.id), unicode(self.course.id),
mode=enrollment_mode, enrollment_mode,
is_active=True True
) )
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
...@@ -72,7 +74,7 @@ class EnrollmentDataTest(ModuleStoreTestCase): ...@@ -72,7 +74,7 @@ class EnrollmentDataTest(ModuleStoreTestCase):
self.assertEqual(is_active, enrollment['is_active']) self.assertEqual(is_active, enrollment['is_active'])
def test_unenroll(self): def test_unenroll(self):
# Enroll the student in the course # Enroll the user in the course
CourseEnrollment.enroll(self.user, self.course.id, mode="honor") CourseEnrollment.enroll(self.user, self.course.id, mode="honor")
enrollment = data.update_course_enrollment( enrollment = data.update_course_enrollment(
...@@ -119,9 +121,11 @@ class EnrollmentDataTest(ModuleStoreTestCase): ...@@ -119,9 +121,11 @@ class EnrollmentDataTest(ModuleStoreTestCase):
for course in created_courses: for course in created_courses:
self._create_course_modes(course_modes, course=course) self._create_course_modes(course_modes, course=course)
# Create the original enrollment. # Create the original enrollment.
created_enrollments.append(data.update_course_enrollment( created_enrollments.append(data.create_course_enrollment(
self.user.username, self.user.username,
unicode(course.id), unicode(course.id),
'honor',
True
)) ))
# Compare the created enrollments with the results # Compare the created enrollments with the results
...@@ -148,18 +152,18 @@ class EnrollmentDataTest(ModuleStoreTestCase): ...@@ -148,18 +152,18 @@ class EnrollmentDataTest(ModuleStoreTestCase):
self.assertIsNone(result) self.assertIsNone(result)
# Create the original enrollment. # Create the original enrollment.
enrollment = data.update_course_enrollment( enrollment = data.create_course_enrollment(
self.user.username, self.user.username,
unicode(self.course.id), unicode(self.course.id),
mode=enrollment_mode, enrollment_mode,
is_active=True True
) )
# Get the enrollment and compare it to the original. # Get the enrollment and compare it to the original.
result = data.get_course_enrollment(self.user.username, unicode(self.course.id)) result = data.get_course_enrollment(self.user.username, unicode(self.course.id))
self.assertEqual(self.user.username, result['student']) self.assertEqual(self.user.username, result['user'])
self.assertEqual(enrollment, result) self.assertEqual(enrollment, result)
@raises(NonExistentCourseError) @raises(CourseNotFoundError)
def test_non_existent_course(self): def test_non_existent_course(self):
data.get_course_enrollment_info("this/is/bananas") data.get_course_enrollment_info("this/is/bananas")
...@@ -172,3 +176,37 @@ class EnrollmentDataTest(ModuleStoreTestCase): ...@@ -172,3 +176,37 @@ class EnrollmentDataTest(ModuleStoreTestCase):
mode_slug=mode_slug, mode_slug=mode_slug,
mode_display_name=mode_slug, mode_display_name=mode_slug,
) )
@raises(UserNotFoundError)
def test_enrollment_for_non_existent_user(self):
data.create_course_enrollment("some_fake_user", unicode(self.course.id), 'honor', True)
@raises(CourseNotFoundError)
def test_enrollment_for_non_existent_course(self):
data.create_course_enrollment(self.user.username, "some/fake/course", 'honor', True)
@raises(CourseEnrollmentClosedError)
@patch.object(CourseEnrollment, "enroll")
def test_enrollment_for_closed_course(self, mock_enroll):
mock_enroll.side_effect = EnrollmentClosedError("Bad things happened")
data.create_course_enrollment(self.user.username, unicode(self.course.id), 'honor', True)
@raises(CourseEnrollmentFullError)
@patch.object(CourseEnrollment, "enroll")
def test_enrollment_for_closed_course(self, mock_enroll):
mock_enroll.side_effect = CourseFullError("Bad things happened")
data.create_course_enrollment(self.user.username, unicode(self.course.id), 'honor', True)
@raises(CourseEnrollmentExistsError)
@patch.object(CourseEnrollment, "enroll")
def test_enrollment_for_closed_course(self, mock_enroll):
mock_enroll.side_effect = AlreadyEnrolledError("Bad things happened")
data.create_course_enrollment(self.user.username, unicode(self.course.id), 'honor', True)
@raises(UserNotFoundError)
def test_update_for_non_existent_user(self):
data.update_course_enrollment("some_fake_user", unicode(self.course.id), is_active=False)
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)
""" """
Tests for student enrollment. Tests for user enrollment.
""" """
import ddt import ddt
import json import json
import unittest import unittest
from mock import patch
from django.test.utils import override_settings from django.test.utils import override_settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
...@@ -14,6 +15,8 @@ from xmodule.modulestore.tests.django_utils import ( ...@@ -14,6 +15,8 @@ from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, mixed_store_config ModuleStoreTestCase, mixed_store_config
) )
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from enrollment import api
from enrollment.errors import CourseEnrollmentError
from student.tests.factories import UserFactory, CourseModeFactory from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
...@@ -27,7 +30,7 @@ MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, incl ...@@ -27,7 +30,7 @@ MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, incl
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EnrollmentTest(ModuleStoreTestCase, APITestCase): class EnrollmentTest(ModuleStoreTestCase, APITestCase):
""" """
Test student enrollment, especially with different course modes. Test user enrollment, especially with different course modes.
""" """
USERNAME = "Bob" USERNAME = "Bob"
EMAIL = "bob@example.com" EMAIL = "bob@example.com"
...@@ -68,6 +71,23 @@ class EnrollmentTest(ModuleStoreTestCase, APITestCase): ...@@ -68,6 +71,23 @@ class EnrollmentTest(ModuleStoreTestCase, APITestCase):
self.assertTrue(is_active) self.assertTrue(is_active)
self.assertEqual(course_mode, enrollment_mode) self.assertEqual(course_mode, enrollment_mode)
def test_check_enrollment(self):
CourseModeFactory.create(
course_id=self.course.id,
mode_slug='honor',
mode_display_name='Honor',
)
# Create an enrollment
self._create_enrollment()
resp = self.client.get(
reverse('courseenrollment', kwargs={"user": self.user.username, "course_id": unicode(self.course.id)})
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
data = json.loads(resp.content)
self.assertEqual(unicode(self.course.id), data['course_details']['course_id'])
self.assertEqual('honor', data['mode'])
self.assertTrue(data['is_active'])
def test_enroll_prof_ed(self): def test_enroll_prof_ed(self):
# Create the prod ed mode. # Create the prod ed mode.
CourseModeFactory.create( CourseModeFactory.create(
...@@ -77,51 +97,125 @@ class EnrollmentTest(ModuleStoreTestCase, APITestCase): ...@@ -77,51 +97,125 @@ class EnrollmentTest(ModuleStoreTestCase, APITestCase):
) )
# Enroll in the course, this will fail if the mode is not explicitly professional. # Enroll in the course, this will fail if the mode is not explicitly professional.
resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': (unicode(self.course.id))})) resp = self._create_enrollment(expected_status=status.HTTP_400_BAD_REQUEST)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
# While the enrollment wrong is invalid, the response content should have # While the enrollment wrong is invalid, the response content should have
# all the valid enrollment modes. # all the valid enrollment modes.
data = json.loads(resp.content) data = json.loads(resp.content)
self.assertEqual(unicode(self.course.id), data['course_id']) self.assertEqual(unicode(self.course.id), data['course_details']['course_id'])
self.assertEqual(1, len(data['course_modes'])) self.assertEqual(1, len(data['course_details']['course_modes']))
self.assertEqual('professional', data['course_modes'][0]['slug']) self.assertEqual('professional', data['course_details']['course_modes'][0]['slug'])
def test_user_not_authenticated(self): def test_user_not_authenticated(self):
# Log out, so we're no longer authenticated # Log out, so we're no longer authenticated
self.client.logout() self.client.logout()
# Try to enroll, this should fail. # Try to enroll, this should fail.
resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': (unicode(self.course.id))})) self._create_enrollment(expected_status=status.HTTP_403_FORBIDDEN)
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
def test_user_not_activated(self): def test_user_not_activated(self):
# Create a user account, but don't activate it # Log out the default user, Bob.
self.client.logout()
# Create a user account
self.user = UserFactory.create( self.user = UserFactory.create(
username="inactive", username="inactive",
email="inactive@example.com", email="inactive@example.com",
password=self.PASSWORD, password=self.PASSWORD,
is_active=False is_active=True
) )
# Log in with the unactivated account # Log in with the unactivated account
self.client.login(username="inactive", password=self.PASSWORD) self.client.login(username="inactive", password=self.PASSWORD)
# Deactivate the user. Has to be done after login to get the user into the
# request and properly logged in.
self.user.is_active = False
self.user.save()
# Enrollment should succeed, even though we haven't authenticated. # Enrollment should succeed, even though we haven't authenticated.
resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': (unicode(self.course.id))})) self._create_enrollment()
self.assertEqual(resp.status_code, 200)
def test_user_does_not_match_url(self):
# Try to enroll a user that is not the authenticated user.
CourseModeFactory.create(
course_id=self.course.id,
mode_slug='honor',
mode_display_name='Honor',
)
self._create_enrollment(username='not_the_user', expected_status=status.HTTP_404_NOT_FOUND)
def test_user_does_not_match_param_for_list(self):
CourseModeFactory.create(
course_id=self.course.id,
mode_slug='honor',
mode_display_name='Honor',
)
resp = self.client.get(reverse('courseenrollments'), {"user": "not_the_user"})
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
def test_user_does_not_match_param(self):
CourseModeFactory.create(
course_id=self.course.id,
mode_slug='honor',
mode_display_name='Honor',
)
resp = self.client.get(
reverse('courseenrollment', kwargs={"user": "not_the_user", "course_id": unicode(self.course.id)})
)
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
def test_get_course_details(self):
CourseModeFactory.create(
course_id=self.course.id,
mode_slug='honor',
mode_display_name='Honor',
)
resp = self.client.get(
reverse('courseenrollmentdetails', kwargs={"course_id": unicode(self.course.id)})
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
data = json.loads(resp.content)
self.assertEqual(unicode(self.course.id), data['course_id'])
def test_with_invalid_course_id(self): def test_with_invalid_course_id(self):
# Create an enrollment self._create_enrollment(course_id='entirely/fake/course', expected_status=status.HTTP_400_BAD_REQUEST)
resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': 'entirely/fake/course'}))
def test_get_enrollment_details_bad_course(self):
resp = self.client.get(
reverse('courseenrollmentdetails', kwargs={"course_id": "some/fake/course"})
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
def _create_enrollment(self): @patch.object(api, "get_enrollment")
def test_get_enrollment_internal_error(self, mock_get_enrollment):
mock_get_enrollment.side_effect = CourseEnrollmentError("Something bad happened.")
resp = self.client.get(
reverse('courseenrollment', kwargs={"user": self.user.username, "course_id": unicode(self.course.id)})
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
def _create_enrollment(self, course_id=None, username=None, expected_status=status.HTTP_200_OK):
course_id = unicode(self.course.id) if course_id is None else course_id
username = self.user.username if username is None else username
"""Enroll in the course and verify the URL we are sent to. """ """Enroll in the course and verify the URL we are sent to. """
resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': (unicode(self.course.id))}))
self.assertEqual(resp.status_code, status.HTTP_200_OK) resp = self.client.post(
data = json.loads(resp.content) reverse('courseenrollments'),
self.assertEqual(unicode(self.course.id), data['course']['course_id']) {
self.assertEqual('honor', data['mode']) 'course_details': {
self.assertTrue(data['is_active']) 'course_id': course_id
},
'user': username
},
format='json'
)
self.assertEqual(resp.status_code, expected_status)
if expected_status == status.HTTP_200_OK:
data = json.loads(resp.content)
self.assertEqual(course_id, data['course_details']['course_id'])
self.assertEqual('honor', data['mode'])
self.assertTrue(data['is_active'])
return resp return resp
...@@ -5,17 +5,25 @@ URLs for the Enrollment API ...@@ -5,17 +5,25 @@ URLs for the Enrollment API
from django.conf import settings from django.conf import settings
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from .views import get_course_enrollment, list_student_enrollments from .views import (
EnrollmentView,
EnrollmentListView,
EnrollmentCourseDetailView
)
urlpatterns = [] USER_PATTERN = '(?P<user>[\w.@+-]+)'
if settings.FEATURES.get('ENABLE_COMBINED_LOGIN_REGISTRATION'): urlpatterns = patterns(
urlpatterns += patterns( 'enrollment.views',
'enrollment.views', url(
url(r'^student$', list_student_enrollments, name='courseenrollments'), r'^enrollment/{user},{course_key}$'.format(user=USER_PATTERN, course_key=settings.COURSE_ID_PATTERN),
url( EnrollmentView.as_view(),
r'^course/{course_key}$'.format(course_key=settings.COURSE_ID_PATTERN), name='courseenrollment'
get_course_enrollment, ),
name='courseenrollment' url(r'^enrollment$', EnrollmentListView.as_view(), name='courseenrollments'),
), url(
) r'^course/{course_key}$'.format(course_key=settings.COURSE_ID_PATTERN),
EnrollmentCourseDetailView.as_view(),
name='courseenrollmentdetails'
),
)
...@@ -5,7 +5,7 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'], ...@@ -5,7 +5,7 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
describe( 'edx.student.account.EnrollmentInterface', function() { describe( 'edx.student.account.EnrollmentInterface', function() {
var COURSE_KEY = 'edX/DemoX/Fall', var COURSE_KEY = 'edX/DemoX/Fall',
ENROLL_URL = '/enrollment/v0/course/edX/DemoX/Fall', ENROLL_URL = '/api/enrollment/v1/enrollment',
FORWARD_URL = '/course_modes/choose/edX/DemoX/Fall/'; FORWARD_URL = '/course_modes/choose/edX/DemoX/Fall/';
beforeEach(function() { beforeEach(function() {
...@@ -21,7 +21,12 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'], ...@@ -21,7 +21,12 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
EnrollmentInterface.enroll( COURSE_KEY ); EnrollmentInterface.enroll( COURSE_KEY );
// Expect that the correct request was made to the server // Expect that the correct request was made to the server
AjaxHelpers.expectRequest( requests, 'POST', ENROLL_URL ); AjaxHelpers.expectRequest(
requests,
'POST',
ENROLL_URL,
'{"course_details":{"course_id":"edX/DemoX/Fall"}}'
);
// Simulate a successful response from the server // Simulate a successful response from the server
AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.respondWithJson(requests, {});
......
...@@ -9,7 +9,7 @@ var edx = edx || {}; ...@@ -9,7 +9,7 @@ var edx = edx || {};
edx.student.account.EnrollmentInterface = { edx.student.account.EnrollmentInterface = {
urls: { urls: {
course: '/enrollment/v0/course/', enrollment: '/api/enrollment/v1/enrollment',
trackSelection: '/course_modes/choose/' trackSelection: '/course_modes/choose/'
}, },
...@@ -23,10 +23,17 @@ var edx = edx || {}; ...@@ -23,10 +23,17 @@ var edx = edx || {};
* @param {string} courseKey Slash-separated course key. * @param {string} courseKey Slash-separated course key.
*/ */
enroll: function( courseKey ) { enroll: function( courseKey ) {
var data_obj = {
course_details: {
course_id: courseKey
}
};
var data = JSON.stringify(data_obj);
$.ajax({ $.ajax({
url: this.courseEnrollmentUrl( courseKey ), url: this.urls.enrollment,
type: 'POST', type: 'POST',
data: {}, contentType: 'application/json; charset=utf-8',
data: data,
headers: this.headers, headers: this.headers,
context: this context: this
}).always(function() { }).always(function() {
...@@ -44,15 +51,6 @@ var edx = edx || {}; ...@@ -44,15 +51,6 @@ var edx = edx || {};
}, },
/** /**
* Construct a URL to enroll in a course.
* @param {string} courseKey Slash-separated course key.
* @return {string} The URL to enroll in a course.
*/
courseEnrollmentUrl: function( courseKey ) {
return this.urls.course + courseKey;
},
/**
* Redirect to a URL. Mainly useful for mocking out in tests. * Redirect to a URL. Mainly useful for mocking out in tests.
* @param {string} url The URL to redirect to. * @param {string} url The URL to redirect to.
*/ */
......
...@@ -74,7 +74,7 @@ urlpatterns = ('', # nopep8 ...@@ -74,7 +74,7 @@ urlpatterns = ('', # nopep8
url(r'^submit_feedback$', 'util.views.submit_feedback'), url(r'^submit_feedback$', 'util.views.submit_feedback'),
# Enrollment API RESTful endpoints # Enrollment API RESTful endpoints
url(r'^enrollment/v0/', include('enrollment.urls')), url(r'^api/enrollment/v1/', include('enrollment.urls')),
) )
......
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