Skip to content

Commit

Permalink
feat: save cornerstone learner information (#2068)
Browse files Browse the repository at this point in the history
  • Loading branch information
sameenfatima78 authored Apr 9, 2024
1 parent aa09eea commit 4c1bd31
Show file tree
Hide file tree
Showing 10 changed files with 405 additions and 72 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Change Log
Unreleased
----------
[4.15.2]
--------
* feat: save cornerstone learner's information received from frontend.

[4.15.1]
--------
* feat: allowing for sorting and filtering of the enterprise group learner endpoints
Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "4.15.1"
__version__ = "4.15.2"
17 changes: 14 additions & 3 deletions enterprise/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2363,6 +2363,7 @@ def get(self, request, *args, **kwargs):
- Look to see whether a request is eligible for direct audit enrollment, and if so, directly enroll the user.
"""
user_id = request.user.id
enterprise_customer_uuid, course_run_id, course_key, program_uuid = RouterView.get_path_variables(**kwargs)
enterprise_customer = get_enterprise_customer_or_404(enterprise_customer_uuid)
if course_key:
Expand All @@ -2382,15 +2383,17 @@ def get(self, request, *args, **kwargs):
'CornerstoneEnterpriseCustomerConfiguration'
)
with transaction.atomic():
# The presense of a sessionToken and subdomain param indicates a Cornerstone redirect
# The presence of a sessionToken and subdomain param indicates a Cornerstone redirect
# We need to store this sessionToken for api access
csod_user_guid = request.GET.get('userGuid')
csod_callback_url = request.GET.get('callbackUrl')
csod_session_token = request.GET.get('sessionToken')
csod_subdomain = request.GET.get("subdomain")
if csod_session_token and csod_subdomain:
LOGGER.info(
f'integrated_channel=CSOD, '
f'integrated_channel_enterprise_customer_uuid={enterprise_customer.uuid}, '
f'integrated_channel_lms_user={request.user.id}, '
f'integrated_channel_lms_user={user_id}, '
f'integrated_channel_course_key={course_key}, '
'enrollment redirect'
)
Expand All @@ -2403,7 +2406,15 @@ def get(self, request, *args, **kwargs):
cornerstone_customer_configuration.session_token = csod_session_token
cornerstone_customer_configuration.session_token_modified = localized_utcnow()
cornerstone_customer_configuration.save()
create_cornerstone_learner_data(request, cornerstone_customer_configuration, course_key)
create_cornerstone_learner_data(
user_id,
csod_user_guid,
csod_session_token,
csod_callback_url,
csod_subdomain,
cornerstone_customer_configuration,
course_key
)
else:
LOGGER.error(
f'integrated_channel=CSOD, '
Expand Down
12 changes: 10 additions & 2 deletions integrated_channels/api/v1/cornerstone/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@

from rest_framework import routers

from .views import CornerstoneConfigurationViewSet
from django.urls import path

from .views import CornerstoneConfigurationViewSet, CornerstoneLearnerInformationView

app_name = 'cornerstone'
router = routers.DefaultRouter()
router.register(r'configuration', CornerstoneConfigurationViewSet, basename="configuration")
urlpatterns = router.urls
urlpatterns = [
path('save-learner-information', CornerstoneLearnerInformationView.as_view(),
name='save-learner-information'
),
]

urlpatterns += router.urls
103 changes: 103 additions & 0 deletions integrated_channels/api/v1/cornerstone/views.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,120 @@
"""
Viewsets for integrated_channels/v1/cornerstone/
"""
from logging import getLogger

from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from rest_framework import permissions, viewsets
from rest_framework.authentication import SessionAuthentication
from rest_framework.response import Response
from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND
from rest_framework.views import APIView

from django.contrib import auth
from django.db import transaction

from enterprise.api.throttles import ServiceUserThrottle
from enterprise.utils import get_enterprise_customer_or_404, get_enterprise_customer_user, localized_utcnow
from integrated_channels.api.v1.mixins import PermissionRequiredForIntegratedChannelMixin
from integrated_channels.cornerstone.models import CornerstoneEnterpriseCustomerConfiguration
from integrated_channels.cornerstone.utils import create_cornerstone_learner_data

from .serializers import CornerstoneConfigSerializer

LOGGER = getLogger(__name__)
User = auth.get_user_model()


class CornerstoneConfigurationViewSet(PermissionRequiredForIntegratedChannelMixin, viewsets.ModelViewSet):
"""Viewset for CornerstoneEnterpriseCustomerConfiguration"""
serializer_class = CornerstoneConfigSerializer
permission_classes = (permissions.IsAuthenticated,)
permission_required = 'enterprise.can_access_admin_dashboard'

configuration_model = CornerstoneEnterpriseCustomerConfiguration


class CornerstoneLearnerInformationView(APIView):
"""Viewset for saving information of a cornerstone learner"""
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (JwtAuthentication, SessionAuthentication,)
throttle_classes = (ServiceUserThrottle,)

def post(self, request):
"""
An endpoint to save a cornerstone learner information received from frontend.
integrated_channels/api/v1/cornerstone/save-learner-information
Requires a JSON object in the following format:
{
"courseKey": "edX+DemoX",
"enterpriseUUID": "enterprise-uuid-goes-right-here",
"userGuid": "user-guid-from-csod",
"callbackUrl": "https://example.com/csod/callback/1",
"sessionToken": "123123123",
"subdomain": "edx.csod.com"
}
"""
user_id = request.user.id
enterprise_customer_uuid = request.data.get('enterpriseUUID')
enterprise_customer = get_enterprise_customer_or_404(enterprise_customer_uuid)
course_key = request.data.get('courseKey')
with transaction.atomic():
csod_user_guid = request.data.get('userGuid')
csod_callback_url = request.data.get('callbackUrl')
csod_session_token = request.data.get('sessionToken')
csod_subdomain = request.data.get("subdomain")

if csod_session_token and csod_subdomain:
LOGGER.info(
f'integrated_channel=CSOD, '
f'integrated_channel_enterprise_customer_uuid={enterprise_customer_uuid}, '
f'integrated_channel_lms_user={user_id}, '
f'integrated_channel_course_key={course_key}, '
'saving CSOD learner information'
)
cornerstone_customer_configuration = \
CornerstoneEnterpriseCustomerConfiguration.get_by_customer_and_subdomain(
enterprise_customer=enterprise_customer,
customer_subdomain=csod_subdomain
)
if cornerstone_customer_configuration:
# check if request user is linked as a learner with the given enterprise before savin anything
enterprise_customer_user = get_enterprise_customer_user(user_id, enterprise_customer_uuid)
if enterprise_customer_user:
# saving session token in enterprise config to access cornerstone apis
cornerstone_customer_configuration.session_token = csod_session_token
cornerstone_customer_configuration.session_token_modified = localized_utcnow()
cornerstone_customer_configuration.save()
# saving learner information received from cornerstone
create_cornerstone_learner_data(
user_id,
csod_user_guid,
csod_session_token,
csod_callback_url,
csod_subdomain,
cornerstone_customer_configuration,
course_key
)
else:
LOGGER.error(
f'integrated_channel=CSOD, '
f'integrated_channel_enterprise_customer_uuid={enterprise_customer_uuid}, '
f'integrated_channel_lms_user={user_id}, '
f'integrated_channel_course_key={course_key}, '
f'user is not linked to the given enterprise'
)
message = (f'Cornerstone information could not be saved for learner with user_id={user_id}'
f'because user is not linked to the given enterprise {enterprise_customer_uuid}')
return Response(data={'error': message}, status=HTTP_404_NOT_FOUND)
else:
LOGGER.error(
f'integrated_channel=CSOD, '
f'integrated_channel_enterprise_customer_uuid={enterprise_customer_uuid}, '
f'integrated_channel_lms_user={user_id}, '
f'integrated_channel_course_key={course_key}, '
f'unable to find cornerstone config matching subdomain {csod_subdomain}'
)
message = (f'Cornerstone information could not be saved for learner with user_id={user_id}'
f'because no config exist with the subdomain {csod_subdomain}')
return Response(data={'error': message}, status=HTTP_404_NOT_FOUND)
return Response(status=HTTP_200_OK)
69 changes: 33 additions & 36 deletions integrated_channels/cornerstone/exporters/learner_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,47 +36,44 @@ def get_learner_data_records(
'cornerstone',
'CornerstoneLearnerDataTransmissionAudit'
)
enterprise_customer_user = enterprise_enrollment.enterprise_customer_user
# get the proper internal representation of the course key
course_id = get_course_id_for_enrollment(enterprise_enrollment)
# because CornerstoneLearnerDataTransmissionAudit records are created with a click-through
# the internal edX course_id is always used on the CornerstoneLearnerDataTransmissionAudit records
# rather than the external_course_id mapped via CornerstoneCourseKey
transmission_exists = CornerstoneLearnerDataTransmissionAudit.objects.filter(
user_id=enterprise_enrollment.enterprise_customer_user.user.id,
course_id=course_id,
plugin_configuration_id=self.enterprise_configuration.id,
enterprise_customer_uuid=self.enterprise_configuration.enterprise_customer.uuid,
).exists()

if transmission_exists or enterprise_customer_user.user_email is not None:
csod_transmission_record, __ = CornerstoneLearnerDataTransmissionAudit.objects.update_or_create(
user_id=enterprise_customer_user.user.id,
try:
# get the proper internal representation of the course key
course_id = get_course_id_for_enrollment(enterprise_enrollment)
# because CornerstoneLearnerDataTransmissionAudit records are created with a click-through
# the internal edX course_id is always used on the CornerstoneLearnerDataTransmissionAudit records
# rather than the external_course_id mapped via CornerstoneCourseKey
csod_learner_data_transmission = CornerstoneLearnerDataTransmissionAudit.objects.get(
user_id=enterprise_enrollment.enterprise_customer_user.user.id,
course_id=course_id,
plugin_configuration_id=self.enterprise_configuration.id,
enterprise_customer_uuid=self.enterprise_configuration.enterprise_customer.uuid,
defaults={
"enterprise_course_enrollment_id": enterprise_enrollment.id,
"grade": grade,
"course_completed": course_completed,
"completed_timestamp": completed_date,
"user_email": enterprise_customer_user.user_email,
},
)
return [csod_transmission_record]
else:
LOGGER.info(
generate_formatted_log(
self.enterprise_configuration.channel_code(),
enterprise_customer_user.enterprise_customer.uuid,
enterprise_customer_user.user_id,
enterprise_enrollment.course_id,
(
'get_learner_data_records finished. No learner data was sent for this LMS User Id because '
'Cornerstone User ID not found for [{name}]'.format(
name=enterprise_customer_user.enterprise_customer.name
)
csod_learner_data_transmission.enterprise_course_enrollment_id = enterprise_enrollment.id
csod_learner_data_transmission.grade = grade
csod_learner_data_transmission.course_completed = course_completed
csod_learner_data_transmission.completed_timestamp = completed_date

# Used for api error reporting
csod_learner_data_transmission.user_email = enterprise_enrollment.enterprise_customer_user.user_email

enterprise_customer = enterprise_enrollment.enterprise_customer_user.enterprise_customer
csod_learner_data_transmission.enterprise_customer_uuid = enterprise_customer.uuid
csod_learner_data_transmission.plugin_configuration_id = self.enterprise_configuration.id
return [
csod_learner_data_transmission
]
except CornerstoneLearnerDataTransmissionAudit.DoesNotExist:
LOGGER.info(generate_formatted_log(
self.enterprise_configuration.channel_code(),
enterprise_enrollment.enterprise_customer_user.enterprise_customer.uuid,
enterprise_enrollment.enterprise_customer_user.user_id,
enterprise_enrollment.course_id,
(
'get_learner_data_records finished. No learner data was sent for this LMS User Id {user_id} '
'because Cornerstone User ID not found'.format(
user_id=enterprise_enrollment.enterprise_customer_user.user_id
)
)
)
))
return None
24 changes: 16 additions & 8 deletions integrated_channels/cornerstone/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,30 @@ def cornerstone_course_key_model():
LOGGER = getLogger(__name__)


def create_cornerstone_learner_data(request, cornerstone_customer_configuration, course_id):
def create_cornerstone_learner_data(
user_id,
user_guid,
session_token,
callback_url,
subdomain,
cornerstone_customer_configuration,
course_id
):
"""
updates or creates CornerstoneLearnerDataTransmissionAudit
"""
enterprise_customer_uuid = cornerstone_customer_configuration.enterprise_customer.uuid
try:
defaults = {
'user_guid': request.GET['userGuid'],
'session_token': request.GET['sessionToken'],
'callback_url': request.GET['callbackUrl'],
'subdomain': request.GET['subdomain'],
'user_guid': user_guid,
'session_token': session_token,
'callback_url': callback_url,
'subdomain': subdomain,
}
cornerstone_learner_data_transmission_audit().objects.update_or_create(
enterprise_customer_uuid=enterprise_customer_uuid,
plugin_configuration_id=cornerstone_customer_configuration.id,
user_id=request.user.id,
user_id=user_id,
course_id=course_id,
defaults=defaults
)
Expand All @@ -49,15 +57,15 @@ def create_cornerstone_learner_data(request, cornerstone_customer_configuration,
LOGGER.exception(
f'integrated_channel=CSOD, '
f'integrated_channel_enterprise_customer_uuid={enterprise_customer_uuid}, '
f'integrated_channel_lms_user={request.user.id}, '
f'integrated_channel_lms_user={user_id}, '
f'integrated_channel_course_key={course_id}, '
'malformed cornerstone request missing a param'
)
except Exception: # pylint: disable=broad-except
LOGGER.exception(
f'integrated_channel=CSOD, '
f'integrated_channel_enterprise_customer_uuid={enterprise_customer_uuid}, '
f'integrated_channel_lms_user={request.user.id}, '
f'integrated_channel_lms_user={user_id}, '
f'integrated_channel_course_key={course_id}, '
f'Unable to Create/Update CornerstoneLearnerDataTransmissionAudit.'
)
Expand Down
Loading

0 comments on commit 4c1bd31

Please sign in to comment.