From d1674ca85fb4003cfd2db65a1f26c14bd21d182e Mon Sep 17 00:00:00 2001 From: Nimisha Asthagiri Date: Wed, 28 Oct 2015 19:08:48 -0400 Subject: [PATCH] Transformer: SplitTestTransformer --- .../course_blocks/transformers/split_test.py | 81 +++++++ .../transformers/tests/test_split_test.py | 226 ++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 lms/djangoapps/course_blocks/transformers/split_test.py create mode 100644 lms/djangoapps/course_blocks/transformers/tests/test_split_test.py diff --git a/lms/djangoapps/course_blocks/transformers/split_test.py b/lms/djangoapps/course_blocks/transformers/split_test.py new file mode 100644 index 0000000000..85e0a15561 --- /dev/null +++ b/lms/djangoapps/course_blocks/transformers/split_test.py @@ -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, + ) diff --git a/lms/djangoapps/course_blocks/transformers/tests/test_split_test.py b/lms/djangoapps/course_blocks/transformers/tests/test_split_test.py new file mode 100644 index 0000000000..f211047d1b --- /dev/null +++ b/lms/djangoapps/course_blocks/transformers/tests/test_split_test.py @@ -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()), + )