Commit 610c255b by Harry Rein Committed by Jeff LaJoie

Display the expired at logic for entitlements.

LEARNER-3304

Displays the expired out status for the course dashboard and the programs pages.
parent 088aec98
import uuid as uuid_tools
from datetime import datetime, timedelta
from util.date_utils import strftime_localized
import pytz
from django.conf import settings
from django.contrib.sites.models import Site
from django.db import models
from certificates.models import GeneratedCertificate # pylint: disable=import-error
from certificates.models import GeneratedCertificate
from model_utils.models import TimeStampedModel
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
......@@ -214,11 +215,28 @@ class CourseEntitlement(TimeStampedModel):
return self.policy.is_entitlement_redeemable(self)
def to_dict(self):
""" Convert entitlement to dictionary representation. """
"""
Convert entitlement to dictionary representation including relevant policy information.
Returns:
The entitlement UUID
The associated course's UUID
The date at which the entitlement expired. None if it is still active.
The localized string representing the date at which the entitlement expires.
"""
expiration_date = None
if self.get_days_until_expiration() < settings.ENTITLEMENT_EXPIRED_ALERT_PERIOD:
expiration_date = strftime_localized(
datetime.now(tz=pytz.UTC) + timedelta(days=self.get_days_until_expiration()),
'SHORT_DATE'
)
expired_at = strftime_localized(self.expired_at_datetime, 'SHORT_DATE') if self.expired_at_datetime else None
return {
'uuid': str(self.uuid),
'course_uuid': str(self.course_uuid),
'expired_at': self.expired_at
'expired_at': expired_at,
'expiration_date': expiration_date
}
def set_enrollment(self, enrollment):
......
......@@ -240,6 +240,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
ENABLED_SIGNALS = ['course_published']
TOMORROW = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1)
THREE_YEARS_AGO = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=(365 * 3))
MOCK_SETTINGS = {
'FEATURES': {
'DISABLE_START_DATES': False,
......@@ -373,6 +374,29 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
@patch('student.views.get_course_runs_for_course')
@patch.object(CourseOverview, 'get_from_id')
def test_unfulfilled_expired_entitlement(self, mock_course_overview, mock_course_runs):
"""
When a learner has an unfulfilled, expired entitlement, their course dashboard should have:
- a hidden 'View Course' button
- a message saying that they can no longer select a session
"""
CourseEntitlementFactory(user=self.user, created=self.THREE_YEARS_AGO)
mock_course_overview.return_value = CourseOverviewFactory(start=self.TOMORROW)
mock_course_runs.return_value = [
{
'key': 'course-v1:FAKE+FA1-MA1.X+3T2017',
'enrollment_end': self.TOMORROW,
'pacing_type': 'instructor_paced',
'type': 'verified'
}
]
response = self.client.get(self.path)
self.assertIn('class="enter-course hidden"', response.content)
self.assertIn('You can no longer select a session', response.content)
self.assertNotIn('<div class="course-entitlement-selection-container ">', response.content)
@patch('student.views.get_course_runs_for_course')
@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):
"""
......@@ -401,6 +425,35 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
self.assertEqual(response.content.count('<li class="course-item">'), 1)
self.assertIn('<button class="change-session btn-link "', response.content)
@patch('student.views.get_course_runs_for_course')
@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):
"""
When a learner has a fulfilled entitlement that is expired, their course dashboard should have:
- exactly one course item, meaning it:
- has an entitlement card
- Message that the learner can no longer change sessions
"""
mocked_course_overview = CourseOverviewFactory(
start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
)
mock_course_overview.return_value = mocked_course_overview
mock_course_key.return_value = mocked_course_overview.id
course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=unicode(mocked_course_overview.id), created=self.THREE_YEARS_AGO)
mock_course_runs.return_value = [
{
'key': mocked_course_overview.id,
'enrollment_end': mocked_course_overview.enrollment_end,
'pacing_type': 'self_paced',
'type': 'verified'
}
]
CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment, created=self.THREE_YEARS_AGO)
response = self.client.get(self.path)
self.assertEqual(response.content.count('<li class="course-item">'), 1)
self.assertIn('You can no longer change sessions.', response.content)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@override_settings(BRANCH_IO_KEY='test_key')
......
......@@ -695,10 +695,11 @@ def dashboard(request):
# Get the entitlements for the user and a mapping to all available sessions for that entitlement
course_entitlements = list(CourseEntitlement.objects.filter(user=user).select_related('enrollment_course_run'))
course_entitlement_available_sessions = {
str(entitlement.uuid): get_course_runs_for_course(str(entitlement.course_uuid))
for entitlement in course_entitlements
}
course_entitlement_available_sessions = {}
for course_entitlement in course_entitlements:
course_entitlement.update_expired_at()
course_entitlement_available_sessions[str(course_entitlement.uuid)] = \
get_course_runs_for_course(str(course_entitlement.course_uuid))
# Record how many courses there are so that we can get a better
# understanding of usage patterns on prod.
......
......@@ -2418,6 +2418,9 @@ SUPPORT_SITE_LINK = ''
PASSWORD_RESET_SUPPORT_LINK = ''
ACTIVATION_EMAIL_SUPPORT_LINK = ''
# Days before the expired date that we warn the user
ENTITLEMENT_EXPIRED_ALERT_PERIOD = 90
############################# SOCIAL MEDIA SHARING #############################
# Social Media Sharing on Student Dashboard
SOCIAL_SHARING_SETTINGS = {
......
......@@ -13,7 +13,9 @@
availableSessions: [],
entitlementUUID: '',
currentSessionId: '',
courseName: ''
courseName: '',
expiredAt: null,
daysUntilExpiration: Number.MAX_VALUE
}
});
}
......
......@@ -91,7 +91,9 @@
currentSessionId: this.model.isEnrolledInSession() ?
this.model.get('course_run_key') : null,
enrollUrl: this.model.get('enroll_url'),
courseHomeUrl: this.model.get('course_url')
courseHomeUrl: this.model.get('course_url'),
expiredAt: this.entitlement.expired_at,
daysUntilExpiration: this.entitlement.days_until_expiration
});
}
......
......@@ -44,6 +44,10 @@
availableSessions: this.formatDates(JSON.parse(options.availableSessions)),
entitlementUUID: options.entitlementUUID,
currentSessionId: options.currentSessionId,
expiredAt: options.expiredAt,
expiresAtDate: this.courseCardModel.formatDate(
new moment().utc().add(options.daysUntilExpiration, 'days')
),
courseName: options.courseName
});
this.listenTo(this.entitlementModel, 'change', this.render);
......
......@@ -234,6 +234,47 @@ define([
expect(view.$('.course-title-link').length).toEqual(0);
});
it('should show an unfulfilled expired user entitlement not allowing the changing of sessions', function() {
course.user_entitlement = {
uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
course_uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
expired_at: '2017-12-06 01:06:12',
expiration_date: '2017-12-05 01:06:12'
};
setupView(course, false);
expect(view.$('.info-expires-at').text().trim()).toContain('You can no longer select a session. Your');
});
it('should show an unfulfilled user entitlement allows you to select a session', function() {
course.user_entitlement = {
uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
course_uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
expiration_date: '2017-12-05 01:06:12'
};
setupView(course, false);
expect(view.$('.info-expires-at').text().trim()).toContain('You must select a session by');
});
it('should show a fulfilled expired user entitlement does not allow the changing of sessions', function() {
course.user_entitlement = {
uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
course_uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
expired_at: '2017-12-06 01:06:12',
expiration_date: '2017-12-05 01:06:12'
};
setupView(course, true);
expect(view.$('.info-expires-at').text().trim()).toContain('You can no longer change sessions.');
});
it('should show a fulfilled user entitlement allows the changing of sessions', function() {
course.user_entitlement = {
uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
course_uuid: '99fc7414c36d4f56b37e8e30acf4c7ba',
expiration_date: '2017-12-05 01:06:12'
};
setupView(course, true);
expect(view.$('.info-expires-at').text().trim()).toContain('You can change sessions until');
});
});
}
);
......@@ -349,7 +349,8 @@
.action {
@include margin-right(0);
&:hover, &:focus {
&:hover,
&:focus {
border: 1px solid transparent;
}
}
......@@ -361,7 +362,7 @@
.course-status {
background: $yellow;
border: 1px solid $border-color-2;
box-shadow: 0 1px 0 0 rgba(255,255,255, 0.6);
box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.6);
margin-top: 17px;
@include margin-right(flex-gutter());
......@@ -971,18 +972,16 @@
.action-certificate .btn {
@extend %btn-inherited-primary;
@include box-sizing(border-box);
padding: 7px $baseline*0.75;
float: none;
border-radius: 3px;
display: block;
@include padding(7px, ($baseline*0.75), 7px, ($baseline*0.75));
text-align: center;
a:link, a:visited {
a:link,
a:visited {
color: #fff;
}
}
......
......@@ -107,7 +107,7 @@
}
.change-session {
@include margin(0, 0, $baseline/4, $baseline/4);
@include margin(0, 0, 0, $baseline/4);
padding: 0;
font-size: $font-size-sm;
......
......@@ -475,7 +475,8 @@
}
}
.run-period {
.run-period,
.info-expires-at {
color: palette(grayscale, base);
font-size: 0.9375em;
}
......@@ -505,7 +506,6 @@
vertical-align: top;
padding: 0 10px 0 0;
float: left;
width: calc(100% - 205px);
}
}
......
......@@ -3,12 +3,14 @@
<%def name="online_help_token()"><% return "learnerdashboard" %></%def>
<%namespace name='static' file='static_content.html'/>
<%!
import pytz
from datetime import datetime, timedelta
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.template import RequestContext
from entitlements.models import CourseEntitlement
import third_party_auth
from third_party_auth import pipeline
from util.date_utils import strftime_localized
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
......@@ -126,6 +128,10 @@ from student.models import CourseEnrollment
# Check if the course run is an entitlement and if it has an associated session
entitlement = enrollment if isinstance(enrollment, CourseEntitlement) else None
entitlement_session = entitlement.enrollment_course_run if entitlement else None
entitlement_days_until_expiration = entitlement.get_days_until_expiration() if entitlement else None
entitlement_expiration = datetime.now(tz=pytz.UTC) + timedelta(days=entitlement_days_until_expiration) if (entitlement and entitlement_days_until_expiration < settings.ENTITLEMENT_EXPIRED_ALERT_PERIOD) else None
entitlement_expiration_date = strftime_localized(entitlement_expiration, 'SHORT_DATE') if entitlement and entitlement_expiration else None
entitlement_expired_at = strftime_localized(entitlement.expired_at_datetime, 'SHORT_DATE') if entitlement and entitlement.expired_at_datetime else None
is_fulfilled_entitlement = True if entitlement and entitlement_session else False
is_unfulfilled_entitlement = True if entitlement and not entitlement_session else False
......@@ -167,7 +173,7 @@ from student.models import CourseEnrollment
show_consent_link = (session_id in consent_required_courses)
course_overview = enrollment.course_overview
%>
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name' />
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name' />
% endfor
</ul>
......
<%page args="course_overview, enrollment, entitlement, entitlement_session, course_card_index, is_unfulfilled_entitlement, is_fulfilled_entitlement, entitlement_available_sessions, show_courseware_link, cert_status, can_unenroll, credit_status, show_email_settings, course_mode_info, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, related_programs, display_course_modes_on_dashboard, show_consent_link, enterprise_customer_name" expression_filter="h"/>
<%page args="course_overview, enrollment, entitlement, entitlement_session, course_card_index, is_unfulfilled_entitlement, is_fulfilled_entitlement, entitlement_available_sessions, entitlement_expiration_date, entitlement_expired_at, show_courseware_link, cert_status, can_unenroll, credit_status, show_email_settings, course_mode_info, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, related_programs, display_course_modes_on_dashboard, show_consent_link, enterprise_customer_name" expression_filter="h"/>
<%!
import urllib
......@@ -131,7 +131,15 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
% if is_unfulfilled_entitlement:
<span class="info-date-block" aria-live="polite">
<span class="icon fa fa-warning" aria-hidden="true"></span>
${_('You must select a session to access the course.')}
% if entitlement_expired_at:
${_('You can no longer select a session, your final day to select a session was {entitlement_expired_at}.').format(entitlement_expired_at=entitlement_expired_at)}
% else:
% if entitlement_expiration_date:
${_('You must select a session by {expiration_date} to access the course.').format(expiration_date=entitlement_expiration_date)}
% else:
${_('You must select a session to access the course.')}
% endif
% endif
</span>
% else:
% if isinstance(course_date, basestring):
......@@ -141,9 +149,21 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
% endif
% endif
% if entitlement:
<button class="change-session btn-link ${'hidden' if is_unfulfilled_entitlement else ''}" aria-controls="change-session-${str(entitlement.uuid)}">${_('Change Session')}</button>
% if not entitlement_expired_at:
<button class="change-session btn-link ${'hidden' if is_unfulfilled_entitlement else ''}" aria-controls="change-session-${str(entitlement.uuid)}">${_('Change Session')}</button>
% endif
% endif
</span>
% if entitlement and not is_unfulfilled_entitlement and entitlement_expiration_date:
<div class="info-expires-at">
<span class="msg-icon fa fa-info" aria-hidden="true"></span>
% if entitlement_expired_at:
${_('You can no longer change sessions.')}
% else:
${_('You can change sessions until {entitlement_expiration_date}.').format(entitlement_expiration_date=entitlement_expiration_date)}
% endif
</div>
% endif
</div>
<div class="wrapper-course-actions">
<div class="course-actions">
......@@ -278,7 +298,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
<footer class="wrapper-messages-primary">
<div class="messages-list">
% if entitlement:
% if entitlement and not entitlement_expired_at:
<div class="course-entitlement-selection-container ${'' if is_unfulfilled_entitlement else 'hidden'}"></div>
<%static:require_module module_name="js/learner_dashboard/course_entitlement_factory" class_name="EntitlementFactory">
EntitlementFactory({
......@@ -293,7 +313,9 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
entitlementUUID: '${ entitlement.course_uuid | n, js_escaped_string }',
currentSessionId: '${ entitlement_session.course_id if entitlement_session else "" | n, js_escaped_string }',
enrollUrl: '${ reverse('entitlements_api:v1:enrollments', args=[str(entitlement.uuid)]) | n, js_escaped_string }',
courseHomeUrl: '${ course_target | n, js_escaped_string }'
courseHomeUrl: '${ course_target | n, js_escaped_string }',
expiredAt: '${ entitlement.expired_at_datetime | n, js_escaped_string }',
daysUntilExpiration: '${ entitlement.get_days_until_expiration() | n, js_escaped_string }'
});
</%static:require_module>
%endif
......
......@@ -17,12 +17,28 @@
<% } %>
<% if (dateString && !is_unfulfilled_entitlement) { %>
<span class="run-period"><%- dateString %></span>
<% if (user_entitlement && !is_unfulfilled_entitlement) { %>
<% if (user_entitlement && !user_entitlement.expired_at && !is_unfulfilled_entitlement) { %>
<button class="change-session btn-link" aria-controls="change-session-<%-user_entitlement.uuid%>"> <%- gettext('Change Session')%></button>
<% } %>
<% } %>
</div>
</div>
<% if (user_entitlement && user_entitlement.expiration_date) { %>
<div class="info-expires-at">
<% if (is_unfulfilled_entitlement) { %>
<% if (user_entitlement.expired_at) { %>
<%- StringUtils.interpolate(gettext('You can no longer select a session. Your final day to select a session was {expiration_date}.'), {expiration_date: user_entitlement.expiration_date}) %>
<% } else { %>
<%- StringUtils.interpolate(gettext('You must select a session by {expiration_date} to access the course.'), {expiration_date: user_entitlement.expiration_date}) %>
<% } %>
<% } else { %>
<% if (user_entitlement.expired_at) { %>
<%- gettext('You can no longer change sessions.')%>
<% } else { %>
<%- StringUtils.interpolate(gettext('You can change sessions until {expiration_date}.'), {expiration_date: user_entitlement.expiration_date}) %>
<% } %>
<% } %>
</div>
<% } %>
<div class="course-actions"></div>
</div>
<div class="course-certificate certificate-status"></div>
......
......@@ -3,12 +3,15 @@
<%def name="online_help_token()"><% return "learnerdashboard" %></%def>
<%namespace name='static' file='static_content.html'/>
<%!
import pytz
from courseware.context_processor import user_timezone_locale_prefs
from datetime import datetime, timedelta
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.template import RequestContext
import third_party_auth
from third_party_auth import pipeline
from django.core.urlresolvers import reverse
import json
from util.date_utils import strftime_localized
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.theming import helpers as theming_helpers
......@@ -121,6 +124,10 @@ from student.models import CourseEnrollment
# Check if the course run is an entitlement and if it has an associated session
entitlement = enrollment if isinstance(enrollment, CourseEntitlement) else None
entitlement_session = entitlement.enrollment_course_run if entitlement else None
entitlement_days_until_expiration = entitlement.get_days_until_expiration() if entitlement else None
entitlement_expiration = datetime.now(tz=pytz.UTC) + timedelta(days=entitlement_days_until_expiration) if (entitlement and entitlement_days_until_expiration < settings.ENTITLEMENT_EXPIRED_ALERT_PERIOD) else None
entitlement_expiration_date = strftime_localized(entitlement_expiration, 'SHORT_DATE') if entitlement and entitlement_expiration else None
entitlement_expired_at = strftime_localized(entitlement.expired_at_datetime, 'SHORT_DATE') if entitlement and entitlement.expired_at_datetime else None
is_fulfilled_entitlement = True if entitlement and entitlement_session else False
is_unfulfilled_entitlement = True if entitlement and not entitlement_session else False
......@@ -162,7 +169,7 @@ from student.models import CourseEnrollment
show_consent_link = (session_id in consent_required_courses)
course_overview = enrollment.course_overview
%>
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name' />
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name' />
% endfor
</ul>
% else:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment