feat: add enterprise course enrollments data to enrollments for support

This commit is contained in:
Long Lin
2022-01-10 13:12:05 -05:00
committed by Longsheng Lin
parent 8610856a30
commit 9514cb5732
7 changed files with 317 additions and 33 deletions

View File

@@ -53,6 +53,16 @@ from lms.djangoapps.verify_student.tests.factories import SSOVerificationFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
from openedx.features.enterprise_support.api import enterprise_is_enabled
from openedx.features.enterprise_support.tests.factories import (
EnterpriseCourseEnrollmentFactory,
EnterpriseCustomerUserFactory
)
try:
from consent.models import DataSharingConsent
except ImportError: # pragma: no cover
pass
class SupportViewTestCase(ModuleStoreTestCase):
@@ -315,6 +325,54 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase
}, data[0])
assert {CourseMode.VERIFIED, CourseMode.AUDIT, CourseMode.HONOR, CourseMode.NO_ID_PROFESSIONAL_MODE,
CourseMode.PROFESSIONAL, CourseMode.CREDIT_MODE} == {mode['slug'] for mode in data[0]['course_modes']}
assert 'enterprise_course_enrollments' not in data[0]
@override_settings(FEATURES=dict(ENABLE_ENTERPRISE_INTEGRATION=True))
@enterprise_is_enabled()
def test_get_enrollments_enterprise_enabled(self):
url = reverse(
'support:enrollment_list',
kwargs={'username_or_email': self.student.username}
)
enterprise_customer_user = EnterpriseCustomerUserFactory(
user_id=self.student.id
)
enterprise_course_enrollment = EnterpriseCourseEnrollmentFactory(
course_id=self.course.id,
enterprise_customer_user=enterprise_customer_user
)
data_sharing_consent = DataSharingConsent(
course_id=self.course.id,
enterprise_customer=enterprise_customer_user.enterprise_customer,
username=self.student.username,
granted=True
)
data_sharing_consent.save()
response = self.client.get(url)
assert response.status_code == 200
data = json.loads(response.content.decode('utf-8'))
assert len(data) == 1
enterprise_course_enrollments_data = data[0]['enterprise_course_enrollments']
assert len(enterprise_course_enrollments_data) == 1
expected = {
'course_id': str(enterprise_course_enrollment.course_id),
'enterprise_customer_name': enterprise_customer_user.enterprise_customer.name,
'enterprise_customer_user_id': enterprise_customer_user.id,
'license': None,
'saved_for_later': enterprise_course_enrollment.saved_for_later,
'data_sharing_consent': {
'username': self.student.username,
'enterprise_customer_uuid': str(enterprise_customer_user.enterprise_customer_id),
'exists': data_sharing_consent.exists,
'consent_provided': data_sharing_consent.granted,
'consent_required': data_sharing_consent.consent_required(),
'course_id': str(enterprise_course_enrollment.course_id),
}
}
assert enterprise_course_enrollments_data[0] == expected
@ddt.data(
(True, 'Self Paced'),

View File

@@ -2,6 +2,8 @@
Support tool for changing course enrollments.
"""
from collections import defaultdict
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.db import transaction
from django.db.models import Q
@@ -15,6 +17,7 @@ from rest_framework.generics import GenericAPIView
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.entitlements.models import CourseEntitlement
from common.djangoapps.student.models import (
ENROLLED_TO_ENROLLED,
UNENROLLED_TO_ENROLLED,
@@ -23,7 +26,6 @@ from common.djangoapps.student.models import (
ManualEnrollmentAudit
)
from common.djangoapps.util.json_request import JsonResponse
from common.djangoapps.entitlements.models import CourseEntitlement
from lms.djangoapps.support.decorators import require_support_permission
from lms.djangoapps.support.serializers import ManualEnrollmentSerializer
from lms.djangoapps.verify_student.models import VerificationDeadline
@@ -31,6 +33,12 @@ from openedx.core.djangoapps.credit.email_utils import get_credit_provider_attri
from openedx.core.djangoapps.enrollments.api import get_enrollments, update_enrollment
from openedx.core.djangoapps.enrollments.errors import CourseModeNotFoundError
from openedx.core.djangoapps.enrollments.serializers import ModeSerializer
from openedx.features.enterprise_support.api import (
enterprise_enabled,
get_data_sharing_consents,
get_enterprise_course_enrollments
)
from openedx.features.enterprise_support.serializers import EnterpriseCourseEnrollmentSerializer
class EnrollmentSupportView(View):
@@ -59,6 +67,35 @@ class EnrollmentSupportListView(GenericAPIView):
# does not specify a serializer class.
exclude_from_schema = True
def _enterprise_course_enrollments_by_course_id(self, user):
"""
Returns a dict containing enterprise course enrollments data with
course ids as keys.
"""
enterprise_course_enrollments = get_enterprise_course_enrollments(user)
data_sharing_consents_for_user = get_data_sharing_consents(user)
enterprise_enrollments_by_course_id = defaultdict(list)
consent_by_course_and_enterprise_customer_id = {}
# Get data sharing consent for each enterprise enrollment
for consent in data_sharing_consents_for_user:
key = f'{consent.course_id}-{consent.enterprise_customer_id}'
consent_by_course_and_enterprise_customer_id[key] = consent.serialize()
for enterprise_course_enrollment in enterprise_course_enrollments:
serialized_enterprise_course_enrollment = EnterpriseCourseEnrollmentSerializer(
enterprise_course_enrollment
).data
course_id = enterprise_course_enrollment.course_id
enterprise_customer_id = enterprise_course_enrollment.enterprise_customer_user.enterprise_customer_id
key = f'{course_id}-{enterprise_customer_id}'
consent = consent_by_course_and_enterprise_customer_id.get(key)
serialized_enterprise_course_enrollment['data_sharing_consent'] = consent
enterprise_enrollments_by_course_id[course_id].append(serialized_enterprise_course_enrollment)
return enterprise_enrollments_by_course_id
@method_decorator(require_support_permission)
def get(self, request, username_or_email):
"""
@@ -71,16 +108,24 @@ class EnrollmentSupportListView(GenericAPIView):
return JsonResponse([])
enrollments = get_enrollments(user.username, include_inactive=True)
for enrollment in enrollments:
# Folds the course_details field up into the main JSON object.
enrollment.update(**enrollment.pop('course_details'))
course_key = CourseKey.from_string(enrollment['course_id'])
# get the all courses modes and replace with existing modes.
# Get the all courses modes and replace with existing modes.
enrollment['course_modes'] = self.get_course_modes(course_key)
# Add the price of the course's verified mode.
self.include_verified_mode_info(enrollment, course_key)
# Add manual enrollment history, if it exists
enrollment['manual_enrollment'] = self.manual_enrollment_data(enrollment, course_key)
if enterprise_enabled():
enterprise_enrollments_by_course_id = self._enterprise_course_enrollments_by_course_id(user)
for enrollment in enrollments:
enterprise_course_enrollments = enterprise_enrollments_by_course_id.get(enrollment['course_id'], [])
enrollment['enterprise_course_enrollments'] = enterprise_course_enrollments
return JsonResponse(enrollments)
@method_decorator(require_support_permission)

View File

@@ -7,20 +7,37 @@ consist primarily of authentication, request validation, and serialization.
import logging
from common.djangoapps.course_modes.models import CourseMode
from django.core.exceptions import ObjectDoesNotExist, ValidationError # lint-amnesty, pylint: disable=wrong-import-order
from django.core.exceptions import ( # lint-amnesty, pylint: disable=wrong-import-order
ObjectDoesNotExist,
ValidationError
)
from django.utils.decorators import method_decorator # lint-amnesty, pylint: disable=wrong-import-order
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication # lint-amnesty, pylint: disable=wrong-import-order
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser # lint-amnesty, pylint: disable=wrong-import-order
from edx_rest_framework_extensions.auth.jwt.authentication import \
JwtAuthentication # lint-amnesty, pylint: disable=wrong-import-order
from edx_rest_framework_extensions.auth.session.authentication import \
SessionAuthenticationAllowInactiveUser # lint-amnesty, pylint: disable=wrong-import-order
from opaque_keys import InvalidKeyError # lint-amnesty, pylint: disable=wrong-import-order
from opaque_keys.edx.keys import CourseKey # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework import permissions, status # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework.generics import ListAPIView # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework.response import Response # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework.throttling import UserRateThrottle # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework.views import APIView # lint-amnesty, pylint: disable=wrong-import-order
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.auth import user_has_role
from common.djangoapps.student.models import CourseEnrollment, User
from common.djangoapps.student.roles import CourseStaffRole, GlobalStaff
from common.djangoapps.util.disable_rate_limit import can_disable_rate_limit
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.course_groups.cohorts import CourseUserGroup, add_user_to_cohort, get_cohort_by_name
from openedx.core.djangoapps.embargo import api as embargo_api
from openedx.core.djangoapps.enrollments import api
from openedx.core.djangoapps.enrollments.errors import (
CourseEnrollmentError, CourseEnrollmentExistsError, CourseModeNotFoundError,
CourseEnrollmentError,
CourseEnrollmentExistsError,
CourseModeNotFoundError
)
from openedx.core.djangoapps.enrollments.forms import CourseEnrollmentsApiListForm
from openedx.core.djangoapps.enrollments.paginators import CourseEnrollmentsApiListPagination
@@ -39,15 +56,6 @@ from openedx.features.enterprise_support.api import (
EnterpriseApiServiceClient,
enterprise_enabled
)
from rest_framework import permissions, status # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework.generics import ListAPIView # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework.response import Response # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework.throttling import UserRateThrottle # lint-amnesty, pylint: disable=wrong-import-order
from rest_framework.views import APIView # lint-amnesty, pylint: disable=wrong-import-order
from common.djangoapps.student.auth import user_has_role
from common.djangoapps.student.models import CourseEnrollment, User
from common.djangoapps.student.roles import CourseStaffRole, GlobalStaff
from common.djangoapps.util.disable_rate_limit import can_disable_rate_limit
log = logging.getLogger(__name__)
REQUIRED_ATTRIBUTES = {
@@ -743,7 +751,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
enterprise_api_client = EnterpriseApiServiceClient()
consent_client = ConsentApiServiceClient()
try:
enterprise_api_client.post_enterprise_course_enrollment(username, str(course_id), None)
enterprise_api_client.post_enterprise_course_enrollment(username, str(course_id))
except EnterpriseApiException as error:
log.exception("An unexpected error occurred while creating the new EnterpriseCourseEnrollment "
"for user [%s] in course run [%s]", username, course_id)

View File

@@ -21,25 +21,26 @@ from edx_django_utils.cache import TieredCache, get_cache_key
from edx_rest_api_client.client import EdxRestApiClient
from slumber.exceptions import HttpClientError, HttpNotFoundError, HttpServerError
from common.djangoapps.third_party_auth.pipeline import get as get_partial_pipeline
from common.djangoapps.third_party_auth.provider import Registry
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangolib.markup import HTML, Text
from openedx.features.enterprise_support.utils import get_data_consent_share_cache_key
from common.djangoapps.third_party_auth.pipeline import get as get_partial_pipeline
from common.djangoapps.third_party_auth.provider import Registry
try:
from consent.models import DataSharingConsent, DataSharingConsentTextOverrides
from enterprise.api.v1.serializers import (
EnterpriseCustomerUserReadOnlySerializer,
EnterpriseCustomerUserWriteSerializer
)
from enterprise.models import (
EnterpriseCourseEnrollment,
EnterpriseCustomer,
EnterpriseCustomerIdentityProvider,
EnterpriseCustomerUser,
PendingEnterpriseCustomerUser
)
from enterprise.api.v1.serializers import (
EnterpriseCustomerUserReadOnlySerializer, EnterpriseCustomerUserWriteSerializer
)
from consent.models import DataSharingConsent, DataSharingConsentTextOverrides
except ImportError: # pragma: no cover
pass
@@ -162,14 +163,13 @@ class EnterpriseApiClient:
endpoint = getattr(self.client, 'enterprise-customer')
return endpoint(uuid).get()
def post_enterprise_course_enrollment(self, username, course_id, consent_granted):
def post_enterprise_course_enrollment(self, username, course_id):
"""
Create an EnterpriseCourseEnrollment by using the corresponding serializer (for validation).
"""
data = {
'username': username,
'course_id': course_id,
'consent_granted': consent_granted,
}
endpoint = getattr(self.client, 'enterprise-course-enrollment')
try:
@@ -177,11 +177,10 @@ class EnterpriseApiClient:
except (HttpClientError, HttpServerError):
message = (
"An error occured while posting EnterpriseCourseEnrollment for user {username} and "
"course run {course_id} (consent_granted value: {consent_granted})"
"course run {course_id}."
).format(
username=username,
course_id=course_id,
consent_granted=consent_granted,
)
LOGGER.exception(message)
raise EnterpriseApiException(message) # lint-amnesty, pylint: disable=raise-missing-from
@@ -786,6 +785,33 @@ def get_enterprise_learner_data_from_db(user):
return serializer.data
@enterprise_is_enabled(otherwise=[])
def get_data_sharing_consents(user):
"""
Returns a list of data sharing consent records for the given user.
"""
return DataSharingConsent.objects.filter(
username=user.username
)
@enterprise_is_enabled(otherwise=[])
def get_enterprise_course_enrollments(user):
"""
Returns a list of enterprise course enrollments for the given user.
"""
return EnterpriseCourseEnrollment.objects.select_related(
'licensed_with',
'enterprise_customer_user'
).prefetch_related(
'enterprise_customer_user__enterprise_customer'
).filter(
enterprise_customer_user__user_id=user.id
)
@enterprise_is_enabled()
def enterprise_customer_from_session_or_learner_data(request):
"""

View File

@@ -0,0 +1,44 @@
"""
Defines serializers for enterprise_support.
"""
from rest_framework import serializers
try:
from enterprise.api.v1.serializers import \
EnterpriseCourseEnrollmentReadOnlySerializer as BaseEnterpriseCourseEnrollmentSerializer
from enterprise.models import EnterpriseCourseEnrollment
except ImportError: # pragma: no cover
pass
class EnterpriseCourseEnrollmentSerializer(BaseEnterpriseCourseEnrollmentSerializer):
"""
Serializer for EnterpriseCourseEnrollment model.
"""
enterprise_customer_name = serializers.SerializerMethodField()
license = serializers.SerializerMethodField()
class Meta:
model = EnterpriseCourseEnrollment
fields = (
'course_id',
'enterprise_customer_name',
'enterprise_customer_user_id',
'license',
'saved_for_later'
)
def get_enterprise_customer_name(self, obj):
return obj.enterprise_customer_user.enterprise_customer.name
def get_license(self, obj):
licensed_ece = obj.license
if licensed_ece:
return {
'uuid': str(licensed_ece.license_uuid),
'is_revoked': licensed_ece.is_revoked
}

View File

@@ -41,7 +41,9 @@ from openedx.features.enterprise_support.api import (
get_consent_notification_data,
get_consent_required_courses,
get_dashboard_consent_notification,
get_data_sharing_consents,
get_enterprise_consent_url,
get_enterprise_course_enrollments,
get_enterprise_learner_data_from_api,
get_enterprise_learner_data_from_db,
get_enterprise_learner_portal_enabled_message,
@@ -50,6 +52,7 @@ from openedx.features.enterprise_support.api import (
)
from openedx.features.enterprise_support.tests import FEATURES_WITH_ENTERPRISE_ENABLED
from openedx.features.enterprise_support.tests.factories import (
EnterpriseCourseEnrollmentFactory,
EnterpriseCustomerIdentityProviderFactory,
EnterpriseCustomerUserFactory
)
@@ -185,18 +188,16 @@ class TestEnterpriseApi(EnterpriseServiceMockMixin, CacheIsolationTestCase):
username = 'spongebob'
course_id = 'burger-flipping-101'
consent_granted = True
if should_raise_http_error:
with pytest.raises(EnterpriseApiException):
api_client.post_enterprise_course_enrollment(username, course_id, consent_granted)
api_client.post_enterprise_course_enrollment(username, course_id)
else:
api_client.post_enterprise_course_enrollment(username, course_id, consent_granted)
api_client.post_enterprise_course_enrollment(username, course_id)
mock_endpoint.post.assert_called_once_with(data={
'username': username,
'course_id': course_id,
'consent_granted': consent_granted,
})
@mock.patch('openedx.features.enterprise_support.api.enterprise_customer_uuid_for_request')
@@ -398,10 +399,45 @@ class TestEnterpriseApi(EnterpriseServiceMockMixin, CacheIsolationTestCase):
assert not get_enterprise_learner_data_from_db(self.user)
def test_get_enterprise_learner_data_from_db(self):
enterprise_customer_user = EnterpriseCustomerUserFactory(user_id=self.user.id) # lint-amnesty, pylint: disable=unused-variable
EnterpriseCustomerUserFactory(user_id=self.user.id)
user_data = get_enterprise_learner_data_from_db(self.user)[0]['user']
assert user_data['username'] == self.user.username
@ddt.data(True, False)
@mock.patch('openedx.features.enterprise_support.api.enterprise_enabled')
def test_get_data_sharing_consents(self, is_enterprise_enabled, mock_enterprise_enabled):
mock_enterprise_enabled.return_value = is_enterprise_enabled
enterprise_customer_user = EnterpriseCustomerUserFactory(user_id=self.user.id)
if not is_enterprise_enabled:
assert get_data_sharing_consents(self.user) == []
else:
course_id = 'fake-course'
data_sharing_consent = DataSharingConsent(
course_id=course_id,
enterprise_customer=enterprise_customer_user.enterprise_customer,
username=self.user.username,
granted=False
)
data_sharing_consent.save()
data_sharing_consents = get_data_sharing_consents(self.user)
assert len(data_sharing_consents) == 1
assert data_sharing_consents[0].id == data_sharing_consent.id
@ddt.data(True, False)
@mock.patch('openedx.features.enterprise_support.api.enterprise_enabled')
def test_get_enterprise_course_enrollments(self, is_enterprise_enabled, mock_enterprise_enabled):
mock_enterprise_enabled.return_value = is_enterprise_enabled
enterprise_customer_user = EnterpriseCustomerUserFactory(user_id=self.user.id)
if not is_enterprise_enabled:
assert get_enterprise_course_enrollments(self.user) == []
else:
ece = EnterpriseCourseEnrollmentFactory(enterprise_customer_user=enterprise_customer_user)
enterprise_course_enrollments = get_enterprise_course_enrollments(self.user)
assert len(enterprise_course_enrollments) == 1
assert enterprise_course_enrollments[0].id == ece.id
@httpretty.activate
@mock.patch('openedx.features.enterprise_support.api.get_enterprise_learner_data_from_db')
@mock.patch('openedx.features.enterprise_support.api.EnterpriseCustomer')

View File

@@ -0,0 +1,67 @@
"""
Tests for custom enterprise_support Serializers.
"""
from uuid import uuid4
from django.test import TestCase
from enterprise.models import LicensedEnterpriseCourseEnrollment
from openedx.features.enterprise_support.serializers import EnterpriseCourseEnrollmentSerializer
from openedx.features.enterprise_support.tests.factories import (
EnterpriseCourseEnrollmentFactory,
EnterpriseCustomerUserFactory
)
class EnterpriseCourseEnrollmentSerializerTests(TestCase):
"""
Tests for EnterpriseCourseEnrollmentSerializer.
"""
@classmethod
def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called
enterprise_customer_user = EnterpriseCustomerUserFactory()
enterprise_course_enrollment = EnterpriseCourseEnrollmentFactory(
enterprise_customer_user=enterprise_customer_user
)
cls.enterprise_customer_user = enterprise_customer_user
cls.enterprise_course_enrollment = enterprise_course_enrollment
def test_data_with_license(self):
""" Verify the correct fields are serialized when the enrollment is licensed. """
license_uuid = uuid4()
licensed_ece = LicensedEnterpriseCourseEnrollment(
license_uuid=license_uuid,
enterprise_course_enrollment=self.enterprise_course_enrollment
)
licensed_ece.save()
serializer = EnterpriseCourseEnrollmentSerializer(self.enterprise_course_enrollment)
expected = {
'enterprise_customer_name': self.enterprise_customer_user.enterprise_customer.name,
'enterprise_customer_user_id': self.enterprise_customer_user.id,
'course_id': self.enterprise_course_enrollment.course_id,
'saved_for_later': self.enterprise_course_enrollment.saved_for_later,
'license': {
'uuid': str(license_uuid),
'is_revoked': licensed_ece.is_revoked,
}
}
self.assertDictEqual(serializer.data, expected)
def test_data_without_license(self):
""" Verify the correct fields are serialized when the enrollment is not licensed. """
serializer = EnterpriseCourseEnrollmentSerializer(self.enterprise_course_enrollment)
expected = {
'enterprise_customer_name': self.enterprise_customer_user.enterprise_customer.name,
'enterprise_customer_user_id': self.enterprise_customer_user.id,
'course_id': self.enterprise_course_enrollment.course_id,
'saved_for_later': self.enterprise_course_enrollment.saved_for_later,
'license': None
}
self.assertDictEqual(serializer.data, expected)