Files
edx-platform/lms/djangoapps/course_blocks/transformers/tests/helpers.py
usamasadiq 3d1f3cea64 Ran pyupgrade on lms/djangoapps/course_blocks
Ran pyupgrade on lms/djangoapps/course_goals
Ran pyugprade on lms/djangoapps/course_home_api
2021-02-19 16:29:52 +05:00

366 lines
14 KiB
Python

"""
Test helpers for testing course block transformers.
"""
from unittest.mock import patch
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from lms.djangoapps.courseware.access import has_access
from openedx.core.djangoapps.content.block_structure.tests.helpers import clear_registered_transformers_cache
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from ...api import get_course_blocks
class TransformerRegistryTestMixin:
"""
Mixin that overrides the TransformerRegistry so that it returns
TRANSFORMER_CLASS_TO_TEST as a registered transformer.
"""
def setUp(self):
super().setUp()
self.patcher = patch(
'openedx.core.djangoapps.content.block_structure.transformer_registry.'
'TransformerRegistry.get_registered_transformers'
)
mock_registry = self.patcher.start()
mock_registry.return_value = {self.TRANSFORMER_CLASS_TO_TEST}
self.transformers = BlockStructureTransformers([self.TRANSFORMER_CLASS_TO_TEST()])
def tearDown(self):
self.patcher.stop()
clear_registered_transformers_cache()
class CourseStructureTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase):
"""
Helper for test cases that need to build course structures.
"""
def setUp(self):
"""
Create users.
"""
super().setUp()
# Set up users.
self.password = 'test'
self.user = UserFactory.create(password=self.password)
self.staff = UserFactory.create(password=self.password, is_staff=True)
def create_block_id(self, block_type, block_ref):
"""
Returns the block id (display name) that is used in the test
course structures for the given block type and block reference
string.
"""
return f'{block_type}_{block_ref}'
def build_xblock(self, block_hierarchy, block_map, parent):
"""
Build an XBlock, add it to block_map, and call build_xblock on
the children defined in block_dict.
Arguments:
block_hierarchy (BlockStructureDict): Definition of
hierarchy, from this block down.
block_map (dict[str: XBlock]): Mapping from '#ref' values to
their XBlocks.
parent (XBlock): Parent block for this xBlock.
"""
block_type = block_hierarchy['#type']
block_ref = block_hierarchy['#ref']
factory = (CourseFactory if block_type == 'course' else ItemFactory)
kwargs = {key: value for key, value in block_hierarchy.items() if key[0] != '#'}
if block_type != 'course':
kwargs['category'] = block_type
kwargs['publish_item'] = True
if parent:
kwargs['parent'] = parent
xblock = factory.create(
display_name=self.create_block_id(block_type, block_ref),
**kwargs
)
block_map[block_ref] = xblock
for child_hierarchy in block_hierarchy.get('#children', []):
self.build_xblock(child_hierarchy, block_map, xblock)
def add_parents(self, block_hierarchy, block_map):
"""
Recursively traverse the block_hierarchy and add additional
parents. This method is expected to be called only after all
blocks have been created.
The additional parents are obtained from the '#parents' field
and is expected to be a list of '#ref' values of the parents.
Note: if a '#parents' field is found, the block is removed from
the course block since it is expected to not belong to the root.
If the block is meant to be a direct child of the course as
well, the course should be explicitly listed in '#parents'.
Arguments:
block_hierarchy (BlockStructureDict):
Definition of block hierarchy.
block_map (dict[str: XBlock]):
Mapping from '#ref' values to their XBlocks.
"""
parents = block_hierarchy.get('#parents', [])
if parents:
block_key = block_map[block_hierarchy['#ref']].location
# First remove the block from the course.
# It would be re-added to the course if the course was
# explicitly listed in parents.
course = modulestore().get_item(block_map['course'].location)
if block_key in course.children:
course.children.remove(block_key)
block_map['course'] = update_block(course)
# Add this to block to each listed parent.
for parent_ref in parents:
parent_block = modulestore().get_item(block_map[parent_ref].location)
parent_block.children.append(block_key)
block_map[parent_ref] = update_block(parent_block)
# recursively call the children
for child_hierarchy in block_hierarchy.get('#children', []):
self.add_parents(child_hierarchy, block_map)
def build_course(self, course_hierarchy):
"""
Build a hierarchy of XBlocks.
Arguments:
course_hierarchy (BlockStructureDict): Definition of course
hierarchy.
where a BlockStructureDict is a list of dicts in the form {
'key1': 'value1',
...
'keyN': 'valueN',
'#type': block_type,
'#ref': short_string_for_referencing_block,
'#children': list[BlockStructureDict],
'#parents': list['#ref' values]
}
Special keys start with '#'; the rest just get passed as
kwargs to Factory.create.
Note: the caller has a choice of whether to create
(1) a nested block structure with children blocks embedded
within their parents, or
(2) a flat block structure with children blocks defined
alongside their parents and attached via the #parents
field, or
(3) a combination of both #1 and #2 used for whichever
blocks.
Note 2: When the #parents field is used in addition to the
nested pattern for a block, it specifies additional parents
that aren't already implied by having the block exist within
another block's #children field.
Returns:
dict[str: XBlock]:
Mapping from '#ref' values to their XBlocks.
"""
block_map = {}
# build the course tree
for block_hierarchy in course_hierarchy:
self.build_xblock(block_hierarchy, block_map, parent=None)
# add additional parents if the course is a DAG or built
# linearly (without specifying '#children' values)
for block_hierarchy in course_hierarchy:
self.add_parents(block_hierarchy, block_map)
publish_course(block_map['course'])
return block_map
def get_block_key_set(self, blocks, *refs):
"""
Gets the set of usage keys that correspond to the list of
#ref values as defined on blocks.
Returns: set[UsageKey]
"""
xblocks = (blocks[ref] for ref in refs)
return {xblock.location for xblock in xblocks} # lint-amnesty, pylint: disable=consider-using-set-comprehension
class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase):
"""
Test helper class for creating a test course of
a graph of vertical blocks based on a parents_map.
"""
# Tree formed by parent_map:
# 0
# / \
# 1 2
# / \ / \
# 3 4 / 5
# \ /
# 6
# Note the parents must always have lower indices than their
# children.
parents_map = [[], [0], [0], [1], [1], [2], [2, 4]]
def setUp(self):
super().setUp()
# create the course
self.course = CourseFactory.create()
# an ordered list of block locations, where the index
# corresponds to the block's index in the parents_map.
self.xblock_keys = [self.course.location]
# create all other blocks in the course
for i, parents_index in enumerate(self.parents_map):
if i == 0:
continue # course already created
self.xblock_keys.append(
ItemFactory.create(
parent=self.get_block(parents_index[0]),
category="vertical",
).location
)
# add additional parents
if len(parents_index) > 1:
for index in range(1, len(parents_index)):
parent_index = parents_index[index]
parent_block = self.get_block(parent_index)
parent_block.children.append(self.xblock_keys[i])
update_block(parent_block)
self.password = 'test'
self.student = UserFactory.create(is_staff=False, username='test_student', password=self.password)
self.staff = UserFactory.create(is_staff=True, username='test_staff', password=self.password)
CourseEnrollmentFactory.create(
is_active=True,
mode=CourseMode.DEFAULT_MODE_SLUG,
user=self.student,
course_id=self.course.id
)
def assert_transform_results(
self,
test_user,
expected_user_accessible_blocks,
blocks_with_differing_access=None,
transformers=None,
):
"""
Verifies the results of transforming the blocks in the course.
Arguments:
test_user (User): The non-staff user that is being tested.
For example, self.student.
expected_user_accessible_blocks (set(int)): Set of blocks
(indices) that a student user is expected to have access
to after the transformers are executed.
blocks_with_differing_access (set(int)): Set of
blocks (indices) whose access will differ from the
transformers result and the current implementation of
has_access. If not provided, does not compare with
has_access results.
transformers (BlockStructureTransformers): An optional collection
of transformers that are to be executed. If not
provided, the default value used by get_course_blocks
is used.
"""
publish_course(self.course)
# verify given test user has access to expected blocks
self._check_results(
test_user,
expected_user_accessible_blocks,
blocks_with_differing_access,
transformers,
)
# verify staff has access to all blocks
self._check_results(self.staff, set(range(len(self.parents_map))), {}, transformers)
def get_block(self, block_index):
"""
Helper method to retrieve the requested block (index) from the
modulestore
"""
return modulestore().get_item(self.xblock_keys[block_index])
def _check_results(self, user, expected_accessible_blocks, blocks_with_differing_access, transformers):
"""
Verifies the results of transforming the blocks in the
course for the given user.
"""
self.client.login(username=user.username, password=self.password)
block_structure = get_course_blocks(user, self.course.location, transformers)
for i, xblock_key in enumerate(self.xblock_keys):
# compute access results of the block
block_structure_result = xblock_key in block_structure
# compare with expected value
assert block_structure_result == (i in expected_accessible_blocks), \
f'block_structure return value {block_structure_result} not equal to expected value for block' \
f' {i} for user {user.username}'
if blocks_with_differing_access:
# compare with has_access_result
has_access_result = bool(has_access(user, 'load', self.get_block(i), course_key=self.course.id))
if i in blocks_with_differing_access:
assert block_structure_result != has_access_result, \
f'block structure ({block_structure_result}) & has_access ({has_access_result})' \
f' results are equal for block {i} for user {user.username}'
else:
assert block_structure_result == has_access_result, \
f'block structure ({block_structure_result}) & has_access ({has_access_result})' \
f' results not equal for block {i} for user {user.username}'
self.client.logout()
def update_block(block):
"""
Helper method to update the block in the modulestore
"""
return modulestore().update_item(block, ModuleStoreEnum.UserID.test)
def publish_course(course):
"""
Helper method to publish the course (from draft to publish branch)
"""
store = modulestore()
with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course.id):
store.publish(course.location, ModuleStoreEnum.UserID.test)
def create_location(org, course, run, block_type, block_id):
"""
Returns the usage key for the given key parameters using the
default modulestore
"""
return modulestore().make_course_key(org, course, run).make_usage_key(block_type, block_id)