* chore: update API endpoints to support default JWT auth The default DRF Auth classes were recently updated to allow for both JWT and Session auth by default. Any endpoint that overrides the AUTHENTICATION_CLASSES but has just session, just JWT or just both of those should be updated to remove the override. Details in https://github.com/openedx/edx-platform/issues/33662
598 lines
26 KiB
Python
598 lines
26 KiB
Python
"""
|
|
Views for the Entitlements v1 API.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.db import IntegrityError, transaction
|
|
from django.db.models import Q
|
|
from django.http import HttpResponseBadRequest
|
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
|
from edx_rest_framework_extensions.paginators import DefaultPagination
|
|
from opaque_keys import InvalidKeyError
|
|
from opaque_keys.edx.keys import CourseKey
|
|
from rest_framework import permissions, status, viewsets
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from common.djangoapps.course_modes.models import CourseMode
|
|
from common.djangoapps.entitlements.models import ( # lint-amnesty, pylint: disable=line-too-long
|
|
CourseEntitlement,
|
|
CourseEntitlementPolicy,
|
|
CourseEntitlementSupportDetail
|
|
)
|
|
from common.djangoapps.entitlements.rest_api.v1.filters import CourseEntitlementFilter
|
|
from common.djangoapps.entitlements.rest_api.v1.permissions import (
|
|
IsAdminOrSupportOrAuthenticatedReadOnly,
|
|
IsSubscriptionWorkerUser
|
|
)
|
|
from common.djangoapps.entitlements.rest_api.v1.serializers import CourseEntitlementSerializer
|
|
from common.djangoapps.entitlements.rest_api.v1.throttles import ServiceUserThrottle
|
|
from common.djangoapps.entitlements.tasks import retry_revoke_subscriptions_verified_access
|
|
from common.djangoapps.entitlements.utils import (
|
|
is_course_run_entitlement_fulfillable,
|
|
revoke_entitlements_and_downgrade_courses_to_audit
|
|
)
|
|
from common.djangoapps.student.models import AlreadyEnrolledError, CourseEnrollment, CourseEnrollmentException
|
|
from openedx.core.djangoapps.catalog.utils import get_course_runs_for_course, get_owners_for_course
|
|
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
|
from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
|
|
from openedx.core.djangoapps.credentials.utils import get_courses_completion_status
|
|
from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in
|
|
|
|
User = get_user_model()
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class EntitlementsPagination(DefaultPagination):
|
|
"""
|
|
Paginator for entitlements API.
|
|
"""
|
|
page_size = 50
|
|
max_page_size = 100
|
|
|
|
|
|
@transaction.atomic
|
|
def _unenroll_entitlement(course_entitlement, course_run_key):
|
|
"""
|
|
Internal method to handle the details of Unenrolling a User in a Course Run.
|
|
"""
|
|
CourseEnrollment.unenroll(course_entitlement.user, course_run_key, skip_refund=True)
|
|
|
|
|
|
@transaction.atomic
|
|
def _process_revoke_and_unenroll_entitlement(course_entitlement, is_refund=False):
|
|
"""
|
|
Process the revoke of the Course Entitlement and refund if needed
|
|
|
|
Arguments:
|
|
course_entitlement: Course Entitlement Object
|
|
|
|
is_refund (bool): True if a refund should be processed
|
|
|
|
Exceptions:
|
|
IntegrityError if there is an issue that should reverse the database changes
|
|
"""
|
|
if course_entitlement.expired_at is None:
|
|
course_entitlement.expire_entitlement()
|
|
log.info(
|
|
'Set expired_at to [%s] for course entitlement [%s]',
|
|
course_entitlement.expired_at,
|
|
course_entitlement.uuid
|
|
)
|
|
|
|
if course_entitlement.enrollment_course_run is not None:
|
|
course_id = course_entitlement.enrollment_course_run.course_id
|
|
_unenroll_entitlement(course_entitlement, course_id)
|
|
log.info(
|
|
'Unenrolled user [%s] from course run [%s] as part of revocation of course entitlement [%s]',
|
|
course_entitlement.user.username,
|
|
course_id,
|
|
course_entitlement.uuid
|
|
)
|
|
|
|
if is_refund:
|
|
course_entitlement.refund()
|
|
|
|
|
|
def set_entitlement_policy(entitlement, site):
|
|
"""
|
|
Assign the appropriate CourseEntitlementPolicy to the given CourseEntitlement based on its mode and site.
|
|
|
|
Arguments:
|
|
entitlement: Course Entitlement object
|
|
site: string representation of a Site object
|
|
|
|
Notes:
|
|
Site-specific, mode-agnostic policies take precedence over mode-specific, site-agnostic policies.
|
|
If no appropriate CourseEntitlementPolicy is found, the default CourseEntitlementPolicy is assigned.
|
|
"""
|
|
policy_mode = entitlement.mode
|
|
if CourseMode.is_professional_slug(policy_mode):
|
|
policy_mode = CourseMode.PROFESSIONAL
|
|
filter_query = (Q(site=site) | Q(site__isnull=True)) & (Q(mode=policy_mode) | Q(mode__isnull=True))
|
|
policy = CourseEntitlementPolicy.objects.filter(filter_query).order_by('-site', '-mode').first()
|
|
entitlement.policy = policy if policy else None
|
|
entitlement.save()
|
|
|
|
|
|
class EntitlementViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
ViewSet for the Entitlements API.
|
|
"""
|
|
ENTITLEMENT_UUID4_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
|
|
|
|
authentication_classes = (JwtAuthentication, SessionAuthenticationCrossDomainCsrf,)
|
|
permission_classes = (permissions.IsAuthenticated, IsAdminOrSupportOrAuthenticatedReadOnly,)
|
|
lookup_value_regex = ENTITLEMENT_UUID4_REGEX
|
|
lookup_field = 'uuid'
|
|
serializer_class = CourseEntitlementSerializer
|
|
filter_backends = (DjangoFilterBackend,)
|
|
filterset_class = CourseEntitlementFilter
|
|
pagination_class = EntitlementsPagination
|
|
throttle_classes = (ServiceUserThrottle,)
|
|
|
|
def get_queryset(self):
|
|
user = self.request.user
|
|
|
|
if self.request.method in permissions.SAFE_METHODS:
|
|
if (user.is_staff and
|
|
(self.request.query_params.get('user', None) is not None or
|
|
self.kwargs.get('uuid', None) is not None)):
|
|
# Return the full query set so that the Filters class can be used to apply,
|
|
# - The UUID Filter
|
|
# - The User Filter to the GET request
|
|
return CourseEntitlement.objects.all().select_related('user').select_related('enrollment_course_run')
|
|
# Non Staff Users will only be able to retrieve their own entitlements
|
|
return CourseEntitlement.objects.filter(user=user).select_related('user').select_related(
|
|
'enrollment_course_run'
|
|
)
|
|
# All other methods require the full Query set and the Permissions class already restricts access to them
|
|
# to Admin users
|
|
return CourseEntitlement.objects.all().select_related('user').select_related('enrollment_course_run')
|
|
|
|
def get_upgradeable_enrollments_for_entitlement(self, entitlement):
|
|
"""
|
|
Retrieve all the CourseEnrollments that are upgradeable for a given CourseEntitlement
|
|
|
|
Arguments:
|
|
entitlement: CourseEntitlement that we are requesting the CourseEnrollments for.
|
|
|
|
Returns:
|
|
list: List of upgradeable CourseEnrollments
|
|
"""
|
|
# find all course_runs within the course
|
|
course_runs = get_course_runs_for_course(entitlement.course_uuid)
|
|
|
|
# check if the user has enrollments for any of the course_runs
|
|
upgradeable_enrollments = []
|
|
for course_run in course_runs:
|
|
course_run_id = CourseKey.from_string(course_run.get('key'))
|
|
enrollment = CourseEnrollment.get_enrollment(entitlement.user, course_run_id)
|
|
|
|
if (enrollment and
|
|
enrollment.is_active and
|
|
is_course_run_entitlement_fulfillable(course_run_id, entitlement)):
|
|
upgradeable_enrollments.append(enrollment)
|
|
|
|
return upgradeable_enrollments
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
support_details = request.data.pop('support_details', [])
|
|
email_opt_in = request.data.pop('email_opt_in', False)
|
|
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
self.perform_create(serializer)
|
|
|
|
entitlement = serializer.instance
|
|
set_entitlement_policy(entitlement, request.site)
|
|
|
|
# The owners for a course are the organizations that own the course. By taking owner.key,
|
|
# we are able to pass in the organization key for email_opt_in
|
|
owners = get_owners_for_course(entitlement.course_uuid)
|
|
for owner in owners:
|
|
update_email_opt_in(entitlement.user, owner['key'], email_opt_in)
|
|
|
|
if support_details:
|
|
for support_detail in support_details:
|
|
support_detail['entitlement'] = entitlement
|
|
support_detail['support_user'] = request.user
|
|
CourseEntitlementSupportDetail.objects.create(**support_detail)
|
|
else:
|
|
user = entitlement.user
|
|
upgradeable_enrollments = self.get_upgradeable_enrollments_for_entitlement(entitlement)
|
|
|
|
# if there is only one upgradeable enrollment, update the mode to the paid entitlement.mode
|
|
# if there is any ambiguity about which enrollment to upgrade
|
|
# (i.e. multiple upgradeable enrollments or no available upgradeable enrollment), don't alter
|
|
# the enrollment
|
|
if len(upgradeable_enrollments) == 1:
|
|
enrollment = upgradeable_enrollments[0]
|
|
log.info(
|
|
'Upgrading enrollment [%s] from %s to %s while adding entitlement for user [%s] for course [%s]',
|
|
enrollment,
|
|
enrollment.mode,
|
|
serializer.data.get('mode'),
|
|
user.username,
|
|
serializer.data.get('course_uuid')
|
|
)
|
|
enrollment.update_enrollment(mode=entitlement.mode)
|
|
entitlement.set_enrollment(enrollment)
|
|
else:
|
|
log.info(
|
|
'No enrollment upgraded while adding entitlement for user [%s] for course [%s] ',
|
|
user.username,
|
|
serializer.data.get('course_uuid')
|
|
)
|
|
|
|
headers = self.get_success_headers(serializer.data)
|
|
# Note, the entitlement is re-serialized before getting added to the Response,
|
|
# so that the 'modified' date reflects changes that occur when upgrading enrollment.
|
|
return Response(
|
|
CourseEntitlementSerializer(entitlement).data,
|
|
status=status.HTTP_201_CREATED, headers=headers
|
|
)
|
|
|
|
def retrieve(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=unused-argument
|
|
"""
|
|
Override the retrieve method to expire a record that is past the
|
|
policy and is requested via the API before returning that record.
|
|
"""
|
|
entitlement = self.get_object()
|
|
entitlement.update_expired_at()
|
|
serializer = self.get_serializer(entitlement)
|
|
return Response(serializer.data)
|
|
|
|
def list(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=unused-argument
|
|
"""
|
|
Override the list method to expire records that are past the
|
|
policy and requested via the API before returning those records.
|
|
"""
|
|
queryset = self.filter_queryset(self.get_queryset())
|
|
user = self.request.user
|
|
if not user.is_staff:
|
|
with transaction.atomic():
|
|
for entitlement in queryset:
|
|
entitlement.update_expired_at()
|
|
|
|
page = self.paginate_queryset(queryset)
|
|
if page is not None:
|
|
serializer = self.get_serializer(page, many=True)
|
|
return self.get_paginated_response(serializer.data)
|
|
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
def perform_destroy(self, instance):
|
|
"""
|
|
This method is an override and is called by the destroy method, which is called when a DELETE operation occurs
|
|
|
|
This method will revoke the User's entitlement and unenroll the user if they are enrolled
|
|
in a Course Run
|
|
|
|
It is assumed the user has already been refunded.
|
|
"""
|
|
log.info(
|
|
'Entitlement Revoke requested for Course Entitlement[%s]',
|
|
instance.uuid
|
|
)
|
|
# This is not called with is_refund=True here because it is assumed the user has already been refunded.
|
|
_process_revoke_and_unenroll_entitlement(instance)
|
|
|
|
def partial_update(self, request, *args, **kwargs):
|
|
entitlement_uuid = kwargs.get('uuid', None)
|
|
|
|
try:
|
|
entitlement = CourseEntitlement.objects.get(uuid=entitlement_uuid)
|
|
except CourseEntitlement.DoesNotExist:
|
|
return HttpResponseBadRequest(
|
|
'Could not find entitlement {entitlement_uuid} to update'.format(
|
|
entitlement_uuid=entitlement_uuid
|
|
)
|
|
)
|
|
support_details = request.data.pop('support_details', [])
|
|
|
|
# If a patch request does not explicitly update an entitlement's refundability status, we want to ensure that
|
|
# changes made to other attributes of the entitlement do not implicitly change its ability to be refunded.
|
|
if request.data.get('refund_locked') is None:
|
|
request.data['refund_locked'] = not entitlement.is_entitlement_refundable()
|
|
|
|
for support_detail in support_details:
|
|
support_detail['entitlement'] = entitlement
|
|
support_detail['support_user'] = request.user
|
|
unenrolled_run_id = support_detail.get('unenrolled_run', None)
|
|
if unenrolled_run_id:
|
|
try:
|
|
unenrolled_run_course_key = CourseKey.from_string(unenrolled_run_id)
|
|
_unenroll_entitlement(entitlement, unenrolled_run_course_key)
|
|
support_detail['unenrolled_run'] = CourseOverview.objects.get(id=unenrolled_run_course_key)
|
|
except (InvalidKeyError, CourseOverview.DoesNotExist) as error:
|
|
return HttpResponseBadRequest(
|
|
'Error raised while trying to unenroll user {user} from course run {course_id}: {error}'
|
|
.format(user=entitlement.user.username, course_id=unenrolled_run_id, error=error)
|
|
)
|
|
CourseEntitlementSupportDetail.objects.create(**support_detail)
|
|
|
|
return super().partial_update(request, *args, **kwargs) # lint-amnesty, pylint: disable=no-member, super-with-arguments
|
|
|
|
|
|
class EntitlementEnrollmentViewSet(viewsets.GenericViewSet):
|
|
"""
|
|
Endpoint in the Entitlement API to handle the Enrollment of a User's Entitlement.
|
|
This API will handle
|
|
- Enroll
|
|
- Unenroll
|
|
- Switch Enrollment
|
|
"""
|
|
# TODO: ARCH-91
|
|
# This view is excluded from Swagger doc generation because it
|
|
# does not specify a serializer class.
|
|
exclude_from_schema = True
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
queryset = CourseEntitlement.objects.all()
|
|
|
|
def _verify_course_run_for_entitlement(self, entitlement, course_run_id):
|
|
"""
|
|
Verifies that a Course run is a child of the Course assigned to the entitlement.
|
|
"""
|
|
course_runs = get_course_runs_for_course(entitlement.course_uuid)
|
|
for run in course_runs:
|
|
if course_run_id == run.get('key', ''):
|
|
return True
|
|
return False
|
|
|
|
@transaction.atomic
|
|
def _enroll_entitlement(self, entitlement, course_run_key, user):
|
|
"""
|
|
Internal method to handle the details of enrolling a User in a Course Run.
|
|
|
|
Returns a response object is there is an error or exception, None otherwise
|
|
"""
|
|
try:
|
|
unexpired_paid_modes = [mode.slug for mode in CourseMode.paid_modes_for_course(course_run_key)]
|
|
can_upgrade = unexpired_paid_modes and entitlement.mode in unexpired_paid_modes
|
|
enrollment = CourseEnrollment.enroll(
|
|
user=user,
|
|
course_key=course_run_key,
|
|
mode=entitlement.mode,
|
|
check_access=True,
|
|
can_upgrade=can_upgrade
|
|
)
|
|
except AlreadyEnrolledError:
|
|
enrollment = CourseEnrollment.get_enrollment(user, course_run_key)
|
|
if enrollment.mode == entitlement.mode:
|
|
entitlement.set_enrollment(enrollment)
|
|
elif enrollment.mode not in unexpired_paid_modes:
|
|
enrollment.update_enrollment(mode=entitlement.mode)
|
|
entitlement.set_enrollment(enrollment)
|
|
# Else the User is already enrolled in another paid Mode and we should
|
|
# not do anything else related to Entitlements.
|
|
except CourseEnrollmentException:
|
|
message = (
|
|
'Course Entitlement Enroll for {username} failed for course: {course_id}, '
|
|
'mode: {mode}, and entitlement: {entitlement}'
|
|
).format(
|
|
username=user.username,
|
|
course_id=course_run_key,
|
|
mode=entitlement.mode,
|
|
entitlement=entitlement.uuid
|
|
)
|
|
return Response(
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
data={'message': message}
|
|
)
|
|
|
|
entitlement.set_enrollment(enrollment)
|
|
return None
|
|
|
|
def create(self, request, uuid):
|
|
"""
|
|
On POST this method will be called and will handle enrolling a user in the
|
|
provided course_run_id from the data. This is called on a specific entitlement
|
|
UUID so the course_run_id has to correspond to the Course that is assigned to
|
|
the Entitlement.
|
|
|
|
When this API is called for a user who is already enrolled in a run that User
|
|
will be unenrolled from their current run and enrolled in the new run if it is
|
|
available.
|
|
"""
|
|
course_run_id = request.data.get('course_run_id', None)
|
|
|
|
if not course_run_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 Entitlement 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 Course as the Course entitlement.
|
|
course_run_valid = self._verify_course_run_for_entitlement(entitlement, course_run_id)
|
|
if not course_run_valid:
|
|
return Response(
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
data={
|
|
'message': 'The Course Run ID is not a match for this Course Entitlement.'
|
|
}
|
|
)
|
|
|
|
try:
|
|
course_run_key = CourseKey.from_string(course_run_id)
|
|
except InvalidKeyError:
|
|
return Response(
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
data={
|
|
'message': f'Invalid {course_run_id}'
|
|
}
|
|
)
|
|
|
|
# Verify that the run is fullfillable
|
|
if not is_course_run_entitlement_fulfillable(course_run_key, entitlement):
|
|
return Response(
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
data={
|
|
'message': 'The User is unable to enroll in Course Run {course_id}, it is not available.'.format(
|
|
course_id=course_run_id
|
|
)
|
|
}
|
|
)
|
|
|
|
# Determine if this is a Switch session or a simple enroll and handle both.
|
|
if entitlement.enrollment_course_run is None:
|
|
response = self._enroll_entitlement(
|
|
entitlement=entitlement,
|
|
course_run_key=course_run_key,
|
|
user=request.user
|
|
)
|
|
if response:
|
|
return response
|
|
elif entitlement.enrollment_course_run.course_id != course_run_id:
|
|
_unenroll_entitlement(
|
|
course_entitlement=entitlement,
|
|
course_run_key=entitlement.enrollment_course_run.course_id
|
|
)
|
|
response = self._enroll_entitlement(
|
|
entitlement=entitlement,
|
|
course_run_key=course_run_key,
|
|
user=request.user
|
|
)
|
|
if response:
|
|
return response
|
|
|
|
return Response(
|
|
status=status.HTTP_201_CREATED,
|
|
data={
|
|
'course_run_id': course_run_id,
|
|
}
|
|
)
|
|
|
|
def destroy(self, request, uuid):
|
|
"""
|
|
On DELETE call to this API we will unenroll the course enrollment for the provided uuid
|
|
|
|
If is_refund parameter is provided then unenroll the user, set Entitlement expiration, and issue
|
|
a refund
|
|
"""
|
|
is_refund = request.query_params.get('is_refund', 'false') == 'true'
|
|
|
|
# Retrieve the entitlement for the UUID belongs to the current user.
|
|
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 is_refund and entitlement.is_entitlement_refundable():
|
|
# Revoke the Course Entitlement and issue Refund
|
|
log.info(
|
|
'Entitlement Refund requested for Course Entitlement[%s]',
|
|
entitlement.uuid
|
|
)
|
|
|
|
try:
|
|
_process_revoke_and_unenroll_entitlement(course_entitlement=entitlement, is_refund=True)
|
|
except IntegrityError:
|
|
# This state is reached when there was a failure in revoke and refund process resulting
|
|
# in a reversion of DB changes
|
|
return Response(
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
data={
|
|
'message': 'Entitlement revoke and refund failed due to refund internal process failure'
|
|
})
|
|
|
|
elif not is_refund:
|
|
if entitlement.enrollment_course_run is not None:
|
|
_unenroll_entitlement(
|
|
course_entitlement=entitlement,
|
|
course_run_key=entitlement.enrollment_course_run.course_id
|
|
)
|
|
else:
|
|
log.info(
|
|
'Entitlement Refund failed for Course Entitlement [%s]. Entitlement is not refundable',
|
|
entitlement.uuid
|
|
)
|
|
return Response(
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
data={
|
|
'message': 'Entitlement refund failed, Entitlement is not refundable'
|
|
})
|
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class SubscriptionsRevokeVerifiedAccessView(APIView):
|
|
"""
|
|
Endpoint for expiring entitlements for a user and downgrading the enrollments
|
|
to Audit mode. This endpoint accepts a list of entitlement UUIDs and will expire
|
|
the entitlements along with downgrading the related enrollments to Audit mode.
|
|
Only those enrollments are downgraded to Audit for which user has not been awarded
|
|
a completion certificate yet.
|
|
"""
|
|
authentication_classes = (JwtAuthentication, SessionAuthenticationCrossDomainCsrf,)
|
|
permission_classes = (permissions.IsAuthenticated, IsSubscriptionWorkerUser,)
|
|
throttle_classes = (ServiceUserThrottle,)
|
|
|
|
def _process_revoke_and_downgrade_to_audit(self, course_entitlements, user_id, revocable_entitlement_uuids):
|
|
"""
|
|
Gets course completion status for the provided course entitlements and triggers the
|
|
revoke and downgrade to audit process for the course entitlements which are not completed.
|
|
Triggers the retry task asynchronously if there is an exception while getting the
|
|
course completion status.
|
|
"""
|
|
entitled_course_ids = []
|
|
user = User.objects.get(id=user_id)
|
|
username = user.username
|
|
for course_entitlement in course_entitlements:
|
|
if course_entitlement.enrollment_course_run is not None:
|
|
entitled_course_ids.append(str(course_entitlement.enrollment_course_run.course_id))
|
|
|
|
log.info('B2C_SUBSCRIPTIONS: Getting course completion status for user [%s] and entitled_course_ids %s',
|
|
username,
|
|
entitled_course_ids)
|
|
awarded_cert_course_ids, is_exception = get_courses_completion_status(username, entitled_course_ids)
|
|
|
|
if is_exception:
|
|
# Trigger the retry task asynchronously
|
|
log.exception('B2C_SUBSCRIPTIONS: Exception occurred while getting course completion status for user %s '
|
|
'and entitled_course_ids %s',
|
|
username,
|
|
entitled_course_ids)
|
|
retry_revoke_subscriptions_verified_access.apply_async(args=(revocable_entitlement_uuids,
|
|
entitled_course_ids,
|
|
username))
|
|
return
|
|
revoke_entitlements_and_downgrade_courses_to_audit(course_entitlements, username, awarded_cert_course_ids,
|
|
revocable_entitlement_uuids)
|
|
|
|
def post(self, request):
|
|
"""
|
|
Invokes the entitlements expiration process for the provided uuids and downgrades the
|
|
enrollments to Audit mode.
|
|
"""
|
|
revocable_entitlement_uuids = request.data.get('entitlement_uuids', [])
|
|
user_id = request.data.get('lms_user_id', None)
|
|
course_entitlements = (CourseEntitlement.objects.filter(uuid__in=revocable_entitlement_uuids).
|
|
select_related('user').
|
|
select_related('enrollment_course_run'))
|
|
|
|
if course_entitlements.exists():
|
|
self._process_revoke_and_downgrade_to_audit(course_entitlements, user_id, revocable_entitlement_uuids)
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
else:
|
|
log.info('B2C_SUBSCRIPTIONS: Entitlements not found for the provided entitlements data: %s and user: %s',
|
|
revocable_entitlement_uuids,
|
|
user_id)
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|