From 3c5caaf76246a384677bdf9bc68082696fb41cb2 Mon Sep 17 00:00:00 2001 From: Gabe Mulley Date: Tue, 27 Nov 2018 10:51:52 -0500 Subject: [PATCH] REVE-103: Fix masquerade for feature based enrollments --- lms/templates/preview_menu.html | 3 +- .../features/content_type_gating/models.py | 22 ++- .../content_type_gating/partitions.py | 17 +- .../content_type_gating/tests/test_access.py | 76 +++++++- .../content_type_gating/tests/test_models.py | 7 +- .../features/course_duration_limits/access.py | 45 +++-- .../features/course_duration_limits/models.py | 52 ++++-- .../tests/test_course_expiration.py | 169 +++++++++++++++++- 8 files changed, 339 insertions(+), 52 deletions(-) diff --git a/lms/templates/preview_menu.html b/lms/templates/preview_menu.html index 949dc4ee5e..0e64476799 100644 --- a/lms/templates/preview_menu.html +++ b/lms/templates/preview_menu.html @@ -22,6 +22,7 @@ show_preview_menu = course and staff_access and supports_preview_menu course_partitions = get_all_partitions_for_course(course) masquerade_user_name = masquerade.user_name if masquerade else None masquerade_group_id = masquerade.group_id if masquerade else None + masquerade_user_partition_id = masquerade.user_partition_id if masquerade else None staff_selected = selected(not masquerade or masquerade.role != "student") specific_student_selected = selected(not staff_selected and masquerade.user_name) student_selected = selected(not staff_selected and not specific_student_selected and not masquerade_group_id) @@ -39,7 +40,7 @@ show_preview_menu = course and staff_access and supports_preview_menu % if course_partitions: % for course_partition in course_partitions: % for group in sorted(course_partition.groups, key=lambda group: group.name): - % endfor diff --git a/openedx/features/content_type_gating/models.py b/openedx/features/content_type_gating/models.py index b3e4482865..f776647365 100644 --- a/openedx/features/content_type_gating/models.py +++ b/openedx/features/content_type_gating/models.py @@ -10,6 +10,7 @@ from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from lms.djangoapps.courseware.masquerade import get_course_masquerade, is_masquerading_as_specific_student from experiments.models import ExperimentData from student.models import CourseEnrollment @@ -92,15 +93,18 @@ class ContentTypeGatingConfig(StackedConfigurationModel): # TODO: clean up as part of REV-100 experiment_data_holdback_key = EXPERIMENT_DATA_HOLDBACK_KEY.format(user) is_in_holdback = False - try: - holdback_value = ExperimentData.objects.get( - user=user, - experiment_id=EXPERIMENT_ID, - key=experiment_data_holdback_key, - ).value - is_in_holdback = holdback_value == 'True' - except ExperimentData.DoesNotExist: - pass + no_masquerade = get_course_masquerade(user, course_key) is None + student_masquerade = is_masquerading_as_specific_student(user, course_key) + if user and (no_masquerade or student_masquerade): + try: + holdback_value = ExperimentData.objects.get( + user=user, + experiment_id=EXPERIMENT_ID, + key=experiment_data_holdback_key, + ).value + is_in_holdback = holdback_value == 'True' + except ExperimentData.DoesNotExist: + pass if is_in_holdback: return False current_config = cls.current(course_key=enrollment.course_id) diff --git a/openedx/features/content_type_gating/partitions.py b/openedx/features/content_type_gating/partitions.py index 3d0383a199..bb192ca5b0 100644 --- a/openedx/features/content_type_gating/partitions.py +++ b/openedx/features/content_type_gating/partitions.py @@ -11,6 +11,7 @@ from course_modes.models import CourseMode import crum from django.apps import apps +from django.conf import settings from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy as _ @@ -21,7 +22,7 @@ from lms.djangoapps.courseware.masquerade import ( is_masquerading_as_specific_student, get_masquerading_user_group, ) -from xmodule.partitions.partitions import Group, UserPartition, UserPartitionError +from xmodule.partitions.partitions import Group, UserPartition, UserPartitionError, ENROLLMENT_TRACK_PARTITION_ID from openedx.core.lib.mobile_utils import is_request_from_mobile_app from openedx.features.content_type_gating.models import ContentTypeGatingConfig from student.roles import CourseBetaTesterRole @@ -140,8 +141,18 @@ class ContentTypeGatingPartitionScheme(object): # same logic as normal to return that student's group. If the current # user is masquerading as a generic student in a specific group, then # return that group. - if get_course_masquerade(user, course_key) and not is_masquerading_as_specific_student(user, course_key): - return get_masquerading_user_group(course_key, user, user_partition) + course_masquerade = get_course_masquerade(user, course_key) + if course_masquerade and not is_masquerading_as_specific_student(user, course_key): + masquerade_group = get_masquerading_user_group(course_key, user, user_partition) + if masquerade_group is not None: + return masquerade_group + else: + audit_mode_id = settings.COURSE_ENROLLMENT_MODES.get(CourseMode.AUDIT, {}).get('id') + if course_masquerade.user_partition_id == ENROLLMENT_TRACK_PARTITION_ID: + if course_masquerade.group_id != audit_mode_id: + return cls.FULL_ACCESS + else: + return cls.LIMITED_ACCESS # For now, treat everyone as a Full-access user, until we have the rest of the # feature gating logic in place. diff --git a/openedx/features/content_type_gating/tests/test_access.py b/openedx/features/content_type_gating/tests/test_access.py index e0a499237b..b618a38dfd 100644 --- a/openedx/features/content_type_gating/tests/test_access.py +++ b/openedx/features/content_type_gating/tests/test_access.py @@ -1,17 +1,20 @@ """ Test audit user's access to various content based on content-gating features. """ - -from datetime import datetime +import json +from datetime import datetime, timedelta import ddt from django.conf import settings from django.test.client import RequestFactory from django.test.utils import override_settings from django.urls import reverse +from django.utils import timezone from mock import patch from course_modes.tests.factories import CourseModeFactory from experiments.models import ExperimentKeyValue +from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID + from lms.djangoapps.courseware.module_render import load_single_xblock from lms.djangoapps.courseware.tests.factories import ( InstructorFactory, @@ -23,16 +26,14 @@ from lms.djangoapps.courseware.tests.factories import ( ) from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory from openedx.core.djangoapps.util.testing import TestConditionalContent -from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag from openedx.core.lib.url_utils import quote_slashes -from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID +from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID, CONTENT_TYPE_GATE_GROUP_IDS from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.config import ( - EXPERIMENT_DATA_HOLDBACK_KEY, EXPERIMENT_ID, ) from student.models import CourseEnrollment -from student.roles import CourseBetaTesterRole, CourseInstructorRole, CourseStaffRole +from student.roles import CourseInstructorRole from student.tests.factories import ( CourseEnrollmentFactory, UserFactory, @@ -201,7 +202,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase): # enroll all users into the all track types course self.users = {} for mode_type in self.MODE_TYPES: - self.users[mode_type] = UserFactory.create() + self.users[mode_type] = UserFactory.create(username=mode_type) CourseEnrollmentFactory.create( user=self.users[mode_type], course_id=self.courses['all_track_types']['course'].id, @@ -245,7 +246,8 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase): .... } """ - course = CourseFactory.create(run=run, display_name=display_name) + start_date = timezone.now() - timedelta(weeks=1) + course = CourseFactory.create(run=run, display_name=display_name, start=start_date) for mode in modes: CourseModeFactory.create(course_id=course.id, mode_slug=mode) @@ -465,6 +467,64 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase): request_factory=self.factory, ) + @ddt.data( + ({'user_partition_id': CONTENT_GATING_PARTITION_ID, + 'group_id': CONTENT_TYPE_GATE_GROUP_IDS['limited_access']}, True), + ({'user_partition_id': CONTENT_GATING_PARTITION_ID, + 'group_id': CONTENT_TYPE_GATE_GROUP_IDS['full_access']}, False), + ({'user_partition_id': ENROLLMENT_TRACK_PARTITION_ID, + 'group_id': settings.COURSE_ENROLLMENT_MODES['audit']['id']}, True), + ({'user_partition_id': ENROLLMENT_TRACK_PARTITION_ID, + 'group_id': settings.COURSE_ENROLLMENT_MODES['verified']['id']}, False), + ({'role': 'staff'}, False), + ({'role': 'student'}, True), + ({'username': 'audit'}, True), + ({'username': 'verified'}, False), + ) + @ddt.unpack + def test_masquerade(self, masquerade_config, is_gated): + instructor = UserFactory.create() + CourseEnrollmentFactory.create( + user=instructor, + course_id=self.course.id, + mode='audit' + ) + CourseInstructorRole(self.course.id).add_users(instructor) + self.client.login(username=instructor.username, password=TEST_PASSWORD) + + self.update_masquerade(**masquerade_config) + + block = self.blocks_dict['problem'] + block_view_url = reverse('render_xblock', kwargs={'usage_key_string': unicode(block.scope_ids.usage_id)}) + response = self.client.get(block_view_url) + if is_gated: + self.assertEquals(response.status_code, 404) + else: + self.assertEquals(response.status_code, 200) + + def update_masquerade(self, role='student', group_id=None, username=None, user_partition_id=None): + """ + Toggle masquerade state. + """ + masquerade_url = reverse( + 'masquerade_update', + kwargs={ + 'course_key_string': unicode(self.course.id), + } + ) + response = self.client.post( + masquerade_url, + json.dumps({ + 'role': role, + 'group_id': group_id, + 'user_name': username, + 'user_partition_id': user_partition_id, + }), + 'application/json' + ) + self.assertEqual(response.status_code, 200) + return response + @override_settings(FIELD_OVERRIDE_PROVIDERS=( 'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride', diff --git a/openedx/features/content_type_gating/tests/test_models.py b/openedx/features/content_type_gating/tests/test_models.py index dc2c2e3e98..693ae87daf 100644 --- a/openedx/features/content_type_gating/tests/test_models.py +++ b/openedx/features/content_type_gating/tests/test_models.py @@ -70,9 +70,12 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase): user = self.user course_key = self.course_overview.id - query_count = 5 - if not pass_enrollment and already_enrolled: + if already_enrolled and pass_enrollment: + query_count = 4 + elif not pass_enrollment and already_enrolled: query_count = 6 + else: + query_count = 5 with self.assertNumQueries(query_count): enabled = ContentTypeGatingConfig.enabled_for_enrollment( diff --git a/openedx/features/course_duration_limits/access.py b/openedx/features/course_duration_limits/access.py index 2232a7806d..a00e46062c 100644 --- a/openedx/features/course_duration_limits/access.py +++ b/openedx/features/course_duration_limits/access.py @@ -9,11 +9,14 @@ from django.apps import apps from django.utils import timezone from django.utils.translation import ugettext as _ -from course_modes.models import CourseMode from util.date_utils import DEFAULT_SHORT_DATE_FORMAT, strftime_localized + +from course_modes.models import CourseMode + 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 lms.djangoapps.courseware.masquerade import get_course_masquerade, is_masquerading_as_student 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 @@ -93,6 +96,10 @@ def check_course_expired(user, course): """ Check if the course expired for the user. """ + # masquerading course staff should always have access + if get_course_masquerade(user, course.id): + return ACCESS_GRANTED + if not CourseDurationLimitConfig.enabled_for_enrollment(user=user, course_key=course.id): return ACCESS_GRANTED @@ -107,15 +114,29 @@ def register_course_expired_message(request, course): """ Add a banner notifying the user of the user course expiration date if it exists. """ - if CourseDurationLimitConfig.enabled_for_enrollment(user=request.user, course_key=course.id): - 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) - ) + if not CourseDurationLimitConfig.enabled_for_enrollment(user=request.user, course_key=course.id): + return + + expiration_date = get_user_course_expiration_date(request.user, course) + if not expiration_date: + return + + if is_masquerading_as_student(request.user, course.id) and timezone.now() > expiration_date: + upgrade_message = _('This learner would not have access to this course. ' + 'Their access expired on {expiration_date}.') + PageLevelMessages.register_warning_message( + request, + HTML(upgrade_message).format( + expiration_date=expiration_date.strftime('%b %-d') ) + ) + else: + 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_duration_limits/models.py b/openedx/features/course_duration_limits/models.py index 76272ee8a5..ac476e8a13 100644 --- a/openedx/features/course_duration_limits/models.py +++ b/openedx/features/course_duration_limits/models.py @@ -5,14 +5,21 @@ Course Duration Limit Configuration Models # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID + +from course_modes.models import CourseMode +from lms.djangoapps.courseware.masquerade import get_masquerade_role, get_course_masquerade, \ + is_masquerading_as_specific_student from experiments.models import ExperimentData from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel +from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID, CONTENT_TYPE_GATE_GROUP_IDS from openedx.features.course_duration_limits.config import ( CONTENT_TYPE_GATING_FLAG, EXPERIMENT_ID, @@ -79,13 +86,25 @@ class CourseDurationLimitConfig(StackedConfigurationModel): # if the user is has a role of staff, instructor or beta tester their access should not expire if user is None and enrollment is not None: user = enrollment.user - if user: - staff_role = CourseStaffRole(course_key).has_user(user) - instructor_role = CourseInstructorRole(course_key).has_user(user) - beta_tester_role = CourseBetaTesterRole(course_key).has_user(user) - if staff_role or instructor_role or beta_tester_role: - return False + if user: + course_masquerade = get_course_masquerade(user, course_key) + if course_masquerade: + verified_mode_id = settings.COURSE_ENROLLMENT_MODES.get(CourseMode.VERIFIED, {}).get('id') + is_verified = (course_masquerade.user_partition_id == ENROLLMENT_TRACK_PARTITION_ID + and course_masquerade.group_id == verified_mode_id) + is_full_access = (course_masquerade.user_partition_id == CONTENT_GATING_PARTITION_ID + and course_masquerade.group_id == CONTENT_TYPE_GATE_GROUP_IDS['full_access']) + is_staff = get_masquerade_role(user, course_key) == 'staff' + if is_verified or is_full_access or is_staff: + return False + else: + staff_role = CourseStaffRole(course_key).has_user(user) + instructor_role = CourseInstructorRole(course_key).has_user(user) + beta_tester_role = CourseBetaTesterRole(course_key).has_user(user) + + if staff_role or instructor_role or beta_tester_role: + return False # enrollment might be None if the user isn't enrolled. In that case, # return enablement as if the user enrolled today @@ -95,15 +114,18 @@ class CourseDurationLimitConfig(StackedConfigurationModel): # TODO: clean up as part of REV-100 experiment_data_holdback_key = EXPERIMENT_DATA_HOLDBACK_KEY.format(user) is_in_holdback = False - try: - holdback_value = ExperimentData.objects.get( - user=user, - experiment_id=EXPERIMENT_ID, - key=experiment_data_holdback_key, - ).value - is_in_holdback = holdback_value == 'True' - except ExperimentData.DoesNotExist: - pass + no_masquerade = get_course_masquerade(user, course_key) is None + student_masquerade = is_masquerading_as_specific_student(user, course_key) + if user and (no_masquerade or student_masquerade): + try: + holdback_value = ExperimentData.objects.get( + user=user, + experiment_id=EXPERIMENT_ID, + key=experiment_data_holdback_key, + ).value + is_in_holdback = holdback_value == 'True' + except ExperimentData.DoesNotExist: + pass if is_in_holdback: return False current_config = cls.current(course_key=enrollment.course_id) diff --git a/openedx/features/course_duration_limits/tests/test_course_expiration.py b/openedx/features/course_duration_limits/tests/test_course_expiration.py index 799311abad..a0be81ca01 100644 --- a/openedx/features/course_duration_limits/tests/test_course_expiration.py +++ b/openedx/features/course_duration_limits/tests/test_course_expiration.py @@ -1,17 +1,27 @@ """ Contains tests to verify correctness of course expiration functionality """ +import json from datetime import timedelta + +from django.conf import settings +from django.urls import reverse from django.utils.timezone import now import ddt import mock - from course_modes.models import CourseMode +from experiments.models import ExperimentData +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID, CONTENT_TYPE_GATE_GROUP_IDS from openedx.features.course_duration_limits.access import get_user_course_expiration_date, MIN_DURATION, MAX_DURATION +from openedx.features.course_duration_limits.config import EXPERIMENT_ID, EXPERIMENT_DATA_HOLDBACK_KEY from openedx.features.course_experience.tests.views.helpers import add_course_mode +from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from student.models import CourseEnrollment -from student.tests.factories import UserFactory +from student.roles import CourseInstructorRole +from student.tests.factories import UserFactory, CourseEnrollmentFactory +from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -101,3 +111,158 @@ class CourseExpirationTestCase(ModuleStoreTestCase): result = get_user_course_expiration_date(self.user, future_course) content_availability_date = start_date self.assertEqual(result, content_availability_date + access_duration) + + @mock.patch("openedx.features.course_duration_limits.access.get_course_run_details") + @ddt.data( + ({'user_partition_id': CONTENT_GATING_PARTITION_ID, + 'group_id': CONTENT_TYPE_GATE_GROUP_IDS['limited_access']}, True), + ({'user_partition_id': CONTENT_GATING_PARTITION_ID, + 'group_id': CONTENT_TYPE_GATE_GROUP_IDS['full_access']}, False), + ({'user_partition_id': ENROLLMENT_TRACK_PARTITION_ID, + 'group_id': settings.COURSE_ENROLLMENT_MODES['audit']['id']}, True), + ({'user_partition_id': ENROLLMENT_TRACK_PARTITION_ID, + 'group_id': settings.COURSE_ENROLLMENT_MODES['verified']['id']}, False), + ({'role': 'staff'}, False), + ({'role': 'student'}, True), + ({'username': 'audit'}, True), + ({'username': 'verified'}, False), + ) + @ddt.unpack + def test_masquerade(self, masquerade_config, show_expiration_banner, mock_get_course_run_details): + mock_get_course_run_details.return_value = {'weeks_to_complete': 12} + audit_student = UserFactory(username='audit') + CourseEnrollmentFactory.create( + user=audit_student, + course_id=self.course.id, + mode='audit' + ) + verified_student = UserFactory(username='verified') + CourseEnrollmentFactory.create( + user=verified_student, + course_id=self.course.id, + mode='verified' + ) + CourseDurationLimitConfig.objects.create( + enabled=True, + course=CourseOverview.get_from_id(self.course.id), + enabled_as_of=self.course.start, + ) + + instructor = UserFactory.create(username='instructor') + CourseEnrollmentFactory.create( + user=instructor, + course_id=self.course.id, + mode='audit' + ) + CourseInstructorRole(self.course.id).add_users(instructor) + self.client.login(username=instructor.username, password='test') + + self.update_masquerade(**masquerade_config) + + course_home_url = reverse('openedx.course_experience.course_home', args=[unicode(self.course.id)]) + response = self.client.get(course_home_url, follow=True) + self.assertEqual(response.status_code, 200) + self.assertItemsEqual(response.redirect_chain, []) + banner_text = 'Your access to this course expires on' + if show_expiration_banner: + self.assertIn(banner_text, response.content) + else: + self.assertNotIn(banner_text, response.content) + + def update_masquerade(self, role='student', group_id=None, username=None, user_partition_id=None): + """ + Toggle masquerade state. + """ + masquerade_url = reverse( + 'masquerade_update', + kwargs={ + 'course_key_string': unicode(self.course.id), + } + ) + response = self.client.post( + masquerade_url, + json.dumps({ + 'role': role, + 'group_id': group_id, + 'user_name': username, + 'user_partition_id': user_partition_id, + }), + 'application/json' + ) + self.assertEqual(response.status_code, 200) + return response + + @mock.patch("openedx.features.course_duration_limits.access.get_course_run_details") + def test_masquerade_in_holdback(self, mock_get_course_run_details): + mock_get_course_run_details.return_value = {'weeks_to_complete': 12} + audit_student = UserFactory(username='audit') + CourseEnrollmentFactory.create( + user=audit_student, + course_id=self.course.id, + mode='audit' + ) + ExperimentData.objects.create( + user=audit_student, + experiment_id=EXPERIMENT_ID, + key=EXPERIMENT_DATA_HOLDBACK_KEY.format(audit_student), + value='True' + ) + CourseDurationLimitConfig.objects.create( + enabled=True, + course=CourseOverview.get_from_id(self.course.id), + enabled_as_of=self.course.start, + ) + + instructor = UserFactory.create(username='instructor') + CourseEnrollmentFactory.create( + user=instructor, + course_id=self.course.id, + mode='audit' + ) + CourseInstructorRole(self.course.id).add_users(instructor) + self.client.login(username=instructor.username, password='test') + + self.update_masquerade(username='audit') + + course_home_url = reverse('openedx.course_experience.course_home', args=[unicode(self.course.id)]) + response = self.client.get(course_home_url, follow=True) + self.assertEqual(response.status_code, 200) + self.assertItemsEqual(response.redirect_chain, []) + banner_text = 'Your access to this course expires on' + self.assertNotIn(banner_text, response.content) + + @mock.patch("openedx.features.course_duration_limits.access.get_course_run_details") + def test_masquerade_expired(self, mock_get_course_run_details): + mock_get_course_run_details.return_value = {'weeks_to_complete': 1} + + audit_student = UserFactory(username='audit') + enrollment = CourseEnrollmentFactory.create( + user=audit_student, + course_id=self.course.id, + mode='audit', + ) + enrollment.created = self.course.start + enrollment.save() + CourseDurationLimitConfig.objects.create( + enabled=True, + course=CourseOverview.get_from_id(self.course.id), + enabled_as_of=self.course.start, + ) + + instructor = UserFactory.create(username='instructor') + CourseEnrollmentFactory.create( + user=instructor, + course_id=self.course.id, + mode='audit' + ) + CourseInstructorRole(self.course.id).add_users(instructor) + self.client.login(username=instructor.username, password='test') + + self.update_masquerade(username='audit') + + course_home_url = reverse('openedx.course_experience.course_home', args=[unicode(self.course.id)]) + response = self.client.get(course_home_url, follow=True) + self.assertEqual(response.status_code, 200) + self.assertItemsEqual(response.redirect_chain, []) + banner_text = 'This learner would not have access to this course. Their access expired on' + self.assertIn(banner_text, response.content)