Commit d1674ca8 by Nimisha Asthagiri Committed by J. Cliff Dyer

Transformer: SplitTestTransformer

parent a1c7c5f7
"""
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,
)
"""
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()),
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment