diff --git a/cms/envs/common.py b/cms/envs/common.py index d6b9f33d8e..980e7adb93 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1161,6 +1161,8 @@ INSTALLED_APPS = [ # API Documentation 'rest_framework_swagger', + + 'openedx.features.course_duration_limits', ] diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py index 4766cca92d..b88613a187 100644 --- a/common/djangoapps/student/tests/test_views.py +++ b/common/djangoapps/student/tests/test_views.py @@ -27,7 +27,11 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context from pyquery import PyQuery as pq +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 @@ -306,7 +310,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, 'type': 'verified' } response = self.client.get(self.path) - self.assertIn('class="enter-course hidden"', response.content) + self.assertIn('class="course-target-link enter-course hidden"', response.content) self.assertIn('You must select a session to access the course.', response.content) self.assertIn('
', response.content) self.assertIn('Related Programs:', response.content) @@ -580,7 +584,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, def _get_html_for_view_course_button(course_key_string, course_run_string): return ''' View Course @@ -593,7 +597,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, def _get_html_for_resume_course_button(course_key_string, resume_block_key_string, course_run_string): return ''' Resume Course @@ -718,6 +722,38 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, dashboard_html ) + @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. + Links will be removed from the course title, course image and button (View Course/Resume Course). + The course card should have an access expired message. + """ + self.override_waffle_switch(True) + + course = CourseFactory.create(start=self.THREE_YEARS_AGO) + enrollment = CourseEnrollmentFactory.create( + user=self.user, + course_id=course.id + ) + schedule = ScheduleFactory(start=self.THREE_YEARS_AGO, enrollment=enrollment) + + response = self.client.get(reverse('dashboard')) + dashboard_html = self._remove_whitespace_from_html_string(response.content) + access_expired_substring = 'Accessexpired' + course_link_class = 'course-target-link' + + self.assertNotIn( + course_link_class, + dashboard_html + ) + + self.assertIn( + access_expired_substring, + dashboard_html + ) + def test_dashboard_with_resume_buttons_and_view_buttons(self): ''' The Test creates a four-course-card dashboard. The user completes course diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index e4a0028693..1884245080 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -767,6 +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 '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 672c9bc38c..bc2383a30c 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. """ @@ -235,18 +237,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): # # of sql queries to default, # # of mongo queries, # ) - ('no_overrides', 1, True, False): (18, 1), - ('no_overrides', 2, True, False): (18, 1), - ('no_overrides', 3, True, False): (18, 1), - ('ccx', 1, True, False): (18, 1), - ('ccx', 2, True, False): (18, 1), - ('ccx', 3, True, False): (18, 1), - ('no_overrides', 1, False, False): (18, 1), - ('no_overrides', 2, False, False): (18, 1), - ('no_overrides', 3, False, False): (18, 1), - ('ccx', 1, False, False): (18, 1), - ('ccx', 2, False, False): (18, 1), - ('ccx', 3, False, False): (18, 1), + ('no_overrides', 1, True, False): (20, 1), + ('no_overrides', 2, True, False): (20, 1), + ('no_overrides', 3, True, False): (20, 1), + ('ccx', 1, True, False): (20, 1), + ('ccx', 2, True, False): (20, 1), + ('ccx', 3, True, False): (20, 1), + ('no_overrides', 1, False, False): (20, 1), + ('no_overrides', 2, False, False): (20, 1), + ('no_overrides', 3, False, False): (20, 1), + ('ccx', 1, False, False): (20, 1), + ('ccx', 2, False, False): (20, 1), + ('ccx', 3, False, False): (20, 1), } @@ -258,19 +260,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): __test__ = True TEST_DATA = { - ('no_overrides', 1, True, False): (18, 3), - ('no_overrides', 2, True, False): (18, 3), - ('no_overrides', 3, True, False): (18, 3), - ('ccx', 1, True, False): (18, 3), - ('ccx', 2, True, False): (18, 3), - ('ccx', 3, True, False): (18, 3), - ('ccx', 1, True, True): (19, 3), - ('ccx', 2, True, True): (19, 3), - ('ccx', 3, True, True): (19, 3), - ('no_overrides', 1, False, False): (18, 3), - ('no_overrides', 2, False, False): (18, 3), - ('no_overrides', 3, False, False): (18, 3), - ('ccx', 1, False, False): (18, 3), - ('ccx', 2, False, False): (18, 3), - ('ccx', 3, False, False): (18, 3), + ('no_overrides', 1, True, False): (20, 3), + ('no_overrides', 2, True, False): (20, 3), + ('no_overrides', 3, True, False): (20, 3), + ('ccx', 1, True, False): (20, 3), + ('ccx', 2, True, False): (20, 3), + ('ccx', 3, True, False): (20, 3), + ('ccx', 1, True, True): (21, 3), + ('ccx', 2, True, True): (21, 3), + ('ccx', 3, True, True): (21, 3), + ('no_overrides', 1, False, False): (20, 3), + ('no_overrides', 2, False, False): (20, 3), + ('no_overrides', 3, False, False): (20, 3), + ('ccx', 1, False, False): (20, 3), + ('ccx', 2, False, False): (20, 3), + ('ccx', 3, False, False): (20, 3), } diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 7e40b4f0c8..3d2acc1402 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -40,6 +40,8 @@ from lms.djangoapps.ccx.models import CustomCourseForEdX 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 ( @@ -318,16 +320,53 @@ def _has_access_course(user, action, courselike): NOTE: this is not checking whether user is actually enrolled in the course. """ - response = ( - _visible_to_nonstaff_users(courselike) and - check_course_open_for_learner(user, courselike) and - _can_view_courseware_with_prerequisites(user, courselike) - ) + # N.B. I'd love a better way to handle this pattern, without breaking the + # shortcircuiting logic. Maybe AccessResponse needs to grow a + # fluent interface? + # + # return ( + # _visible_to_nonstaff_users(courselike).and( + # check_course_open_for_learner, user, courselike + # ).and( + # _can_view_courseware_with_prerequisites, user, courselike + # ) + # ).or( + # _has_staff_access_to_descriptor, user, courselike, courselike.id + # ) + visible_to_nonstaff = _visible_to_nonstaff_users(courselike) + if not visible_to_nonstaff: + staff_access = _has_staff_access_to_descriptor(user, courselike, courselike.id) + if staff_access: + return staff_access + else: + return visible_to_nonstaff - return ( - ACCESS_GRANTED if (response or _has_staff_access_to_descriptor(user, courselike, courselike.id)) - else response - ) + open_for_learner = check_course_open_for_learner(user, courselike) + if not open_for_learner: + staff_access = _has_staff_access_to_descriptor(user, courselike, courselike.id) + if staff_access: + return staff_access + else: + return open_for_learner + + view_with_prereqs = _can_view_courseware_with_prerequisites(user, courselike) + if not view_with_prereqs: + staff_access = _has_staff_access_to_descriptor(user, courselike, courselike.id) + if staff_access: + return staff_access + else: + return view_with_prereqs + + 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 def can_enroll(): """ diff --git a/lms/djangoapps/courseware/access_response.py b/lms/djangoapps/courseware/access_response.py index 4f254887e8..3b1502aab3 100644 --- a/lms/djangoapps/courseware/access_response.py +++ b/lms/djangoapps/courseware/access_response.py @@ -9,7 +9,8 @@ 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, user_fragment=None): + def __init__(self, has_access, error_code=None, developer_message=None, user_message=None, + additional_context_user_message=None, user_fragment=None): """ Creates an AccessResponse object. @@ -21,13 +22,16 @@ class AccessResponse(object): to show the developer user_message (String): optional - default is None. Message to show the user + 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. + An html fragment to display to the user if their access is denied """ self.has_access = has_access self.error_code = error_code self.developer_message = developer_message self.user_message = user_message + self.additional_context_user_message = additional_context_user_message self.user_fragment = user_fragment if has_access: assert error_code is None @@ -58,15 +62,17 @@ class AccessResponse(object): "error_code": self.error_code, "developer_message": self.developer_message, "user_message": self.user_message, + "additional_context_user_message": self.additional_context_user_message, "user_fragment": self.user_fragment, } def __repr__(self): - return "AccessResponse({!r}, {!r}, {!r}, {!r}, {!r})".format( + return "AccessResponse({!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( self.has_access, self.error_code, self.developer_message, self.user_message, + self.additional_context_user_message, self.user_fragment, ) @@ -79,6 +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.additional_context_user_message == other.additional_context_user_message and self.user_fragment == other.user_fragment ) @@ -89,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, user_fragment=None): + def __init__(self, error_code, developer_message, user_message, + additional_context_user_message=None, user_fragment=None): """ Creates an AccessError object. @@ -100,10 +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 + 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, user_fragment) + super(AccessError, self).__init__(False, error_code, developer_message, user_message, + additional_context_user_message, user_fragment) class StartDateError(AccessError): diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index b18820d14d..4418961a16 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -8,6 +8,7 @@ from datetime import datetime import branding import pytz +from openedx.features.course_duration_limits.access import AuditExpiredError from courseware.access import has_access from courseware.access_response import StartDateError, MilestoneAccessError from courseware.date_summary import ( @@ -143,6 +144,15 @@ def check_course_access(course, user, action, check_if_enrolled=False, check_sur params=params.urlencode() ), access_response) + # Redirect if AuditExpiredError + if isinstance(access_response, AuditExpiredError): + params = QueryDict(mutable=True) + params['access_response_error'] = access_response.additional_context_user_message + raise CourseAccessRedirect('{dashboard_url}?{params}'.format( + dashboard_url=reverse('dashboard'), + params=params.urlencode() + ), access_response) + # Redirect if the user must answer a survey before entering the course. if isinstance(access_response, MilestoneAccessError): raise CourseAccessRedirect('{dashboard_url}'.format( diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py index 260101cb74..f5662c2e6a 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) @@ -836,15 +838,15 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase): user = getattr(self, user_attr_name) user = User.objects.get(id=user.id) - if (user_attr_name == 'user_staff' and - action == 'see_exists' and - course_attr_name in - ['course_default', 'course_not_started']): + if user_attr_name == 'user_staff' and action == 'see_exists': # checks staff role num_queries = 1 - elif user_attr_name == 'user_normal' and action == 'see_exists' and course_attr_name != 'course_started': - # checks staff role and enrollment data - num_queries = 2 + elif user_attr_name == 'user_normal' and action == 'see_exists': + if course_attr_name == 'course_started': + num_queries = 1 + else: + # checks staff role and enrollment data + num_queries = 2 else: num_queries = 0 diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py index 1d15f3b60f..68eecc91c6 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, 28, 3) + 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, 28, 3) + 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 be6d9d92ff..0ed6538ba0 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,9 +205,10 @@ class IndexQueryTestCase(ModuleStoreTestCase): CREATE_USER = False NUM_PROBLEMS = 20 + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) @ddt.data( - (ModuleStoreEnum.Type.mongo, 10, 147), - (ModuleStoreEnum.Type.split, 4, 147), + (ModuleStoreEnum.Type.mongo, 10, 157), + (ModuleStoreEnum.Type.split, 4, 153), ) @ddt.unpack def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): @@ -1429,9 +1431,10 @@ 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, 38), - (False, 37) + (True, 40), + (False, 39) ) @ddt.unpack def test_progress_queries_paced_courses(self, self_paced, query_count): @@ -1440,10 +1443,11 @@ 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, 45, 28), - (True, 37, 24) + (False, 47, 30), + (True, 39, 26) ) @ddt.unpack def test_progress_queries(self, enable_waffle, initial, subsequent): diff --git a/lms/djangoapps/discussion/tests/test_views.py b/lms/djangoapps/discussion/tests/test_views.py index 375fb79800..672889653f 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, @@ -430,18 +432,18 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase): # course is outside the context manager that is verifying the number of queries, # and with split mongo, that method ends up querying disabled_xblocks (which is then # cached and hence not queried as part of call_single_thread). - (ModuleStoreEnum.Type.mongo, False, 1, 5, 2, 17, 5), - (ModuleStoreEnum.Type.mongo, False, 50, 5, 2, 17, 5), + (ModuleStoreEnum.Type.mongo, False, 1, 5, 2, 19, 7), + (ModuleStoreEnum.Type.mongo, False, 50, 5, 2, 19, 7), # split mongo: 3 queries, regardless of thread response size. - (ModuleStoreEnum.Type.split, False, 1, 3, 3, 17, 5), - (ModuleStoreEnum.Type.split, False, 50, 3, 3, 17, 5), + (ModuleStoreEnum.Type.split, False, 1, 3, 3, 19, 7), + (ModuleStoreEnum.Type.split, False, 50, 3, 3, 19, 7), # Enabling Enterprise integration should have no effect on the number of mongo queries made. - (ModuleStoreEnum.Type.mongo, True, 1, 5, 2, 17, 5), - (ModuleStoreEnum.Type.mongo, True, 50, 5, 2, 17, 5), + (ModuleStoreEnum.Type.mongo, True, 1, 5, 2, 19, 7), + (ModuleStoreEnum.Type.mongo, True, 50, 5, 2, 19, 7), # split mongo: 3 queries, regardless of thread response size. - (ModuleStoreEnum.Type.split, True, 1, 3, 3, 17, 5), - (ModuleStoreEnum.Type.split, True, 50, 3, 3, 17, 5), + (ModuleStoreEnum.Type.split, True, 1, 3, 3, 19, 7), + (ModuleStoreEnum.Type.split, True, 50, 3, 3, 19, 7), ) @ddt.unpack def test_number_of_mongo_queries( diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py index 07d60509ee..7e2368a2c5 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,18 +403,20 @@ class ViewsQueryCountTestCase( func(self, *args, **kwargs) return inner + @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True) @ddt.data( - (ModuleStoreEnum.Type.mongo, 3, 4, 35), - (ModuleStoreEnum.Type.split, 3, 13, 35), + (ModuleStoreEnum.Type.mongo, 3, 4, 37), + (ModuleStoreEnum.Type.split, 3, 13, 37), ) @ddt.unpack @count_queries 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, 31), - (ModuleStoreEnum.Type.split, 3, 10, 31), + (ModuleStoreEnum.Type.mongo, 3, 3, 33), + (ModuleStoreEnum.Type.split, 3, 10, 33), ) @ddt.unpack @count_queries diff --git a/lms/envs/common.py b/lms/envs/common.py index 7fda273083..6cd6d0ae54 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2286,6 +2286,7 @@ INSTALLED_APPS = [ 'openedx.features.learner_profile', 'openedx.features.learner_analytics', 'openedx.features.portfolio_project', + 'openedx.features.course_duration_limits', 'experiments', diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index 1b16d0ba33..def15e99e4 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -33,6 +33,8 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_ if cert_name_long == "": cert_name_long = settings.CERT_NAME_LONG billing_email = settings.PAYMENT_SUPPORT_EMAIL + + is_course_expired = hasattr(show_courseware_link, 'error_code') and show_courseware_link.error_code == 'audit_expired' %> <%namespace name='static' file='../static_content.html'/> @@ -65,8 +67,8 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_

${_('Course details')}