diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py index 7e6c321db2e375e7e90b1be50272eb11443d5690..b88613a1871f61fc1d04f7926e595f35abb74ae1 100644 --- a/common/djangoapps/student/tests/test_views.py +++ b/common/djangoapps/student/tests/test_views.py @@ -31,6 +31,7 @@ from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory from openedx.core.djangoapps.user_authn.cookies import _get_user_info_cookie_data from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag +from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG from student.helpers import DISABLE_UNENROLL_CERT_STATES from student.models import CourseEnrollment, UserProfile from student.signals import REFUND_ORDER @@ -722,6 +723,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, ) @override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True) + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) def test_content_gating_course_card_changes(self): """ When a course is expired, the links on the course card should be removed. diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 8a18d52e4913b0f382a6fb9101c365123463cf8b..23e8015806d9a9e5e2b7077bd0687977afd80fcf 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -767,8 +767,9 @@ def student_dashboard(request): redirect_message = _("The course you are looking for is closed for enrollment as of {date}.").format( date=request.GET['course_closed'] ) - elif 'expired_message' in request.GET: - redirect_message = request.GET['expired_message'] + elif 'access_response_error' in request.GET: + # This can be populated in a generalized way with fields from access response errors + redirect_message = request.GET['access_response_error'] else: redirect_message = '' diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 1e91d7e15f60ad4a926568e60d5ee4660621e240..bc2383a30cad1bad5d5fb9769d1f869b576b5fb4 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -21,6 +21,7 @@ from lms.djangoapps.ccx.tests.factories import CcxFactory from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES +from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG from pytz import UTC from student.models import CourseEnrollment from student.tests.factories import UserFactory @@ -194,7 +195,8 @@ class FieldOverridePerformanceTestCase(FieldOverrideTestMixin, ProceduralCourseT XBLOCK_FIELD_DATA_WRAPPERS=[], MODULESTORE_FIELD_OVERRIDE_PROVIDERS=[], ) - def test_field_overrides(self, overrides, course_width, enable_ccx, view_as_ccx): + @mock.patch.object(CONTENT_TYPE_GATING_FLAG, 'is_enabled', return_value=True) + def test_field_overrides(self, overrides, course_width, enable_ccx, view_as_ccx, _mock_flag): """ Test without any field overrides. """ diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index ae822e836b592b947d93979f457d85a636d426ea..3d2acc14022b2f7c8d43bfdaa328362724347283 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -41,6 +41,7 @@ from mobile_api.models import IgnoreMobileAvailableFlagConfig from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.external_auth.models import ExternalAuthMap from openedx.features.course_duration_limits.access import check_course_expired +from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG from student import auth from student.models import CourseEnrollmentAllowed from student.roles import ( @@ -356,13 +357,14 @@ def _has_access_course(user, action, courselike): else: return view_with_prereqs - has_not_expired = check_course_expired(user, courselike) - if not has_not_expired: - staff_access = _has_staff_access_to_descriptor(user, courselike, courselike.id) - if staff_access: - return staff_access - else: - return has_not_expired + if CONTENT_TYPE_GATING_FLAG.is_enabled(): + has_not_expired = check_course_expired(user, courselike) + if not has_not_expired: + staff_access = _has_staff_access_to_descriptor(user, courselike, courselike.id) + if staff_access: + return staff_access + else: + return has_not_expired return ACCESS_GRANTED diff --git a/lms/djangoapps/courseware/access_response.py b/lms/djangoapps/courseware/access_response.py index a12dfa05a7112f0931615de23a37deacff04f3e1..3b1502aab3f0569f76c86baa3700ffeb347f71cc 100644 --- a/lms/djangoapps/courseware/access_response.py +++ b/lms/djangoapps/courseware/access_response.py @@ -10,7 +10,7 @@ from xmodule.course_metadata_utils import DEFAULT_START_DATE class AccessResponse(object): """Class that represents a response from a has_access permission check.""" def __init__(self, has_access, error_code=None, developer_message=None, user_message=None, - detailed_user_message=None, user_fragment=None): + additional_context_user_message=None, user_fragment=None): """ Creates an AccessResponse object. @@ -22,7 +22,7 @@ class AccessResponse(object): to show the developer user_message (String): optional - default is None. Message to show the user - detailed_user_message (String): optional - default is None. Message to + additional_context_user_message (String): optional - default is None. Message to show the user when additional context like the course name is necessary user_fragment (:py:class:`~web_fragments.fragment.Fragment`): optional - An html fragment to display to the user if their access is denied @@ -31,7 +31,7 @@ class AccessResponse(object): self.error_code = error_code self.developer_message = developer_message self.user_message = user_message - self.detailed_user_message = detailed_user_message + self.additional_context_user_message = additional_context_user_message self.user_fragment = user_fragment if has_access: assert error_code is None @@ -62,7 +62,7 @@ class AccessResponse(object): "error_code": self.error_code, "developer_message": self.developer_message, "user_message": self.user_message, - "detailed_user_message": self.detailed_user_message, + "additional_context_user_message": self.additional_context_user_message, "user_fragment": self.user_fragment, } @@ -72,7 +72,7 @@ class AccessResponse(object): self.error_code, self.developer_message, self.user_message, - self.detailed_user_message, + self.additional_context_user_message, self.user_fragment, ) @@ -85,7 +85,7 @@ class AccessResponse(object): self.error_code == other.error_code and self.developer_message == other.developer_message and self.user_message == other.user_message and - self.detailed_user_message == other.detailed_user_message and + self.additional_context_user_message == other.additional_context_user_message and self.user_fragment == other.user_fragment ) @@ -96,7 +96,8 @@ class AccessError(AccessResponse): denial in has_access. Contains the error code, user and developer messages. Subclasses represent specific errors. """ - def __init__(self, error_code, developer_message, user_message, detailed_user_message=None, user_fragment=None): + def __init__(self, error_code, developer_message, user_message, + additional_context_user_message=None, user_fragment=None): """ Creates an AccessError object. @@ -107,12 +108,12 @@ class AccessError(AccessResponse): error_code (String): unique identifier for the specific type of error developer_message (String): message to show the developer user_message (String): message to show the user - detailed_user_message (String): message to show the user with additional context like the course name + additional_context_user_message (String): message to show user with additional context like the course name user_fragment (:py:class:`~web_fragments.fragment.Fragment`): HTML to show the user """ super(AccessError, self).__init__(False, error_code, developer_message, user_message, - detailed_user_message, user_fragment) + additional_context_user_message, user_fragment) class StartDateError(AccessError): diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index a9d131b3922565ce69d444d7fe46bd756c7c188b..4418961a169adfb7cb48b34df8947fbc21ea088f 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -147,7 +147,7 @@ def check_course_access(course, user, action, check_if_enrolled=False, check_sur # Redirect if AuditExpiredError if isinstance(access_response, AuditExpiredError): params = QueryDict(mutable=True) - params['expired_message'] = access_response.detailed_user_message + params['access_response_error'] = access_response.additional_context_user_message raise CourseAccessRedirect('{dashboard_url}?{params}'.format( dashboard_url=reverse('dashboard'), params=params.urlencode() diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py index 9020bcaf5341d1bb6b1a4bfb61ce94d4fc1b047b..f5662c2e6aeea4b271fe3a339631d946d96e41b1 100644 --- a/lms/djangoapps/courseware/tests/test_access.py +++ b/lms/djangoapps/courseware/tests/test_access.py @@ -29,8 +29,9 @@ from courseware.tests.factories import ( from courseware.tests.helpers import LoginEnrollmentTestCase, masquerade_as_group_member from lms.djangoapps.ccx.models import CustomCourseForEdX from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES +from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag from openedx.core.lib.tests import attr +from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG from student.models import CourseEnrollment from student.roles import CourseCcxCoachRole, CourseStaffRole from student.tests.factories import ( @@ -826,6 +827,7 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase): ) @ddt.unpack @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) def test_course_catalog_access_num_queries(self, user_attr_name, action, course_attr_name): course = getattr(self, course_attr_name) diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py index 8b2ead3fa5036fa1ebb81cfb8d47098d70ec130f..68eecc91c6fd3ed6271cf2b0f986da1266488827 100644 --- a/lms/djangoapps/courseware/tests/test_course_info.py +++ b/lms/djangoapps/courseware/tests/test_course_info.py @@ -15,6 +15,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag from openedx.core.lib.tests import attr +from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from pyquery import PyQuery as pq @@ -430,8 +431,10 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest resp = self.client.get(url) self.assertEqual(resp.status_code, 200) + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) def test_num_queries_instructor_paced(self): self.fetch_course_info_with_queries(self.instructor_paced_course, 29, 3) + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) def test_num_queries_self_paced(self): self.fetch_course_info_with_queries(self.self_paced_course, 29, 3) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index f86a926430aa721c9ce7fae3e61d9a087bac82b0..0ed6538ba0194041f644175cc876ae9e7f170d7b 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -64,6 +64,7 @@ from openedx.core.djangolib.testing.utils import get_mock_request from openedx.core.lib.gating import api as gating_api from openedx.core.lib.tests import attr from openedx.core.lib.url_utils import quote_slashes +from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG, UNIFIED_COURSE_TAB_FLAG from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from student.models import CourseEnrollment @@ -204,6 +205,7 @@ class IndexQueryTestCase(ModuleStoreTestCase): CREATE_USER = False NUM_PROBLEMS = 20 + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) @ddt.data( (ModuleStoreEnum.Type.mongo, 10, 157), (ModuleStoreEnum.Type.split, 4, 153), @@ -1429,6 +1431,7 @@ class ProgressPageTests(ProgressPageBaseTests): resp = self._get_progress_page() self.assertContains(resp, u"Download Your Certificate") + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) @ddt.data( (True, 40), (False, 39) @@ -1440,6 +1443,7 @@ class ProgressPageTests(ProgressPageBaseTests): with self.assertNumQueries(query_count, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1): self._get_progress_page() + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) @patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False}) @ddt.data( (False, 47, 30), diff --git a/lms/djangoapps/discussion/tests/test_views.py b/lms/djangoapps/discussion/tests/test_views.py index b6a4c2be3362f1408a913fd8b66925ff16207613..672889653f7f2395880b06af1e77c3fda2e11d94 100644 --- a/lms/djangoapps/discussion/tests/test_views.py +++ b/lms/djangoapps/discussion/tests/test_views.py @@ -45,7 +45,8 @@ from openedx.core.djangoapps.course_groups.models import CourseUserGroup from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts from openedx.core.djangoapps.course_groups.tests.test_views import CohortViewsTestCase from openedx.core.djangoapps.util.testing import ContentGroupTestCase -from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES +from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag +from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from student.roles import CourseStaffRole, UserBasedRole from student.tests.factories import CourseEnrollmentFactory, UserFactory @@ -423,6 +424,7 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase): def setUp(self): super(SingleThreadQueryCountTestCase, self).setUp() + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) @ddt.data( # Old mongo with cache. There is an additional SQL query for old mongo # because the first time that disabled_xblocks is queried is in call_single_thread, diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py index 26ae5a59d954ad754bc289bda99280759da74182..7e2368a2c5c4afa56a687b2f613b4900ffa44244 100644 --- a/lms/djangoapps/django_comment_client/base/tests.py +++ b/lms/djangoapps/django_comment_client/base/tests.py @@ -37,8 +37,9 @@ from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMe from lms.lib.comment_client import Thread from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory -from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES +from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag from openedx.core.lib.tests import attr +from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG from student.roles import CourseStaffRole, UserBasedRole from student.tests.factories import CourseAccessRoleFactory, CourseEnrollmentFactory, UserFactory from util.testing import UrlResetMixin @@ -402,6 +403,7 @@ class ViewsQueryCountTestCase( func(self, *args, **kwargs) return inner + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) @ddt.data( (ModuleStoreEnum.Type.mongo, 3, 4, 37), (ModuleStoreEnum.Type.split, 3, 13, 37), @@ -411,6 +413,7 @@ class ViewsQueryCountTestCase( def test_create_thread(self, mock_request): self.create_thread_helper(mock_request) + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) @ddt.data( (ModuleStoreEnum.Type.mongo, 3, 3, 33), (ModuleStoreEnum.Type.split, 3, 10, 33), diff --git a/openedx/features/course_duration_limits/access.py b/openedx/features/course_duration_limits/access.py index 0ce4b2368689203fb602c2e3a0b844d49d788e19..6a523d57f2581aa2721c2fa53430af50d3a553f1 100644 --- a/openedx/features/course_duration_limits/access.py +++ b/openedx/features/course_duration_limits/access.py @@ -26,15 +26,17 @@ class AuditExpiredError(AccessError): user_message = _("Access expired on {expiration_date}").format(expiration_date=expiration_date) try: course_name = CourseOverview.get_from_id(course.id).display_name_with_default - detailed_user_message = _("Access to {course_name} expired on {expiration_date}").format( + additional_context_user_message = _("Access to {course_name} expired on {expiration_date}").format( course_name=course_name, expiration_date=expiration_date ) except CourseOverview.DoesNotExist: - detailed_user_message = _("Access to the course you were looking for expired on {expiration_date}").format( + additional_context_user_message = _("Access to the course you were looking" + "for expired on {expiration_date}").format( expiration_date=expiration_date ) - super(AuditExpiredError, self).__init__(error_code, developer_message, user_message, detailed_user_message) + super(AuditExpiredError, self).__init__(error_code, developer_message, user_message, + additional_context_user_message) def get_user_course_expiration_date(user, course): diff --git a/openedx/features/course_duration_limits/config.py b/openedx/features/course_duration_limits/config.py new file mode 100644 index 0000000000000000000000000000000000000000..eff4f675e7f3a54967c1fd0a6613053d83e2cc61 --- /dev/null +++ b/openedx/features/course_duration_limits/config.py @@ -0,0 +1,13 @@ +""" +Content type gating waffle flag +""" +from openedx.core.djangoapps.waffle_utils import WaffleFlagNamespace, WaffleFlag + + +WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name=u'content_type_gating') + +CONTENT_TYPE_GATING_FLAG = WaffleFlag( + waffle_namespace=WAFFLE_FLAG_NAMESPACE, + flag_name=u'debug', + flag_undefined_default=False +) 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 3cf291800726c05bb424cab1f3abea90803e3657..cfbcac43f312244d669a41f4048087d62195b987 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -23,6 +23,7 @@ from lms.djangoapps.course_goals.api import add_course_goal, remove_course_goal from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag +from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG from openedx.features.course_experience import ( SHOW_REVIEWS_TOOL_FLAG, SHOW_UPGRADE_MSG_ON_COURSE_HOME, @@ -167,6 +168,7 @@ class TestCourseHomePage(CourseHomePageTestCase): response = self.client.get(url) self.assertContains(response, TEST_COURSE_UPDATES_TOOL, status_code=200) + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) def test_queries(self): """ Verify that the view's query count doesn't regress. @@ -319,6 +321,7 @@ class TestCourseHomePageAccess(CourseHomePageTestCase): ) self.assertRedirects(response, expected_url) + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) @mock.patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False}) def test_expired_course(self): """ @@ -337,7 +340,7 @@ class TestCourseHomePageAccess(CourseHomePageTestCase): expiration_date = strftime_localized(course.start + timedelta(weeks=8), 'SHORT_DATE') expected_params = QueryDict(mutable=True) course_name = CourseOverview.get_from_id(course.id).display_name_with_default - expected_params['expired_message'] = 'Access to {run} expired on {expiration_date}'.format( + expected_params['access_response_error'] = 'Access to {run} expired on {expiration_date}'.format( run=course_name, expiration_date=expiration_date ) 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 67c79757e4f69c989c5a8664c0bb0356b3169842..fe06df99cc3ffd324dc5cf8bfdc9bb8b9c05a791 100644 --- a/openedx/features/course_experience/tests/views/test_course_updates.py +++ b/openedx/features/course_experience/tests/views/test_course_updates.py @@ -3,7 +3,8 @@ Tests for the course updates page. """ from courseware.courses import get_course_info_usage_key from django.urls import reverse -from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES +from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag +from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG from openedx.features.course_experience.views.course_updates import STATUS_VISIBLE from student.models import CourseEnrollment from student.tests.factories import UserFactory @@ -117,6 +118,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase): self.assertContains(response, 'First Message') self.assertContains(response, 'Second Message') + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) def test_queries(self): create_course_update(self.course, self.user, 'First Message')