From b4041730468fd3526c6bab7df5802cc67adec721 Mon Sep 17 00:00:00 2001
From: Harry Rein <hrein@edx.org>
Date: Mon, 8 Jan 2018 11:50:05 -0500
Subject: [PATCH] Show more sessions coming soon on course dashboard.

LEARNER-3808

Ensures that users can see their entitlement purchase whether there
are available sessions or not. Notifies them with a message stating that
more sessions are coming soon, as is currently implemented on the programs
dashboard.
---
 common/djangoapps/entitlements/models.py      |  1 -
 common/djangoapps/student/tests/test_views.py | 15 ++++--
 common/djangoapps/student/views.py            | 15 ++++--
 .../course_entitlement_view_spec.js           | 49 +++++++++++++------
 lms/templates/dashboard.html                  | 11 ++---
 openedx/core/djangoapps/catalog/utils.py      | 21 +++++++-
 themes/edx.org/lms/templates/dashboard.html   |  9 ++--
 7 files changed, 83 insertions(+), 38 deletions(-)

diff --git a/common/djangoapps/entitlements/models.py b/common/djangoapps/entitlements/models.py
index cae62123ba4..80e74fc5a41 100644
--- a/common/djangoapps/entitlements/models.py
+++ b/common/djangoapps/entitlements/models.py
@@ -133,7 +133,6 @@ class CourseEntitlement(TimeStampedModel):
     """
     Represents a Student's Entitlement to a Course Run for a given Course.
     """
-
     user = models.ForeignKey(settings.AUTH_USER_MODEL)
     uuid = models.UUIDField(default=uuid_tools.uuid4, editable=False, unique=True)
     course_uuid = models.UUIDField(help_text='UUID for the Course, not the Course Run')
diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py
index 4ab6b954222..38b96578f86 100644
--- a/common/djangoapps/student/tests/test_views.py
+++ b/common/djangoapps/student/tests/test_views.py
@@ -347,9 +347,10 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
         self.assertNotIn('<div class="prerequisites">', response.content)
 
     @patch('openedx.core.djangoapps.programs.utils.get_programs')
-    @patch('student.views.get_visible_course_runs_for_entitlement')
+    @patch('student.views.get_visible_sessions_for_entitlement')
+    @patch('student.views.get_pseudo_session_for_entitlement')
     @patch.object(CourseOverview, 'get_from_id')
-    def test_unfulfilled_entitlement(self, mock_course_overview, mock_course_runs, mock_get_programs):
+    def test_unfulfilled_entitlement(self, mock_course_overview, mock_pseudo_session, mock_course_runs, mock_get_programs):
         """
         When a learner has an unfulfilled entitlement, their course dashboard should have:
             - a hidden 'View Course' button
@@ -369,13 +370,17 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
                 'type': 'verified'
             }
         ]
+        mock_pseudo_session.return_value = {
+            'key': 'course-v1:FAKE+FA1-MA1.X+3T2017',
+            'type': 'verified'
+        }
         response = self.client.get(self.path)
         self.assertIn('class="enter-course hidden"', response.content)
         self.assertIn('You must select a session to access the course.', response.content)
         self.assertIn('<div class="course-entitlement-selection-container ">', response.content)
         self.assertIn('Related Programs:', response.content)
 
-    @patch('student.views.get_visible_course_runs_for_entitlement')
+    @patch('student.views.get_visible_sessions_for_entitlement')
     @patch.object(CourseOverview, 'get_from_id')
     def test_unfulfilled_expired_entitlement(self, mock_course_overview, mock_course_runs):
         """
@@ -468,7 +473,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
         # self.assertNotIn(noAvailableSessions, response.content)
 
     @patch('openedx.core.djangoapps.programs.utils.get_programs')
-    @patch('student.views.get_visible_course_runs_for_entitlement')
+    @patch('student.views.get_visible_sessions_for_entitlement')
     @patch.object(CourseOverview, 'get_from_id')
     @patch('opaque_keys.edx.keys.CourseKey.from_string')
     def test_fulfilled_entitlement(self, mock_course_key, mock_course_overview, mock_course_runs, mock_get_programs):
@@ -505,7 +510,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
         self.assertIn('Related Programs:', response.content)
 
     @patch('openedx.core.djangoapps.programs.utils.get_programs')
-    @patch('student.views.get_visible_course_runs_for_entitlement')
+    @patch('student.views.get_visible_sessions_for_entitlement')
     @patch.object(CourseOverview, 'get_from_id')
     @patch('opaque_keys.edx.keys.CourseKey.from_string')
     def test_fulfilled_expired_entitlement(self, mock_course_key, mock_course_overview, mock_course_runs, mock_get_programs):
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index ba01a9c4339..cfe10b9f8cf 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -74,7 +74,9 @@ from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
 # Note that this lives in LMS, so this dependency should be refactored.
 from notification_prefs.views import enable_notifications
 from openedx.core.djangoapps import monitoring_utils
-from openedx.core.djangoapps.catalog.utils import get_programs_with_type, get_visible_course_runs_for_entitlement
+from openedx.core.djangoapps.catalog.utils import (
+    get_programs_with_type, get_visible_sessions_for_entitlement, get_pseudo_session_for_entitlement
+)
 from openedx.core.djangoapps.certificates.api import certificates_viewable_for_course
 from openedx.core.djangoapps.credit.email_utils import get_credit_provider_display_names, make_providers_strings
 from openedx.core.djangoapps.embargo import api as embargo_api
@@ -699,12 +701,18 @@ def dashboard(request):
     course_enrollments = list(get_course_enrollments(user, site_org_whitelist, site_org_blacklist))
 
     # Get the entitlements for the user and a mapping to all available sessions for that entitlement
+    # If an entitlement has no available sessions, pass through a mock course overview object
     course_entitlements = list(CourseEntitlement.get_active_entitlements_for_user(user))
     course_entitlement_available_sessions = {}
+    unfulfilled_entitlement_pseudo_sessions = {}
     for course_entitlement in course_entitlements:
         course_entitlement.update_expired_at()
-        valid_course_runs = get_visible_course_runs_for_entitlement(course_entitlement)
-        course_entitlement_available_sessions[str(course_entitlement.uuid)] = valid_course_runs
+        available_sessions = get_visible_sessions_for_entitlement(course_entitlement)
+        course_entitlement_available_sessions[str(course_entitlement.uuid)] = available_sessions
+        if not course_entitlement.enrollment_course_run:
+            # Unfulfilled entitlements need a mock session for metadata
+            pseudo_session = get_pseudo_session_for_entitlement(course_entitlement)
+            unfulfilled_entitlement_pseudo_sessions[str(course_entitlement.uuid)] = pseudo_session
 
     # Record how many courses there are so that we can get a better
     # understanding of usage patterns on prod.
@@ -915,6 +923,7 @@ def dashboard(request):
         'course_enrollments': course_enrollments,
         'course_entitlements': course_entitlements,
         'course_entitlement_available_sessions': course_entitlement_available_sessions,
+        'unfulfilled_entitlement_pseudo_sessions': unfulfilled_entitlement_pseudo_sessions,
         'course_optouts': course_optouts,
         'banner_account_activation_message': banner_account_activation_message,
         'sidebar_account_activation_message': sidebar_account_activation_message,
diff --git a/lms/static/js/spec/learner_dashboard/course_entitlement_view_spec.js b/lms/static/js/spec/learner_dashboard/course_entitlement_view_spec.js
index caf546607de..5239ee6325e 100644
--- a/lms/static/js/spec/learner_dashboard/course_entitlement_view_spec.js
+++ b/lms/static/js/spec/learner_dashboard/course_entitlement_view_spec.js
@@ -13,26 +13,33 @@ define([
             selectOptions,
             entitlementAvailableSessions,
             initialSessionId,
+            alreadyEnrolled,
+            hasSessions,
             entitlementUUID = 'a9aiuw76a4ijs43u18',
             testSessionIds = ['test_session_id_1', 'test_session_id_2'];
 
-        setupView = function(isAlreadyEnrolled) {
+        setupView = function(isAlreadyEnrolled, hasAvailableSessions) {
             setFixtures('<div class="course-entitlement-selection-container"></div>');
-
-            initialSessionId = isAlreadyEnrolled ? testSessionIds[0] : '';
-            entitlementAvailableSessions = [{
-                enrollment_end: null,
-                start: '2019-02-05T05:00:00+00:00',
-                pacing_type: 'instructor_paced',
-                session_id: testSessionIds[0],
-                end: null
-            }, {
-                enrollment_end: '2019-12-22T03:30:00Z',
-                start: '2020-01-03T13:00:00+00:00',
-                pacing_type: 'self_paced',
-                session_id: testSessionIds[1],
-                end: '2020-03-09T21:30:00+00:00'
-            }];
+            alreadyEnrolled = (typeof isAlreadyEnrolled !== 'undefined') ? isAlreadyEnrolled : true;
+            hasSessions = (typeof hasAvailableSessions !== 'undefined') ? hasAvailableSessions : true;
+
+            initialSessionId = alreadyEnrolled ? testSessionIds[0] : '';
+            entitlementAvailableSessions = [];
+            if (hasSessions) {
+                entitlementAvailableSessions = [{
+                    enrollment_end: null,
+                    start: '2019-02-05T05:00:00+00:00',
+                    pacing_type: 'instructor_paced',
+                    session_id: testSessionIds[0],
+                    end: null
+                }, {
+                    enrollment_end: '2019-12-22T03:30:00Z',
+                    start: '2020-01-03T13:00:00+00:00',
+                    pacing_type: 'self_paced',
+                    session_id: testSessionIds[1],
+                    end: '2020-03-09T21:30:00+00:00'
+                }];
+            }
 
             view = new CourseEntitlementView({
                 el: '.course-entitlement-selection-container',
@@ -95,6 +102,16 @@ define([
             });
         });
 
+        describe('Available Sessions Select - Unfulfilled Entitlement without available sessions', function() {
+            beforeEach(function() {
+                setupView(false, false);
+            });
+
+            it('Should notify user that more sessions are coming soon if none available.', function() {
+                expect(view.$('.action-header').text().includes('More sessions coming soon.')).toBe(true);
+            });
+        });
+
         describe('Available Sessions Select - Fulfilled Entitlement', function() {
             beforeEach(function() {
                 setupView(true);
diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html
index c0e36410eb6..1961fbcff1e 100644
--- a/lms/templates/dashboard.html
+++ b/lms/templates/dashboard.html
@@ -161,17 +161,16 @@ from student.models import CourseEnrollment
                     # If the user has a fulfilled entitlement, pass through the entitlements CourseEnrollment object
                     enrollment = entitlement_session
                   else:
-                    # If the user has an unfulfilled entitlement, pass through a bare CourseEnrollment object built off of the next available session
-                    upcoming_sessions = course_entitlement_available_sessions[str(entitlement.uuid)]
-                    next_session = upcoming_sessions[0] if upcoming_sessions else None
-                    if not next_session:
+                    # If the user has an unfulfilled entitlement, pass through a bare CourseEnrollment object to populate card with metadata
+                    pseudo_session = unfulfilled_entitlement_pseudo_sessions[str(entitlement.uuid)]
+                    if not pseudo_session:
                         continue
-                    enrollment = CourseEnrollment(user=user, course_id=next_session['key'], mode=next_session['type'])
+                    enrollment = CourseEnrollment(user=user, course_id=pseudo_session['key'], mode=pseudo_session['type'])
                   # We only show email settings for entitlement cards if the entitlement has an associated enrollment
                   show_email_settings = is_fulfilled_entitlement and (entitlement_session.course_id in show_email_settings_for)
                 else:
                   show_email_settings = (enrollment.course_id in show_email_settings_for)
-      
+
                 session_id = enrollment.course_id
                 show_courseware_link = (session_id in show_courseware_links_for)
                 cert_status = cert_statuses.get(session_id)
diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py
index 4575b931f03..a407e5baf10 100644
--- a/openedx/core/djangoapps/catalog/utils.py
+++ b/openedx/core/djangoapps/catalog/utils.py
@@ -243,9 +243,26 @@ def get_course_runs_for_course(course_uuid):
         return []
 
 
-def get_visible_course_runs_for_entitlement(entitlement):
+def get_pseudo_session_for_entitlement(entitlement):
     """
-    Returns only the course runs that the user can currently enroll in.
+    This function is used to pass pseudo-data to the front end, returning a single session, regardless of whether that
+    session is currently available.
+
+    First tries to return the first available session, followed by the first session regardless of availability.
+    Returns None if there are no sessions for that course.
+    """
+    sessions_for_course = get_course_runs_for_course(entitlement.course_uuid)
+    available_sessions = get_fulfillable_course_runs_for_entitlement(entitlement, sessions_for_course)
+    if available_sessions:
+        return available_sessions[0]
+    if sessions_for_course:
+        return sessions_for_course[0]
+    return None
+
+
+def get_visible_sessions_for_entitlement(entitlement):
+    """
+    Takes an entitlement object and returns the course runs that a user can currently enroll in.
     """
     sessions_for_course = get_course_runs_for_course(entitlement.course_uuid)
     return get_fulfillable_course_runs_for_entitlement(entitlement, sessions_for_course)
diff --git a/themes/edx.org/lms/templates/dashboard.html b/themes/edx.org/lms/templates/dashboard.html
index 1da55c5130e..7b25e71cd6d 100644
--- a/themes/edx.org/lms/templates/dashboard.html
+++ b/themes/edx.org/lms/templates/dashboard.html
@@ -157,12 +157,11 @@ from student.models import CourseEnrollment
                 # If the user has a fulfilled entitlement, pass through the entitlements CourseEnrollment object
                 enrollment = entitlement_session
               else:
-                # If the user has an unfulfilled entitlement, pass through a bare CourseEnrollment object built off of the next available session
-                upcoming_sessions = course_entitlement_available_sessions[str(entitlement.uuid)]
-                next_session = upcoming_sessions[0] if upcoming_sessions else None
-                if not next_session:
+                # If the user has an unfulfilled entitlement, pass through a bare CourseEnrollment object to populate card with metadata
+                pseudo_session = unfulfilled_entitlement_pseudo_sessions[str(entitlement.uuid)]
+                if not pseudo_session:
                     continue
-                enrollment = CourseEnrollment(user=user, course_id=next_session['key'], mode=next_session['type'])
+                enrollment = CourseEnrollment(user=user, course_id=pseudo_session['key'], mode=pseudo_session['type'])
               # We only show email settings for entitlement cards if the entitlement has an associated enrollment
               show_email_settings = is_fulfilled_entitlement and (entitlement_session.course_id in show_email_settings_for)
             else:
-- 
GitLab