diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 5f56f0da17..3fe16f99bd 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -27,7 +27,6 @@ from xblock.fields import Boolean, Integer, List, Scope, String from edx_toggles.toggles import LegacyWaffleFlag from edx_toggles.toggles import WaffleFlag from lms.djangoapps.courseware.toggles import COURSEWARE_PROCTORING_IMPROVEMENTS -from openedx.core.lib.graph_traversals import get_children, leaf_filter, traverse_pre_order from .exceptions import NotFoundError from .fields import Date @@ -283,37 +282,6 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): datetime.now(UTC) < date ) - def _get_user(self): - """ - Return the current runtime Django user. - """ - return get_user_model().objects.get(id=self.runtime.user_id) - - def _check_children_for_content_type_gating_paywall(self, item): - """ - If: - This xblock contains problems which this user cannot load due to content type gating - Then: - Return the first content type gating paywall (Fragment) - Else: - Return None - """ - try: - user = self._get_user() - course_id = self.runtime.course_id - content_type_gating_service = self.runtime.service(self, 'content_type_gating') - if not (content_type_gating_service and - content_type_gating_service.enabled_for_enrollment(user=user, course_key=course_id)): - return None - - for block in traverse_pre_order(item, get_children, leaf_filter): - gate_fragment = content_type_gating_service.content_type_gate_for_block(user, block, course_id) - if gate_fragment is not None: - return gate_fragment.content - except get_user_model().DoesNotExist: - pass - return None - def gate_entire_sequence_if_it_is_a_timed_exam_and_contains_content_type_gated_problems(self): """ Problem: @@ -344,7 +312,11 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): Note that this will break compatability with using sequences outside of edx-platform but we are ok with this for now """ - self.gated_sequence_paywall = self._check_children_for_content_type_gating_paywall(self) + content_type_gating_service = self.runtime.service(self, 'content_type_gating') + if content_type_gating_service: + self.gated_sequence_paywall = content_type_gating_service.check_children_for_content_type_gating_paywall( + self, self.course_id + ) def student_view(self, context): _ = self.runtime.service(self, "i18n").ugettext @@ -669,7 +641,12 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): else: content = '' - contains_content_type_gated_content = self._check_children_for_content_type_gating_paywall(item) is not None + content_type_gating_service = self.runtime.service(self, 'content_type_gating') + contains_content_type_gated_content = False + if content_type_gating_service: + contains_content_type_gated_content = content_type_gating_service.check_children_for_content_type_gating_paywall( # pylint:disable=line-too-long + item, self.course_id + ) is not None iteminfo = { 'content': content, 'page_title': getattr(item, 'tooltip_title', ''), diff --git a/common/lib/xmodule/xmodule/tests/test_sequence.py b/common/lib/xmodule/xmodule/tests/test_sequence.py index 00bfbd3d4e..e8c93923e9 100644 --- a/common/lib/xmodule/xmodule/tests/test_sequence.py +++ b/common/lib/xmodule/xmodule/tests/test_sequence.py @@ -144,13 +144,12 @@ class SequenceBlockTestCase(XModuleXmlImportTest): seq_module = SequenceModule(runtime=Mock(position=2), descriptor=Mock(), scope_ids=Mock()) self.assertEqual(seq_module.position, 2) # matches position set in the runtime - @patch('xmodule.seq_module.SequenceModule._get_user', return_value=UserFactory.build()) @ddt.unpack @ddt.data( {'view': STUDENT_VIEW}, {'view': PUBLIC_VIEW}, ) - def test_render_student_view(self, mocked_user, view): # pylint: disable=unused-argument + def test_render_student_view(self, view): html = self._get_rendered_view( self.sequence_3_1, extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'), @@ -164,9 +163,8 @@ class SequenceBlockTestCase(XModuleXmlImportTest): self.assertNotIn("fa fa-check-circle check-circle is-hidden", html) # pylint: disable=line-too-long - @patch('xmodule.seq_module.SequenceModule._get_user', return_value=UserFactory.build()) @patch('xmodule.seq_module.SequenceModule.gate_entire_sequence_if_it_is_a_timed_exam_and_contains_content_type_gated_problems') - def test_timed_exam_gating_waffle_flag(self, mocked_function, mocked_user): # pylint: disable=unused-argument + def test_timed_exam_gating_waffle_flag(self, mocked_function): # pylint: disable=unused-argument """ Verify the code inside the waffle flag is not executed with the flag off Verify the code inside the waffle flag is executed with the flag on @@ -190,11 +188,9 @@ class SequenceBlockTestCase(XModuleXmlImportTest): mocked_function.assert_called_once() @override_waffle_flag(TIMED_EXAM_GATING_WAFFLE_FLAG, active=True) - @patch('xmodule.seq_module.SequenceModule._get_user', return_value=UserFactory.build()) - def test_that_timed_sequence_gating_respects_access_configurations(self, mocked_user): # pylint: disable=unused-argument + def test_that_timed_sequence_gating_respects_access_configurations(self): """ Verify that if a time limited sequence contains content type gated problems, we gate the sequence - Verify that if a time limited sequence does not contain content type gated problems, we do not gate the sequence """ # the one problem in this sequence needs to have graded set to true in order to test content type gating self.sequence_5_1.get_children()[0].get_children()[0].graded = True @@ -202,8 +198,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest): # When a time limited sequence contains content type gated problems, the sequence itself is gated self.sequence_5_1.runtime._services['content_type_gating'] = Mock(return_value=Mock( # pylint: disable=protected-access - enabled_for_enrollment=Mock(return_value=True), - content_type_gate_for_block=Mock(return_value=gated_fragment) + check_children_for_content_type_gating_paywall=Mock(return_value=gated_fragment.content), )) view = self._get_rendered_view( self.sequence_5_1, @@ -216,62 +211,27 @@ class SequenceBlockTestCase(XModuleXmlImportTest): self.assertIn('NextSequential', view) self.assertIn('PrevSequential', view) - # When enabled_for_enrollment is false, the sequence itself is not gated - self.sequence_5_1.runtime._services['content_type_gating'] = Mock(return_value=Mock( # pylint: disable=protected-access - enabled_for_enrollment=Mock(return_value=False), - content_type_gate_for_block=Mock(return_value=gated_fragment) - )) - view = self._get_rendered_view( - self.sequence_5_1, - extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'), - view=STUDENT_VIEW - ) - self.assertNotIn('i_am_gated', view) - # check a few elements to ensure the correct page was loaded - self.assertIn("seq_module.html", view) - self.assertIn('NextSequential', view) - self.assertIn('PrevSequential', view) - - # When content_type_gate_for_block returns None, the sequence itself is not gated - self.sequence_5_1.runtime._services['content_type_gating'] = Mock(return_value=Mock( # pylint: disable=protected-access - enabled_for_enrollment=Mock(return_value=True), - content_type_gate_for_block=Mock(return_value=None) - )) - view = self._get_rendered_view( - self.sequence_5_1, - extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'), - view=STUDENT_VIEW - ) - self.assertNotIn('i_am_gated', view) - # check a few elements to ensure the correct page was loaded - self.assertIn("seq_module.html", view) - self.assertIn('NextSequential', view) - self.assertIn('PrevSequential', view) - - @patch('xmodule.seq_module.SequenceModule._get_user', return_value=UserFactory.build()) @ddt.unpack @ddt.data( {'view': STUDENT_VIEW}, {'view': PUBLIC_VIEW}, ) - def test_student_view_first_child(self, mocked_user, view): # pylint: disable=unused-argument + def test_student_view_first_child(self, view): html = self._get_rendered_view( self.sequence_3_1, requested_child='first', view=view ) self._assert_view_at_position(html, expected_position=1) - @patch('xmodule.seq_module.SequenceModule._get_user', return_value=UserFactory.build()) @ddt.unpack @ddt.data( {'view': STUDENT_VIEW}, {'view': PUBLIC_VIEW}, ) - def test_student_view_last_child(self, mocked_user, view): # pylint: disable=unused-argument + def test_student_view_last_child(self, view): html = self._get_rendered_view(self.sequence_3_1, requested_child='last', view=view) self._assert_view_at_position(html, expected_position=3) - @patch('xmodule.seq_module.SequenceModule._get_user', return_value=UserFactory.build()) - def test_tooltip(self, mocked_user): # pylint: disable=unused-argument + def test_tooltip(self): html = self._get_rendered_view(self.sequence_3_1, requested_child=None) for child in self.sequence_3_1.children: self.assertIn("'page_title': '{}'".format(child.block_id), html) @@ -445,8 +405,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest): ) self.assertIs(completion_return, None) - @patch('xmodule.seq_module.SequenceModule._get_user', return_value=UserFactory.build()) - def test_handle_ajax_metadata(self, mocked_user): # pylint: disable=unused-argument + def test_handle_ajax_metadata(self): """ Test that the sequence metadata is returned from the metadata ajax handler. @@ -459,11 +418,10 @@ class SequenceBlockTestCase(XModuleXmlImportTest): self.assertEqual(metadata['tag'], 'sequential') self.assertEqual(metadata['display_name'], self.sequence_3_1.display_name_with_default) - @patch('xmodule.seq_module.SequenceModule._get_user', return_value=UserFactory.build()) @override_settings(FIELD_OVERRIDE_PROVIDERS=( 'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride', )) - def test_handle_ajax_metadata_content_type_gated_content(self, mocked_user): # pylint: disable=unused-argument + def test_handle_ajax_metadata_content_type_gated_content(self): """ The contains_content_type_gated_content field should reflect whether the given item contains content type gated content diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 87ae45ae01..1ec90fbce2 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -269,8 +269,8 @@ class IndexQueryTestCase(ModuleStoreTestCase): NUM_PROBLEMS = 20 @ddt.data( - (ModuleStoreEnum.Type.mongo, 10, 174), - (ModuleStoreEnum.Type.split, 4, 170), + (ModuleStoreEnum.Type.mongo, 10, 172), + (ModuleStoreEnum.Type.split, 4, 168), ) @ddt.unpack def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py index 98dd432b31..51dc84bd1a 100644 --- a/openedx/core/djangoapps/courseware_api/views.py +++ b/openedx/core/djangoapps/courseware_api/views.py @@ -479,6 +479,13 @@ class SequenceMetadata(DeveloperErrorViewMixin, APIView): except InvalidKeyError: raise NotFound("Invalid usage key: '{}'.".format(usage_key_string)) + _, request.user = setup_masquerade( + request, + usage_key.course_key, + staff_access=has_access(request.user, 'staff', usage_key.course_key), + reset_masquerade_data=True, + ) + sequence, _ = get_module_by_usage_id( self.request, str(usage_key.course_key), diff --git a/openedx/features/content_type_gating/services.py b/openedx/features/content_type_gating/services.py index b1c8f6622b..f7b2381867 100644 --- a/openedx/features/content_type_gating/services.py +++ b/openedx/features/content_type_gating/services.py @@ -1,8 +1,13 @@ """ Content Type Gating service. """ +import crum from lms.djangoapps.courseware.access import has_access +from lms.djangoapps.courseware.masquerade import ( + is_masquerading_as_limited_access, is_masquerading_as_audit_enrollment, +) +from openedx.core.lib.graph_traversals import get_children, leaf_filter, traverse_pre_order from openedx.features.content_type_gating.models import ContentTypeGatingConfig @@ -12,13 +17,23 @@ class ContentTypeGatingService(object): and field overrides to gate course content. This service was created as a helper class for handling timed exams that contain content type gated problems. """ - def enabled_for_enrollment(self, **kwargs): + def _enabled_for_enrollment(self, **kwargs): """ Returns whether content type gating is enabled for a given user/course pair """ return ContentTypeGatingConfig.enabled_for_enrollment(**kwargs) - def content_type_gate_for_block(self, user, block, course_id): + def _is_masquerading_as_audit_or_limited_access(self, user, course_id): + return (is_masquerading_as_limited_access(user, course_id) or + is_masquerading_as_audit_enrollment(user, course_id)) + + def _get_user(self): + """ + Return the current request user. + """ + return crum.get_current_user() + + def _content_type_gate_for_block(self, user, block, course_id): """ Returns a Fragment of the content type gate (if any) that would appear for a given block """ @@ -29,4 +44,33 @@ class ContentTypeGatingService(object): access = has_access(user, 'load', block, course_id) if (not access and access.error_code == 'incorrect_user_group'): return access.user_fragment + + return None + + def check_children_for_content_type_gating_paywall(self, item, course_id): + """ + Arguments: + item (xblock such as a sequence or vertical block) + course_id (CourseLocator) + + If: + This xblock contains problems which this user cannot load due to content type gating + Then: + Return the first content type gating paywall (Fragment) + Else: + Return None + """ + user = self._get_user() + if not user: return None + + if not self._enabled_for_enrollment(user=user, course_key=course_id): + return None + + # Check children for content type gated content + for block in traverse_pre_order(item, get_children, leaf_filter): + gate_fragment = self._content_type_gate_for_block(user, block, course_id) + if gate_fragment is not None: + return gate_fragment.content + + return None diff --git a/openedx/features/content_type_gating/tests/test_access.py b/openedx/features/content_type_gating/tests/test_access.py index faa0baae42..4134518e45 100644 --- a/openedx/features/content_type_gating/tests/test_access.py +++ b/openedx/features/content_type_gating/tests/test_access.py @@ -1156,7 +1156,7 @@ class TestContentTypeGatingService(ModuleStoreTestCase): # The method returns a content type gate for blocks that should be gated self.assertIn( 'content-paywall', - ContentTypeGatingService().content_type_gate_for_block( + ContentTypeGatingService()._content_type_gate_for_block( self.user, blocks_dict['graded_1'], course['course'].id ).content ) @@ -1164,7 +1164,47 @@ class TestContentTypeGatingService(ModuleStoreTestCase): # The method returns None for blocks that should not be gated self.assertEquals( None, - ContentTypeGatingService().content_type_gate_for_block( + ContentTypeGatingService()._content_type_gate_for_block( self.user, blocks_dict['not_graded_1'], course['course'].id ) ) + + @patch.object(ContentTypeGatingService, '_get_user', return_value=UserFactory.build()) + def test_check_children_for_content_type_gating_paywall(self, mocked_user): # pylint: disable=unused-argument + ''' Verify that the method returns a content type gate when appropriate ''' + course = self._create_course() + blocks_dict = course['blocks'] + CourseEnrollmentFactory.create( + user=self.user, + course_id=course['course'].id, + mode='audit' + ) + blocks_dict['not_graded_1'] = ItemFactory.create( + parent=blocks_dict['vertical'], + category='problem', + graded=False, + metadata=METADATA, + ) + + # The method returns a content type gate for blocks that should be gated + self.assertEquals( + None, + ContentTypeGatingService().check_children_for_content_type_gating_paywall( + blocks_dict['vertical'], course['course'].id + ) + ) + + blocks_dict['graded_1'] = ItemFactory.create( + parent=blocks_dict['vertical'], + category='problem', + graded=True, + metadata=METADATA, + ) + + # The method returns None for blocks that should not be gated + self.assertIn( + 'content-paywall', + ContentTypeGatingService().check_children_for_content_type_gating_paywall( + blocks_dict['vertical'], course['course'].id + ) + )