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:
@@ -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', ''),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user