REVE-18: Test Content Type Gating For All Modes
This commit is contained in:
committed by
Bessie Steinberg
parent
4d05cf2872
commit
adffc11856
@@ -25,7 +25,11 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
))
|
||||
class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
|
||||
PROBLEM_TYPES = ['problem', 'openassessment', 'drag-and-drop-v2', 'done', 'edx_sga', ]
|
||||
PROBLEM_TYPES = ['problem', 'openassessment', 'drag-and-drop-v2', 'done', 'edx_sga']
|
||||
# 'html' is a component that just displays html, in these tests it is used to test that users who do not have access
|
||||
# to graded problems still have access to non-problems
|
||||
COMPONENT_TYPES = PROBLEM_TYPES + ['html']
|
||||
MODE_TYPES = ['credit', 'honor', 'audit', 'verified', 'professional', 'no-id-professional']
|
||||
|
||||
GRADED_SCORE_WEIGHT_TEST_CASES = [
|
||||
# graded, has_score, weight, is_gated
|
||||
@@ -40,93 +44,182 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
super(TestProblemTypeAccess, self).setUpClass()
|
||||
self.factory = RequestFactory()
|
||||
self.course = CourseFactory.create(run='testcourse1', display_name='Test Course Title')
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug='audit')
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug='verified')
|
||||
with self.store.bulk_operations(self.course.id):
|
||||
self.chapter = ItemFactory.create(
|
||||
parent=self.course,
|
||||
display_name='Overview'
|
||||
)
|
||||
self.welcome = ItemFactory.create(
|
||||
parent=self.chapter,
|
||||
display_name='Welcome'
|
||||
)
|
||||
ItemFactory.create(
|
||||
parent=self.course,
|
||||
category='chapter',
|
||||
display_name='Week 1'
|
||||
)
|
||||
self.chapter_subsection = ItemFactory.create(
|
||||
parent=self.chapter,
|
||||
category='sequential',
|
||||
display_name='Lesson 1'
|
||||
)
|
||||
self.vertical = ItemFactory.create(
|
||||
parent=self.chapter_subsection,
|
||||
category='vertical',
|
||||
display_name='Lesson 1 Vertical - Unit 1'
|
||||
)
|
||||
self.lti_block = ItemFactory.create(
|
||||
parent=self.vertical,
|
||||
category='lti_consumer',
|
||||
display_name='lti_consumer',
|
||||
has_score=True,
|
||||
graded=True,
|
||||
)
|
||||
self.lti_block_not_scored = ItemFactory.create(
|
||||
parent=self.vertical,
|
||||
category='lti_consumer',
|
||||
display_name='lti_consumer_2',
|
||||
has_score=False,
|
||||
)
|
||||
self.problem_dict = {}
|
||||
for prob_type in self.PROBLEM_TYPES:
|
||||
block = ItemFactory.create(
|
||||
parent=self.vertical,
|
||||
category=prob_type,
|
||||
display_name=prob_type,
|
||||
graded=True,
|
||||
)
|
||||
self.problem_dict[prob_type] = block
|
||||
def setUpClass(cls):
|
||||
super(TestProblemTypeAccess, cls).setUpClass()
|
||||
cls.factory = RequestFactory()
|
||||
|
||||
'''
|
||||
Create components with the cartesian product of possible values of
|
||||
graded/has_score/weight for the test_graded_score_weight_values test.
|
||||
'''
|
||||
self.graded_score_weight_blocks = {}
|
||||
for graded, has_score, weight, gated in self.GRADED_SCORE_WEIGHT_TEST_CASES:
|
||||
case_name = ' Graded: ' + str(graded) + ' Has Score: ' + str(has_score) + ' Weight: ' + str(weight)
|
||||
block = ItemFactory.create(
|
||||
parent=self.vertical,
|
||||
# has_score is determined by XBlock type. It is not a value set on an instance of an XBlock.
|
||||
# Therefore, we create a problem component when has_score is True
|
||||
# and an html component when has_score is False.
|
||||
category='problem' if has_score else 'html',
|
||||
display_name=case_name,
|
||||
graded=graded,
|
||||
weight=weight,
|
||||
)
|
||||
self.graded_score_weight_blocks[(graded, has_score, weight)] = block
|
||||
cls.courses = {}
|
||||
|
||||
# default course is used for most tests, it includes an audit and verified track and all the problem types
|
||||
# defined in 'PROBLEM_TYPES' and 'GRADED_SCORE_WEIGHT_TEST_CASES'
|
||||
cls.courses['default'] = cls._create_course(
|
||||
run='testcourse1',
|
||||
display_name='Test Course Title',
|
||||
modes=['audit', 'verified'],
|
||||
component_types=cls.COMPONENT_TYPES
|
||||
)
|
||||
# because default course is used for most tests self.course and self.problem_dict are set for ease of reference
|
||||
cls.course = cls.courses['default']['course']
|
||||
cls.blocks_dict = cls.courses['default']['blocks']
|
||||
|
||||
# Create components with the cartesian product of possible values of
|
||||
# graded/has_score/weight for the test_graded_score_weight_values test.
|
||||
cls.graded_score_weight_blocks = {}
|
||||
for graded, has_score, weight, gated in cls.GRADED_SCORE_WEIGHT_TEST_CASES:
|
||||
case_name = ' Graded: ' + str(graded) + ' Has Score: ' + str(has_score) + ' Weight: ' + str(weight)
|
||||
block = ItemFactory.create(
|
||||
parent=cls.blocks_dict['vertical'],
|
||||
# has_score is determined by XBlock type. It is not a value set on an instance of an XBlock.
|
||||
# Therefore, we create a problem component when has_score is True
|
||||
# and an html component when has_score is False.
|
||||
category='problem' if has_score else 'html',
|
||||
display_name=case_name,
|
||||
graded=graded,
|
||||
weight=weight,
|
||||
)
|
||||
cls.graded_score_weight_blocks[(graded, has_score, weight)] = block
|
||||
|
||||
# add LTI blocks to default course
|
||||
cls.lti_block = ItemFactory.create(
|
||||
parent=cls.blocks_dict['vertical'],
|
||||
category='lti_consumer',
|
||||
display_name='lti_consumer',
|
||||
has_score=True,
|
||||
graded=True,
|
||||
)
|
||||
cls.lti_block_not_scored = ItemFactory.create(
|
||||
parent=cls.blocks_dict['vertical'],
|
||||
category='lti_consumer',
|
||||
display_name='lti_consumer_2',
|
||||
has_score=False,
|
||||
)
|
||||
|
||||
# audit_only course only has an audit track available
|
||||
cls.courses['audit_only'] = cls._create_course(
|
||||
run='audit_only_course_run_1',
|
||||
display_name='Audit Only Test Course Title',
|
||||
modes=['audit'],
|
||||
component_types=['problem', 'html']
|
||||
)
|
||||
|
||||
# all_track_types course has all track types defined in MODE_TYPES
|
||||
cls.courses['all_track_types'] = cls._create_course(
|
||||
run='all_track_types_run_1',
|
||||
display_name='All Track/Mode Types Test Course Title',
|
||||
modes=cls.MODE_TYPES,
|
||||
component_types=['problem', 'html']
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(TestProblemTypeAccess, self).setUp()
|
||||
self.audit_user = UserFactory.create()
|
||||
self.enrollment = CourseEnrollmentFactory.create(user=self.audit_user, course_id=self.course.id, mode='audit')
|
||||
|
||||
# enroll all users into the all track types course
|
||||
self.users = {}
|
||||
for mode_type in self.MODE_TYPES:
|
||||
self.users[mode_type] = UserFactory.create()
|
||||
CourseEnrollmentFactory.create(
|
||||
user=self.users[mode_type],
|
||||
course_id=self.courses['all_track_types']['course'].id,
|
||||
mode=mode_type
|
||||
)
|
||||
|
||||
# create audit_user for ease of reference
|
||||
self.audit_user = self.users['audit']
|
||||
|
||||
# enroll audit and verified users into default course
|
||||
for mode_type in ['audit', 'verified']:
|
||||
CourseEnrollmentFactory.create(
|
||||
user=self.users[mode_type],
|
||||
course_id=self.course.id,
|
||||
mode=mode_type
|
||||
)
|
||||
|
||||
# enroll audit user into the audit_only course
|
||||
CourseEnrollmentFactory.create(
|
||||
user=self.audit_user,
|
||||
course_id=self.courses['audit_only']['course'].id,
|
||||
mode='audit'
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _create_course(cls, run, display_name, modes, component_types):
|
||||
"""
|
||||
Helper method to create a course
|
||||
|
||||
Arguments:
|
||||
run (str): name of course run
|
||||
display_name (str): display name of course
|
||||
modes (list of str): list of modes/tracks this course should have
|
||||
component_types (list of str): list of problem types this course should have
|
||||
|
||||
Returns:
|
||||
(dict): {
|
||||
'course': (CourseDescriptorWithMixins): course definition
|
||||
'blocks': (dict) {
|
||||
'block_category_1': XBlock representing that block,
|
||||
'block_category_2': XBlock representing that block,
|
||||
....
|
||||
}
|
||||
|
||||
"""
|
||||
course = CourseFactory.create(run=run, display_name=display_name)
|
||||
|
||||
for mode in modes:
|
||||
CourseModeFactory.create(course_id=course.id, mode_slug=mode)
|
||||
|
||||
with cls.store.bulk_operations(course.id):
|
||||
blocks_dict = {}
|
||||
chapter = ItemFactory.create(
|
||||
parent=course,
|
||||
display_name='Overview'
|
||||
)
|
||||
blocks_dict['chapter'] = ItemFactory.create(
|
||||
parent=course,
|
||||
category='chapter',
|
||||
display_name='Week 1'
|
||||
)
|
||||
blocks_dict['sequential'] = ItemFactory.create(
|
||||
parent=chapter,
|
||||
category='sequential',
|
||||
display_name='Lesson 1'
|
||||
)
|
||||
blocks_dict['vertical'] = ItemFactory.create(
|
||||
parent=blocks_dict['sequential'],
|
||||
category='vertical',
|
||||
display_name='Lesson 1 Vertical - Unit 1'
|
||||
)
|
||||
|
||||
for problem_type in component_types:
|
||||
block = ItemFactory.create(
|
||||
parent=blocks_dict['vertical'],
|
||||
category=problem_type,
|
||||
display_name=problem_type,
|
||||
graded=True,
|
||||
)
|
||||
blocks_dict[problem_type] = block
|
||||
|
||||
return {
|
||||
'course': course,
|
||||
'blocks': blocks_dict,
|
||||
}
|
||||
|
||||
@patch("crum.get_current_request")
|
||||
def assert_block_is_gated(self, block, is_gated, mock_get_current_request):
|
||||
'''
|
||||
def _assert_block_is_gated(self, mock_get_current_request, block, is_gated, user_id, course_id):
|
||||
"""
|
||||
Asserts that a block in a specific course is gated for a specific user
|
||||
|
||||
This functions asserts whether the passed in block is gated by content type gating.
|
||||
This is determined by checking whether the has_access method called the IncorrectPartitionGroupError.
|
||||
This error gets swallowed up and is raised as a 404, which is why we are checking for a 404 being raised.
|
||||
However, the 404 could also be caused by other errors, which is why the actual assertion is checking
|
||||
whether the IncorrectPartitionGroupError was called.
|
||||
'''
|
||||
fake_request = Mock()
|
||||
|
||||
Arguments:
|
||||
block: some soft of xblock descriptor, must implement .scope_ids.usage_id
|
||||
is_gated (bool): if True, this user is expected to be gated from this block
|
||||
user_id (int): id of user, if not set will be set to self.audit_user.id
|
||||
course_id (CourseLocator): id of course, if not set will be set to self.course.id
|
||||
"""
|
||||
fake_request = self.factory.get('')
|
||||
mock_get_current_request.return_value = fake_request
|
||||
|
||||
@@ -134,13 +227,23 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
wraps=IncorrectPartitionGroupError.__init__) as mock_access_error:
|
||||
if is_gated:
|
||||
with self.assertRaises(Http404):
|
||||
block = load_single_xblock(fake_request, self.audit_user.id, unicode(self.course.id),
|
||||
unicode(block.scope_ids.usage_id), course=None)
|
||||
load_single_xblock(
|
||||
request=fake_request,
|
||||
user_id=user_id,
|
||||
course_id=unicode(course_id),
|
||||
usage_key_string=unicode(block.scope_ids.usage_id),
|
||||
course=None
|
||||
)
|
||||
# check that has_access raised the IncorrectPartitionGroupError in order to gate the block
|
||||
self.assertTrue(mock_access_error.called)
|
||||
else:
|
||||
block = load_single_xblock(fake_request, self.audit_user.id, unicode(self.course.id),
|
||||
unicode(block.scope_ids.usage_id), course=None)
|
||||
load_single_xblock(
|
||||
request=fake_request,
|
||||
user_id=user_id,
|
||||
course_id=unicode(course_id),
|
||||
usage_key_string=unicode(block.scope_ids.usage_id),
|
||||
course=None
|
||||
)
|
||||
# check that has_access did not raise the IncorrectPartitionGroupError thereby not gating the block
|
||||
self.assertFalse(mock_access_error.called)
|
||||
|
||||
@@ -150,16 +253,31 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
outside sources. This tests that audit users cannot see LTI components with graded content but can see the LTI
|
||||
components which do not have graded content.
|
||||
"""
|
||||
self.assert_block_is_gated(self.lti_block, True)
|
||||
self.assert_block_is_gated(self.lti_block_not_scored, False)
|
||||
self._assert_block_is_gated(
|
||||
block=self.lti_block,
|
||||
user_id=self.audit_user.id,
|
||||
course_id=self.course.id,
|
||||
is_gated=True
|
||||
)
|
||||
self._assert_block_is_gated(
|
||||
block=self.lti_block_not_scored,
|
||||
user_id=self.audit_user.id,
|
||||
course_id=self.course.id,
|
||||
is_gated=False
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
*PROBLEM_TYPES
|
||||
)
|
||||
def test_audit_fails_access_graded_problems(self, prob_type):
|
||||
block = self.problem_dict[prob_type]
|
||||
block = self.blocks_dict[prob_type]
|
||||
is_gated = True
|
||||
self.assert_block_is_gated(block, is_gated)
|
||||
self._assert_block_is_gated(
|
||||
block=block,
|
||||
user_id=self.audit_user.id,
|
||||
course_id=self.course.id,
|
||||
is_gated=is_gated
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
*GRADED_SCORE_WEIGHT_TEST_CASES
|
||||
@@ -168,4 +286,43 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
def test_graded_score_weight_values(self, graded, has_score, weight, is_gated):
|
||||
# Verify that graded, has_score and weight must all be true for a component to be gated
|
||||
block = self.graded_score_weight_blocks[(graded, has_score, weight)]
|
||||
self.assert_block_is_gated(block, is_gated)
|
||||
self._assert_block_is_gated(
|
||||
block=block,
|
||||
user_id=self.audit_user.id,
|
||||
course_id=self.course.id,
|
||||
is_gated=is_gated
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
('audit', 'problem', 'default', True),
|
||||
('verified', 'problem', 'default', False),
|
||||
('audit', 'html', 'default', False),
|
||||
('verified', 'html', 'default', False),
|
||||
('audit', 'problem', 'audit_only', False),
|
||||
('audit', 'html', 'audit_only', False),
|
||||
('credit', 'problem', 'all_track_types', False),
|
||||
('credit', 'html', 'all_track_types', False),
|
||||
('honor', 'problem', 'all_track_types', False),
|
||||
('honor', 'html', 'all_track_types', False),
|
||||
('audit', 'problem', 'all_track_types', True),
|
||||
('audit', 'html', 'all_track_types', False),
|
||||
('verified', 'problem', 'all_track_types', False),
|
||||
('verified', 'html', 'all_track_types', False),
|
||||
('professional', 'problem', 'all_track_types', False),
|
||||
('professional', 'html', 'all_track_types', False),
|
||||
('no-id-professional', 'problem', 'all_track_types', False),
|
||||
('no-id-professional', 'html', 'all_track_types', False),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_access_based_on_track(self, user_track, component_type, course, is_gated):
|
||||
"""
|
||||
If a user is enrolled as an audit user they should not have access to graded problems, unless there is no paid
|
||||
track option. All paid type tracks should have access to all types of content.
|
||||
All users should have access to non-problem component types, the 'html' components test that.
|
||||
"""
|
||||
self._assert_block_is_gated(
|
||||
block=self.courses[course]['blocks'][component_type],
|
||||
user_id=self.users[user_track].id,
|
||||
course_id=self.courses[course]['course'].id,
|
||||
is_gated=is_gated
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user