Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into yusuf-musleh/collec…
Browse files Browse the repository at this point in the history
…tions-crud-rest-api
  • Loading branch information
pomegranited committed Sep 6, 2024
2 parents 007f80a + ec76baf commit 197733b
Show file tree
Hide file tree
Showing 72 changed files with 182 additions and 1,860 deletions.
5 changes: 2 additions & 3 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1449,9 +1449,8 @@
'edx-ui-toolkit/js/utils/string-utils.js',
'edx-ui-toolkit/js/utils/html-utils.js',

# Load Bootstrap and supporting libraries
'common/js/vendor/popper.js',
'common/js/vendor/bootstrap.js',
# Here we were loading Bootstrap and supporting libraries, but it no longer seems to be needed for any Studio UI.
# 'common/js/vendor/bootstrap.bundle.js',

# Finally load RequireJS
'common/js/vendor/require.js'
Expand Down
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)
Loading

0 comments on commit 197733b

Please sign in to comment.