From 610c255bb628ef3bf855cbd8b4c0574a91731abf Mon Sep 17 00:00:00 2001
From: Harry Rein <hrein@edx.org>
Date: Thu, 7 Dec 2017 16:28:21 -0500
Subject: [PATCH] Display the expired at logic for entitlements.

LEARNER-3304

Displays the expired out status for the course dashboard and the programs pages.
---
 common/djangoapps/entitlements/models.py      | 24 +++++++--
 common/djangoapps/student/tests/test_views.py | 53 +++++++++++++++++++
 common/djangoapps/student/views.py            |  9 ++--
 lms/envs/common.py                            |  3 ++
 .../models/course_entitlement_model.js        |  4 +-
 .../views/course_card_view.js                 |  4 +-
 .../views/course_entitlement_view.js          |  4 ++
 .../course_card_view_spec.js                  | 41 ++++++++++++++
 lms/static/sass/multicourse/_dashboard.scss   | 13 +++--
 .../sass/views/_course-entitlements.scss      |  2 +-
 lms/static/sass/views/_program-details.scss   |  4 +-
 lms/templates/dashboard.html                  | 10 +++-
 .../dashboard/_dashboard_course_listing.html  | 32 +++++++++--
 .../learner_dashboard/course_card.underscore  | 20 ++++++-
 themes/edx.org/lms/templates/dashboard.html   | 13 +++--
 15 files changed, 205 insertions(+), 31 deletions(-)

diff --git a/common/djangoapps/entitlements/models.py b/common/djangoapps/entitlements/models.py
index 14813fdc81c..d55ab0e1bb1 100644
--- a/common/djangoapps/entitlements/models.py
+++ b/common/djangoapps/entitlements/models.py
@@ -1,12 +1,13 @@
 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):
diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py
index 89037598a2a..933ce59cdd5 100644
--- a/common/djangoapps/student/tests/test_views.py
+++ b/common/djangoapps/student/tests/test_views.py
@@ -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,
@@ -371,6 +372,29 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
         self.assertIn('<div class="course-entitlement-selection-container ">', response.content)
         self.assertIn('Related Programs:', response.content)
 
+    @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')
@@ -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')
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 488c70bd19a..dec0f6a3a6b 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -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.
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 5c5906e238b..2105ce29644 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -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 = {
diff --git a/lms/static/js/learner_dashboard/models/course_entitlement_model.js b/lms/static/js/learner_dashboard/models/course_entitlement_model.js
index 8350a89110c..d80e4fc3e01 100644
--- a/lms/static/js/learner_dashboard/models/course_entitlement_model.js
+++ b/lms/static/js/learner_dashboard/models/course_entitlement_model.js
@@ -13,7 +13,9 @@
                     availableSessions: [],
                     entitlementUUID: '',
                     currentSessionId: '',
-                    courseName: ''
+                    courseName: '',
+                    expiredAt: null,
+                    daysUntilExpiration: Number.MAX_VALUE
                 }
             });
         }
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 baa2b35140f..34400b55593 100644
--- a/lms/static/js/learner_dashboard/views/course_card_view.js
+++ b/lms/static/js/learner_dashboard/views/course_card_view.js
@@ -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
                          });
                      }
 
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 1522ef23666..b3e42abbb05 100644
--- a/lms/static/js/learner_dashboard/views/course_entitlement_view.js
+++ b/lms/static/js/learner_dashboard/views/course_entitlement_view.js
@@ -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);
diff --git a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js
index 2ef08531ddd..2fa0c250d39 100644
--- a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js
+++ b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js
@@ -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');
+        });
     });
 }
 );
diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss
index 36c495c6fef..1af3598aed5 100644
--- a/lms/static/sass/multicourse/_dashboard.scss
+++ b/lms/static/sass/multicourse/_dashboard.scss
@@ -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;
             }
           }
diff --git a/lms/static/sass/views/_course-entitlements.scss b/lms/static/sass/views/_course-entitlements.scss
index 22a4cf13c30..ea3a4b1cf24 100644
--- a/lms/static/sass/views/_course-entitlements.scss
+++ b/lms/static/sass/views/_course-entitlements.scss
@@ -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;
diff --git a/lms/static/sass/views/_program-details.scss b/lms/static/sass/views/_program-details.scss
index 03f931d2857..cb0724d40f0 100644
--- a/lms/static/sass/views/_program-details.scss
+++ b/lms/static/sass/views/_program-details.scss
@@ -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);
       }
     }
 
diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html
index d3fcfd671c2..1c9a51f7564 100644
--- a/lms/templates/dashboard.html
+++ b/lms/templates/dashboard.html
@@ -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>
diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html
index eddb8287001..f6ee89513eb 100644
--- a/lms/templates/dashboard/_dashboard_course_listing.html
+++ b/lms/templates/dashboard/_dashboard_course_listing.html
@@ -1,4 +1,4 @@
-<%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
diff --git a/lms/templates/learner_dashboard/course_card.underscore b/lms/templates/learner_dashboard/course_card.underscore
index aac66f0a8db..53f8d354696 100644
--- a/lms/templates/learner_dashboard/course_card.underscore
+++ b/lms/templates/learner_dashboard/course_card.underscore
@@ -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>
diff --git a/themes/edx.org/lms/templates/dashboard.html b/themes/edx.org/lms/templates/dashboard.html
index d7d841f606c..27d4e18783b 100644
--- a/themes/edx.org/lms/templates/dashboard.html
+++ b/themes/edx.org/lms/templates/dashboard.html
@@ -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:
-- 
GitLab