From 6cc4fb34bea8f83b87f6f95e9b3d7cd3dd17c224 Mon Sep 17 00:00:00 2001 From: Matthew Piatetsky Date: Tue, 16 Oct 2018 16:52:08 -0400 Subject: [PATCH] add expiration banner --- lms/djangoapps/courseware/tests/helpers.py | 20 +++++- lms/djangoapps/courseware/tests/test_views.py | 67 ++++++++++++++++++- lms/djangoapps/courseware/views/index.py | 3 + lms/djangoapps/courseware/views/views.py | 2 + .../features/course_duration_limits/access.py | 23 ++++++- .../tests/views/test_course_home.py | 21 +++++- .../tests/views/test_course_updates.py | 2 +- 7 files changed, 130 insertions(+), 8 deletions(-) diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index 8cc81edf70..92ec3090a1 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -1,18 +1,21 @@ """ Helpers for courseware tests. """ +from datetime import timedelta import json from django.contrib import messages from django.contrib.auth.models import User -from django.urls import reverse from django.test import TestCase from django.test.client import Client, RequestFactory +from django.urls import reverse +from django.utils.timezone import now from six import text_type from courseware.access import has_access from courseware.masquerade import handle_ajax, setup_masquerade from edxmako.shortcuts import render_to_string +from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link from lms.djangoapps.lms_xblock.field_data import LmsFieldData from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.lib.url_utils import quote_slashes @@ -348,3 +351,18 @@ def _create_mock_json_request(user, data, method='POST'): request.user = user request.session = {} return request + + +def get_expiration_banner_text(user, course): + """ + Get text for banner that messages user course expiration date + for different tests that depend on it. + """ + expiration_date = (now() + timedelta(weeks=4)).strftime('%b %-d') + upgrade_link = verified_upgrade_deadline_link(user=user, course=course) + bannerText = 'Your access to this course expires on {expiration_date}. \ + Upgrade now for unlimited access.'.format( + expiration_date=expiration_date, + upgrade_link=upgrade_link + ) + return bannerText diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index e4d909e657..2fa5bea93b 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -40,6 +40,7 @@ from courseware.access_utils import check_course_open_for_learner from courseware.model_data import FieldDataCache, set_score from courseware.module_render import get_module, handle_xblock_callback from courseware.tests.factories import GlobalStaffFactory, StudentModuleFactory +from courseware.tests.helpers import get_expiration_banner_text from courseware.testutils import RenderXBlockTestMixin from courseware.url_helpers import get_redirect_url from courseware.user_state_client import DjangoXBlockUserStateClient @@ -211,8 +212,8 @@ class IndexQueryTestCase(ModuleStoreTestCase): @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) @ddt.data( - (ModuleStoreEnum.Type.mongo, 10, 157), - (ModuleStoreEnum.Type.split, 4, 153), + (ModuleStoreEnum.Type.mongo, 10, 160), + (ModuleStoreEnum.Type.split, 4, 156), ) @ddt.unpack def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): @@ -2645,11 +2646,71 @@ class TestIndexViewWithGating(ModuleStoreTestCase, MilestonesTestCaseMixin): } ) ) - self.assertEquals(response.status_code, 200) self.assertIn("Content Locked", response.content) +@attr(shard=5) +class TestIndexViewWithCourseDurationLimits(ModuleStoreTestCase): + """ + Test the index view for a course with course duration limits enabled. + """ + + def setUp(self): + """ + Set up the initial test data. + """ + super(TestIndexViewWithCourseDurationLimits, self).setUp() + + self.user = UserFactory() + self.course = CourseFactory.create(start=datetime.now() - timedelta(weeks=1)) + with self.store.bulk_operations(self.course.id): + self.chapter = ItemFactory.create(parent=self.course, category="chapter") + self.sequential = ItemFactory.create(parent=self.chapter, category='sequential') + + CourseEnrollmentFactory(user=self.user, course_id=self.course.id) + + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) + def test_index_with_course_duration_limits(self): + """ + Test that the courseware contains the course expiration banner + when course_duration_limits are enabled. + """ + self.assertTrue(self.client.login(username=self.user.username, password='test')) + response = self.client.get( + reverse( + 'courseware_section', + kwargs={ + 'course_id': unicode(self.course.id), + 'chapter': self.chapter.url_name, + 'section': self.sequential.url_name, + } + ) + ) + bannerText = get_expiration_banner_text(self.user, self.course) + self.assertContains(response, bannerText, html=True) + + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, False) + def test_index_without_course_duration_limits(self): + """ + Test that the courseware does not contain the course expiration banner + when course_duration_limits are disabled. + """ + self.assertTrue(self.client.login(username=self.user.username, password='test')) + response = self.client.get( + reverse( + 'courseware_section', + kwargs={ + 'course_id': unicode(self.course.id), + 'chapter': self.chapter.url_name, + 'section': self.sequential.url_name, + } + ) + ) + bannerText = get_expiration_banner_text(self.user, self.course) + self.assertNotContains(response, bannerText, html=True) + + @attr(shard=5) class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase, CompletionWaffleTestMixin): """ diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index e7a8890cd7..304db62b49 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -36,6 +36,7 @@ from openedx.core.djangoapps.user_api.preferences.api import get_user_preference from openedx.core.djangoapps.util.user_messages import PageLevelMessages from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace from openedx.core.djangolib.markup import HTML, Text +from openedx.features.course_duration_limits.access import register_course_expired_message from openedx.features.course_experience import ( COURSE_OUTLINE_PAGE_FLAG, default_course_url_name, COURSE_ENABLE_UNENROLLED_ACCESS_FLAG ) @@ -132,6 +133,8 @@ class CoursewareIndex(View): self.is_staff = has_access(request.user, 'staff', self.course) self._setup_masquerade_for_effective_user() + register_course_expired_message(request, self.course) + return self.render(request) except Exception as exception: # pylint: disable=broad-except return CourseTabView.handle_exceptions(request, self.course, exception) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 8ec7a810a9..0eafcb3ddb 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -88,6 +88,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.util.user_messages import PageLevelMessages from openedx.core.djangolib.markup import HTML, Text +from openedx.features.course_duration_limits.access import register_course_expired_message from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, course_home_url_name from openedx.features.course_experience.course_tools import CourseToolsPluginManager from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView @@ -504,6 +505,7 @@ class CourseTabView(EdxFragmentView): # Show warnings if the user has limited access # Must come after masquerading on creation of page context self.register_user_access_warning_messages(request, course_key) + register_course_expired_message(request, course) set_custom_metrics_for_course_key(course_key) return super(CourseTabView, self).get(request, course=course, page_context=page_context, **kwargs) diff --git a/openedx/features/course_duration_limits/access.py b/openedx/features/course_duration_limits/access.py index 62be42580a..ecaee92806 100644 --- a/openedx/features/course_duration_limits/access.py +++ b/openedx/features/course_duration_limits/access.py @@ -12,9 +12,12 @@ from django.utils.translation import ugettext as _ from util.date_utils import DEFAULT_SHORT_DATE_FORMAT, strftime_localized from lms.djangoapps.courseware.access_response import AccessError from lms.djangoapps.courseware.access_utils import ACCESS_GRANTED +from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link from openedx.core.djangoapps.catalog.utils import get_course_run_details from openedx.core.djangoapps.content.course_overviews.models import CourseOverview - +from openedx.core.djangoapps.util.user_messages import PageLevelMessages +from openedx.core.djangolib.markup import HTML +from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG MIN_DURATION = timedelta(weeks=4) MAX_DURATION = timedelta(weeks=12) @@ -91,3 +94,21 @@ def check_course_expired(user, course): return AuditExpiredError(user, course, expiration_date) return ACCESS_GRANTED + + +def register_course_expired_message(request, course): + """ + Add a banner notifying the user of the user course expiration date if it exists. + """ + if CONTENT_TYPE_GATING_FLAG.is_enabled(): + expiration_date = get_user_course_expiration_date(request.user, course) + if expiration_date: + upgrade_message = _('Your access to this course expires on {expiration_date}. \ + Upgrade now for unlimited access.') + PageLevelMessages.register_info_message( + request, + HTML(upgrade_message).format( + expiration_date=expiration_date.strftime('%b %-d'), + upgrade_link=verified_upgrade_deadline_link(user=request.user, course=course) + ) + ) diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index d1fa9e4b84..1f248eb1a9 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -17,6 +17,7 @@ from waffle.testutils import override_flag from course_modes.models import CourseMode from courseware.tests.factories import StaffFactory +from courseware.tests.helpers import get_expiration_banner_text from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.course_goals.api import add_course_goal, remove_course_goal @@ -180,7 +181,7 @@ class TestCourseHomePage(CourseHomePageTestCase): course_home_url(self.course) # Fetch the view and verify the query counts - with self.assertNumQueries(67, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): + with self.assertNumQueries(70, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): with check_mongo_calls(4): url = course_home_url(self.course) self.client.get(url) @@ -414,7 +415,9 @@ class TestCourseHomePageAccess(CourseHomePageTestCase): 2) Unenrolled users are shown a course message allowing them to enroll 3) Enrolled users who show up on the course page after the course has begun are not shown a course message. - 4) Enrolled users who show up on the course page before the course begins + 4) Enrolled users who show up on the course page after the course has begun will + see the course expiration banner if course duration limits are on for the course. + 5) Enrolled users who show up on the course page before the course begins are shown a message explaining when the course starts as well as a call to action button that allows them to add a calendar event. """ @@ -439,6 +442,20 @@ class TestCourseHomePageAccess(CourseHomePageTestCase): self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED) self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START) + # Verify that enrolled users are shown the course expiration banner if content gating is enabled + with override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True): + url = course_home_url(self.course) + response = self.client.get(url) + bannerText = get_expiration_banner_text(user, self.course) + self.assertContains(response, bannerText, html=True) + + # Verify that enrolled users are not shown the course expiration banner if content gating is disabled + with override_waffle_flag(CONTENT_TYPE_GATING_FLAG, False): + url = course_home_url(self.course) + response = self.client.get(url) + bannerText = get_expiration_banner_text(user, self.course) + self.assertNotContains(response, bannerText, html=True) + # Verify that enrolled users are shown 'days until start' message before start date future_course = self.create_future_course() CourseEnrollment.enroll(user, future_course.id) diff --git a/openedx/features/course_experience/tests/views/test_course_updates.py b/openedx/features/course_experience/tests/views/test_course_updates.py index 10ea37078b..cd710e8186 100644 --- a/openedx/features/course_experience/tests/views/test_course_updates.py +++ b/openedx/features/course_experience/tests/views/test_course_updates.py @@ -127,7 +127,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase): course_updates_url(self.course) # Fetch the view and verify that the query counts haven't changed - with self.assertNumQueries(39, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): + with self.assertNumQueries(42, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): with check_mongo_calls(4): url = course_updates_url(self.course) self.client.get(url)