diff --git a/common/djangoapps/entitlements/api/v1/tests/test_views.py b/common/djangoapps/entitlements/api/v1/tests/test_views.py index 0cd4c06e85..59ab869780 100644 --- a/common/djangoapps/entitlements/api/v1/tests/test_views.py +++ b/common/djangoapps/entitlements/api/v1/tests/test_views.py @@ -350,6 +350,20 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase): self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course') self.course2 = CourseFactory.create(org='edX', number='DemoX2', display_name='Demo_Course 2') + self.course_mode = CourseModeFactory( + course_id=self.course.id, + mode_slug=CourseMode.VERIFIED, + # This must be in the future to ensure it is returned by downstream code. + expiration_datetime=now() + timedelta(days=1) + ) + + self.course_mode = CourseModeFactory( + course_id=self.course2.id, + mode_slug=CourseMode.VERIFIED, + # This must be in the future to ensure it is returned by downstream code. + expiration_datetime=now() + timedelta(days=1) + ) + self.return_values = [ {'key': str(self.course.id)}, {'key': str(self.course2.id)} @@ -474,9 +488,6 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase): assert response.status_code == 201 assert CourseEnrollment.is_enrolled(self.user, self.course.id) - - course_entitlement.refresh_from_db() - assert CourseEnrollment.is_enrolled(self.user, self.course.id) assert course_entitlement.enrollment_course_run is not None @patch("entitlements.api.v1.views.get_course_runs_for_course") diff --git a/common/djangoapps/entitlements/api/v1/views.py b/common/djangoapps/entitlements/api/v1/views.py index f9e15ba662..486fdfd5f5 100644 --- a/common/djangoapps/entitlements/api/v1/views.py +++ b/common/djangoapps/entitlements/api/v1/views.py @@ -34,6 +34,7 @@ class EntitlementsPagination(DefaultPagination): page_size = 50 max_page_size = 100 + @transaction.atomic def _unenroll_entitlement(course_entitlement, course_run_key): """ @@ -42,6 +43,7 @@ def _unenroll_entitlement(course_entitlement, course_run_key): CourseEnrollment.unenroll(course_entitlement.user, course_run_key, skip_refund=True) course_entitlement.set_enrollment(None) + @transaction.atomic def _process_revoke_and_unenroll_entitlement(course_entitlement, is_refund=False): """ @@ -341,7 +343,6 @@ class EntitlementEnrollmentViewSet(viewsets.GenericViewSet): ) # 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, diff --git a/common/djangoapps/entitlements/tests/test_utils.py b/common/djangoapps/entitlements/tests/test_utils.py index 1c212a42f1..85c7e4877b 100644 --- a/common/djangoapps/entitlements/tests/test_utils.py +++ b/common/djangoapps/entitlements/tests/test_utils.py @@ -90,7 +90,7 @@ class TestCourseRunFullfillableForEntitlement(ModuleStoreTestCase): assert not is_course_run_entitlement_fullfillable(course_overview.id, now() + timedelta(days=3), entitlement) - def test_course_run_not_fullfillable_user_enrolled(self): + def test_course_run_fullfillable_user_enrolled(self): course_overview = self.create_course( start_from_now=-3, end_from_now=2, @@ -102,7 +102,7 @@ class TestCourseRunFullfillableForEntitlement(ModuleStoreTestCase): # Enroll User in the Course, but do not update the entitlement CourseEnrollmentFactory.create(user=entitlement.user, course_id=course_overview.id) - assert not is_course_run_entitlement_fullfillable(course_overview.id, now() + timedelta(days=3), entitlement) + assert is_course_run_entitlement_fullfillable(course_overview.id, now() + timedelta(days=3), entitlement) def test_course_run_not_fullfillable_upgrade_ended(self): course_overview = self.create_course( diff --git a/common/djangoapps/entitlements/utils.py b/common/djangoapps/entitlements/utils.py index 5809cc3256..3f21230f95 100644 --- a/common/djangoapps/entitlements/utils.py +++ b/common/djangoapps/entitlements/utils.py @@ -7,23 +7,21 @@ def is_course_run_entitlement_fullfillable(course_run_id, compare_date, entitlem """ Checks that the current run meets the following criteria for an entitlement - 1) Are currently running or start in the future - 2) A user can enroll in - 3) A user can upgrade to the entitlement mode - 4) Are not already currently enrolled in a session + 1) Is currently running or start in the future + 2) A User can enroll in + 3) A User can upgrade to the entitlement mode Arguments: course_run_id (String): The id of the Course run that is being checked. compare_date: The date and time that we are comparing against. Generally the current date and time. - user_enrolled_sessions (list): The list of sessions that the user is enrolled in currently. - mode (String): The current mode of the entitlement that we are verifying against. + entitlement: The Entitlement that we are checking against. Returns: bool: True is the Course Run is fullfillable for the CourseEntitlement. """ - # Only courses that have not ended will be displayed course_overview = CourseOverview.get_from_id(course_run_id) + # Only courses that have not ended will be displayed run_start = course_overview.start run_end = course_overview.end is_running = run_start and (not run_end or (run_end and (run_end > compare_date))) @@ -31,17 +29,13 @@ def is_course_run_entitlement_fullfillable(course_run_id, compare_date, entitlem # Only courses that can currently be enrolled in will be displayed enrollment_start = course_overview.enrollment_start enrollment_end = course_overview.enrollment_end - is_enrolled = CourseEnrollment.is_enrolled(entitlement.user, course_run_id) can_enroll = ( (not enrollment_start or enrollment_start < compare_date) and (not enrollment_end or enrollment_end > compare_date) - and not is_enrolled ) - # Check if it is an upgradable mode and mode matches entitlement mode - can_upgrade = False + # Ensure the course is upgradeable and the mode matches the entitlement's mode. unexpired_paid_modes = [mode.slug for mode in CourseMode.paid_modes_for_course(course_run_id)] - if unexpired_paid_modes and entitlement.mode in unexpired_paid_modes: - can_upgrade = True + can_upgrade = unexpired_paid_modes and entitlement.mode in unexpired_paid_modes return is_running and can_upgrade and can_enroll diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py index 389a7036ad..8b0943fcaa 100644 --- a/openedx/core/djangoapps/catalog/utils.py +++ b/openedx/core/djangoapps/catalog/utils.py @@ -12,6 +12,7 @@ from opaque_keys.edx.keys import CourseKey from pytz import UTC from entitlements.utils import is_course_run_entitlement_fullfillable +from student.models import CourseEnrollment from openedx.core.djangoapps.catalog.cache import (PROGRAM_CACHE_KEY_TPL, SITE_PROGRAM_UUIDS_CACHE_KEY_TPL) from openedx.core.djangoapps.catalog.models import CatalogIntegration @@ -315,11 +316,6 @@ def get_fulfillable_course_runs_for_entitlement(entitlement, course_runs): """ Takes a list of course runs and returns only the course runs, sorted by start date, that: - 1) Are currently running or start in the future - 2) A user can enroll in - 3) A user can upgrade to the entitlement mode - 4) Are not already currently enrolled in a session - These are the only sessions that can be selected for an entitlement. """ enrollable_sessions = [] @@ -328,7 +324,8 @@ def get_fulfillable_course_runs_for_entitlement(entitlement, course_runs): now = datetime.datetime.now(UTC) for course_run in course_runs: course_id = CourseKey.from_string(course_run.get('key')) - if is_course_run_entitlement_fullfillable(course_id, now, entitlement): + is_enrolled = CourseEnrollment.is_enrolled(entitlement.user, str(course_id)) + if is_course_run_entitlement_fullfillable(course_id, now, entitlement) and not is_enrolled: enrollable_sessions.append(course_run) enrollable_sessions.sort(key=lambda session: session.get('start'))