Return content type gate for staff users when masquerading as the Learner in Audit or Learner in Limited Access Roles

This is necessary to display the content type gate in the UI
AA-613
This commit is contained in:
Matthew Piatetsky
2021-02-01 22:15:42 -05:00
parent 71b15df66c
commit ae7d0a1ed8
6 changed files with 117 additions and 91 deletions

View File

@@ -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', ''),

View File

@@ -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

View File

@@ -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):

View File

@@ -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),

View File

@@ -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

View File

@@ -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
)
)