add expiration banner

This commit is contained in:
Matthew Piatetsky
2018-10-16 16:52:08 -04:00
committed by Matthew Piatetsky
parent 644cf1e60a
commit 6cc4fb34be
7 changed files with 130 additions and 8 deletions

View File

@@ -1,18 +1,21 @@
""" """
Helpers for courseware tests. Helpers for courseware tests.
""" """
from datetime import timedelta
import json import json
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import Client, RequestFactory from django.test.client import Client, RequestFactory
from django.urls import reverse
from django.utils.timezone import now
from six import text_type from six import text_type
from courseware.access import has_access from courseware.access import has_access
from courseware.masquerade import handle_ajax, setup_masquerade from courseware.masquerade import handle_ajax, setup_masquerade
from edxmako.shortcuts import render_to_string 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 lms.djangoapps.lms_xblock.field_data import LmsFieldData
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.url_utils import quote_slashes 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.user = user
request.session = {} request.session = {}
return request 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}. \
<a href="{upgrade_link}">Upgrade now</a> for unlimited access.'.format(
expiration_date=expiration_date,
upgrade_link=upgrade_link
)
return bannerText

View File

@@ -40,6 +40,7 @@ from courseware.access_utils import check_course_open_for_learner
from courseware.model_data import FieldDataCache, set_score from courseware.model_data import FieldDataCache, set_score
from courseware.module_render import get_module, handle_xblock_callback from courseware.module_render import get_module, handle_xblock_callback
from courseware.tests.factories import GlobalStaffFactory, StudentModuleFactory from courseware.tests.factories import GlobalStaffFactory, StudentModuleFactory
from courseware.tests.helpers import get_expiration_banner_text
from courseware.testutils import RenderXBlockTestMixin from courseware.testutils import RenderXBlockTestMixin
from courseware.url_helpers import get_redirect_url from courseware.url_helpers import get_redirect_url
from courseware.user_state_client import DjangoXBlockUserStateClient from courseware.user_state_client import DjangoXBlockUserStateClient
@@ -211,8 +212,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
@override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 10, 157), (ModuleStoreEnum.Type.mongo, 10, 160),
(ModuleStoreEnum.Type.split, 4, 153), (ModuleStoreEnum.Type.split, 4, 156),
) )
@ddt.unpack @ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): 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.assertEquals(response.status_code, 200)
self.assertIn("Content Locked", response.content) 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) @attr(shard=5)
class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase, CompletionWaffleTestMixin): class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase, CompletionWaffleTestMixin):
""" """

View File

@@ -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.util.user_messages import PageLevelMessages
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
from openedx.core.djangolib.markup import HTML, Text 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 ( from openedx.features.course_experience import (
COURSE_OUTLINE_PAGE_FLAG, default_course_url_name, COURSE_ENABLE_UNENROLLED_ACCESS_FLAG 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.is_staff = has_access(request.user, 'staff', self.course)
self._setup_masquerade_for_effective_user() self._setup_masquerade_for_effective_user()
register_course_expired_message(request, self.course)
return self.render(request) return self.render(request)
except Exception as exception: # pylint: disable=broad-except except Exception as exception: # pylint: disable=broad-except
return CourseTabView.handle_exceptions(request, self.course, exception) return CourseTabView.handle_exceptions(request, self.course, exception)

View File

@@ -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.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.util.user_messages import PageLevelMessages from openedx.core.djangoapps.util.user_messages import PageLevelMessages
from openedx.core.djangolib.markup import HTML, Text 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 import UNIFIED_COURSE_TAB_FLAG, course_home_url_name
from openedx.features.course_experience.course_tools import CourseToolsPluginManager from openedx.features.course_experience.course_tools import CourseToolsPluginManager
from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView 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 # Show warnings if the user has limited access
# Must come after masquerading on creation of page context # Must come after masquerading on creation of page context
self.register_user_access_warning_messages(request, course_key) self.register_user_access_warning_messages(request, course_key)
register_course_expired_message(request, course)
set_custom_metrics_for_course_key(course_key) set_custom_metrics_for_course_key(course_key)
return super(CourseTabView, self).get(request, course=course, page_context=page_context, **kwargs) return super(CourseTabView, self).get(request, course=course, page_context=page_context, **kwargs)

View File

@@ -12,9 +12,12 @@ from django.utils.translation import ugettext as _
from util.date_utils import DEFAULT_SHORT_DATE_FORMAT, strftime_localized from util.date_utils import DEFAULT_SHORT_DATE_FORMAT, strftime_localized
from lms.djangoapps.courseware.access_response import AccessError from lms.djangoapps.courseware.access_response import AccessError
from lms.djangoapps.courseware.access_utils import ACCESS_GRANTED 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.catalog.utils import get_course_run_details
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview 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) MIN_DURATION = timedelta(weeks=4)
MAX_DURATION = timedelta(weeks=12) MAX_DURATION = timedelta(weeks=12)
@@ -91,3 +94,21 @@ def check_course_expired(user, course):
return AuditExpiredError(user, course, expiration_date) return AuditExpiredError(user, course, expiration_date)
return ACCESS_GRANTED 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}. \
<a href="{upgrade_link}">Upgrade now</a> 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)
)
)

View File

@@ -17,6 +17,7 @@ from waffle.testutils import override_flag
from course_modes.models import CourseMode from course_modes.models import CourseMode
from courseware.tests.factories import StaffFactory 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.models import CommerceConfiguration
from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.course_goals.api import add_course_goal, remove_course_goal 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) course_home_url(self.course)
# Fetch the view and verify the query counts # 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): with check_mongo_calls(4):
url = course_home_url(self.course) url = course_home_url(self.course)
self.client.get(url) self.client.get(url)
@@ -414,7 +415,9 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
2) Unenrolled users are shown a course message allowing them to enroll 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 3) Enrolled users who show up on the course page after the course has begun
are not shown a course message. 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 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. 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_UNENROLLED)
self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START) 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 # Verify that enrolled users are shown 'days until start' message before start date
future_course = self.create_future_course() future_course = self.create_future_course()
CourseEnrollment.enroll(user, future_course.id) CourseEnrollment.enroll(user, future_course.id)

View File

@@ -127,7 +127,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
course_updates_url(self.course) course_updates_url(self.course)
# Fetch the view and verify that the query counts haven't changed # 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): with check_mongo_calls(4):
url = course_updates_url(self.course) url = course_updates_url(self.course)
self.client.get(url) self.client.get(url)