Commit 69410948 by Nimisha Asthagiri Committed by Eric Fischer

Reduce number of transformer traversals (#12881)

This change improves grading performance by combining what had previously been
multiple course tree traversals into a single traversal with combined
filtering logic.
parent b918b4da
...@@ -58,6 +58,6 @@ class BlockDepthTransformer(BlockStructureTransformer): ...@@ -58,6 +58,6 @@ class BlockDepthTransformer(BlockStructureTransformer):
) )
if self.requested_depth is not None: if self.requested_depth is not None:
block_structure.remove_block_if( block_structure.remove_block_traversal(
lambda block_key: self.get_block_depth(block_structure, block_key) > self.requested_depth lambda block_key: self.get_block_depth(block_structure, block_key) > self.requested_depth
) )
...@@ -6,10 +6,10 @@ from django.conf import settings ...@@ -6,10 +6,10 @@ from django.conf import settings
from edx_proctoring.api import get_attempt_status_summary from edx_proctoring.api import get_attempt_status_summary
from edx_proctoring.models import ProctoredExamStudentAttemptStatus from edx_proctoring.models import ProctoredExamStudentAttemptStatus
from openedx.core.lib.block_structure.transformer import BlockStructureTransformer from openedx.core.lib.block_structure.transformer import BlockStructureTransformer, FilteringTransformerMixin
class ProctoredExamTransformer(BlockStructureTransformer): class ProctoredExamTransformer(FilteringTransformerMixin, BlockStructureTransformer):
""" """
Exclude proctored exams unless the user is not a verified student or has Exclude proctored exams unless the user is not a verified student or has
declined taking the exam. declined taking the exam.
...@@ -33,12 +33,9 @@ class ProctoredExamTransformer(BlockStructureTransformer): ...@@ -33,12 +33,9 @@ class ProctoredExamTransformer(BlockStructureTransformer):
block_structure.request_xblock_fields('is_proctored_enabled') block_structure.request_xblock_fields('is_proctored_enabled')
block_structure.request_xblock_fields('is_practice_exam') block_structure.request_xblock_fields('is_practice_exam')
def transform(self, usage_info, block_structure): def transform_block_filters(self, usage_info, block_structure):
"""
Mutates block_structure based on the given usage_info.
"""
if not settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False): if not settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False):
return return [block_structure.create_universal_filter()]
def is_proctored_exam_for_user(block_key): def is_proctored_exam_for_user(block_key):
""" """
...@@ -60,4 +57,4 @@ class ProctoredExamTransformer(BlockStructureTransformer): ...@@ -60,4 +57,4 @@ class ProctoredExamTransformer(BlockStructureTransformer):
) )
return user_exam_summary and user_exam_summary['status'] != ProctoredExamStudentAttemptStatus.declined return user_exam_summary and user_exam_summary['status'] != ProctoredExamStudentAttemptStatus.declined
block_structure.remove_block_if(is_proctored_exam_for_user) return [block_structure.create_removal_filter(is_proctored_exam_for_user)]
...@@ -3,13 +3,13 @@ Content Library Transformer. ...@@ -3,13 +3,13 @@ Content Library Transformer.
""" """
import json import json
from courseware.models import StudentModule from courseware.models import StudentModule
from openedx.core.lib.block_structure.transformer import BlockStructureTransformer from openedx.core.lib.block_structure.transformer import BlockStructureTransformer, FilteringTransformerMixin
from xmodule.library_content_module import LibraryContentModule from xmodule.library_content_module import LibraryContentModule
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from eventtracking import tracker from eventtracking import tracker
class ContentLibraryTransformer(BlockStructureTransformer): class ContentLibraryTransformer(FilteringTransformerMixin, BlockStructureTransformer):
""" """
A transformer that manipulates the block structure by removing all A transformer that manipulates the block structure by removing all
blocks within a library_content module to which a user should not blocks within a library_content module to which a user should not
...@@ -59,17 +59,12 @@ class ContentLibraryTransformer(BlockStructureTransformer): ...@@ -59,17 +59,12 @@ class ContentLibraryTransformer(BlockStructureTransformer):
summary = summarize_block(child_key) summary = summarize_block(child_key)
block_structure.set_transformer_block_field(child_key, cls, 'block_analytics_summary', summary) block_structure.set_transformer_block_field(child_key, cls, 'block_analytics_summary', summary)
def transform(self, usage_info, block_structure): def transform_block_filters(self, usage_info, block_structure):
"""
Mutates block_structure based on the given usage_info.
"""
all_library_children = set() all_library_children = set()
all_selected_children = set() all_selected_children = set()
for block_key in block_structure.topological_traversal( for block_key in block_structure:
filter_func=lambda block_key: block_key.block_type == 'library_content', if block_key.block_type != 'library_content':
yield_descendants_of_unyielded=True, continue
):
library_children = block_structure.get_children(block_key) library_children = block_structure.get_children(block_key)
if library_children: if library_children:
all_library_children.update(library_children) all_library_children.update(library_children)
...@@ -110,11 +105,7 @@ class ContentLibraryTransformer(BlockStructureTransformer): ...@@ -110,11 +105,7 @@ class ContentLibraryTransformer(BlockStructureTransformer):
return False return False
return True return True
# Check and remove all non-selected children from course return [block_structure.create_removal_filter(check_child_removal)]
# structure.
block_structure.remove_block_if(
check_child_removal
)
@classmethod @classmethod
def _get_student_module(cls, user, course_key, block_key): def _get_student_module(cls, user, course_key, block_key):
......
""" """
Split Test Block Transformer Split Test Block Transformer
""" """
from openedx.core.lib.block_structure.transformer import BlockStructureTransformer from openedx.core.lib.block_structure.transformer import BlockStructureTransformer, FilteringTransformerMixin
class SplitTestTransformer(BlockStructureTransformer): class SplitTestTransformer(FilteringTransformerMixin, BlockStructureTransformer):
""" """
A nested transformer of the UserPartitionTransformer that honors the A nested transformer of the UserPartitionTransformer that honors the
block structure pathways created by split_test modules. block structure pathways created by split_test modules.
...@@ -68,14 +68,16 @@ class SplitTestTransformer(BlockStructureTransformer): ...@@ -68,14 +68,16 @@ class SplitTestTransformer(BlockStructureTransformer):
group = child_to_group.get(child_location, None) group = child_to_group.get(child_location, None)
child.group_access[partition_for_this_block.id] = [group] if group is not None else [] child.group_access[partition_for_this_block.id] = [group] if group is not None else []
def transform(self, usage_info, block_structure): def transform_block_filters(self, usage_info, block_structure):
""" """
Mutates block_structure based on the given usage_info. Mutates block_structure based on the given usage_info.
""" """
# The UserPartitionTransformer will enforce group access, so # The UserPartitionTransformer will enforce group access, so
# go ahead and remove all extraneous split_test modules. # go ahead and remove all extraneous split_test modules.
block_structure.remove_block_if( return [
lambda block_key: block_key.block_type == 'split_test', block_structure.create_removal_filter(
keep_descendants=True, lambda block_key: block_key.block_type == 'split_test',
) keep_descendants=True,
)
]
""" """
Start Date Transformer implementation. Start Date Transformer implementation.
""" """
from openedx.core.lib.block_structure.transformer import BlockStructureTransformer from openedx.core.lib.block_structure.transformer import BlockStructureTransformer, FilteringTransformerMixin
from lms.djangoapps.courseware.access_utils import check_start_date from lms.djangoapps.courseware.access_utils import check_start_date
from xmodule.course_metadata_utils import DEFAULT_START_DATE from xmodule.course_metadata_utils import DEFAULT_START_DATE
from .utils import get_field_on_block from .utils import get_field_on_block
class StartDateTransformer(BlockStructureTransformer): class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer):
""" """
A transformer that enforces the 'start' and 'days_early_for_beta' A transformer that enforces the 'start' and 'days_early_for_beta'
fields on blocks by removing blocks from the block structure for fields on blocks by removing blocks from the block structure for
...@@ -83,19 +83,15 @@ class StartDateTransformer(BlockStructureTransformer): ...@@ -83,19 +83,15 @@ class StartDateTransformer(BlockStructureTransformer):
merged_start_value merged_start_value
) )
def transform(self, usage_info, block_structure): def transform_block_filters(self, usage_info, block_structure):
"""
Mutates block_structure based on the given usage_info.
"""
# Users with staff access bypass the Start Date check. # Users with staff access bypass the Start Date check.
if usage_info.has_staff_access: if usage_info.has_staff_access:
return return [block_structure.create_universal_filter()]
block_structure.remove_block_if( removal_condition = lambda block_key: not check_start_date(
lambda block_key: not check_start_date( usage_info.user,
usage_info.user, block_structure.get_xblock_field(block_key, 'days_early_for_beta'),
block_structure.get_xblock_field(block_key, 'days_early_for_beta'), self.get_merged_start_date(block_structure, block_key),
self.get_merged_start_date(block_structure, block_key), usage_info.course_key,
usage_info.course_key,
)
) )
return [block_structure.create_removal_filter(removal_condition)]
""" """
User Partitions Transformer User Partitions Transformer
""" """
from openedx.core.lib.block_structure.transformer import BlockStructureTransformer from openedx.core.lib.block_structure.transformer import BlockStructureTransformer, FilteringTransformerMixin
from .split_test import SplitTestTransformer from .split_test import SplitTestTransformer
from .utils import get_field_on_block from .utils import get_field_on_block
class UserPartitionTransformer(BlockStructureTransformer): class UserPartitionTransformer(FilteringTransformerMixin, BlockStructureTransformer):
""" """
A transformer that enforces the group access rules on course blocks, A transformer that enforces the group access rules on course blocks,
by honoring their user_partitions and group_access fields, and by honoring their user_partitions and group_access fields, and
...@@ -64,26 +64,25 @@ class UserPartitionTransformer(BlockStructureTransformer): ...@@ -64,26 +64,25 @@ class UserPartitionTransformer(BlockStructureTransformer):
merged_group_access = _MergedGroupAccess(user_partitions, xblock, merged_parent_access_list) merged_group_access = _MergedGroupAccess(user_partitions, xblock, merged_parent_access_list)
block_structure.set_transformer_block_field(block_key, cls, 'merged_group_access', merged_group_access) block_structure.set_transformer_block_field(block_key, cls, 'merged_group_access', merged_group_access)
def transform(self, usage_info, block_structure): def transform_block_filters(self, usage_info, block_structure):
""" result_list = SplitTestTransformer().transform_block_filters(usage_info, block_structure)
Mutates block_structure based on the given usage_info.
"""
SplitTestTransformer().transform(usage_info, block_structure)
user_partitions = block_structure.get_transformer_data(self, 'user_partitions') user_partitions = block_structure.get_transformer_data(self, 'user_partitions')
if not user_partitions: if not user_partitions:
return return [block_structure.create_universal_filter()]
user_groups = _get_user_partition_groups( user_groups = _get_user_partition_groups(
usage_info.course_key, user_partitions, usage_info.user usage_info.course_key, user_partitions, usage_info.user
) )
block_structure.remove_block_if( group_access_filter = block_structure.create_removal_filter(
lambda block_key: not block_structure.get_transformer_block_field( lambda block_key: not block_structure.get_transformer_block_field(
block_key, self, 'merged_group_access' block_key, self, 'merged_group_access'
).check_group_access(user_groups) ).check_group_access(user_groups)
) )
result_list.append(group_access_filter)
return result_list
class _MergedGroupAccess(object): class _MergedGroupAccess(object):
""" """
......
""" """
Visibility Transformer implementation. Visibility Transformer implementation.
""" """
from openedx.core.lib.block_structure.transformer import BlockStructureTransformer from openedx.core.lib.block_structure.transformer import BlockStructureTransformer, FilteringTransformerMixin
class VisibilityTransformer(BlockStructureTransformer): class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer):
""" """
A transformer that enforces the visible_to_staff_only field on A transformer that enforces the visible_to_staff_only field on
blocks by removing blocks from the block structure for which the blocks by removing blocks from the block structure for which the
...@@ -67,14 +67,13 @@ class VisibilityTransformer(BlockStructureTransformer): ...@@ -67,14 +67,13 @@ class VisibilityTransformer(BlockStructureTransformer):
) )
) )
def transform(self, usage_info, block_structure): def transform_block_filters(self, usage_info, block_structure):
"""
Mutates block_structure based on the given usage_info.
"""
# Users with staff access bypass the Visibility check. # Users with staff access bypass the Visibility check.
if usage_info.has_staff_access: if usage_info.has_staff_access:
return return [block_structure.create_universal_filter()]
block_structure.remove_block_if( return [
lambda block_key: self.get_visible_to_staff_only(block_structure, block_key) block_structure.create_removal_filter(
) lambda block_key: self.get_visible_to_staff_only(block_structure, block_key),
)
]
...@@ -8,6 +8,7 @@ The following internal data structures are implemented: ...@@ -8,6 +8,7 @@ The following internal data structures are implemented:
_BlockRelations - Data structure for a single block's relations. _BlockRelations - Data structure for a single block's relations.
_BlockData - Data structure for a single block's data. _BlockData - Data structure for a single block's data.
""" """
from functools import partial
from logging import getLogger from logging import getLogger
from openedx.core.lib.graph_traversals import traverse_topologically, traverse_post_order from openedx.core.lib.graph_traversals import traverse_topologically, traverse_post_order
...@@ -608,11 +609,57 @@ class BlockStructureBlockData(BlockStructure): ...@@ -608,11 +609,57 @@ class BlockStructureBlockData(BlockStructure):
for parent in parents: for parent in parents:
self._add_relation(parent, child) self._add_relation(parent, child)
def remove_block_if(self, removal_condition, keep_descendants=False, **kwargs): def create_universal_filter(self):
"""
Returns a filter function that always returns True for all blocks.
"""
return lambda block_key: True
def create_removal_filter(self, removal_condition, keep_descendants=False):
"""
Returns a filter function that automatically removes blocks that satisfy
the removal_condition.
Arguments:
removal_condition ((usage_key)->bool) - A function that
takes a block's usage key as input and returns whether
or not to remove that block from the block structure.
keep_descendants (bool) - See the description in
remove_block.
"""
return partial(
self.retain_or_remove,
removal_condition=removal_condition,
keep_descendants=keep_descendants,
)
def retain_or_remove(self, block_key, removal_condition, keep_descendants=False):
"""
Removes the given block if it satisfies the removal_condition.
Returns True if the block was retained, and False if the block
was removed.
Arguments:
block_key (usage_key) - Usage key of the block.
removal_condition ((usage_key)->bool) - A function that
takes a block's usage key as input and returns whether
or not to remove that block from the block structure.
keep_descendants (bool) - See the description in
remove_block.
"""
if removal_condition(block_key):
self.remove_block(block_key, keep_descendants)
return False
return True
def remove_block_traversal(self, removal_condition, keep_descendants=False):
""" """
A higher-order function that traverses the block structure A higher-order function that traverses the block structure
using topological sort and removes any blocks encountered that using topological sort and removes all blocks satisfying the given
satisfy the removal_condition. removal_condition.
Arguments: Arguments:
removal_condition ((usage_key)->bool) - A function that removal_condition ((usage_key)->bool) - A function that
...@@ -621,19 +668,26 @@ class BlockStructureBlockData(BlockStructure): ...@@ -621,19 +668,26 @@ class BlockStructureBlockData(BlockStructure):
keep_descendants (bool) - See the description in keep_descendants (bool) - See the description in
remove_block. remove_block.
"""
self.filter_topological_traversal(
filter_func=self.create_removal_filter(
removal_condition, keep_descendants
)
)
def filter_topological_traversal(self, filter_func, **kwargs):
"""
A higher-order function that traverses the block structure
using topological sort and applies the given filter.
Arguments:
filter_func ((usage_key)->bool) - Function that returns
whether or not to yield the given block key.
If None, the True function is assumed.
kwargs (dict) - Optional keyword arguments to be forwarded kwargs (dict) - Optional keyword arguments to be forwarded
to topological_traversal. to topological_traversal.
""" """
def filter_func(block_key):
"""
Filter function for removing blocks that satisfy the
removal_condition.
"""
if removal_condition(block_key):
self.remove_block(block_key, keep_descendants)
return False
return True
# Note: For optimization, we remove blocks using the filter # Note: For optimization, we remove blocks using the filter
# function, since the graph traversal method can skip over # function, since the graph traversal method can skip over
......
...@@ -6,7 +6,7 @@ from mock import patch ...@@ -6,7 +6,7 @@ from mock import patch
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from ..block_structure import BlockStructureBlockData from ..block_structure import BlockStructureBlockData
from ..transformer import BlockStructureTransformer from ..transformer import BlockStructureTransformer, FilteringTransformerMixin
class MockXBlock(object): class MockXBlock(object):
...@@ -147,6 +147,21 @@ class MockTransformer(BlockStructureTransformer): ...@@ -147,6 +147,21 @@ class MockTransformer(BlockStructureTransformer):
pass pass
class MockFilteringTransformer(FilteringTransformerMixin, BlockStructureTransformer):
"""
A mock FilteringTransformerMixin class.
"""
VERSION = 1
@classmethod
def name(cls):
# Use the class' name for Mock transformers.
return cls.__name__
def transform_block_filters(self, usage_info, block_structure):
return [block_structure.create_universal_filter()]
@contextmanager @contextmanager
def mock_registered_transformers(transformers): def mock_registered_transformers(transformers):
""" """
......
...@@ -217,7 +217,7 @@ class TestBlockStructureData(TestCase, ChildrenMapTestMixin): ...@@ -217,7 +217,7 @@ class TestBlockStructureData(TestCase, ChildrenMapTestMixin):
self.assert_block_structure(block_structure, pruned_children_map, missing_blocks) self.assert_block_structure(block_structure, pruned_children_map, missing_blocks)
def test_remove_block_if(self): def test_remove_block_traversal(self):
block_structure = self.create_block_structure(ChildrenMapTestMixin.LINEAR_CHILDREN_MAP) block_structure = self.create_block_structure(ChildrenMapTestMixin.LINEAR_CHILDREN_MAP)
block_structure.remove_block_if(lambda block: block == 2) block_structure.remove_block_traversal(lambda block: block == 2)
self.assert_block_structure(block_structure, [[1], [], [], []], missing_blocks=[2]) self.assert_block_structure(block_structure, [[1], [], [], []], missing_blocks=[2])
...@@ -9,7 +9,7 @@ from ..block_structure import BlockStructureModulestoreData ...@@ -9,7 +9,7 @@ from ..block_structure import BlockStructureModulestoreData
from ..exceptions import TransformerException from ..exceptions import TransformerException
from ..transformers import BlockStructureTransformers from ..transformers import BlockStructureTransformers
from .helpers import ( from .helpers import (
ChildrenMapTestMixin, MockTransformer, mock_registered_transformers ChildrenMapTestMixin, MockTransformer, MockFilteringTransformer, mock_registered_transformers
) )
...@@ -27,7 +27,7 @@ class TestBlockStructureTransformers(ChildrenMapTestMixin, TestCase): ...@@ -27,7 +27,7 @@ class TestBlockStructureTransformers(ChildrenMapTestMixin, TestCase):
def setUp(self): def setUp(self):
super(TestBlockStructureTransformers, self).setUp() super(TestBlockStructureTransformers, self).setUp()
self.transformers = BlockStructureTransformers(usage_info=MagicMock()) self.transformers = BlockStructureTransformers(usage_info=MagicMock())
self.registered_transformers = [MockTransformer] self.registered_transformers = [MockTransformer(), MockFilteringTransformer()]
def add_mock_transformer(self): def add_mock_transformer(self):
""" """
...@@ -38,13 +38,21 @@ class TestBlockStructureTransformers(ChildrenMapTestMixin, TestCase): ...@@ -38,13 +38,21 @@ class TestBlockStructureTransformers(ChildrenMapTestMixin, TestCase):
def test_add_registered(self): def test_add_registered(self):
self.add_mock_transformer() self.add_mock_transformer()
self.assertIn(MockTransformer, self.transformers._transformers) # pylint: disable=protected-access self.assertIn(
self.registered_transformers[0],
self.transformers._transformers['no_filter'] # pylint: disable=protected-access
)
self.assertIn(
self.registered_transformers[1],
self.transformers._transformers['supports_filter'] # pylint: disable=protected-access
)
def test_add_unregistered(self): def test_add_unregistered(self):
with self.assertRaises(TransformerException): with self.assertRaises(TransformerException):
self.transformers += [self.UnregisteredTransformer] self.transformers += [self.UnregisteredTransformer()]
self.assertEquals(self.transformers._transformers, []) # pylint: disable=protected-access self.assertEquals(self.transformers._transformers['no_filter'], []) # pylint: disable=protected-access
self.assertEquals(self.transformers._transformers['supports_filter'], []) # pylint: disable=protected-access
def test_collect(self): def test_collect(self):
with mock_registered_transformers(self.registered_transformers): with mock_registered_transformers(self.registered_transformers):
......
...@@ -101,15 +101,16 @@ class BlockStructureTransformer(object): ...@@ -101,15 +101,16 @@ class BlockStructureTransformer(object):
A Transformer may choose to remove entire sub-structures during A Transformer may choose to remove entire sub-structures during
the transform method and may do so using the remove_block and the transform method and may do so using the remove_block and
remove_block_if methods. filter_with_removal methods.
Amongst the many methods available for a block_structure, the Amongst the many methods available for a block_structure, the
following methods are commonly used during transforms: following methods are commonly used during transforms:
get_xblock_field get_xblock_field
get_transformer_data get_transformer_data
get_transformer_block_field get_transformer_block_field
remove_block remove_block_traversal
remove_block_if filter_with_removal
filter_topological_traversal
topological_traversal topological_traversal
post_order_traversal post_order_traversal
...@@ -125,4 +126,57 @@ class BlockStructureTransformer(object): ...@@ -125,4 +126,57 @@ class BlockStructureTransformer(object):
block structure, with already collected data for the block structure, with already collected data for the
transformer, that is to be transformed in place. transformer, that is to be transformed in place.
""" """
pass raise NotImplementedError
class FilteringTransformerMixin(BlockStructureTransformer):
"""
Transformers may optionally choose to implement this mixin if their
transform logic can be broken apart into a lambda for optimization of
combined tree traversals.
For performance reasons, developers should try to implement this mixin
whenever possible - with this alternative, traversal of the entire block
structure happens only once for all transformers that implement
FilteringTransformerMixin.
"""
def transform(self, usage_info, block_structure):
"""
By defining this method, FilteringTransformers can be run individually
if desired. In normal operations, the filters returned from multiple
transform_block_filters calls will be combined and used in a single
tree traversal.
"""
block_structure.filter_topological_traversal(self.transform_block_filters(usage_info, block_structure))
@abstractmethod
def transform_block_filters(self, usage_info, block_structure):
"""
This is an alternative to the standard transform method.
Returns a list of filter functions to be used for filtering out
any unwanted blocks in the given block_structure.
In addition to the commonly used methods listed above, the following
methods are commonly used by implementations of transform_block_filters:
create_universal_filter
create_removal_filter
Note: Transformers that implement this alternative should be
independent of all other registered transformers as they may not
be applied in the order in which they were listed in the registry.
Arguments:
usage_info (any negotiated type) - A usage-specific object
that is passed to the block_structure and forwarded to all
requested Transformers in order to apply a
usage-specific transform. For example, an instance of
usage_info would contain a user object for which the
transform should be applied.
block_structure (BlockStructureBlockData) - A mutable
block structure, with already collected data for the
transformer, that is to be transformed in place.
"""
raise NotImplementedError
""" """
Module for a collection of BlockStructureTransformers. Module for a collection of BlockStructureTransformers.
""" """
import functools
from logging import getLogger from logging import getLogger
from .exceptions import TransformerException from .exceptions import TransformerException
from .transformer import FilteringTransformerMixin
from .transformer_registry import TransformerRegistry from .transformer_registry import TransformerRegistry
...@@ -39,7 +41,7 @@ class BlockStructureTransformers(object): ...@@ -39,7 +41,7 @@ class BlockStructureTransformers(object):
Transformer Registry. Transformer Registry.
""" """
self.usage_info = usage_info self.usage_info = usage_info
self._transformers = [] self._transformers = {'supports_filter': [], 'no_filter': []}
if transformers: if transformers:
self.__iadd__(transformers) self.__iadd__(transformers)
...@@ -61,7 +63,11 @@ class BlockStructureTransformers(object): ...@@ -61,7 +63,11 @@ class BlockStructureTransformers(object):
"The following requested transformers are not registered: {}".format(unregistered_transformers) "The following requested transformers are not registered: {}".format(unregistered_transformers)
) )
self._transformers.extend(transformers) for transformer in transformers:
if isinstance(transformer, FilteringTransformerMixin):
self._transformers['supports_filter'].append(transformer)
else:
self._transformers['no_filter'].append(transformer)
return self return self
@classmethod @classmethod
...@@ -76,17 +82,6 @@ class BlockStructureTransformers(object): ...@@ -76,17 +82,6 @@ class BlockStructureTransformers(object):
# Collect all fields that were requested by the transformers. # Collect all fields that were requested by the transformers.
block_structure._collect_requested_xblock_fields() # pylint: disable=protected-access block_structure._collect_requested_xblock_fields() # pylint: disable=protected-access
def transform(self, block_structure):
"""
The given block structure is transformed by each transformer in the
collection, in the order that the transformers were added.
"""
for transformer in self._transformers:
transformer.transform(self.usage_info, block_structure)
# Prune the block structure to remove any unreachable blocks.
block_structure._prune_unreachable() # pylint: disable=protected-access
@classmethod @classmethod
def is_collected_outdated(cls, block_structure): def is_collected_outdated(cls, block_structure):
""" """
...@@ -105,3 +100,50 @@ class BlockStructureTransformers(object): ...@@ -105,3 +100,50 @@ class BlockStructureTransformers(object):
) )
return bool(outdated_transformers) return bool(outdated_transformers)
def transform(self, block_structure):
"""
The given block structure is transformed by each transformer in the
collection. Tranformers with filters are combined and run first in a
single course tree traversal, then remaining transformers are run in
the order that they were added.
"""
self._transform_with_filters(block_structure)
self._transform_without_filters(block_structure)
# Prune the block structure to remove any unreachable blocks.
block_structure._prune_unreachable() # pylint: disable=protected-access
def _transform_with_filters(self, block_structure):
"""
Transforms the given block_structure using the transform_block_filters
method from the given transformers.
"""
if not self._transformers['supports_filter']:
return
filters = []
for transformer in self._transformers['supports_filter']:
filters.extend(transformer.transform_block_filters(self.usage_info, block_structure))
combined_filters = functools.reduce(
self._filter_chain,
filters,
block_structure.create_universal_filter()
)
block_structure.filter_topological_traversal(combined_filters)
def _filter_chain(self, accumulated, additional):
"""
Given two functions that take a block_key and return a boolean, yield
a function that takes a block key, and 'ands' the functions together
"""
return lambda block_key: accumulated(block_key) and additional(block_key)
def _transform_without_filters(self, block_structure):
"""
Transforms the given block_structure using the transform
method from the given transformers.
"""
for transformer in self._transformers['no_filter']:
transformer.transform(self.usage_info, block_structure)
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