Merge pull request #26293 from edx/AA-613

[AA-613] Return content type gate for staff users when masquerading as the Learner in Audit or Learner in Limited Access Roles
This commit is contained in:
Matthew Piatetsky
2021-02-05 09:18:34 -05:00
committed by GitHub
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)) # lint-amnesty, pylint: disable=raise-missing-from
_, 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
)
)