diff --git a/common/djangoapps/entitlements/models.py b/common/djangoapps/entitlements/models.py index cae62123ba46e5962c1214db6f6d6f81c10cbb1b..80e74fc5a412765853c82555a552710eeffdc59c 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 4ab6b954222608c70ce72fb4f8e36775f11b66ae..38b96578f86bd79255d4b01437903f50a7d1caec 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 ba01a9c4339c97b2f2e7b34787cef0e492967f60..cfe10b9f8cf2734fbc9b8d75dd66123d17101d7c 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 caf546607dee9ed1b6ca3c8e90adb16e7153436d..5239ee6325e794f970fc2dbf50fbe214c2d91a7f 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 c0e36410eb6aa5a2e3fc731ba20b9e6f71c4fb62..1961fbcff1ed5960698a8def8f6beee94976cd69 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 4575b931f03b357bf3ed21deb2a20929b8e6d940..a407e5baf101343fefd6c656115d624618d19e4e 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 1da55c5130e6fe226f736e9f0f58f0708f2f719c..7b25e71cd6d117e57d0dbf473b7a0eb7e9ea816b 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: