Transformer: BlockNavigationTransformer
This commit is contained in:
committed by
J. Cliff Dyer
parent
9abebe0599
commit
ced7fde37f
92
lms/djangoapps/course_api/blocks/transformers/navigation.py
Normal file
92
lms/djangoapps/course_api/blocks/transformers/navigation.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
TODO
|
||||
"""
|
||||
from openedx.core.lib.block_cache.transformer import BlockStructureTransformer
|
||||
from .block_depth import BlockDepthTransformer
|
||||
|
||||
|
||||
class DescendantList(object):
|
||||
"""
|
||||
Contain
|
||||
"""
|
||||
def __init__(self):
|
||||
self.items = []
|
||||
|
||||
|
||||
class BlockNavigationTransformer(BlockStructureTransformer):
|
||||
"""
|
||||
Creates a table of contents for the course.
|
||||
|
||||
Prerequisites: BlockDepthTransformer must be run before this in the
|
||||
transform phase.
|
||||
"""
|
||||
VERSION = 1
|
||||
BLOCK_NAVIGATION = 'block_nav'
|
||||
BLOCK_NAVIGATION_FOR_CHILDREN = 'children_block_nav'
|
||||
|
||||
def __init__(self, nav_depth):
|
||||
self.nav_depth = nav_depth
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
return "blocks_api:block_navigation"
|
||||
|
||||
@classmethod
|
||||
def collect(cls, block_structure):
|
||||
"""
|
||||
Collects any information that's necessary to execute this transformer's
|
||||
transform method.
|
||||
"""
|
||||
# collect basic xblock fields
|
||||
block_structure.request_xblock_fields('hide_from_toc')
|
||||
|
||||
def transform(self, usage_info, block_structure): # pylint: disable=unused-argument
|
||||
"""
|
||||
Mutates block_structure based on the given usage_info.
|
||||
"""
|
||||
if self.nav_depth is None:
|
||||
return
|
||||
|
||||
for block_key in block_structure.topological_traversal():
|
||||
|
||||
parents = block_structure.get_parents(block_key)
|
||||
parents_descendants_list = set()
|
||||
for parent_key in parents:
|
||||
parent_nav = block_structure.get_transformer_block_field(
|
||||
parent_key,
|
||||
self,
|
||||
self.BLOCK_NAVIGATION_FOR_CHILDREN,
|
||||
)
|
||||
if parent_nav is not None:
|
||||
parents_descendants_list |= parent_nav
|
||||
|
||||
children_descendants_list = None
|
||||
if (
|
||||
not block_structure.get_xblock_field(block_key, 'hide_from_toc', False) and (
|
||||
not parents or
|
||||
any(parent_desc_list is not None for parent_desc_list in parents_descendants_list)
|
||||
)
|
||||
):
|
||||
# add self to parent's descendants
|
||||
for parent_desc_list in parents_descendants_list:
|
||||
if parent_desc_list is not None:
|
||||
parent_desc_list.items.append(unicode(block_key))
|
||||
|
||||
if BlockDepthTransformer.get_block_depth(block_structure, block_key) > self.nav_depth:
|
||||
children_descendants_list = parents_descendants_list
|
||||
else:
|
||||
block_nav_list = DescendantList()
|
||||
children_descendants_list = {block_nav_list}
|
||||
block_structure.set_transformer_block_field(
|
||||
block_key,
|
||||
self,
|
||||
self.BLOCK_NAVIGATION,
|
||||
block_nav_list.items
|
||||
)
|
||||
|
||||
block_structure.set_transformer_block_field(
|
||||
block_key,
|
||||
self,
|
||||
self.BLOCK_NAVIGATION_FOR_CHILDREN,
|
||||
children_descendants_list
|
||||
)
|
||||
@@ -0,0 +1,122 @@
|
||||
# pylint: disable=protected-access
|
||||
"""
|
||||
Tests for BlockNavigationTransformer.
|
||||
"""
|
||||
import ddt
|
||||
from unittest import TestCase
|
||||
|
||||
from lms.djangoapps.course_api.blocks.transformers.block_depth import BlockDepthTransformer
|
||||
from lms.djangoapps.course_api.blocks.transformers.navigation import BlockNavigationTransformer
|
||||
from openedx.core.lib.block_cache.tests.test_utils import ChildrenMapTestMixin
|
||||
from openedx.core.lib.block_cache.block_structure import BlockStructureModulestoreData
|
||||
from openedx.core.lib.block_cache.block_structure_factory import BlockStructureFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import SampleCourseFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class BlockNavigationTransformerTestCase(TestCase, ChildrenMapTestMixin):
|
||||
"""
|
||||
Course-agnostic test class for testing the Navigation transformer.
|
||||
"""
|
||||
|
||||
@ddt.data(
|
||||
(0, 0, [], []),
|
||||
|
||||
(0, 0, ChildrenMapTestMixin.LINEAR_CHILDREN_MAP, [[], [], [], []]),
|
||||
(None, 0, ChildrenMapTestMixin.LINEAR_CHILDREN_MAP, [[1, 2, 3], [], [], []]),
|
||||
(None, 1, ChildrenMapTestMixin.LINEAR_CHILDREN_MAP, [[1], [2, 3], [], []]),
|
||||
(None, 2, ChildrenMapTestMixin.LINEAR_CHILDREN_MAP, [[1], [2], [3], []]),
|
||||
(None, 3, ChildrenMapTestMixin.LINEAR_CHILDREN_MAP, [[1], [2], [3], []]),
|
||||
(None, 4, ChildrenMapTestMixin.LINEAR_CHILDREN_MAP, [[1], [2], [3], []]),
|
||||
(1, 4, ChildrenMapTestMixin.LINEAR_CHILDREN_MAP, [[1], [], [], []]),
|
||||
(2, 4, ChildrenMapTestMixin.LINEAR_CHILDREN_MAP, [[1], [2], [], []]),
|
||||
|
||||
(0, 0, ChildrenMapTestMixin.DAG_CHILDREN_MAP, [[], [], [], [], [], [], []]),
|
||||
(0, 0, ChildrenMapTestMixin.DAG_CHILDREN_MAP, [[], [], [], [], [], [], []]),
|
||||
(None, 0, ChildrenMapTestMixin.DAG_CHILDREN_MAP, [[1, 2, 3, 4, 5, 6], [], [], [], [], [], []]),
|
||||
(None, 1, ChildrenMapTestMixin.DAG_CHILDREN_MAP, [[1, 2], [3, 5, 6], [3, 4, 5, 6], [], [], [], []]),
|
||||
(None, 2, ChildrenMapTestMixin.DAG_CHILDREN_MAP, [[1, 2], [3], [3, 4], [5, 6], [], [], []]),
|
||||
(None, 3, ChildrenMapTestMixin.DAG_CHILDREN_MAP, [[1, 2], [3], [3, 4], [5, 6], [], [], []]),
|
||||
(None, 4, ChildrenMapTestMixin.DAG_CHILDREN_MAP, [[1, 2], [3], [3, 4], [5, 6], [], [], []]),
|
||||
(1, 4, ChildrenMapTestMixin.DAG_CHILDREN_MAP, [[1, 2], [], [], [], [], [], []]),
|
||||
(2, 4, ChildrenMapTestMixin.DAG_CHILDREN_MAP, [[1, 2], [3], [3, 4], [], [], [], []]),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_navigation(self, depth, nav_depth, children_map, expected_nav_map):
|
||||
|
||||
block_structure = self.create_block_structure(BlockStructureModulestoreData, children_map)
|
||||
BlockDepthTransformer(depth).transform(usage_info=None, block_structure=block_structure)
|
||||
BlockNavigationTransformer(nav_depth).transform(usage_info=None, block_structure=block_structure)
|
||||
block_structure._prune_unreachable()
|
||||
|
||||
for block_key, expected_nav in enumerate(expected_nav_map):
|
||||
self.assertSetEqual(
|
||||
set(unicode(block) for block in expected_nav),
|
||||
set(
|
||||
block_structure.get_transformer_block_field(
|
||||
block_key,
|
||||
BlockNavigationTransformer,
|
||||
BlockNavigationTransformer.BLOCK_NAVIGATION,
|
||||
[]
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class BlockNavigationTransformerCourseTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Uses SampleCourseFactory and Modulestore to test the Navigation transformer,
|
||||
specifically to test enforcement of the hide_from_toc field
|
||||
"""
|
||||
|
||||
def test_hide_from_toc(self):
|
||||
course_key = SampleCourseFactory.create().id
|
||||
course_usage_key = self.store.make_course_usage_key(course_key)
|
||||
|
||||
# hide chapter_x from TOC
|
||||
chapter_x_key = course_key.make_usage_key('chapter', 'chapter_x')
|
||||
chapter_x = self.store.get_item(chapter_x_key)
|
||||
chapter_x.hide_from_toc = True
|
||||
self.store.update_item(chapter_x, ModuleStoreEnum.UserID.test)
|
||||
|
||||
block_structure = BlockStructureFactory.create_from_modulestore(course_usage_key, self.store)
|
||||
|
||||
# collect phase
|
||||
BlockDepthTransformer.collect(block_structure)
|
||||
BlockNavigationTransformer.collect(block_structure)
|
||||
block_structure._collect_requested_xblock_fields()
|
||||
|
||||
self.assertTrue(block_structure.has_block(chapter_x_key))
|
||||
|
||||
# transform phase
|
||||
BlockDepthTransformer().transform(usage_info=None, block_structure=block_structure)
|
||||
BlockNavigationTransformer(0).transform(usage_info=None, block_structure=block_structure)
|
||||
block_structure._prune_unreachable()
|
||||
|
||||
self.assertTrue(block_structure.has_block(chapter_x_key))
|
||||
|
||||
course_descendants = block_structure.get_transformer_block_field(
|
||||
course_usage_key,
|
||||
BlockNavigationTransformer,
|
||||
BlockNavigationTransformer.BLOCK_NAVIGATION,
|
||||
)
|
||||
|
||||
# chapter_y and its descendants should be included
|
||||
for block_key in [
|
||||
course_key.make_usage_key('chapter', 'chapter_y'),
|
||||
course_key.make_usage_key('sequential', 'sequential_y1'),
|
||||
course_key.make_usage_key('vertical', 'vertical_y1a'),
|
||||
course_key.make_usage_key('problem', 'problem_y1a_1'),
|
||||
]:
|
||||
self.assertIn(unicode(block_key), course_descendants)
|
||||
|
||||
# chapter_x and its descendants should not be included
|
||||
for block_key in [
|
||||
chapter_x_key,
|
||||
course_key.make_usage_key('sequential', 'sequential_x1'),
|
||||
course_key.make_usage_key('vertical', 'vertical_x1a'),
|
||||
course_key.make_usage_key('problem', 'problem_x1a_1'),
|
||||
]:
|
||||
self.assertNotIn(unicode(block_key), course_descendants)
|
||||
Reference in New Issue
Block a user