Skip to content

Commit

Permalink
fix: Remove B2C Subscriptions (#35303)
Browse files Browse the repository at this point in the history
REV-3697
  • Loading branch information
julianajlk authored Sep 4, 2024
1 parent ef4e037 commit 51d538c
Show file tree
Hide file tree
Showing 50 changed files with 9 additions and 1,709 deletions.
10 changes: 0 additions & 10 deletions common/djangoapps/entitlements/rest_api/v1/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""


from django.conf import settings
from rest_framework.permissions import SAFE_METHODS, BasePermission

from lms.djangoapps.courseware.access import has_access
Expand All @@ -22,12 +21,3 @@ def has_permission(self, request, view):
return request.user.is_authenticated
else:
return request.user.is_staff or has_access(request.user, "support", "global")


class IsSubscriptionWorkerUser(BasePermission):
"""
Method that will require the request to be coming from the subscriptions service worker user.
"""

def has_permission(self, request, view):
return request.user.username == settings.SUBSCRIPTIONS_SERVICE_WORKER_USERNAME
158 changes: 0 additions & 158 deletions common/djangoapps/entitlements/rest_api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import uuid
from datetime import datetime, timedelta
from unittest.mock import patch
from uuid import uuid4

from django.conf import settings
from django.urls import reverse
Expand Down Expand Up @@ -1236,160 +1235,3 @@ def test_user_is_not_unenrolled_on_failed_refund(
assert CourseEnrollment.is_enrolled(self.user, self.course.id)
assert course_entitlement.enrollment_course_run is not None
assert course_entitlement.expired_at is None


@skip_unless_lms
class RevokeSubscriptionsVerifiedAccessViewTest(ModuleStoreTestCase):
"""
Tests for the RevokeVerifiedAccessView
"""
REVOKE_VERIFIED_ACCESS_PATH = 'entitlements_api:v1:revoke_subscriptions_verified_access'

def setUp(self):
super().setUp()
self.user = UserFactory(username="subscriptions_worker", is_staff=True)
self.client.login(username=self.user.username, password=TEST_PASSWORD)
self.course = CourseFactory()
self.course_mode1 = CourseModeFactory(
course_id=self.course.id, # pylint: disable=no-member
mode_slug=CourseMode.VERIFIED,
expiration_datetime=now() + timedelta(days=1)
)
self.course_mode2 = CourseModeFactory(
course_id=self.course.id, # pylint: disable=no-member
mode_slug=CourseMode.AUDIT,
expiration_datetime=now() + timedelta(days=1)
)

@patch('common.djangoapps.entitlements.rest_api.v1.views.get_courses_completion_status')
def test_revoke_access_success(self, mock_get_courses_completion_status):
mock_get_courses_completion_status.return_value = ([], False)
enrollment = CourseEnrollmentFactory.create(
user=self.user,
course_id=self.course.id, # pylint: disable=no-member
is_active=True,
mode=CourseMode.VERIFIED
)
course_entitlement = CourseEntitlementFactory.create(user=self.user, enrollment_course_run=enrollment)
url = reverse(self.REVOKE_VERIFIED_ACCESS_PATH)

assert course_entitlement.enrollment_course_run is not None

response = self.client.post(
url,
data={
"entitlement_uuids": [str(course_entitlement.uuid)],
"lms_user_id": self.user.id
},
content_type='application/json',
)
assert response.status_code == 204

course_entitlement.refresh_from_db()
enrollment.refresh_from_db()
assert course_entitlement.expired_at is not None
assert course_entitlement.enrollment_course_run is None
assert enrollment.mode == CourseMode.AUDIT

@patch('common.djangoapps.entitlements.rest_api.v1.views.get_courses_completion_status')
def test_already_completed_course(self, mock_get_courses_completion_status):
enrollment = CourseEnrollmentFactory.create(
user=self.user,
course_id=self.course.id, # pylint: disable=no-member
is_active=True,
mode=CourseMode.VERIFIED
)
mock_get_courses_completion_status.return_value = ([str(enrollment.course_id)], False)
course_entitlement = CourseEntitlementFactory.create(user=self.user, enrollment_course_run=enrollment)
url = reverse(self.REVOKE_VERIFIED_ACCESS_PATH)

assert course_entitlement.enrollment_course_run is not None

response = self.client.post(
url,
data={
"entitlement_uuids": [str(course_entitlement.uuid)],
"lms_user_id": self.user.id
},
content_type='application/json',
)
assert response.status_code == 204

course_entitlement.refresh_from_db()
assert course_entitlement.expired_at is None
assert course_entitlement.enrollment_course_run.mode == CourseMode.VERIFIED

@patch('common.djangoapps.entitlements.rest_api.v1.views.log.info')
def test_revoke_access_invalid_uuid(self, mock_log):
url = reverse(self.REVOKE_VERIFIED_ACCESS_PATH)
entitlement_uuids = [str(uuid4())]
response = self.client.post(
url,
data={
"entitlement_uuids": entitlement_uuids,
"lms_user_id": self.user.id
},
content_type='application/json',
)

mock_log.assert_called_once_with("B2C_SUBSCRIPTIONS: Entitlements not found for the provided"
" entitlements data: %s and user: %s",
entitlement_uuids,
self.user.id)
assert response.status_code == 204

def test_revoke_access_unauthorized_user(self):
user = UserFactory(is_staff=True, username='not_subscriptions_worker')
self.client.login(username=user.username, password=TEST_PASSWORD)

enrollment = CourseEnrollmentFactory.create(
user=self.user,
course_id=self.course.id, # pylint: disable=no-member
is_active=True,
mode=CourseMode.VERIFIED
)
course_entitlement = CourseEntitlementFactory.create(user=self.user, enrollment_course_run=enrollment)
url = reverse(self.REVOKE_VERIFIED_ACCESS_PATH)

assert course_entitlement.enrollment_course_run is not None

response = self.client.post(
url,
data={
"entitlement_uuids": [],
"lms_user_id": self.user.id
},
content_type='application/json',
)
assert response.status_code == 403

course_entitlement.refresh_from_db()
assert course_entitlement.expired_at is None
assert course_entitlement.enrollment_course_run.mode == CourseMode.VERIFIED

@patch('common.djangoapps.entitlements.tasks.retry_revoke_subscriptions_verified_access.apply_async')
@patch('common.djangoapps.entitlements.rest_api.v1.views.get_courses_completion_status')
def test_course_completion_exception_triggers_task(self, mock_get_courses_completion_status, mock_task):
mock_get_courses_completion_status.return_value = ([], True)
enrollment = CourseEnrollmentFactory.create(
user=self.user,
course_id=self.course.id, # pylint: disable=no-member
is_active=True,
mode=CourseMode.VERIFIED
)
course_entitlement = CourseEntitlementFactory.create(user=self.user, enrollment_course_run=enrollment)

url = reverse(self.REVOKE_VERIFIED_ACCESS_PATH)

response = self.client.post(
url,
data={
"entitlement_uuids": [str(course_entitlement.uuid)],
"lms_user_id": self.user.id
},
content_type='application/json',
)
assert response.status_code == 204
mock_task.assert_called_once_with(args=([str(course_entitlement.uuid)],
[str(enrollment.course_id)],
self.user.username))
21 changes: 0 additions & 21 deletions common/djangoapps/entitlements/rest_api/v1/throttles.py

This file was deleted.

7 changes: 1 addition & 6 deletions common/djangoapps/entitlements/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.urls import path, re_path
from rest_framework.routers import DefaultRouter

from .views import EntitlementEnrollmentViewSet, EntitlementViewSet, SubscriptionsRevokeVerifiedAccessView
from .views import EntitlementEnrollmentViewSet, EntitlementViewSet

router = DefaultRouter()
router.register(r'entitlements', EntitlementViewSet, basename='entitlements')
Expand All @@ -24,9 +24,4 @@
ENROLLMENTS_VIEW,
name='enrollments'
),
path(
'subscriptions/entitlements/revoke',
SubscriptionsRevokeVerifiedAccessView.as_view(),
name='revoke_subscriptions_verified_access'
)
]
80 changes: 2 additions & 78 deletions common/djangoapps/entitlements/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
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
Expand All @@ -24,22 +23,13 @@
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.permissions import IsAdminOrSupportOrAuthenticatedReadOnly
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.entitlements.utils import is_course_run_entitlement_fulfillable
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()
Expand Down Expand Up @@ -132,7 +122,6 @@ class EntitlementViewSet(viewsets.ModelViewSet):
filter_backends = (DjangoFilterBackend,)
filterset_class = CourseEntitlementFilter
pagination_class = EntitlementsPagination
throttle_classes = (ServiceUserThrottle,)

def get_queryset(self):
user = self.request.user
Expand Down Expand Up @@ -530,68 +519,3 @@ def destroy(self, request, uuid):
})

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)
40 changes: 0 additions & 40 deletions common/djangoapps/entitlements/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
import logging

from celery import shared_task
from celery.exceptions import MaxRetriesExceededError
from celery.utils.log import get_task_logger
from django.conf import settings # lint-amnesty, pylint: disable=unused-import
from django.contrib.auth import get_user_model
from edx_django_utils.monitoring import set_code_owner_attribute

from common.djangoapps.entitlements.models import CourseEntitlement, CourseEntitlementSupportDetail
from common.djangoapps.entitlements.utils import revoke_entitlements_and_downgrade_courses_to_audit
from openedx.core.djangoapps.credentials.utils import get_courses_completion_status

LOGGER = get_task_logger(__name__)
log = logging.getLogger(__name__)
Expand Down Expand Up @@ -154,40 +151,3 @@ def expire_and_create_entitlements(self, entitlement_ids, support_username):
'%d entries, task id :%s',
len(entitlement_ids),
self.request.id)


@shared_task(bind=True)
@set_code_owner_attribute
def retry_revoke_subscriptions_verified_access(self, revocable_entitlement_uuids, entitled_course_ids, username):
"""
Task to process course access revoke and move to audit.
This is called only if call to get_courses_completion_status fails due to any exception.
"""
LOGGER.info("B2C_SUBSCRIPTIONS: Running retry_revoke_subscriptions_verified_access for user [%s],"
" entitlement_uuids %s and entitled_course_ids %s",
username,
revocable_entitlement_uuids,
entitled_course_ids)
course_entitlements = CourseEntitlement.objects.filter(uuid__in=revocable_entitlement_uuids)
course_entitlements = course_entitlements.select_related('user').select_related('enrollment_course_run')
if course_entitlements.exists():
awarded_cert_course_ids, is_exception = get_courses_completion_status(username, entitled_course_ids)
if is_exception:
try:
countdown = 2 ** self.request.retries
self.retry(countdown=countdown, max_retries=3)
except MaxRetriesExceededError:
LOGGER.exception(
'B2C_SUBSCRIPTIONS: Failed to process retry_revoke_subscriptions_verified_access '
'for user [%s] and entitlement_uuids %s',
username,
revocable_entitlement_uuids
)
return
revoke_entitlements_and_downgrade_courses_to_audit(course_entitlements, username, awarded_cert_course_ids,
revocable_entitlement_uuids)
else:
LOGGER.info('B2C_SUBSCRIPTIONS: Entitlements not found for the provided entitlements uuids %s '
'for user [%s] duing the retry_revoke_subscriptions_verified_access task',
revocable_entitlement_uuids,
username)
Loading

0 comments on commit 51d538c

Please sign in to comment.