Commit a3a48a98 by Dino Cikatic Committed by Nimisha Asthagiri

Block Transformers: Split Test and Content Library

parent 82f3c0df
......@@ -10,6 +10,8 @@ from .transformers import (
start_date,
user_partitions,
visibility,
split_test,
library_content,
)
from .user_info import CourseUserInfo
......@@ -18,6 +20,8 @@ LMS_COURSE_TRANSFORMERS = [
visibility.VisibilityTransformer(),
start_date.StartDateTransformer(),
user_partitions.UserPartitionTransformer(),
split_test.SplitTestTransformer(),
library_content.ContentLibraryTransformer(),
]
......
"""
Transformers helpers functions.
"""
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
from openedx.core.djangoapps.course_groups.partition_scheme import CohortPartitionScheme
# TODO 8874: Make it so we support all schemes instead of manually declaring them here.
INCLUDE_SCHEMES = [CohortPartitionScheme, RandomUserPartitionScheme, ]
SCHEME_SUPPORTS_ASSIGNMENT = [RandomUserPartitionScheme, ]
def get_user_partition_groups(course_key, user_partitions, user):
"""
Collect group ID for each partition in this course for this user.
Arguments:
course_key (CourseKey)
user_partitions (list[UserPartition])
user (User)
Returns:
dict[int: Group]: Mapping from user partitions to the group to which
the user belongs in each partition. If the user isn't in a group
for a particular partition, then that partition's ID will not be
in the dict.
"""
partition_groups = {}
for partition in user_partitions:
if partition.scheme not in INCLUDE_SCHEMES:
continue
group = partition.scheme.get_group_for_user(
course_key,
user,
partition,
**({'assign': False} if partition.scheme in SCHEME_SUPPORTS_ASSIGNMENT else {})
)
if group is not None:
partition_groups[partition.id] = group
return partition_groups
"""
Content Library Transformer, used to filter course structure per user.
"""
import json
from courseware.access import _has_access_to_course
from courseware.models import StudentModule
from opaque_keys.edx.locator import BlockUsageLocator
from openedx.core.lib.block_cache.transformer import BlockStructureTransformer
class ContentLibraryTransformer(BlockStructureTransformer):
"""
Content Library Transformer Class
"""
VERSION = 1
@classmethod
def _get_selected_modules(cls, user, course_key, block_key):
"""
Get list of selected modules in a library,
for user.
Arguments:
user (User)
course_key (CourseLocator)
block_key (BlockUsageLocator)
Returns:
list[modules]
"""
return StudentModule.objects.filter(
student=user,
course_id=course_key,
module_state_key=block_key,
state__contains='"selected": [['
)
@classmethod
def collect(cls, block_structure):
"""
Computes any information for each XBlock that's necessary to execute
this transformer's transform method.
Arguments:
block_structure (BlockStructureCollectedData)
Returns:
dict[UsageKey: dict]
"""
# For each block check if block is library_content.
# If library_content add children array to content_library_children field
for block_key in block_structure.topological_traversal():
xblock = block_structure.get_xblock(block_key)
block_structure.set_transformer_block_data(block_key, cls, 'content_library_children', [])
if getattr(xblock, 'category', None) == 'library_content':
block_structure.set_transformer_block_data(block_key, cls, 'content_library_children', xblock.children)
def transform(self, user_info, block_structure):
"""
Mutates block_structure and block_data based on the given user_info.
Arguments:
user_info(object)
block_structure (BlockStructureCollectedData)
"""
def check_child_removal(block_key):
"""
Check if selected block should be removed.
Block is removed if it is part of library_content, but has not been selected
for current user.
"""
if block_key not in children:
return False
if block_key in children and block_key in selected_children:
return False
return True
children = []
selected_children = []
for block_key in block_structure.get_block_keys():
library_children = block_structure.get_transformer_block_data(block_key, self, 'content_library_children')
if library_children:
children.extend(library_children)
# Retrieve "selected" json from LMS MySQL database.
modules = self._get_selected_modules(user_info.user, user_info.course_key, block_key)
for module in modules:
module_state = module.state
state_dict = json.loads(module_state)
# Check all selected entries for this user on selected library.
# Add all selected to selected_children list.
for state in state_dict['selected']:
usage_key = BlockUsageLocator(
user_info.course_key, block_type=state[0], block_id=state[1]
)
if usage_key in library_children:
selected_children.append(usage_key)
# Check and remove all non-selected children from course structure.
if not user_info.has_staff_access:
block_structure.remove_block_if(
check_child_removal
)
"""
Split Test Block Transformer, used to filter course structure per user.
"""
from openedx.core.lib.block_cache.transformer import BlockStructureTransformer
from .helpers import get_user_partition_groups
class SplitTestTransformer(BlockStructureTransformer):
"""
Split Test Transformer Class
"""
VERSION = 1
@staticmethod
def check_split_access(split_test_groups, user_groups):
"""
Check that user has access to specific split test group.
Arguments:
split_test_groups (list)
user_groups (dict[Partition Id: Group])
Returns:
bool
"""
if split_test_groups:
for _, group in user_groups.iteritems():
if group.id in split_test_groups:
return True
return False
return True
@classmethod
def collect(cls, block_structure):
"""
Computes any information for each XBlock that's necessary to execute
this transformer's transform method.
Arguments:
block_structure (BlockStructureCollectedData)
"""
# Check potential previously set values for user_partitions and split_test_partitions
xblock = block_structure.get_xblock(block_structure.root_block_key)
user_partitions = getattr(xblock, 'user_partitions', [])
split_test_partitions = getattr(xblock, 'split_test_partition', []) or []
# For each block, check if there is an split_test block.
# If split_test is found, check it's user_partition value and get children.
# Set split_test_group on each of the children for fast retrival in transform phase.
# Add same group to childrens children, because due to structure restrictions first level
# children are verticals.
for block_key in block_structure.topological_traversal():
xblock = block_structure.get_xblock(block_key)
category = getattr(xblock, 'category', None)
if category == 'split_test':
for user_partition in user_partitions:
if user_partition.id == xblock.user_partition_id:
if user_partition not in split_test_partitions:
split_test_partitions.append(user_partition)
for child in xblock.children:
for group in user_partition.groups:
child_location = xblock.group_id_to_child.get(
unicode(group.id),
None
)
if child_location == child:
block_structure.set_transformer_block_data(
child,
cls,
'split_test_groups',
[group.id]
)
for component in block_structure.get_xblock(child).children:
block_structure.set_transformer_block_data(
component,
cls,
'split_test_groups',
[group.id]
)
block_structure.set_transformer_data(cls, 'split_test_partition', split_test_partitions)
def transform(self, user_info, block_structure):
"""
Mutates block_structure and block_data based on the given user_info.
Arguments:
user_info (object)
block_structure (BlockStructureCollectedData)
"""
user_partitions = block_structure.get_transformer_data(self, 'split_test_partition')
# If there are no split test user partitions, this transformation is a no-op,
# so there is nothing to transform.
if not user_partitions:
return
user_groups = get_user_partition_groups(
user_info.course_key, user_partitions, user_info.user
)
if not user_info.has_staff_access:
block_structure.remove_block_if(
lambda block_key: not SplitTestTransformer.check_split_access(
block_structure.get_transformer_block_data(
block_key, self, 'split_test_groups', default=[]
), user_groups
)
)
......@@ -15,6 +15,7 @@ class CourseStructureTestCase(ModuleStoreTestCase):
"""
Helper for test cases that need to build course structures.
"""
blocks = []
def build_course(self, course_hierarchy):
"""
......@@ -76,6 +77,16 @@ class CourseStructureTestCase(ModuleStoreTestCase):
return block_map
def get_block_key_set(self, *refs):
"""
Gets the set of usage keys that correspond to the list of
#ref values as defined on self.blocks.
Returns: set[UsageKey]
"""
xblocks = (self.blocks[ref] for ref in refs)
return set([xblock.location for xblock in xblocks])
class BlockParentsMapTestCase(ModuleStoreTestCase):
# Tree formed by parent_map:
......@@ -137,7 +148,8 @@ class BlockParentsMapTestCase(ModuleStoreTestCase):
i in expected_accessible_blocks,
"block_structure return value {0} not equal to expected value for block {1}".format(
block_structure_result, i
))
)
)
if i in blocks_with_differing_access:
self.assertNotEqual(
......@@ -160,4 +172,4 @@ class BlockParentsMapTestCase(ModuleStoreTestCase):
return modulestore().get_item(self.xblock_keys[i])
def update_block(self, block):
return modulestore().update_item(block, 'test_user')
\ No newline at end of file
return modulestore().update_item(block, 'test_user')
"""
Tests for ContentLibraryTransformer.
"""
import mock
from student.tests.factories import UserFactory
from student.tests.factories import CourseEnrollmentFactory
from course_blocks.transformers.library_content import ContentLibraryTransformer
from course_blocks.api import get_course_blocks, clear_course_from_cache
from lms.djangoapps.course_blocks.transformers.tests.test_helpers import CourseStructureTestCase
class MockedModules(object):
"""
Object with mocked selected modules for user.
"""
def __init__(self, state):
"""
Set state attribute on initialize.
"""
self.state = state
class ContentLibraryTransformerTestCase(CourseStructureTestCase):
"""
ContentLibraryTransformer Test
"""
def setUp(self):
"""
Setup course structure and create user for content library transformer test.
"""
super(ContentLibraryTransformerTestCase, self).setUp()
# Build course.
self.course_hierarchy = self.get_test_course_hierarchy()
self.blocks = self.build_course(self.course_hierarchy)
self.course = self.blocks['course']
clear_course_from_cache(self.course.id)
# Set up user and enroll in course.
self.password = 'test'
self.user = UserFactory.create(password=self.password)
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
self.selected_modules = [MockedModules('{"selected": [["vertical", "vertical_vertical2"]]}')]
self.transformer = []
def get_test_course_hierarchy(self):
"""
Get a course hierarchy to test with.
"""
return {
'org': 'ContentLibraryTransformer',
'course': 'CL101F',
'run': 'test_run',
'#ref': 'course',
'#children': [
{
'#type': 'chapter',
'#ref': 'chapter1',
'#children': [
{
'#type': 'sequential',
'#ref': 'lesson1',
'#children': [
{
'#type': 'vertical',
'#ref': 'vertical1',
'#children': [
{
'metadata': {'category': 'library_content'},
'#type': 'library_content',
'#ref': 'library_content1',
'#children': [
{
'metadata': {'display_name': "CL Vertical 1"},
'#type': 'vertical',
'#ref': 'vertical2',
'#children': [
{
'metadata': {'display_name': "HTML1"},
'#type': 'html',
'#ref': 'html1',
}
]
},
{
'metadata': {'display_name': "CL Vertical 2"},
'#type': 'vertical',
'#ref': 'vertical3',
'#children': [
{
'metadata': {'display_name': "HTML2"},
'#type': 'html',
'#ref': 'html2',
}
]
}
]
}
],
}
],
}
],
}
]
}
def test_course_structure_with_user_course_library(self):
"""
Test course structure integrity if course has content library section.
First test user can't see any content library section,
and after that mock response from MySQL db.
Check user can see mocked sections in content library.
"""
self.transformer = ContentLibraryTransformer()
raw_block_structure = get_course_blocks(
self.user,
self.course.location,
transformers={}
)
self.assertEqual(len(list(raw_block_structure.get_block_keys())), len(self.blocks))
clear_course_from_cache(self.course.id)
trans_block_structure = get_course_blocks(
self.user,
self.course.location,
transformers={self.transformer}
)
self.assertEqual(
set(trans_block_structure.get_block_keys()),
self.get_block_key_set('course', 'chapter1', 'lesson1', 'vertical1', 'library_content1')
)
# Check course structure again, with mocked selected modules for a user.
with mock.patch(
'course_blocks.transformers.library_content.ContentLibraryTransformer._get_selected_modules',
return_value=self.selected_modules
):
clear_course_from_cache(self.course.id)
trans_block_structure = get_course_blocks(
self.user,
self.course.location,
transformers={self.transformer}
)
self.assertEqual(
set(trans_block_structure.get_block_keys()),
self.get_block_key_set(
'course',
'chapter1',
'lesson1',
'vertical1',
'library_content1',
'vertical2',
'html1'
)
)
"""
Tests for SplitTestTransformer.
"""
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
from opaque_keys.edx.keys import CourseKey
from student.tests.factories import UserFactory
from student.tests.factories import CourseEnrollmentFactory
from xmodule.modulestore.django import modulestore
from xmodule.partitions.partitions import Group, UserPartition
from course_blocks.transformers.split_test import SplitTestTransformer
from course_blocks.api import get_course_blocks, clear_course_from_cache
from lms.djangoapps.course_blocks.transformers.tests.test_helpers import CourseStructureTestCase
from course_blocks.transformers.helpers import get_user_partition_groups
class SplitTestTransformerTestCase(CourseStructureTestCase):
"""
SplitTestTransformer Test
"""
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(3, 'Group A'), Group(4, 'Group B')]
self.content_groups = [3, 4]
self.split_test_user_partition = UserPartition(
id=0,
name='Partition 2',
description='This is partition 2',
groups=self.groups,
scheme=RandomUserPartitionScheme
)
self.split_test_user_partition.scheme.name = "random"
# Build course.
self.course_hierarchy = self.get_test_course_hierarchy()
self.blocks = self.build_course(self.course_hierarchy)
self.course = self.blocks['course']
clear_course_from_cache(self.course.id)
# Set up user and enroll in course.
self.password = 'test'
self.user = UserFactory.create(password=self.password)
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
self.transformation = []
def get_test_course_hierarchy(self):
"""
Get a course hierarchy to test with.
Assumes self.split_test_user_partition has already been initialized.
Returns: dict[course_structure]
"""
return {
'org': 'SplitTestTransformer',
'course': 'ST101F',
'run': 'test_run',
'user_partitions': [self.split_test_user_partition],
'#ref': 'course',
'#children': [
{
'#type': 'chapter',
'#ref': 'chapter1',
'#children': [
{
'#type': 'sequential',
'#ref': 'lesson1',
'#children': [
{
'#type': 'vertical',
'#ref': 'vertical1',
'#children': [
{
'metadata': {'category': 'split_test'},
'user_partition_id': 0,
'group_id_to_child': {
"3": "i4x://SplitTestTransformer/ST101F/vertical/vertical_vertical2",
"4": "i4x://SplitTestTransformer/ST101F/vertical/vertical_vertical3"
},
'#type': 'split_test',
'#ref': 'split_test1',
'#children': [
{
'metadata': {'display_name': "Group ID 3"},
'#type': 'vertical',
'#ref': 'vertical2',
'#children': [
{
'metadata': {'display_name': "Group A"},
'#type': 'html',
'#ref': 'html1',
}
]
},
{
'metadata': {'display_name': "Group ID 4"},
'#type': 'vertical',
'#ref': 'vertical3',
'#children': [
{
'metadata': {'display_name': "Group A"},
'#type': 'html',
'#ref': 'html2',
}
]
}
]
}
],
}
],
}
],
}
]
}
def add_user_to_splittest_group(self, assign=True):
"""
Add user to split test, get group for him and update blocks.
"""
self.split_test_user_partition.scheme.get_group_for_user(
CourseKey.from_string(unicode(self.course.id)),
self.user,
self.split_test_user_partition,
assign=assign,
)
store = modulestore()
for __, block in self.blocks.iteritems():
block.save()
store.update_item(block, self.user.id)
def test_course_structure_with_user_split_test(self):
"""
Test course structure integrity if course has split test section
and user is not assigned to any group in user partition.
"""
self.transformation = SplitTestTransformer()
# Add user to split test.
self.add_user_to_splittest_group(assign=False)
raw_block_structure = get_course_blocks(
self.user,
self.course.location,
transformers={}
)
self.assertEqual(len(list(raw_block_structure.get_block_keys())), len(self.blocks))
clear_course_from_cache(self.course.id)
trans_block_structure = get_course_blocks(
self.user,
self.course.location,
transformers={self.transformation}
)
self.assertEqual(
set(trans_block_structure.get_block_keys()),
self.get_block_key_set('course', 'chapter1', 'lesson1', 'vertical1', 'split_test1')
)
def test_course_structure_with_user_split_test_group_assigned(self):
"""
Test course structure integrity if course has split test section
and user is assigned to any group in user partition.
"""
self.transformation = SplitTestTransformer()
# Add user to split test.
self.add_user_to_splittest_group()
raw_block_structure = get_course_blocks(
self.user,
self.course.location,
transformers={}
)
self.assertEqual(len(list(raw_block_structure.get_block_keys())), len(self.blocks))
clear_course_from_cache(self.course.id)
trans_block_structure = get_course_blocks(
self.user,
self.course.location,
transformers={self.transformation}
)
user_groups = get_user_partition_groups(
self.course.id, [self.split_test_user_partition], self.user
)
for group in user_groups.itervalues():
if group.id == 3:
self.assertEqual(
set(trans_block_structure.get_block_keys()),
self.get_block_key_set(
'course',
'chapter1',
'lesson1',
'vertical1',
'split_test1',
'vertical2',
'html1'
)
)
else:
self.assertEqual(
set(trans_block_structure.get_block_keys()),
self.get_block_key_set(
'course',
'chapter1',
'lesson1',
'vertical1',
'split_test1',
'vertical3',
'html2'
)
)
"""
Tests for UserPartitionTransformation.
Tests for UserPartitionTransformer.
"""
from openedx.core.djangoapps.course_groups.partition_scheme import CohortPartitionScheme
......@@ -8,20 +8,21 @@ from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort
from openedx.core.djangoapps.course_groups.views import link_cohort_to_partition_group
from student.tests.factories import UserFactory
from student.tests.factories import CourseEnrollmentFactory
from xmodule.modulestore.django import modulestore
from xmodule.partitions.partitions import Group, UserPartition
from course_blocks.transformers.user_partitions import UserPartitionTransformer
from course_blocks.api import get_course_blocks, clear_course_from_cache
from test_helpers import CourseStructureTestCase
from lms.djangoapps.course_blocks.transformers.tests.test_helpers import CourseStructureTestCase
class UserPartitionTransformerTestCase(CourseStructureTestCase):
"""
...
UserPartitionTransformer Test
"""
def setUp(self):
"""
Setup course structure and create user for user partition transformer test.
"""
super(UserPartitionTransformerTestCase, self).setUp()
# Set up user partitions and groups.
......@@ -73,8 +74,8 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase):
'metadata': {
'group_access': {0: [0, 1, 2]},
},
'#type': 'sequential',
'#ref': 'lesson1',
'#type': 'sequential',
'#ref': 'lesson1',
'#children': [
{
'#type': 'vertical',
......@@ -110,17 +111,11 @@ class UserPartitionTransformerTestCase(CourseStructureTestCase):
group.id,
)
def get_block_key_set(self, *refs):
def test_course_structure_with_user_partition(self):
"""
Gets the set of usage keys that correspond to the list of
#ref values as defined on self.blocks.
Returns: set[UsageKey]
Test course structure integrity if course has user partition section
and user is assigned to group in user partition.
"""
xblocks = (self.blocks[ref] for ref in refs)
return set([xblock.location for xblock in xblocks])
def test_course_structure_with_user_partition(self):
self.transformation = UserPartitionTransformer()
raw_block_structure = get_course_blocks(
......
"""
...
User Partitions Transformer, used to filter course structure per user.
"""
from courseware.access import _has_access_to_course
from openedx.core.lib.block_cache.transformer import BlockStructureTransformer
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
from openedx.core.djangoapps.course_groups.partition_scheme import CohortPartitionScheme
# TODO 8874: Make it so we support all schemes instead of manually declaring them here.
INCLUDE_SCHEMES = [CohortPartitionScheme, RandomUserPartitionScheme,]
SCHEME_SUPPORTS_ASSIGNMENT = [RandomUserPartitionScheme,]
from .helpers import get_user_partition_groups
class MergedGroupAccess(object):
"""
...
"""
# TODO 8874: Make it so LmsBlockMixin.merged_group_access use MergedGroupAccess
def __init__(self, user_partitions, xblock, merged_parent_access_list):
"""
Arguments:
......@@ -98,7 +90,6 @@ class MergedGroupAccess(object):
for partition_id, allowed_group_ids in self._access.iteritems():
# If the user is not assigned to a group for this partition, deny access.
# TODO 8874: Ensure that denying access to users who aren't in a group is the correct action.
if partition_id not in user_groups:
return False
......@@ -122,49 +113,14 @@ class UserPartitionTransformer(BlockStructureTransformer):
"""
VERSION = 1
@staticmethod
def _get_user_partition_groups(course_key, user_partitions, user):
"""
Collect group ID for each partition in this course for this user.
Arguments:
course_key (CourseKey)
user_partitions (list[UserPartition])
user (User)
Returns:
dict[int: Group]: Mapping from user partitions to the group to which
the user belongs in each partition. If the user isn't in a group
for a particular partition, then that partition's ID will not be
in the dict.
"""
partition_groups = {}
for partition in user_partitions:
if partition.scheme not in INCLUDE_SCHEMES:
continue
group = partition.scheme.get_group_for_user(
course_key,
user,
partition,
**({'assign': False} if partition.scheme in SCHEME_SUPPORTS_ASSIGNMENT else {})
)
if group is not None:
partition_groups[partition.id] = group
return partition_groups
@classmethod
def collect(cls, block_structure):
"""
Computes any information for each XBlock that's necessary to execute
this transformation's apply method.
this transformer's transform method.
Arguments:
course_key (CourseKey)
block_structure (BlockStructure)
xblock_dict (dict[UsageKey: XBlock])
Returns:
dict[UsageKey: dict]
block_structure (BlockStructureCollectedData)
"""
# Because user partitions are course-wide, only store data for them on the root block.
root_block = block_structure.get_xblock(block_structure.root_block_key)
......@@ -192,21 +148,21 @@ class UserPartitionTransformer(BlockStructureTransformer):
def transform(self, user_info, block_structure):
"""
Mutates block_structure and block_data based on the given user_info.
Arguments:
user_info (object)
block_structure (BlockStructureCollectedData)
"""
# TODO 8874: Factor out functionality of UserPartitionTransformation.apply and access._has_group_access into a common utility function.
user_partitions = block_structure.get_transformer_data(self, 'user_partitions')
# If there are no user partitions, this transformation is a no-op,
# so there is nothing to apply.
if not user_partitions:
if not user_partitions or user_info.has_staff_access:
return
user_groups = self._get_user_partition_groups(
user_groups = get_user_partition_groups(
user_info.course_key, user_partitions, user_info.user
)
if not user_info.has_staff_access:
block_structure.remove_block_if(
lambda block_key: not block_structure.get_transformer_block_data(
block_key, self, 'merged_group_access'
).check_group_access(user_groups)
)
block_structure.remove_block_if(
lambda block_key: not block_structure.get_transformer_block_data(
block_key, self, 'merged_group_access'
).check_group_access(user_groups)
)
......@@ -52,6 +52,8 @@ setup(
"visibility = lms.djangoapps.course_blocks.transformers.visibility:VisibilityTransformer",
"start_date = lms.djangoapps.course_blocks.transformers.start_date:StartDateTransformer",
"user_partitions = lms.djangoapps.course_blocks.transformers.user_partitions:UserPartitionTransformer",
"split_test = lms.djangoapps.course_blocks.transformers.split_test:SplitTestTransformer",
"library_content = lms.djangoapps.course_blocks.transformers.library_content:ContentLibraryTransformer",
],
}
)
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