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)