Ran pyupgrade on lms/djangoapps/course_goals Ran pyugprade on lms/djangoapps/course_home_api
366 lines
14 KiB
Python
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)
|