Commit eb5fc311 by Albert St. Aubin

Refactored the API to be part of the Entitlement API, removed it from

the Enrollment API
parent b0a19e94
...@@ -5,7 +5,6 @@ import datetime ...@@ -5,7 +5,6 @@ import datetime
import itertools import itertools
import json import json
import unittest import unittest
import uuid
import ddt import ddt
import httpretty import httpretty
...@@ -25,11 +24,9 @@ from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls ...@@ -25,11 +24,9 @@ from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
from course_modes.models import CourseMode from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory from course_modes.tests.factories import CourseModeFactory
from entitlements.tests.factories import CourseEntitlementFactory
from enrollment import api from enrollment import api
from enrollment.errors import CourseEnrollmentError from enrollment.errors import CourseEnrollmentError
from enrollment.views import EnrollmentUserThrottle from enrollment.views import EnrollmentUserThrottle
from entitlements.models import CourseEntitlement
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.embargo.models import Country, CountryAccessRule, RestrictedCourse from openedx.core.djangoapps.embargo.models import Country, CountryAccessRule, RestrictedCourse
from openedx.core.djangoapps.embargo.test_utils import restrict_course from openedx.core.djangoapps.embargo.test_utils import restrict_course
...@@ -38,7 +35,7 @@ from openedx.core.lib.django_test_client_utils import get_absolute_url ...@@ -38,7 +35,7 @@ from openedx.core.lib.django_test_client_utils import get_absolute_url
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseServiceMockMixin from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseServiceMockMixin
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.roles import CourseStaffRole from student.roles import CourseStaffRole
from student.tests.factories import AdminFactory, UserFactory, TEST_PASSWORD from student.tests.factories import AdminFactory, UserFactory
from util.models import RateLimitConfiguration from util.models import RateLimitConfiguration
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
...@@ -50,7 +47,6 @@ class EnrollmentTestMixin(object): ...@@ -50,7 +47,6 @@ class EnrollmentTestMixin(object):
def assert_enrollment_status( def assert_enrollment_status(
self, self,
course_id=None, course_id=None,
course_uuid=None,
username=None, username=None,
expected_status=status.HTTP_200_OK, expected_status=status.HTTP_200_OK,
email_opt_in=None, email_opt_in=None,
...@@ -81,9 +77,6 @@ class EnrollmentTestMixin(object): ...@@ -81,9 +77,6 @@ class EnrollmentTestMixin(object):
'enrollment_attributes': enrollment_attributes 'enrollment_attributes': enrollment_attributes
} }
if course_uuid:
data['course_details']['course_uuid'] = course_uuid
if is_active is not None: if is_active is not None:
data['is_active'] = is_active data['is_active'] = is_active
...@@ -140,93 +133,6 @@ class EnrollmentTestMixin(object): ...@@ -140,93 +133,6 @@ class EnrollmentTestMixin(object):
self.assertEqual(actual_mode, expected_mode) self.assertEqual(actual_mode, expected_mode)
# @override_settings(EDX_API_KEY="i am a key")
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EntitlementEnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
def setUp(self):
super(EntitlementEnrollmentTest, self).setUp()
self.course = CourseFactory()
self.user = UserFactory()
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=CourseMode.VERIFIED,
mode_display_name=CourseMode.VERIFIED,
)
self.client.login(username=self.user.username, password=TEST_PASSWORD)
def test_enroll_entitlement(self):
entitlement = CourseEntitlementFactory.create(user=self.user, mode='verified')
resp = self.assert_enrollment_status(
course_id=unicode(self.course.id),
course_uuid=str(entitlement.course_uuid),
is_active=True,
mode=None,
max_mongo_calls=4
)
data = json.loads(resp.content)
self.assertEqual(self.course.display_name_with_default, data['course_details']['course_name'])
# Verify that the enrollment was created correctly
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, entitlement.mode)
entitlement.refresh_from_db()
# Verify the Entitlement settings are correct
self.assertIsNotNone(entitlement.enrollment_course_run)
self.assertEqual(entitlement.enrollment_course_run.course_id, self.course.id)
def test_unenroll_entitlement(self):
entitlement = CourseEntitlementFactory.create(user=self.user, mode='verified')
# Enroll user
self.assert_enrollment_status(
course_id=unicode(self.course.id),
course_uuid=str(entitlement.course_uuid),
is_active=True,
mode=None,
max_mongo_calls=4
)
# Unenroll the user
resp = self.assert_enrollment_status(
course_id=unicode(self.course.id),
course_uuid=str(entitlement.course_uuid),
is_active=False,
mode=None,
max_mongo_calls=4
)
data = json.loads(resp.content)
self.assertEqual(self.course.display_name_with_default, data['course_details']['course_name'])
# Verify that the enrollment was created correctly
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
self.assertFalse(is_active)
self.assertEqual(course_mode, entitlement.mode)
entitlement.refresh_from_db()
self.assertIsNone(entitlement.enrollment_course_run)
def test_enroll_no_entitlement(self):
resp = self.assert_enrollment_status(
course_id=unicode(self.course.id),
course_uuid=str(uuid.uuid4()),
is_active=True,
mode=None,
max_mongo_calls=4,
expected_status=status.HTTP_400_BAD_REQUEST
)
data = json.loads(resp.content)
self.assertEqual(self.course.display_name_with_default, data['course_details']['course_name'])
# Verify that the enrollment was created correctly
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
@attr(shard=3) @attr(shard=3)
@override_settings(EDX_API_KEY="i am a key") @override_settings(EDX_API_KEY="i am a key")
@ddt.ddt @ddt.ddt
......
...@@ -18,7 +18,6 @@ from rest_framework.views import APIView ...@@ -18,7 +18,6 @@ from rest_framework.views import APIView
from course_modes.models import CourseMode from course_modes.models import CourseMode
from enrollment import api from enrollment import api
from enrollment.errors import CourseEnrollmentError, CourseEnrollmentExistsError, CourseModeNotFoundError from enrollment.errors import CourseEnrollmentError, CourseEnrollmentExistsError, CourseModeNotFoundError
from entitlements.models import CourseEntitlement
from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
from openedx.core.djangoapps.cors_csrf.decorators import ensure_csrf_cookie_cross_domain from openedx.core.djangoapps.cors_csrf.decorators import ensure_csrf_cookie_cross_domain
from openedx.core.djangoapps.embargo import api as embargo_api from openedx.core.djangoapps.embargo import api as embargo_api
...@@ -37,7 +36,6 @@ from openedx.features.enterprise_support.api import ( ...@@ -37,7 +36,6 @@ from openedx.features.enterprise_support.api import (
enterprise_enabled enterprise_enabled
) )
from student.auth import user_has_role from student.auth import user_has_role
from student.models import CourseEnrollment
from student.models import User from student.models import User
from student.roles import CourseStaffRole, GlobalStaff from student.roles import CourseStaffRole, GlobalStaff
from util.disable_rate_limit import can_disable_rate_limit from util.disable_rate_limit import can_disable_rate_limit
...@@ -530,11 +528,25 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): ...@@ -530,11 +528,25 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
# Get the User, Course ID, and Mode from the request. # Get the User, Course ID, and Mode from the request.
username = request.data.get('user', request.user.username) username = request.data.get('user', request.user.username)
# Note that course_id is actually the Course Run Key
course_id = request.data.get('course_details', {}).get('course_id') course_id = request.data.get('course_details', {}).get('course_id')
course_uuid = request.data.get('course_details', {}).get('course_uuid')
if not course_id:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"message": u"Course ID must be specified to create a new enrollment."}
)
try:
course_id = CourseKey.from_string(course_id)
except InvalidKeyError:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": u"No course '{course_id}' found for enrollment".format(course_id=course_id)
}
)
mode = request.data.get('mode') mode = request.data.get('mode')
is_active = request.data.get('is_active')
has_api_key_permissions = self.has_api_key_permissions(request) has_api_key_permissions = self.has_api_key_permissions(request)
...@@ -567,46 +579,13 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): ...@@ -567,46 +579,13 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
} }
) )
course_entitlement = None
if course_uuid:
course_entitlement = CourseEntitlement.get_active_user_course_entitlements(user, course_uuid)
if course_entitlement and course_entitlement.enrollment_course_run is not None and is_active:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": u"Entitlement for {course_uuid} already has an enrollment applied".format(
course_uuid=course_uuid
)
}
)
if not course_id:
if course_entitlement and course_entitlement.enrollment_course_run is not None:
course_id = course_entitlement.enrollment_course_run.course_id
else:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"message": u"Course ID must be specified to create a new enrollment."}
)
else:
try:
course_id = CourseKey.from_string(course_id)
except InvalidKeyError:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": u"No course '{course_id}' found for enrollment".format(course_id=course_id)
}
)
embargo_response = embargo_api.get_embargo_response(request, course_id, user) embargo_response = embargo_api.get_embargo_response(request, course_id, user)
if embargo_response: if embargo_response:
return embargo_response return embargo_response
try: try:
is_active = request.data.get('is_active')
# Check if the requested activation status is None or a Boolean # Check if the requested activation status is None or a Boolean
if is_active is not None and not isinstance(is_active, bool): if is_active is not None and not isinstance(is_active, bool):
return Response( return Response(
...@@ -633,87 +612,60 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): ...@@ -633,87 +612,60 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
} }
consent_client.provide_consent(**kwargs) consent_client.provide_consent(**kwargs)
# Add Enrollment for the Entitlement user with the correct Mode enrollment_attributes = request.data.get('enrollment_attributes')
# This should only occur if the User has a Course Entitlement in place. enrollment = api.get_enrollment(username, unicode(course_id))
# As a reault the api_key_permissions do not apply the User may enroll themselves based on the entitlement. mode_changed = enrollment and mode is not None and enrollment['mode'] != mode
if course_entitlement and is_active: active_changed = enrollment and is_active is not None and enrollment['is_active'] != is_active
mode = course_entitlement.mode missing_attrs = []
response = api.add_enrollment( 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
# the request (on the assumption that the requester had outdated information
# about the currently active enrollment).
msg = u"Enrollment mode mismatch: active mode={}, requested mode={}. Won't deactivate.".format(
enrollment["mode"], mode
)
log.warning(msg)
return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg})
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, username,
unicode(course_id), unicode(course_id),
mode=mode, mode=mode,
is_active=True, is_active=is_active,
) enrollment_attributes=enrollment_attributes,
CourseEntitlement.set_enrollment( # If we are updating enrollment by authorized api caller, we should allow expired modes
entitlement=course_entitlement, include_expired=has_api_key_permissions
enrollment=CourseEnrollment.get_enrollment(user, course_id)
) )
log.info('Enrolling [%s] entitlement for run [%s] of Course [%s].', username, course_id, course_uuid) else:
elif course_entitlement and is_active is not None and not is_active: # Will reactivate inactive enrollments.
# Unenroll the course as part of the entitlement response = api.add_enrollment(
response = api.update_enrollment(
username, username,
unicode(course_id), unicode(course_id),
mode=mode, mode=mode,
is_active=is_active, is_active=is_active,
enrollment_attributes=enrollment_attributes
) )
CourseEntitlement.set_enrollment(course_entitlement, None)
log.info('Unenrolling [%s] entitlement for run [%s] of Course [%s].', username, course_id, course_uuid)
else:
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
# the request (on the assumption that the requester had outdated information
# about the currently active enrollment).
msg = u"Enrollment mode mismatch: active mode={}, requested mode={}. Won't deactivate.".format(
enrollment["mode"], mode
)
log.warning(msg)
return Response(status=status.HTTP_400_BAD_REQUEST, data={"message": msg})
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,
# If we are updating enrollment by authorized api caller, we should allow expired modes
include_expired=has_api_key_permissions
)
else:
# Will reactivate inactive enrollments.
response = api.add_enrollment(
username,
unicode(course_id),
mode=mode,
is_active=is_active,
enrollment_attributes=enrollment_attributes
)
email_opt_in = request.data.get('email_opt_in', None) email_opt_in = request.data.get('email_opt_in', None)
if email_opt_in is not None: if email_opt_in is not None:
org = course_id.org org = course_id.org
update_email_opt_in(request.user, org, email_opt_in) update_email_opt_in(request.user, org, email_opt_in)
log.info('The user [%s] has already been enrolled in course run [%s].', username, course_id) log.info('The user [%s] has already been enrolled in course run [%s].', username, course_id)
return Response(response) return Response(response)
except CourseModeNotFoundError as error: except CourseModeNotFoundError as error:
return Response( return Response(
......
from django.conf.urls import url, include from django.conf.urls import url, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .views import EntitlementViewSet from .views import EntitlementViewSet, EntitlementEnrollmentViewSet
router = DefaultRouter() router = DefaultRouter()
router.register(r'entitlements', EntitlementViewSet, base_name='entitlements') router.register(r'entitlements', EntitlementViewSet, base_name='entitlements')
enrollments_view = EntitlementEnrollmentViewSet.as_view({
'post': 'create',
'delete': 'destroy',
})
urlpatterns = [ urlpatterns = [
url(r'', include(router.urls)), url(r'', include(router.urls)),
url(
r'entitlements/(?P<uuid>[0-9a-f-]+)/enrollments/$',
enrollments_view,
name='enrollments'
)
] ]
...@@ -3,13 +3,19 @@ import logging ...@@ -3,13 +3,19 @@ import logging
from django.utils import timezone from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from edx_rest_framework_extensions.authentication import JwtAuthentication from edx_rest_framework_extensions.authentication import JwtAuthentication
from rest_framework import permissions, viewsets from rest_framework import permissions, viewsets, status
from rest_framework.response import Response
from rest_framework.authentication import SessionAuthentication
from openedx.core.djangoapps.catalog.utils import get_course_runs_for_course
from entitlements.api.v1.filters import CourseEntitlementFilter from entitlements.api.v1.filters import CourseEntitlementFilter
from entitlements.api.v1.permissions import IsAdminOrAuthenticatedReadOnly from entitlements.api.v1.permissions import IsAdminOrAuthenticatedReadOnly
from entitlements.models import CourseEntitlement
from entitlements.api.v1.serializers import CourseEntitlementSerializer from entitlements.api.v1.serializers import CourseEntitlementSerializer
from entitlements.models import CourseEntitlement from entitlements.models import CourseEntitlement
from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
from opaque_keys.edx.keys import CourseKey
from student.models import CourseEnrollment from student.models import CourseEnrollment
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -57,3 +63,105 @@ class EntitlementViewSet(viewsets.ModelViewSet): ...@@ -57,3 +63,105 @@ class EntitlementViewSet(viewsets.ModelViewSet):
) )
if save_model: if save_model:
instance.save() instance.save()
class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
authentication_classes = (JwtAuthentication, SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
queryset = CourseEntitlement.objects.all()
serializer_class = CourseEntitlementSerializer
def _enroll_entitlement(self, entitlement, course_session_key, user):
enrollment = CourseEnrollment.enroll(
user=user,
course_key=course_session_key,
mode=entitlement.mode,
)
CourseEntitlement.set_enrollment(entitlement, enrollment)
def _unenroll_entitlement(self, entitlement, course_session_key, user):
CourseEnrollment.unenroll(user, course_session_key, skip_refund=True)
CourseEntitlement.set_enrollment(entitlement, None)
def create(self, request, uuid):
course_session_id = request.data.get('course_session_id', None)
if not course_session_id:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data="The Course Run ID was not provided."
)
# Verify that the user has an Entitlement for the provided Course UUID.
try:
entitlement = CourseEntitlement.objects.get(uuid=uuid, user=request.user, expired_at=None)
except CourseEntitlement.DoesNotExist:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data="The Entitlement for this UUID does not exist or is Expired."
)
# Verify the course run ID is of the same type as the Course entitlement.
course_run_valid = False
course_runs = get_course_runs_for_course(entitlement.course_uuid)
for run in course_runs:
if course_session_id == run.get('key', ''):
course_run_valid = True
if not course_run_valid:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data="The Course Run ID is not a match for this Course Entitlement."
)
# Determine if this is a Switch session or a simple enroll and handle both.
if entitlement.enrollment_course_run is None:
self._enroll_entitlement(
entitlement=entitlement,
course_session_key=CourseKey.from_string(course_session_id),
user=request.user
)
else:
if entitlement.enrollment_course_run.course_id != course_session_id:
self._unenroll_entitlement(
entitlement=entitlement,
course_session_key=entitlement.enrollment_course_run.course_id,
user=request.user
)
self._enroll_entitlement(
entitlement=entitlement,
course_session_key=CourseKey.from_string(course_session_id),
user=request.user
)
return Response(
status=status.HTTP_201_CREATED,
data={
'uuid': entitlement.uuid,
'course_run_id': course_session_id,
'is_active': True
}
)
def destroy(self, request, uuid):
"""
On DELETE call to this API we will unenroll the course enrollment for the provided uuid
"""
try:
entitlement = CourseEntitlement.objects.get(uuid=uuid, user=request.user, expired_at=None)
except CourseEntitlement.DoesNotExist:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data="The Entitlement for this UUID does not exist or is Expired."
)
if entitlement.enrollment_course_run is None:
return Response()
self._unenroll_entitlement(
entitlement=entitlement,
course_session_key=entitlement.enrollment_course_run.course_id,
user=request.user
)
return Response(status=status.HTTP_204_NO_CONTENT)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('entitlements', '0002_auto_20171102_0719'),
]
operations = [
migrations.AlterField(
model_name='courseentitlement',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, unique=True, editable=False),
),
]
...@@ -11,7 +11,7 @@ class CourseEntitlement(TimeStampedModel): ...@@ -11,7 +11,7 @@ class CourseEntitlement(TimeStampedModel):
""" """
user = models.ForeignKey(settings.AUTH_USER_MODEL) user = models.ForeignKey(settings.AUTH_USER_MODEL)
uuid = models.UUIDField(default=uuid_tools.uuid4, editable=False) uuid = models.UUIDField(default=uuid_tools.uuid4, editable=False, unique=True)
course_uuid = models.UUIDField(help_text='UUID for the Course, not the Course Run') course_uuid = models.UUIDField(help_text='UUID for the Course, not the Course Run')
expired_at = models.DateTimeField( expired_at = models.DateTimeField(
null=True, null=True,
......
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