Commit 0e58e7cb by Stephen Sanchez

Merge pull request #6216 from edx/sanchez/cleanup-restful-layer

Enrollment API Cleanup
parents 7a227bb2 22a18e35
......@@ -6,50 +6,24 @@ course level, such as available course modes.
from django.utils import importlib
import logging
from django.conf import settings
from enrollment import errors
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'
def get_enrollments(student_id):
"""Retrieves all the courses a student is enrolled in.
def get_enrollments(user_id):
"""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.
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:
A list of enrollment information for the given student.
A list of enrollment information for the given user.
Examples:
>>> get_enrollments("Bob")
......@@ -58,7 +32,7 @@ def get_enrollments(student_id):
"created": "2014-10-20T20:18:00Z",
"mode": "honor",
"is_active": True,
"student": "Bob",
"user": "Bob",
"course": {
"course_id": "edX/DemoX/2014T2",
"enrollment_end": 2014-12-20T20:18:00Z,
......@@ -81,7 +55,7 @@ def get_enrollments(student_id):
"created": "2014-10-25T20:18:00Z",
"mode": "verified",
"is_active": True,
"student": "Bob",
"user": "Bob",
"course": {
"course_id": "edX/edX-Insider/2014T2",
"enrollment_end": 2014-12-20T20:18:00Z,
......@@ -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):
"""Retrieves all enrollment information for the student in respect to a specific course.
def get_enrollment(user_id, course_id):
"""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:
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.
Returns:
......@@ -124,7 +98,7 @@ def get_enrollment(student_id, course_id):
"created": "2014-10-20T20:18:00Z",
"mode": "honor",
"is_active": True,
"student": "Bob",
"user": "Bob",
"course": {
"course_id": "edX/DemoX/2014T2",
"enrollment_end": 2014-12-20T20:18:00Z,
......@@ -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):
"""Enrolls a student in a course.
def add_enrollment(user_id, course_id, mode='honor', is_active=True):
"""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:
student_id (str): The student to enroll.
course_id (str): The course to enroll the student in.
user_id (str): The user to enroll.
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',
'professional'. If not specified, this defaults to 'honor'.
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):
"created": "2014-10-20T20:18:00Z",
"mode": "honor",
"is_active": True,
"student": "Bob",
"user": "Bob",
"course": {
"course_id": "edX/DemoX/2014T2",
"enrollment_end": 2014-12-20T20:18:00Z,
......@@ -191,66 +165,19 @@ def add_enrollment(student_id, course_id, mode='honor', is_active=True):
}
"""
_validate_course_mode(course_id, mode)
return _data_api().update_course_enrollment(student_id, course_id, mode=mode, is_active=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)
return _data_api().create_course_enrollment(user_id, course_id, mode, is_active)
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.
Update a course enrollment for the given student and course.
Update a course enrollment for the given user and course.
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.
mode (str): The new course mode for this enrollment.
is_active (bool): Sets whether the enrollment is active or not.
Returns:
A serializable dictionary representing the updated enrollment.
......@@ -261,7 +188,7 @@ def update_enrollment(student_id, course_id, mode):
"created": "2014-10-20T20:18:00Z",
"mode": "honor",
"is_active": True,
"student": "Bob",
"user": "Bob",
"course": {
"course_id": "edX/DemoX/2014T2",
"enrollment_end": 2014-12-20T20:18:00Z,
......@@ -283,7 +210,12 @@ def update_enrollment(student_id, 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):
......@@ -354,7 +286,7 @@ def _validate_course_mode(course_id, mode):
available=", ".join(available_modes)
)
log.warn(msg)
raise CourseModeNotFoundError(msg, course_enrollment_info)
raise errors.CourseModeNotFoundError(msg, course_enrollment_info)
def _data_api():
......@@ -371,4 +303,4 @@ def _data_api():
return importlib.import_module(api_path)
except (ImportError, ValueError):
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
from django.contrib.auth.models import User
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore
from enrollment.errors import CourseNotFoundError, CourseEnrollmentClosedError, CourseEnrollmentFullError, \
CourseEnrollmentExistsError, UserNotFoundError
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__)
def get_course_enrollments(student_id):
"""Retrieve a list representing all aggregated data for a student's course enrollments.
def get_course_enrollments(user_id):
"""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:
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:
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(
user__username=student_id, is_active=True
user__username=user_id, is_active=True
).order_by('created')
return CourseEnrollmentSerializer(qset).data # pylint: disable=no-member
def get_course_enrollment(student_id, course_id):
"""Retrieve an object representing all aggregated data for a student's course enrollment.
def get_course_enrollment(username, course_id):
"""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:
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.
Returns:
......@@ -47,22 +50,65 @@ def get_course_enrollment(student_id, course_id):
course_key = CourseKey.from_string(course_id)
try:
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
except CourseEnrollment.DoesNotExist:
return None
def update_course_enrollment(student_id, course_id, mode=None, is_active=None):
"""Modify a course enrollment for a student.
def create_course_enrollment(username, course_id, mode, is_active):
"""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.
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.
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.
Returns:
......@@ -70,12 +116,22 @@ def update_course_enrollment(student_id, course_id, mode=None, is_active=None):
"""
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.save()
return CourseEnrollmentSerializer(enrollment).data # pylint: disable=no-member
......@@ -92,13 +148,14 @@ def get_course_enrollment_info(course_id):
Returns:
A serializable dictionary representing the course's enrollment information.
Raises:
CourseNotFoundError
"""
course_key = CourseKey.from_string(course_id)
course = modulestore().get_course(course_key)
if course is None:
log.warning(
u"Requested enrollment information for unknown course {course}"
.format(course=course_id)
)
raise NonExistentCourseError
msg = u"Requested enrollment information for unknown course {course}".format(course=course_id)
log.warning(msg)
raise CourseNotFoundError(msg)
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):
the Course Descriptor and course modes, to give a complete representation of course enrollment.
"""
course = CourseField()
student = serializers.SerializerMethodField('get_username')
course_details = serializers.SerializerMethodField('get_course_details')
user = serializers.SerializerMethodField('get_username')
def get_course_details(self, model):
field = CourseField()
return field.to_native(model.course)
def get_username(self, model):
"""Retrieves the username from the associated model."""
......@@ -62,7 +66,7 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
class Meta: # pylint: disable=missing-docstring
model = CourseEnrollment
fields = ('created', 'mode', 'is_active', 'course', 'student')
fields = ('created', 'mode', 'is_active', 'course_details', 'user')
lookup_field = 'username'
......
......@@ -31,14 +31,17 @@ def get_course_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):
"""Stubbed out Enrollment data request."""
enrollment = _get_fake_enrollment(student_id, course_id)
if not enrollment:
enrollment = add_enrollment(student_id, course_id)
if mode is not None:
if enrollment and mode is not None:
enrollment['mode'] = mode
if is_active is not None:
if enrollment and is_active is not None:
enrollment['is_active'] = is_active
return enrollment
......
......@@ -8,6 +8,7 @@ from django.test import TestCase
from django.test.utils import override_settings
from django.conf import settings
from enrollment import api
from enrollment.errors import EnrollmentApiLoadError, EnrollmentNotFoundError, CourseModeNotFoundError
from enrollment.tests import fake_data_api
......@@ -51,7 +52,7 @@ class EnrollmentTest(TestCase):
get_result = api.get_enrollment(self.USERNAME, self.COURSE_ID)
self.assertEquals(result, get_result)
@raises(api.CourseModeNotFoundError)
@raises(CourseModeNotFoundError)
def test_prof_ed_enroll(self):
# Add a fake course enrollment information to the fake data API
fake_data_api.add_course(self.COURSE_ID, course_modes=['professional'])
......@@ -83,18 +84,18 @@ class EnrollmentTest(TestCase):
self.assertEquals(result['mode'], mode)
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.assertEquals(result['student'], self.USERNAME)
self.assertEquals(result['course']['course_id'], self.COURSE_ID)
self.assertEquals(result['mode'], mode)
self.assertFalse(result['is_active'])
@raises(api.EnrollmentNotFoundError)
@raises(EnrollmentNotFoundError)
def test_unenroll_not_enrolled_in_course(self):
# Add a fake course enrollment information to the fake data API
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(
# Simple test of honor and verified.
......@@ -145,7 +146,7 @@ class EnrollmentTest(TestCase):
self.assertEquals(3, len(result['course_modes']))
@override_settings(ENROLLMENT_DATA_API='foo.bar.biz.baz')
@raises(api.EnrollmentApiLoadError)
@raises(EnrollmentApiLoadError)
def test_data_api_config_error(self):
# Enroll in the course and verify the URL we get sent to
api.add_enrollment(self.USERNAME, self.COURSE_ID, mode='audit')
......@@ -3,6 +3,7 @@ Test the Data Aggregation Layer for Course Enrollments.
"""
import ddt
from mock import patch
from nose.tools import raises
import unittest
......@@ -12,8 +13,10 @@ from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, mixed_store_config
)
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.models import CourseEnrollment, NonExistentCourseError
from student.models import CourseEnrollment, EnrollmentClosedError, CourseFullError, AlreadyEnrolledError
from enrollment import data
# Since we don't need any XML course fixtures, use a modulestore configuration
......@@ -54,12 +57,11 @@ class EnrollmentDataTest(ModuleStoreTestCase):
def test_enroll(self, course_modes, enrollment_mode):
# Create the course modes (if any) required for this test case
self._create_course_modes(course_modes)
enrollment = data.update_course_enrollment(
enrollment = data.create_course_enrollment(
self.user.username,
unicode(self.course.id),
mode=enrollment_mode,
is_active=True
enrollment_mode,
True
)
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
......@@ -72,7 +74,7 @@ class EnrollmentDataTest(ModuleStoreTestCase):
self.assertEqual(is_active, enrollment['is_active'])
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")
enrollment = data.update_course_enrollment(
......@@ -119,9 +121,11 @@ class EnrollmentDataTest(ModuleStoreTestCase):
for course in created_courses:
self._create_course_modes(course_modes, course=course)
# Create the original enrollment.
created_enrollments.append(data.update_course_enrollment(
created_enrollments.append(data.create_course_enrollment(
self.user.username,
unicode(course.id),
'honor',
True
))
# Compare the created enrollments with the results
......@@ -148,18 +152,18 @@ class EnrollmentDataTest(ModuleStoreTestCase):
self.assertIsNone(result)
# Create the original enrollment.
enrollment = data.update_course_enrollment(
enrollment = data.create_course_enrollment(
self.user.username,
unicode(self.course.id),
mode=enrollment_mode,
is_active=True
enrollment_mode,
True
)
# Get the enrollment and compare it to the original.
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)
@raises(NonExistentCourseError)
@raises(CourseNotFoundError)
def test_non_existent_course(self):
data.get_course_enrollment_info("this/is/bananas")
......@@ -172,3 +176,37 @@ class EnrollmentDataTest(ModuleStoreTestCase):
mode_slug=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 json
import unittest
from mock import patch
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase
......@@ -14,6 +15,8 @@ from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, mixed_store_config
)
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.models import CourseEnrollment
......@@ -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')
class EnrollmentTest(ModuleStoreTestCase, APITestCase):
"""
Test student enrollment, especially with different course modes.
Test user enrollment, especially with different course modes.
"""
USERNAME = "Bob"
EMAIL = "bob@example.com"
......@@ -68,6 +71,23 @@ class EnrollmentTest(ModuleStoreTestCase, APITestCase):
self.assertTrue(is_active)
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):
# Create the prod ed mode.
CourseModeFactory.create(
......@@ -77,51 +97,125 @@ class EnrollmentTest(ModuleStoreTestCase, APITestCase):
)
# 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))}))
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
resp = self._create_enrollment(expected_status=status.HTTP_400_BAD_REQUEST)
# While the enrollment wrong is invalid, the response content should have
# all the valid enrollment modes.
data = json.loads(resp.content)
self.assertEqual(unicode(self.course.id), data['course_id'])
self.assertEqual(1, len(data['course_modes']))
self.assertEqual('professional', data['course_modes'][0]['slug'])
self.assertEqual(unicode(self.course.id), data['course_details']['course_id'])
self.assertEqual(1, len(data['course_details']['course_modes']))
self.assertEqual('professional', data['course_details']['course_modes'][0]['slug'])
def test_user_not_authenticated(self):
# Log out, so we're no longer authenticated
self.client.logout()
# Try to enroll, this should fail.
resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': (unicode(self.course.id))}))
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
self._create_enrollment(expected_status=status.HTTP_403_FORBIDDEN)
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(
username="inactive",
email="inactive@example.com",
password=self.PASSWORD,
is_active=False
is_active=True
)
# Log in with the unactivated account
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.
resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': (unicode(self.course.id))}))
self.assertEqual(resp.status_code, 200)
self._create_enrollment()
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):
# Create an enrollment
resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': 'entirely/fake/course'}))
self._create_enrollment(course_id='entirely/fake/course', expected_status=status.HTTP_400_BAD_REQUEST)
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)
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. """
resp = self.client.post(reverse('courseenrollment', 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']['course_id'])
self.assertEqual('honor', data['mode'])
self.assertTrue(data['is_active'])
resp = self.client.post(
reverse('courseenrollments'),
{
'course_details': {
'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
......@@ -5,17 +5,25 @@ URLs for the Enrollment API
from django.conf import settings
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(
'enrollment.views',
url(r'^student$', list_student_enrollments, name='courseenrollments'),
url(
r'^course/{course_key}$'.format(course_key=settings.COURSE_ID_PATTERN),
get_course_enrollment,
name='courseenrollment'
),
)
urlpatterns = patterns(
'enrollment.views',
url(
r'^enrollment/{user},{course_key}$'.format(user=USER_PATTERN, course_key=settings.COURSE_ID_PATTERN),
EnrollmentView.as_view(),
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'],
describe( 'edx.student.account.EnrollmentInterface', function() {
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/';
beforeEach(function() {
......@@ -21,7 +21,12 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
EnrollmentInterface.enroll( COURSE_KEY );
// 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
AjaxHelpers.respondWithJson(requests, {});
......
......@@ -9,7 +9,7 @@ var edx = edx || {};
edx.student.account.EnrollmentInterface = {
urls: {
course: '/enrollment/v0/course/',
enrollment: '/api/enrollment/v1/enrollment',
trackSelection: '/course_modes/choose/'
},
......@@ -23,10 +23,17 @@ var edx = edx || {};
* @param {string} courseKey Slash-separated course key.
*/
enroll: function( courseKey ) {
var data_obj = {
course_details: {
course_id: courseKey
}
};
var data = JSON.stringify(data_obj);
$.ajax({
url: this.courseEnrollmentUrl( courseKey ),
url: this.urls.enrollment,
type: 'POST',
data: {},
contentType: 'application/json; charset=utf-8',
data: data,
headers: this.headers,
context: this
}).always(function() {
......@@ -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.
* @param {string} url The URL to redirect to.
*/
......
......@@ -74,7 +74,7 @@ urlpatterns = ('', # nopep8
url(r'^submit_feedback$', 'util.views.submit_feedback'),
# 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