diff --git a/common/djangoapps/entitlements/models.py b/common/djangoapps/entitlements/models.py index 14813fdc81ccca26cba1a673295b552b2f60770a..d55ab0e1bb17cf3271343a8d400d5af86b6b79f1 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 89037598a2a525f2460be169d92cf4ffaea42262..933ce59cdd544ada0f86191fe49a1456bc259352 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 488c70bd19a2065e5c6bfb1f5ea086b52e93e568..dec0f6a3a6b5d70c0df3e754e53aed82c5a5e76c 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 5c5906e238b033a6809da4677a088a2261a8443b..2105ce29644bdf03261165e6e169e6c231f1fcde 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 8350a89110c269774b8976bb97ba729110cb04e3..d80e4fc3e0100bc252374e66714041f298a3363b 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 baa2b35140ff3b29be59c667d97e3eae27fc652c..34400b5559393a147ce1adc2b340af3c9fdb546e 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 1522ef23666d6165bc189d9c65eca4244b7ad8ab..b3e42abbb05a3db22d614ac979288f01efa3925a 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 2ef08531ddde34a7dd6d3f3e6e702ece68e3c035..2fa0c250d3999fe95051cabc29e75b7ab39105b3 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 36c495c6fef2a8af5e40e7c474194d79e6f32935..1af3598aed51ecd8e6d086034261267856a60ecf 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 22a4cf13c30b40b8f67b85ef1afd27d0a1037031..ea3a4b1cf24bef1396c38a0abdd5564493029cf5 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 03f931d2857f1d1723bca6539fd885ad5e4d7d89..cb0724d40f036347fef1ee4dde11b09b5192aeff 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 d3fcfd671c21398552019d5e14d09dbb3063a2b3..1c9a51f7564d18ff85b9c150c55e18feea480ae1 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 eddb8287001770745a802ba342f90a62f846a246..f6ee89513ebfc195ca8161ad27e56cc2ba96bbf3 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 aac66f0a8db87856f39d7b3f47af3cf16db4a78b..53f8d35469637edbaeaf5e240e89d40e0be9039a 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 d7d841f606c1bbefeab94caa2c110628b9d3cff5..27d4e18783bcd09d320b3677969060ddd1047df2 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: