Transformer: SplitTestTransformer
This commit is contained in:
committed by
J. Cliff Dyer
parent
a1c7c5f7e9
commit
d1674ca85f
81
lms/djangoapps/course_blocks/transformers/split_test.py
Normal file
81
lms/djangoapps/course_blocks/transformers/split_test.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Split Test Block Transformer
|
||||
"""
|
||||
from openedx.core.lib.block_cache.transformer import BlockStructureTransformer
|
||||
|
||||
|
||||
class SplitTestTransformer(BlockStructureTransformer):
|
||||
"""
|
||||
A nested transformer of the UserPartitionTransformer that honors the
|
||||
block structure pathways created by split_test modules.
|
||||
|
||||
To avoid code duplication, the implementation transforms its block
|
||||
access representation to the representation used by user_partitions.
|
||||
Namely, the 'group_id_to_child' field on a split_test module is
|
||||
transformed into the, now standard, 'group_access' fields in the
|
||||
split_test module's children.
|
||||
|
||||
The implementation therefore relies on the UserPartitionTransformer
|
||||
to actually enforce the access using the 'user_partitions' and
|
||||
'group_access' fields.
|
||||
"""
|
||||
VERSION = 1
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
"""
|
||||
Unique identifier for the transformer's class;
|
||||
same identifier used in setup.py.
|
||||
"""
|
||||
return "split_test"
|
||||
|
||||
@classmethod
|
||||
def collect(cls, block_structure):
|
||||
"""
|
||||
Collects any information that's necessary to execute this
|
||||
transformer's transform method.
|
||||
"""
|
||||
|
||||
root_block = block_structure.get_xblock(block_structure.root_block_usage_key)
|
||||
user_partitions = getattr(root_block, 'user_partitions', [])
|
||||
|
||||
for block_key in block_structure.topological_traversal(
|
||||
filter_func=lambda block_key: block_key.block_type == 'split_test',
|
||||
yield_descendants_of_unyielded=True,
|
||||
):
|
||||
xblock = block_structure.get_xblock(block_key)
|
||||
partition_for_this_block = next(
|
||||
(
|
||||
partition for partition in user_partitions
|
||||
if partition.id == xblock.user_partition_id
|
||||
),
|
||||
None
|
||||
)
|
||||
if not partition_for_this_block:
|
||||
continue
|
||||
|
||||
# Create dict of child location to group_id, using the
|
||||
# group_id_to_child field on the split_test module.
|
||||
child_to_group = {
|
||||
xblock.group_id_to_child.get(unicode(group.id), None): group.id
|
||||
for group in partition_for_this_block.groups
|
||||
}
|
||||
|
||||
# Set group access for each child using its group_access
|
||||
# field so the user partitions transformer enforces it.
|
||||
for child_location in xblock.children:
|
||||
child = block_structure.get_xblock(child_location)
|
||||
group = child_to_group.get(child_location, None)
|
||||
child.group_access[partition_for_this_block.id] = [group] if group else []
|
||||
|
||||
def transform(self, usage_info, block_structure): # pylint: disable=unused-argument
|
||||
"""
|
||||
Mutates block_structure based on the given usage_info.
|
||||
"""
|
||||
|
||||
# The UserPartitionTransformer will enforce group access, so
|
||||
# go ahead and remove all extraneous split_test modules.
|
||||
block_structure.remove_block_if(
|
||||
lambda block_key: block_key.block_type == 'split_test',
|
||||
keep_descendants=True,
|
||||
)
|
||||
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Tests for SplitTestTransformer.
|
||||
"""
|
||||
import ddt
|
||||
|
||||
import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api
|
||||
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
|
||||
from student.tests.factories import CourseEnrollmentFactory
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
from xmodule.modulestore.tests.factories import check_mongo_calls, check_mongo_calls_range
|
||||
|
||||
from ...api import get_course_blocks
|
||||
from ..user_partitions import UserPartitionTransformer, _get_user_partition_groups
|
||||
from .test_helpers import CourseStructureTestCase, create_location
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class SplitTestTransformerTestCase(CourseStructureTestCase):
|
||||
"""
|
||||
SplitTestTransformer Test
|
||||
"""
|
||||
TEST_PARTITION_ID = 0
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Setup course structure and create user for split test transformer test.
|
||||
"""
|
||||
super(SplitTestTransformerTestCase, self).setUp()
|
||||
|
||||
# Set up user partitions and groups.
|
||||
self.groups = [Group(1, 'Group 1'), Group(2, 'Group 2'), Group(3, 'Group 3')]
|
||||
self.split_test_user_partition_id = self.TEST_PARTITION_ID
|
||||
self.split_test_user_partition = UserPartition(
|
||||
id=self.split_test_user_partition_id,
|
||||
name='Split Partition',
|
||||
description='This is split partition',
|
||||
groups=self.groups,
|
||||
scheme=RandomUserPartitionScheme
|
||||
)
|
||||
self.split_test_user_partition.scheme.name = "random"
|
||||
|
||||
# Build course.
|
||||
self.course_hierarchy = self.get_course_hierarchy()
|
||||
self.blocks = self.build_course(self.course_hierarchy)
|
||||
self.course = self.blocks['course']
|
||||
|
||||
# Enroll user in course.
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
|
||||
|
||||
self.transformer = UserPartitionTransformer()
|
||||
|
||||
def get_course_hierarchy(self):
|
||||
"""
|
||||
Get a course hierarchy to test with.
|
||||
|
||||
Assumes self.split_test_user_partition has already been initialized.
|
||||
|
||||
Returns: dict[course_structure]
|
||||
"""
|
||||
|
||||
org_name = 'SplitTestTransformer'
|
||||
course_name = 'ST101F'
|
||||
run_name = 'test_run'
|
||||
|
||||
def location(block_ref, block_type='vertical'):
|
||||
"""
|
||||
Returns the usage key for the given block_type and block reference string in the test course.
|
||||
"""
|
||||
return create_location(
|
||||
org_name, course_name, run_name, block_type, self.create_block_id(block_type, block_ref)
|
||||
)
|
||||
|
||||
# course
|
||||
# / | \
|
||||
# / | \
|
||||
# A BSplit CSplit
|
||||
# / \ / | \ | \
|
||||
# / \ / | \ | \
|
||||
# D E[1] F[2] G[3] H[1] I[2]
|
||||
# / \ \ |
|
||||
# / \ \ |
|
||||
# J KSplit \ L
|
||||
# / | \ / \
|
||||
# / | \ / \
|
||||
# M[2] N[3] O P
|
||||
#
|
||||
return [
|
||||
{
|
||||
'org': org_name,
|
||||
'course': course_name,
|
||||
'run': run_name,
|
||||
'user_partitions': [self.split_test_user_partition],
|
||||
'#type': 'course',
|
||||
'#ref': 'course',
|
||||
},
|
||||
{
|
||||
'#type': 'vertical',
|
||||
'#ref': 'A',
|
||||
'#children': [{'#type': 'vertical', '#ref': 'D'}],
|
||||
},
|
||||
{
|
||||
'#type': 'split_test',
|
||||
'#ref': 'BSplit',
|
||||
'metadata': {'category': 'split_test'},
|
||||
'user_partition_id': self.TEST_PARTITION_ID,
|
||||
'group_id_to_child': {
|
||||
'1': location('E'),
|
||||
'2': location('F'),
|
||||
'3': location('G'),
|
||||
},
|
||||
'#children': [{'#type': 'vertical', '#ref': 'G'}],
|
||||
},
|
||||
{
|
||||
'#type': 'vertical',
|
||||
'#ref': 'E',
|
||||
'#parents': ['A', 'BSplit'],
|
||||
},
|
||||
{
|
||||
'#type': 'vertical',
|
||||
'#ref': 'F',
|
||||
'#parents': ['BSplit'],
|
||||
'#children': [
|
||||
{'#type': 'vertical', '#ref': 'J'},
|
||||
],
|
||||
},
|
||||
{
|
||||
'#type': 'split_test',
|
||||
'#ref': 'KSplit',
|
||||
'metadata': {'category': 'split_test'},
|
||||
'user_partition_id': self.TEST_PARTITION_ID,
|
||||
'group_id_to_child': {
|
||||
'2': location('M'),
|
||||
'3': location('N'),
|
||||
},
|
||||
'#parents': ['F'],
|
||||
'#children': [
|
||||
{'#type': 'vertical', '#ref': 'M'},
|
||||
{'#type': 'vertical', '#ref': 'N'},
|
||||
],
|
||||
},
|
||||
{
|
||||
'#type': 'split_test',
|
||||
'#ref': 'CSplit',
|
||||
'metadata': {'category': 'split_test'},
|
||||
'user_partition_id': self.TEST_PARTITION_ID,
|
||||
'group_id_to_child': {
|
||||
'1': location('H'),
|
||||
'2': location('I'),
|
||||
},
|
||||
'#children': [
|
||||
{'#type': 'vertical', '#ref': 'I'},
|
||||
{
|
||||
'#type': 'vertical',
|
||||
'#ref': 'H',
|
||||
'#children': [
|
||||
{
|
||||
'#type': 'vertical',
|
||||
'#ref': 'L',
|
||||
'#children': [{'#type': 'vertical', '#ref': 'P'}],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'#type': 'vertical',
|
||||
'#ref': 'O',
|
||||
'#parents': ['G', 'L'],
|
||||
},
|
||||
]
|
||||
|
||||
@ddt.data(
|
||||
# Note: Theoretically, block E should be accessible by users
|
||||
# not in Group 1, since there's an open path through block A.
|
||||
# Since the split_test transformer automatically sets the block
|
||||
# access on its children, it bypasses the paths via other
|
||||
# parents. However, we don't think this is a use case we need to
|
||||
# support for split_test components (since they are now deprecated
|
||||
# in favor of content groups and user partitions).
|
||||
(1, ('course', 'A', 'D', 'E', 'H', 'L', 'O', 'P',)),
|
||||
(2, ('course', 'A', 'D', 'F', 'J', 'M', 'I',)),
|
||||
(3, ('course', 'A', 'D', 'G', 'O',)),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_user(self, group_id, expected_blocks):
|
||||
course_tag_api.set_course_tag(
|
||||
self.user,
|
||||
self.course.id,
|
||||
RandomUserPartitionScheme.key_for_partition(self.split_test_user_partition),
|
||||
group_id,
|
||||
)
|
||||
|
||||
block_structure1 = get_course_blocks(
|
||||
self.user,
|
||||
self.course.location,
|
||||
transformers={self.transformer},
|
||||
)
|
||||
self.assertEqual(
|
||||
set(block_structure1.get_block_keys()),
|
||||
set(self.get_block_key_set(self.blocks, *expected_blocks)),
|
||||
)
|
||||
|
||||
def test_user_randomly_assigned(self):
|
||||
# user was randomly assigned to one of the groups
|
||||
user_groups = _get_user_partition_groups( # pylint: disable=protected-access
|
||||
self.course.id, [self.split_test_user_partition], self.user
|
||||
)
|
||||
self.assertEquals(len(user_groups), 1)
|
||||
|
||||
# calling twice should result in the same block set
|
||||
with check_mongo_calls_range(min_finds=1):
|
||||
block_structure1 = get_course_blocks(
|
||||
self.user,
|
||||
self.course.location,
|
||||
transformers={self.transformer},
|
||||
)
|
||||
with check_mongo_calls(0):
|
||||
block_structure2 = get_course_blocks(
|
||||
self.user,
|
||||
self.course.location,
|
||||
transformers={self.transformer},
|
||||
)
|
||||
self.assertEqual(
|
||||
set(block_structure1.get_block_keys()),
|
||||
set(block_structure2.get_block_keys()),
|
||||
)
|
||||
Reference in New Issue
Block a user