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.
This commit is contained in:
Harry Rein
2018-01-08 11:50:05 -05:00
parent e7c869f87a
commit b404173046
7 changed files with 82 additions and 37 deletions

View File

@@ -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')

View File

@@ -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):

View File

@@ -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,

View File

@@ -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>');
alreadyEnrolled = (typeof isAlreadyEnrolled !== 'undefined') ? isAlreadyEnrolled : true;
hasSessions = (typeof hasAvailableSessions !== 'undefined') ? hasAvailableSessions : true;
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'
}];
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);

View File

@@ -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)

View File

@@ -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)

View File

@@ -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: