From 13b3764f209dda280d61d7af6872271aba9ce078 Mon Sep 17 00:00:00 2001
From: Waheed Ahmed <waheed.ahmed@arbisoft.com>
Date: Thu, 6 Aug 2020 15:24:20 +0500
Subject: [PATCH] Allow entitlements to be used past course has ended.

Allow entitlements to be used past the course has ended but
upgrade deadline is still in future for already enrolled
learners.

PROD-1497
---
 .../rest_api/v1/tests/test_views.py           | 15 +++++++------
 .../entitlements/tests/test_utils.py          |  5 +++--
 common/djangoapps/entitlements/utils.py       | 22 ++++++++-----------
 .../spec/course_card_view_spec.js             |  2 +-
 .../views/course_card_view.js                 |  7 +++---
 .../views/course_entitlement_view.js          |  4 +++-
 .../djangoapps/catalog/tests/test_utils.py    |  9 ++++++--
 openedx/core/djangoapps/catalog/utils.py      |  4 +---
 8 files changed, 36 insertions(+), 32 deletions(-)

diff --git a/common/djangoapps/entitlements/rest_api/v1/tests/test_views.py b/common/djangoapps/entitlements/rest_api/v1/tests/test_views.py
index 4e9275182d3..4195e4a208a 100644
--- a/common/djangoapps/entitlements/rest_api/v1/tests/test_views.py
+++ b/common/djangoapps/entitlements/rest_api/v1/tests/test_views.py
@@ -970,23 +970,22 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
         assert course_entitlement.enrollment_course_run is not None
 
     @patch("entitlements.rest_api.v1.views.get_course_runs_for_course")
-    def test_enrollment_closed_upgrade_open(self, mock_get_course_runs):
+    def test_already_enrolled_course_ended(self, mock_get_course_runs):
         """
-        Test that user can still select a session while course enrollment
-        is closed and upgrade deadline is in future.
+        Test that already enrolled user can still select a session while
+        course has ended but upgrade deadline is in future.
         """
         course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED)
         mock_get_course_runs.return_value = self.return_values
 
         # Setup enrollment period to be in the past
         utc_now = datetime.now(UTC)
-        self.course.enrollment_start = utc_now - timedelta(days=15)
-        self.course.enrollment_end = utc_now - timedelta(days=1)
+        self.course.start = utc_now - timedelta(days=15)
+        self.course.end = utc_now - timedelta(days=1)
         self.course = self.update_course(self.course, self.user.id)
         CourseOverview.update_select_courses([self.course.id], force_update=True)
 
-        assert CourseEnrollment.is_enrollment_closed(self.user, self.course)
-        assert not CourseEnrollment.is_enrolled(self.user, self.course.id)
+        CourseEnrollment.enroll(self.user, self.course.id, mode=CourseMode.AUDIT)
 
         url = reverse(
             self.ENTITLEMENTS_ENROLLMENT_NAMESPACE,
@@ -1005,6 +1004,8 @@ class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
 
         assert response.status_code == 201
         assert CourseEnrollment.is_enrolled(self.user, self.course.id)
+        (enrolled_mode, is_active) = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
+        assert is_active and (enrolled_mode == course_entitlement.mode)
         assert course_entitlement.enrollment_course_run is not None
 
     @patch("entitlements.rest_api.v1.views.get_course_runs_for_course")
diff --git a/common/djangoapps/entitlements/tests/test_utils.py b/common/djangoapps/entitlements/tests/test_utils.py
index 0cd598b33f1..b1a91ac7206 100644
--- a/common/djangoapps/entitlements/tests/test_utils.py
+++ b/common/djangoapps/entitlements/tests/test_utils.py
@@ -155,14 +155,15 @@ class TestCourseRunFulfillableForEntitlement(ModuleStoreTestCase):
 
         assert not is_course_run_entitlement_fulfillable(course_overview.id, entitlement)
 
-    def test_course_run_fulfillable_enrollment_ended_upgrade_open(self):
+    def test_course_run_fulfillable_already_enrolled_course_ended(self):
         course_overview = self.create_course(
             start_from_now=-3,
-            end_from_now=2,
+            end_from_now=-1,
             enrollment_start_from_now=-2,
             enrollment_end_from_now=-1,
         )
 
         entitlement = CourseEntitlementFactory.create(mode=CourseMode.VERIFIED)
+        CourseEnrollmentFactory.create(user=entitlement.user, course_id=course_overview.id)
 
         assert is_course_run_entitlement_fulfillable(course_overview.id, entitlement)
diff --git a/common/djangoapps/entitlements/utils.py b/common/djangoapps/entitlements/utils.py
index a16bfc52ba6..0b4594e7cff 100644
--- a/common/djangoapps/entitlements/utils.py
+++ b/common/djangoapps/entitlements/utils.py
@@ -22,9 +22,8 @@ def is_course_run_entitlement_fulfillable(
     """
     Checks that the current run meets the following criteria for an entitlement
 
-    1) Is currently running or start in the future
-    2) A User can enroll in or is currently enrolled
-    3) A User can upgrade to the entitlement mode
+    1) A User can enroll in or is currently enrolled
+    2) A User can upgrade to the entitlement mode
 
     Arguments:
         course_run_key (CourseKey): The id of the Course run that is being checked.
@@ -43,16 +42,13 @@ def is_course_run_entitlement_fulfillable(
         ))
         return False
 
-    # Verify that the course is still running
-    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)))
-
-    # Verify that the course run can currently be enrolled, explicitly
-    # not checking for enrollment end date becasue course run is still
-    # fulfillable if enrollment has ended but VUD is in future
+    # Verify that the course run can currently be enrolled
     enrollment_start = course_overview.enrollment_start
-    can_enroll = not enrollment_start or enrollment_start < compare_date
+    enrollment_end = course_overview.enrollment_end
+    can_enroll = (
+        (not enrollment_start or enrollment_start < compare_date)
+        and (not enrollment_end or enrollment_end > compare_date)
+    )
 
     # Is the user already enrolled in the Course Run
     is_enrolled = CourseEnrollment.is_enrolled(entitlement.user, course_run_key)
@@ -61,4 +57,4 @@ def is_course_run_entitlement_fulfillable(
     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
 
-    return is_running and can_upgrade and (is_enrolled or can_enroll)
+    return course_overview.start and can_upgrade and (is_enrolled or can_enroll)
diff --git a/lms/static/js/learner_dashboard/spec/course_card_view_spec.js b/lms/static/js/learner_dashboard/spec/course_card_view_spec.js
index b63c91f7853..c6c4a1029b8 100644
--- a/lms/static/js/learner_dashboard/spec/course_card_view_spec.js
+++ b/lms/static/js/learner_dashboard/spec/course_card_view_spec.js
@@ -169,7 +169,7 @@ describe('Course Card View', () => {
       );
   });
 
-  it('should show a message if an there is an upcoming course run', () => {
+  it('should show a message if there is an upcoming course run', () => {
     course.course_runs[0].is_enrollment_open = false;
 
     setupView(course, false);
diff --git a/lms/static/js/learner_dashboard/views/course_card_view.js b/lms/static/js/learner_dashboard/views/course_card_view.js
index 96cfec4157c..797c48d1af4 100644
--- a/lms/static/js/learner_dashboard/views/course_card_view.js
+++ b/lms/static/js/learner_dashboard/views/course_card_view.js
@@ -46,7 +46,9 @@ class CourseCardView extends Backbone.View {
     const $upgradeMessage = this.$('.upgrade-message');
     const $certStatus = this.$('.certificate-status');
     const $expiredNotification = this.$('.expired-notification');
+    const courseKey = this.model.get('course_run_key');
     const expired = this.model.get('expired');
+    const canUpgrade = this.model.get('upgrade_url') && !(expired === true);
     const courseUUID = this.model.get('uuid');
     const containerSelector = `#course-${courseUUID}`;
 
@@ -72,8 +74,7 @@ class CourseCardView extends Backbone.View {
         enterCourseBtn: `${containerSelector} .view-course-button`,
         availableSessions: JSON.stringify(this.model.get('course_runs')),
         entitlementUUID: this.entitlement.uuid,
-        currentSessionId: this.model.isEnrolledInSession() ?
-                                 this.model.get('course_run_key') : null,
+        currentSessionId: this.model.isEnrolledInSession() && !canUpgrade ? courseKey : null,
         enrollUrl: this.model.get('enroll_url'),
         courseHomeUrl: this.model.get('course_url'),
         expiredAt: this.entitlement.expired_at,
@@ -81,7 +82,7 @@ class CourseCardView extends Backbone.View {
       });
     }
 
-    if (this.model.get('upgrade_url') && !(expired === true)) {
+    if (canUpgrade) {
       this.upgradeMessage = new UpgradeMessageView({
         $el: $upgradeMessage,
         model: this.model,
diff --git a/lms/static/js/learner_dashboard/views/course_entitlement_view.js b/lms/static/js/learner_dashboard/views/course_entitlement_view.js
index b19ce2f1b3d..3f2e65f4c28 100644
--- a/lms/static/js/learner_dashboard/views/course_entitlement_view.js
+++ b/lms/static/js/learner_dashboard/views/course_entitlement_view.js
@@ -141,7 +141,8 @@ class CourseEntitlementView extends Backbone.View {
     this.trackSessionChange(eventPage, eventAction, prevSession);
 
     // With a containing backbone view, we can simply re-render the parent card
-    if (this.$parentEl) {
+    if (this.$parentEl &&
+        this.courseCardModel.get('course_run_key') !== this.currentSessionSelection) {
       this.courseCardModel.updateCourseRun(this.currentSessionSelection);
       return;
     }
@@ -388,6 +389,7 @@ class CourseEntitlementView extends Backbone.View {
     sessionData.forEach((session) => {
       Object.assign(session, {
         enrollment_end: CourseEntitlementView.formatDate(session.enrollment_end, dateFormat),
+        session_id: session.session_id ? session.session_id : session.key,
         session_dates: this.courseCardModel.formatDateString({
           start_date: CourseEntitlementView.formatDate(session.start, dateFormat),
           advertised_start: session.advertised_start,
diff --git a/openedx/core/djangoapps/catalog/tests/test_utils.py b/openedx/core/djangoapps/catalog/tests/test_utils.py
index 77bffb06c2e..7886b7148c7 100644
--- a/openedx/core/djangoapps/catalog/tests/test_utils.py
+++ b/openedx/core/djangoapps/catalog/tests/test_utils.py
@@ -655,14 +655,19 @@ class TestSessionEntitlement(CatalogIntegrationMixin, TestCase):
     def test_unpublished_sessions_for_entitlement(self, mock_get_edx_api_data):
         """
         Test unpublished course runs are not part of visible session entitlements when the user
-        is not enrolled.
+        is not enrolled and upgrade deadline is passed.
         """
         catalog_course_run = CourseRunFactory.create(status=COURSE_UNPUBLISHED)
         catalog_course = CourseFactory(course_runs=[catalog_course_run])
         mock_get_edx_api_data.return_value = catalog_course
         course_key = CourseKey.from_string(catalog_course_run.get('key'))
         course_overview = CourseOverviewFactory.create(id=course_key, start=self.tomorrow)
-        CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, min_price=100, course_id=course_overview.id)
+        CourseModeFactory.create(
+            mode_slug=CourseMode.VERIFIED,
+            min_price=100,
+            course_id=course_overview.id,
+            expiration_datetime=now() - timedelta(days=1)
+        )
         entitlement = CourseEntitlementFactory(
             user=self.user, mode=CourseMode.VERIFIED
         )
diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py
index 60a1cf34c95..e87e7442842 100644
--- a/openedx/core/djangoapps/catalog/utils.py
+++ b/openedx/core/djangoapps/catalog/utils.py
@@ -553,9 +553,7 @@ def get_fulfillable_course_runs_for_entitlement(entitlement, course_runs):
             # User is enrolled in the course so we should include it in the list of enrollable sessions always
             # this will ensure it is available for the UI
             enrollable_sessions.append(course_run)
-        elif (course_run.get('status') == COURSE_PUBLISHED and not
-                is_enrolled_in_mode and
-                is_course_run_entitlement_fulfillable(course_id, entitlement, search_time)):
+        elif not is_enrolled_in_mode and is_course_run_entitlement_fulfillable(course_id, entitlement, search_time):
             enrollable_sessions.append(course_run)
 
     enrollable_sessions.sort(key=lambda session: session.get('start'))
-- 
GitLab