Commit 2021ba63 by Nimisha Asthagiri

fixup! user_partitions unit tests, DAG errors, etc.

parent 2d64c54c
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
""" """
from openedx.core.lib.block_cache.transformer import BlockStructureTransformer from openedx.core.lib.block_cache.transformer import BlockStructureTransformer
from courseware.access_utils import check_start_date from courseware.access_utils import check_start_date
from xmodule.course_metadata_utils import DEFAULT_START_DATE
from .utils import get_field_on_block
class StartDateTransformer(BlockStructureTransformer): class StartDateTransformer(BlockStructureTransformer):
...@@ -36,13 +39,21 @@ class StartDateTransformer(BlockStructureTransformer): ...@@ -36,13 +39,21 @@ class StartDateTransformer(BlockStructureTransformer):
) if parents else None ) if parents else None
# set the merged value for this block # set the merged value for this block
block_start = block_structure.get_xblock(block_key).start block_start = get_field_on_block(block_structure.get_xblock(block_key), 'start')
if min_all_parents_start_date is None:
# no parents so just use value on block or default
merged_start_value = block_start or DEFAULT_START_DATE
elif not block_start:
# no value on this block so take value from parents
merged_start_value = min_all_parents_start_date
else:
# max of merged-start-from-all-parents and this block
merged_start_value = max(min_all_parents_start_date, block_start)
block_structure.set_transformer_block_data( block_structure.set_transformer_block_data(
block_key, block_key,
cls, cls,
cls.MERGED_START_DATE, cls.MERGED_START_DATE,
# max of merged-start-from-all-parents and this block merged_start_value
max(min_all_parents_start_date or block_start, block_start)
) )
def transform(self, user_info, block_structure): def transform(self, user_info, block_structure):
......
...@@ -15,8 +15,6 @@ class CourseStructureTestCase(ModuleStoreTestCase): ...@@ -15,8 +15,6 @@ class CourseStructureTestCase(ModuleStoreTestCase):
""" """
Helper for test cases that need to build course structures. Helper for test cases that need to build course structures.
""" """
blocks = []
def setUp(self): def setUp(self):
""" """
Create users. Create users.
...@@ -27,6 +25,56 @@ class CourseStructureTestCase(ModuleStoreTestCase): ...@@ -27,6 +25,56 @@ class CourseStructureTestCase(ModuleStoreTestCase):
self.user = UserFactory.create(password=self.password) self.user = UserFactory.create(password=self.password)
self.staff = UserFactory.create(password=self.password, is_staff=True) self.staff = UserFactory.create(password=self.password, is_staff=True)
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.iteritems() if key[0] != '#'}
if block_type != 'course':
kwargs['category'] = block_type
if parent:
kwargs['parent'] = parent
xblock = factory.create(
display_name='{} {}'.format(block_type, block_ref),
publish_item=True,
**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.
"""
parents = block_hierarchy.get('#parents', [])
for parent_ref in parents:
parent_block = block_map[parent_ref]
parent_block.children.append(
block_map[block_hierarchy['#ref']].location
)
update_block(parent_block)
for child_hierarchy in block_hierarchy.get('#children', []):
self.add_parents(child_hierarchy, block_map)
def build_course(self, course_hierarchy): def build_course(self, course_hierarchy):
""" """
Build a hierarchy of XBlocks. Build a hierarchy of XBlocks.
...@@ -34,92 +82,75 @@ class CourseStructureTestCase(ModuleStoreTestCase): ...@@ -34,92 +82,75 @@ class CourseStructureTestCase(ModuleStoreTestCase):
Arguments: Arguments:
course_hierarchy (BlockStructureDict): Definition of course hierarchy. course_hierarchy (BlockStructureDict): Definition of course hierarchy.
where a BlockStructureDict is a dict in the form { where a BlockStructureDict is a list of dicts in the form {
'key1': 'value1', 'key1': 'value1',
... ...
'keyN': 'valueN', 'keyN': 'valueN',
'#type': block_type, '#type': block_type,
'#ref': short_string_for_referencing_block, '#ref': short_string_for_referencing_block,
'#children': list[BlockStructureDict] '#children': list[BlockStructureDict],
'#parents': list['#ref' values]
} }
Special keys start with '#'; the rest just get passed as kwargs to Special keys start with '#'; the rest just get passed as kwargs to
Factory.create. 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: Returns:
dict[str: XBlock]: Mapping from '#ref' values to their XBlocks. dict[str: XBlock]: Mapping from '#ref' values to their XBlocks.
""" """
block_map = {} block_map = {}
def build_xblock(block_hierarchy, parent): # build the course tree
""" for block_hierarchy in course_hierarchy:
Build an XBlock, add it to result_dict, and call build_xblock on the self.build_xblock(block_hierarchy, block_map, parent=None)
children defined in block_dict.
Arguments:
block_hierarchy (BlockStructureDict): Definition of hierarchy,
from this block down.
is_root (bool): Whether this is the course's root 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.iteritems() if key[0] != '#'}
if block_type != 'course':
kwargs['category'] = block_type
if parent:
kwargs['parent'] = parent
xblock = factory.create(
display_name='{} {}'.format(block_type, block_ref),
publish_item=True,
**kwargs
)
block_map[block_ref] = xblock
for child_hierarchy in block_hierarchy.get('#children', []):
build_xblock(child_hierarchy, xblock)
if '#type' not in course_hierarchy: # add additional parents if the course is a DAG or built
course_hierarchy['#type'] = 'course' # linearly (without specifying '#children' values)
build_xblock(course_hierarchy, None) for block_hierarchy in course_hierarchy:
self.add_parents(block_hierarchy, block_map)
return block_map return block_map
def get_block_key_set(self, *refs): def get_block_key_set(self, blocks, *refs):
""" """
Gets the set of usage keys that correspond to the list of Gets the set of usage keys that correspond to the list of
#ref values as defined on self.blocks. #ref values as defined on blocks.
Returns: set[UsageKey] Returns: set[UsageKey]
""" """
xblocks = (self.blocks[ref] for ref in refs) xblocks = (blocks[ref] for ref in refs)
return set([xblock.location for xblock in xblocks]) return set([xblock.location for xblock in xblocks])
def assert_staff_access_to_all_blocks(self, staff, course, blocks, transformer): def assert_staff_access_to_all_blocks(self, course, blocks, transformer):
""" """
Assert staff users have access to all blocks Assert staff users have access to all blocks
""" """
raw_block_structure = get_course_blocks(
staff,
course.location,
transformers={}
)
self.assertEqual(len(list(raw_block_structure.get_block_keys())), len(blocks))
trans_block_structure = get_course_blocks( trans_block_structure = get_course_blocks(
staff, self.staff,
course.location, course.location,
transformers={transformer} transformers={transformer}
) )
self.assertEqual( self.assertEqual(len(list(trans_block_structure.get_block_keys())), len(blocks))
len(list(raw_block_structure.get_block_keys())),
len(list(trans_block_structure.get_block_keys()))
)
class BlockParentsMapTestCase(ModuleStoreTestCase): class BlockParentsMapTestCase(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: # Tree formed by parent_map:
# 0 # 0
# / \ # / \
...@@ -135,13 +166,19 @@ class BlockParentsMapTestCase(ModuleStoreTestCase): ...@@ -135,13 +166,19 @@ class BlockParentsMapTestCase(ModuleStoreTestCase):
def setUp(self, **kwargs): def setUp(self, **kwargs):
super(BlockParentsMapTestCase, self).setUp() super(BlockParentsMapTestCase, self).setUp()
# create the course
self.course = CourseFactory.create() 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] self.xblock_keys = [self.course.location]
# create all other blocks in the course
for i, parents_index in enumerate(self.parents_map): for i, parents_index in enumerate(self.parents_map):
if i == 0: if i == 0:
continue # course already created continue # course already created
# create the block as a vertical
self.xblock_keys.append( self.xblock_keys.append(
ItemFactory.create( ItemFactory.create(
parent=self.get_block(parents_index[0]), parent=self.get_block(parents_index[0]),
...@@ -155,7 +192,7 @@ class BlockParentsMapTestCase(ModuleStoreTestCase): ...@@ -155,7 +192,7 @@ class BlockParentsMapTestCase(ModuleStoreTestCase):
parent_index = parents_index[index] parent_index = parents_index[index]
parent_block = self.get_block(parent_index) parent_block = self.get_block(parent_index)
parent_block.children.append(self.xblock_keys[i]) parent_block.children.append(self.xblock_keys[i])
self.update_block(parent_block) update_block(parent_block)
self.password = 'test' self.password = 'test'
self.student = UserFactory.create(is_staff=False, password=self.password) self.student = UserFactory.create(is_staff=False, password=self.password)
...@@ -165,13 +202,36 @@ class BlockParentsMapTestCase(ModuleStoreTestCase): ...@@ -165,13 +202,36 @@ class BlockParentsMapTestCase(ModuleStoreTestCase):
def check_transformer_results( def check_transformer_results(
self, expected_student_accessible_blocks, blocks_with_differing_student_access, transformers=None self, expected_student_accessible_blocks, blocks_with_differing_student_access, transformers=None
): ):
"""
Verifies the results of transforming the blocks in the course.
Arguments:
expected_student_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_student_access (set(int)): Set of blocks (indices) whose access
will differ from the transformers result and the current implementation of has_access.
transformers (BlockStructureTransformer): An optional list of transformer that are
to be executed. If not provided, the default value used by get_course_blocks
is used.
"""
def check_results(user, expected_accessible_blocks, blocks_with_differing_access): def check_results(user, expected_accessible_blocks, blocks_with_differing_access):
"""
Verifies the results of transforming the blocks in the course for the given user.
"""
self.client.login(username=user.username, password=self.password) self.client.login(username=user.username, password=self.password)
block_structure = get_course_blocks(user, self.course.location, transformers=transformers) block_structure = get_course_blocks(user, self.course.location, transformers=transformers)
# Enumerate through all the blocks that were created in the course
for i, xblock_key in enumerate(self.xblock_keys): for i, xblock_key in enumerate(self.xblock_keys):
# verify existence of the block
block_structure_result = block_structure.has_block(xblock_key) block_structure_result = block_structure.has_block(xblock_key)
has_access_result = bool(has_access(user, 'load', self.get_block(i))) has_access_result = bool(has_access(user, 'load', self.get_block(i)))
# compare with expected value
self.assertEquals( self.assertEquals(
block_structure_result, block_structure_result,
i in expected_accessible_blocks, i in expected_accessible_blocks,
...@@ -180,25 +240,41 @@ class BlockParentsMapTestCase(ModuleStoreTestCase): ...@@ -180,25 +240,41 @@ class BlockParentsMapTestCase(ModuleStoreTestCase):
) )
) )
# compare with has_access result
if i in blocks_with_differing_access: if i in blocks_with_differing_access:
self.assertNotEqual( self.assertNotEqual(
block_structure_result, block_structure_result,
has_access_result, has_access_result,
"block structure and has_access results are equal for block {0}".format(i) "block structure ({0}) and has_access ({1}) results are equal for block {2}".format(
block_structure_result, has_access_result, i
)
) )
else: else:
self.assertEquals( self.assertEquals(
block_structure_result, block_structure_result,
has_access_result, has_access_result,
"block structure and has_access results are not equal for block {0}".format(i) "block structure ({0}) and has_access ({1}) results are not equal for block {2}".format(
block_structure_result, has_access_result, i
)
) )
self.client.logout() self.client.logout()
# verify student has access to expected blocks
check_results(self.student, expected_student_accessible_blocks, blocks_with_differing_student_access) check_results(self.student, expected_student_accessible_blocks, blocks_with_differing_student_access)
check_results(self.staff, {0, 1, 2, 3, 4, 5, 6}, {})
def get_block(self, i): # verify staff has access to all blocks
return modulestore().get_item(self.xblock_keys[i]) check_results(self.staff, set(range(len(self.parents_map))), {})
def update_block(self, block): def get_block(self, block_index):
return modulestore().update_item(block, 'test_user') """
Helper method to retrieve the requested block (index) from the modulestore
"""
return modulestore().get_item(self.xblock_keys[block_index])
def update_block(block):
"""
Helper method to update the block in the modulestore
"""
return modulestore().update_item(block, 'test_user')
...@@ -47,10 +47,11 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase): ...@@ -47,10 +47,11 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase):
""" """
Get a course hierarchy to test with. Get a course hierarchy to test with.
""" """
return { return [{
'org': 'ContentLibraryTransformer', 'org': 'ContentLibraryTransformer',
'course': 'CL101F', 'course': 'CL101F',
'run': 'test_run', 'run': 'test_run',
'#type': 'course',
'#ref': 'course', '#ref': 'course',
'#children': [ '#children': [
{ {
...@@ -103,7 +104,7 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase): ...@@ -103,7 +104,7 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase):
], ],
} }
] ]
} }]
def test_course_structure_with_user_course_library(self): def test_course_structure_with_user_course_library(self):
""" """
...@@ -128,7 +129,7 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase): ...@@ -128,7 +129,7 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase):
self.assertEqual( self.assertEqual(
set(trans_block_structure.get_block_keys()), set(trans_block_structure.get_block_keys()),
self.get_block_key_set('course', 'chapter1', 'lesson1', 'vertical1', 'library_content1') self.get_block_key_set(self.blocks, 'course', 'chapter1', 'lesson1', 'vertical1', 'library_content1')
) )
# Check course structure again, with mocked selected modules for a user. # Check course structure again, with mocked selected modules for a user.
...@@ -145,6 +146,7 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase): ...@@ -145,6 +146,7 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase):
self.assertEqual( self.assertEqual(
set(trans_block_structure.get_block_keys()), set(trans_block_structure.get_block_keys()),
self.get_block_key_set( self.get_block_key_set(
self.blocks,
'course', 'course',
'chapter1', 'chapter1',
'lesson1', 'lesson1',
...@@ -156,4 +158,4 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase): ...@@ -156,4 +158,4 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase):
) )
def test_staff_user(self): def test_staff_user(self):
self.assert_staff_access_to_all_blocks(self.staff, self.course, self.blocks, self.transformer) self.assert_staff_access_to_all_blocks(self.course, self.blocks, self.transformer)
...@@ -51,11 +51,12 @@ class SplitTestTransformerTestCase(CourseStructureTestCase): ...@@ -51,11 +51,12 @@ class SplitTestTransformerTestCase(CourseStructureTestCase):
Returns: dict[course_structure] Returns: dict[course_structure]
""" """
return { return [{
'org': 'SplitTestTransformer', 'org': 'SplitTestTransformer',
'course': 'ST101F', 'course': 'ST101F',
'run': 'test_run', 'run': 'test_run',
'user_partitions': [self.split_test_user_partition], 'user_partitions': [self.split_test_user_partition],
'#type': 'course',
'#ref': 'course', '#ref': 'course',
'#children': [ '#children': [
{ {
...@@ -113,15 +114,9 @@ class SplitTestTransformerTestCase(CourseStructureTestCase): ...@@ -113,15 +114,9 @@ class SplitTestTransformerTestCase(CourseStructureTestCase):
], ],
} }
] ]
} }]
def test_user(self): def test_user(self):
trans_block_structure = get_course_blocks(
self.user,
self.course.location,
transformers={self.transformer},
)
# user was randomly assigned to one of the groups # user was randomly assigned to one of the groups
user_groups = get_user_partition_groups( user_groups = get_user_partition_groups(
self.course.id, [self.split_test_user_partition], self.user self.course.id, [self.split_test_user_partition], self.user
...@@ -129,19 +124,18 @@ class SplitTestTransformerTestCase(CourseStructureTestCase): ...@@ -129,19 +124,18 @@ class SplitTestTransformerTestCase(CourseStructureTestCase):
self.assertEquals(len(user_groups), 1) self.assertEquals(len(user_groups), 1)
group = user_groups[self.split_test_user_partition_id] group = user_groups[self.split_test_user_partition_id]
# determine expected blocks
expected_blocks = ['course', 'chapter1', 'lesson1', 'vertical1'] expected_blocks = ['course', 'chapter1', 'lesson1', 'vertical1']
if group.id == 3: expected_blocks += (['vertical2', 'html1'] if group.id == 3 else ['vertical3', 'html2'])
expected_blocks += ['vertical2', 'html1']
else:
expected_blocks += ['vertical3', 'html2']
self.assertEqual(set(trans_block_structure.get_block_keys()), set(self.get_block_key_set(*expected_blocks)))
# calling again should result in the same block set
reloaded_structure = get_course_blocks(
self.user,
self.course.location,
transformers={self.transformer}
)
self.assertEqual(set(reloaded_structure.get_block_keys()), set(self.get_block_key_set(*expected_blocks)))
# calling twice should result in the same block set
for _ in range(2):
trans_block_structure = get_course_blocks(
self.user,
self.course.location,
transformers={self.transformer},
)
self.assertEqual(
set(trans_block_structure.get_block_keys()),
set(self.get_block_key_set(self.blocks, *expected_blocks))
)
...@@ -7,10 +7,8 @@ from datetime import timedelta ...@@ -7,10 +7,8 @@ from datetime import timedelta
from django.utils.timezone import now from django.utils.timezone import now
from mock import patch from mock import patch
from xmodule.course_metadata_utils import DEFAULT_START_DATE from ..start_date import StartDateTransformer, DEFAULT_START_DATE
from .test_helpers import BlockParentsMapTestCase, update_block
from ..start_date import StartDateTransformer
from .test_helpers import BlockParentsMapTestCase
@ddt.ddt @ddt.ddt
...@@ -43,10 +41,19 @@ class StartDateTransformerTestCase(BlockParentsMapTestCase): ...@@ -43,10 +41,19 @@ class StartDateTransformerTestCase(BlockParentsMapTestCase):
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
@ddt.data( @ddt.data(
({}, {}, {}), ({}, {}, {}),
({0: StartDateType.default}, {}, {}),
({0: StartDateType.future}, {}, {}),
({0: StartDateType.released}, {0, 1, 2, 3, 4, 5, 6}, {}), ({0: StartDateType.released}, {0, 1, 2, 3, 4, 5, 6}, {}),
# has_access checks on block directly and doesn't follow negative access set on parent/ancestor (i.e., 0)
({1: StartDateType.released}, {}, {1, 3, 4, 6}), ({1: StartDateType.released}, {}, {1, 3, 4, 6}),
({2: StartDateType.released}, {}, {2, 5, 6}),
({1: StartDateType.released, 2: StartDateType.released}, {}, {1, 2, 3, 4, 5, 6}), ({1: StartDateType.released, 2: StartDateType.released}, {}, {1, 2, 3, 4, 5, 6}),
({0: StartDateType.released, 4: StartDateType.future}, {0, 1, 2, 3, 5}, {}),
# DAG conflicts: has_access relies on field inheritance so it takes only the value from the first parent-chain
({0: StartDateType.released, 4: StartDateType.future}, {0, 1, 2, 3, 5, 6}, {6}),
({0: StartDateType.released, 2: StartDateType.released, 4: StartDateType.future}, {0, 1, 2, 3, 5, 6}, {6}),
({0: StartDateType.released, 2: StartDateType.future, 4: StartDateType.released}, {0, 1, 3, 4, 6}, {}),
) )
@ddt.unpack @ddt.unpack
def test_block_start_date( def test_block_start_date(
...@@ -55,7 +62,7 @@ class StartDateTransformerTestCase(BlockParentsMapTestCase): ...@@ -55,7 +62,7 @@ class StartDateTransformerTestCase(BlockParentsMapTestCase):
for i, start_date_type in start_date_type_values.iteritems(): for i, start_date_type in start_date_type_values.iteritems():
block = self.get_block(i) block = self.get_block(i)
block.start = self.StartDateType.start(start_date_type) block.start = self.StartDateType.start(start_date_type)
self.update_block(block) update_block(block)
self.check_transformer_results( self.check_transformer_results(
expected_student_visible_blocks, blocks_with_differing_student_access, [StartDateTransformer()] expected_student_visible_blocks, blocks_with_differing_student_access, [StartDateTransformer()]
......
...@@ -18,6 +18,8 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase): ...@@ -18,6 +18,8 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase):
""" """
UserPartitionTransformer Test UserPartitionTransformer Test
""" """
TEST_PARTITION_ID = 0
def setUp(self): def setUp(self):
""" """
Setup course structure and create user for user partition transformer test. Setup course structure and create user for user partition transformer test.
...@@ -25,10 +27,9 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase): ...@@ -25,10 +27,9 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase):
super(UserPartitionTransformerTestCase, self).setUp() super(UserPartitionTransformerTestCase, self).setUp()
# Set up user partitions and groups. # Set up user partitions and groups.
self.groups = [Group(1, 'Group 1'), Group(2, 'Group 2')] self.groups = [Group(1, 'Group 1'), Group(2, 'Group 2'), Group(3, 'Group 3'), Group(4, 'Group 4')]
self.content_groups = [1, 2]
self.user_partition = UserPartition( self.user_partition = UserPartition(
id=0, id=self.TEST_PARTITION_ID,
name='Partition 1', name='Partition 1',
description='This is partition 1', description='This is partition 1',
groups=self.groups, groups=self.groups,
...@@ -46,8 +47,18 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase): ...@@ -46,8 +47,18 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase):
# Set up cohorts. # Set up cohorts.
config_course_cohorts(self.course, is_cohorted=True) config_course_cohorts(self.course, is_cohorted=True)
self.cohorts = [CohortFactory(course_id=self.course.id) for __ in enumerate(self.groups)] self.cohorts = []
self.add_user_to_cohort_group(self.cohorts[0], self.groups[0]) for group in self.groups:
cohort = CohortFactory(course_id=self.course.id)
self.cohorts.append(cohort)
link_cohort_to_partition_group(
cohort,
self.user_partition.id,
group.id,
)
add_user_to_cohort(self.cohorts[0], self.user.username)
self.transformer = UserPartitionTransformer() self.transformer = UserPartitionTransformer()
def get_course_hierarchy(self): def get_course_hierarchy(self):
...@@ -56,62 +67,104 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase): ...@@ -56,62 +67,104 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase):
Assumes self.user_partition has already been initialized. Assumes self.user_partition has already been initialized.
""" """
return { # course
'org': 'UserPartitionTransformer', # / \
'course': 'UP101F', # / \
'run': 'test_run', # A[1, 2, 3] B
'user_partitions': [self.user_partition], # / | \ |
'#ref': 'course', # / | \ |
'#children': [ # / | \ |
{ # C[1, 2] D[2, 3] E /
'#type': 'chapter', # / | \ | / \ /
'#ref': 'chapter1', # / | \ | / \ /
'#children': [ # / | \ | / \ /
{ # F G[1] H[2] I J K[4] /
'metadata': { # / \ / / \ /
'group_access': {0: [0, 1, 2]}, # / \ / / \ /
}, # / \ / / \/
'#type': 'sequential', # L[1, 2] M[1, 2, 3] N O
'#ref': 'lesson1', #
'#children': [ return [
{ {
'#type': 'vertical', 'org': 'UserPartitionTransformer',
'#ref': 'vertical1', 'course': 'UP101F',
'#children': [ 'run': 'test_run',
{ 'user_partitions': [self.user_partition],
'metadata': {'group_access': {0: [0]}}, '#type': 'course',
'#type': 'html', '#ref': 'course',
'#ref': 'html1', '#children': [
}, {
{ '#type': 'vertical',
'metadata': {'group_access': {0: [1]}}, '#ref': 'A',
'#type': 'html', 'metadata': {'group_access': {self.TEST_PARTITION_ID: [0, 1, 2, 3]}},
'#ref': 'html2', },
} {'#type': 'vertical', '#ref': 'B'},
], ],
} },
], {
} '#type': 'vertical',
], '#ref': 'C',
} '#parents': ['A'],
] 'metadata': {'group_access': {self.TEST_PARTITION_ID: [1, 2]}},
} '#children': [
{'#type': 'vertical', '#ref': 'F'},
def add_user_to_cohort_group(self, cohort, group): {
""" '#type': 'vertical',
Add user to cohort, link cohort to content group, and update blocks. '#ref': 'G',
""" 'metadata': {'group_access': {self.TEST_PARTITION_ID: [1]}},
add_user_to_cohort(cohort, self.user.username) },
link_cohort_to_partition_group( {
cohort, '#type': 'vertical',
self.user_partition.id, '#ref': 'H',
group.id, 'metadata': {'group_access': {self.TEST_PARTITION_ID: [2]}},
) },
],
},
{
'#type': 'vertical',
'#ref': 'D',
'#parents': ['A'],
'metadata': {'group_access': {self.TEST_PARTITION_ID: [2, 3]}},
'#children': [{'#type': 'vertical', '#ref': 'I'}],
},
{
'#type': 'vertical',
'#ref': 'E',
'#parents': ['A'],
'#children': [{'#type': 'vertical', '#ref': 'J'}],
},
{
'#type': 'vertical',
'#ref': 'K',
'#parents': ['E'],
'metadata': {'group_access': {self.TEST_PARTITION_ID: [4]}},
'#children': [{'#type': 'vertical', '#ref': 'N'}],
},
{
'#type': 'vertical',
'#ref': 'L',
'#parents': ['G'],
'metadata': {'group_access': {self.TEST_PARTITION_ID: [1, 2]}},
},
{
'#type': 'vertical',
'#ref': 'M',
'#parents': ['G', 'H'],
'metadata': {'group_access': {self.TEST_PARTITION_ID: [1, 2, 3]}},
},
{
'#type': 'vertical',
'#ref': 'O',
'#parents': ['K', 'B'],
},
]
def test_user_assigned(self): def test_user_assigned(self):
""" """
Test when user is assigned to group in user partition. Test when user is assigned to group in user partition.
""" """
# TODO ddt with testing user in different groups
trans_block_structure = get_course_blocks( trans_block_structure = get_course_blocks(
self.user, self.user,
self.course.location, self.course.location,
...@@ -119,8 +172,8 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase): ...@@ -119,8 +172,8 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase):
) )
self.assertSetEqual( self.assertSetEqual(
set(trans_block_structure.get_block_keys()), set(trans_block_structure.get_block_keys()),
self.get_block_key_set('course', 'chapter1', 'lesson1', 'vertical1', 'html2') self.get_block_key_set(self.blocks, 'course', 'A', 'B', 'C', 'E', 'F', 'G', 'J', 'L', 'M', 'O')
) )
def test_staff_user(self): def test_staff_user(self):
self.assert_staff_access_to_all_blocks(self.staff, self.course, self.blocks, self.transformer) self.assert_staff_access_to_all_blocks(self.course, self.blocks, self.transformer)
...@@ -4,7 +4,7 @@ Tests for VisibilityTransformer. ...@@ -4,7 +4,7 @@ Tests for VisibilityTransformer.
import ddt import ddt
from course_blocks.transformers.visibility import VisibilityTransformer from course_blocks.transformers.visibility import VisibilityTransformer
from .test_helpers import BlockParentsMapTestCase from .test_helpers import BlockParentsMapTestCase, update_block
@ddt.ddt @ddt.ddt
...@@ -33,7 +33,7 @@ class VisibilityTransformerTestCase(BlockParentsMapTestCase): ...@@ -33,7 +33,7 @@ class VisibilityTransformerTestCase(BlockParentsMapTestCase):
for i, _ in enumerate(self.parents_map): for i, _ in enumerate(self.parents_map):
block = self.get_block(i) block = self.get_block(i)
block.visible_to_staff_only = (i in staff_only_blocks) block.visible_to_staff_only = (i in staff_only_blocks)
self.update_block(block) update_block(block)
self.check_transformer_results( self.check_transformer_results(
expected_student_visible_blocks, blocks_with_differing_student_access, [VisibilityTransformer()] expected_student_visible_blocks, blocks_with_differing_student_access, [VisibilityTransformer()]
......
...@@ -4,7 +4,7 @@ User Partitions Transformer ...@@ -4,7 +4,7 @@ User Partitions Transformer
from openedx.core.lib.block_cache.transformer import BlockStructureTransformer from openedx.core.lib.block_cache.transformer import BlockStructureTransformer
from .split_test import SplitTestTransformer from .split_test import SplitTestTransformer
from .utils import get_field_on_block
class MergedGroupAccess(object): class MergedGroupAccess(object):
""" """
...@@ -36,19 +36,26 @@ class MergedGroupAccess(object): ...@@ -36,19 +36,26 @@ class MergedGroupAccess(object):
# Note that a user must have access to all partitions in group_access # Note that a user must have access to all partitions in group_access
# or _access in order to access a block. # or _access in order to access a block.
block_group_access = getattr(xblock, 'group_access', {})
self._access = {} # { partition.id: set(IDs of groups that can access partition } self._access = {} # { partition.id: set(IDs of groups that can access partition }
# Get the group_access value that is directly set on the xblock.
# Do not get the inherited value since field inheritance doesn't
# take a union of them for DAGs.
block_group_access = get_field_on_block(xblock, 'group_access', default_value={})
for partition in user_partitions: for partition in user_partitions:
# Within this loop, None <=> Universe set <=> "No access restriction" # Within this loop, None <=> Universe set <=> "No access restriction"
block_group_ids = set(block_group_access.get(partition.id, [])) or None block_group_ids = set(block_group_access.get(partition.id, [])) or None
parents_group_ids = [ parents_group_ids = []
merged_parent_access._access[partition.id] for merged_parent_access in merged_parent_access_list:
for merged_parent_access in merged_parent_access_list if partition.id in merged_parent_access._access:
if partition.id in merged_parent_access._access parents_group_ids.append(merged_parent_access._access[partition.id])
] else:
parents_group_ids = []
break
merged_parent_group_ids = ( merged_parent_group_ids = (
set().union(*parents_group_ids) set().union(*parents_group_ids)
if parents_group_ids != [] if parents_group_ids != []
...@@ -193,6 +200,7 @@ class UserPartitionTransformer(BlockStructureTransformer): ...@@ -193,6 +200,7 @@ class UserPartitionTransformer(BlockStructureTransformer):
user_groups = get_user_partition_groups( user_groups = get_user_partition_groups(
user_info.course_key, user_partitions, user_info.user user_info.course_key, user_partitions, user_info.user
) )
# TODO test this when deserializing across processes
block_structure.remove_block_if( block_structure.remove_block_if(
lambda block_key: not block_structure.get_transformer_block_data( lambda block_key: not block_structure.get_transformer_block_data(
block_key, self, 'merged_group_access' block_key, self, 'merged_group_access'
......
"""
Common Helper utilities for transformers
"""
def get_field_on_block(block, field_name, default_value=None):
# Get the field value that is directly set on the xblock.
# Do not get the inherited value since field inheritance
# returns value from only a single parent chain
# (e.g., doesn't take a union in DAGs).
if block.fields[field_name].is_set_on(block):
return getattr(block, field_name)
else:
return default_value
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