From 610c255bb628ef3bf855cbd8b4c0574a91731abf Mon Sep 17 00:00:00 2001 From: Harry Rein 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 14813fdc81..d55ab0e1bb 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 89037598a2..933ce59cdd 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('
', 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('
', 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('
  • '), 1) self.assertIn(' + % if not entitlement_expired_at: + + % endif % endif + % if entitlement and not is_unfulfilled_entitlement and entitlement_expiration_date: +
    + + % 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 +
    + % endif
  • @@ -278,7 +298,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
    - % if entitlement: + % if entitlement and not entitlement_expired_at:
    <%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 }' }); %endif diff --git a/lms/templates/learner_dashboard/course_card.underscore b/lms/templates/learner_dashboard/course_card.underscore index aac66f0a8d..53f8d35469 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) { %> <%- dateString %> - <% if (user_entitlement && !is_unfulfilled_entitlement) { %> + <% if (user_entitlement && !user_entitlement.expired_at && !is_unfulfilled_entitlement) { %> <% } %> <% } %>
    -
    + <% if (user_entitlement && user_entitlement.expiration_date) { %> +
    + <% 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}) %> + <% } %> + <% } %> +
    + <% } %>
    diff --git a/themes/edx.org/lms/templates/dashboard.html b/themes/edx.org/lms/templates/dashboard.html index d7d841f606..27d4e18783 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" %> <%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 % else: