Merge pull request #23265 from open-craft/guruprasad/BB-112-fix-randomized-content-block-order
[BB-112] Fix the unpredictable order randomization issue with randomized content blocks
This commit is contained in:
@@ -158,37 +158,40 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
|
||||
"""
|
||||
rand = random.Random()
|
||||
|
||||
selected = set(tuple(k) for k in selected) # set of (block_type, block_id) tuples assigned to this student
|
||||
selected_keys = set(tuple(k) for k in selected) # set of (block_type, block_id) tuples assigned to this student
|
||||
|
||||
# Determine which of our children we will show:
|
||||
valid_block_keys = set([(c.block_type, c.block_id) for c in children])
|
||||
|
||||
# Remove any selected blocks that are no longer valid:
|
||||
invalid_block_keys = (selected - valid_block_keys)
|
||||
invalid_block_keys = (selected_keys - valid_block_keys)
|
||||
if invalid_block_keys:
|
||||
selected -= invalid_block_keys
|
||||
selected_keys -= invalid_block_keys
|
||||
|
||||
# If max_count has been decreased, we may have to drop some previously selected blocks:
|
||||
overlimit_block_keys = set()
|
||||
if len(selected) > max_count:
|
||||
num_to_remove = len(selected) - max_count
|
||||
overlimit_block_keys = set(rand.sample(selected, num_to_remove))
|
||||
selected -= overlimit_block_keys
|
||||
if len(selected_keys) > max_count:
|
||||
num_to_remove = len(selected_keys) - max_count
|
||||
overlimit_block_keys = set(rand.sample(selected_keys, num_to_remove))
|
||||
selected_keys -= overlimit_block_keys
|
||||
|
||||
# Do we have enough blocks now?
|
||||
num_to_add = max_count - len(selected)
|
||||
num_to_add = max_count - len(selected_keys)
|
||||
|
||||
added_block_keys = None
|
||||
if num_to_add > 0:
|
||||
# We need to select [more] blocks to display to this user:
|
||||
pool = valid_block_keys - selected
|
||||
pool = valid_block_keys - selected_keys
|
||||
if mode == "random":
|
||||
num_to_add = min(len(pool), num_to_add)
|
||||
added_block_keys = set(rand.sample(pool, num_to_add))
|
||||
# We now have the correct n random children to show for this user.
|
||||
else:
|
||||
raise NotImplementedError("Unsupported mode.")
|
||||
selected |= added_block_keys
|
||||
selected_keys |= added_block_keys
|
||||
|
||||
if any([invalid_block_keys, overlimit_block_keys, added_block_keys]):
|
||||
selected = selected_keys
|
||||
|
||||
return {
|
||||
'selected': selected,
|
||||
@@ -268,19 +271,15 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
|
||||
|
||||
def selected_children(self):
|
||||
"""
|
||||
Returns a set() of block_ids indicating which of the possible children
|
||||
Returns a list() of block_ids indicating which of the possible children
|
||||
have been selected to display to the current user.
|
||||
|
||||
This reads and updates the "selected" field, which has user_state scope.
|
||||
|
||||
Note: self.selected and the return value contain block_ids. To get
|
||||
Note: the return value (self.selected) contains block_ids. To get
|
||||
actual BlockUsageLocators, it is necessary to use self.children,
|
||||
because the block_ids alone do not specify the block type.
|
||||
"""
|
||||
if hasattr(self, "_selected_set"):
|
||||
# Already done:
|
||||
return self._selected_set # pylint: disable=access-member-before-definition
|
||||
|
||||
block_keys = self.make_selection(self.selected, self.children, self.max_count, "random") # pylint: disable=no-member
|
||||
|
||||
# Publish events for analytics purposes:
|
||||
@@ -292,13 +291,13 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
|
||||
self._publish_event,
|
||||
)
|
||||
|
||||
# Save our selections to the user state, to ensure consistency:
|
||||
selected = block_keys['selected']
|
||||
self.selected = list(selected) # TODO: this doesn't save from the LMS "Progress" page.
|
||||
# Cache the results
|
||||
self._selected_set = selected # pylint: disable=attribute-defined-outside-init
|
||||
if any(block_keys[changed] for changed in ('invalid', 'overlimit', 'added')):
|
||||
# Save our selections to the user state, to ensure consistency:
|
||||
selected = list(block_keys['selected'])
|
||||
random.shuffle(selected)
|
||||
self.selected = selected # TODO: this doesn't save from the LMS "Progress" page.
|
||||
|
||||
return selected
|
||||
return self.selected
|
||||
|
||||
def _get_selected_child_blocks(self):
|
||||
"""
|
||||
|
||||
@@ -272,9 +272,8 @@ class LibraryContentModuleTestMixin(object):
|
||||
Helper method that changes the max_count of self.lc_block, refreshes
|
||||
children, and asserts that the number of selected children equals the count provided.
|
||||
"""
|
||||
# Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
|
||||
if hasattr(self.lc_block._xmodule, '_selected_set'):
|
||||
del self.lc_block._xmodule._selected_set
|
||||
# Construct the XModule for the descriptor, if not present already.
|
||||
self.lc_block._xmodule # pylint: disable=pointless-statement,protected-access
|
||||
self.lc_block.max_count = count
|
||||
selected = self.lc_block.get_child_descriptors()
|
||||
self.assertEqual(len(selected), count)
|
||||
@@ -287,7 +286,7 @@ class TestLibraryContentModuleNoSearchIndex(LibraryContentModuleTestMixin, Libra
|
||||
Tests for library container when no search index is available.
|
||||
Tests fallback low-level CAPA problem introspection
|
||||
"""
|
||||
pass
|
||||
pass # pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
search_index_mock = Mock(spec=SearchEngine) # pylint: disable=invalid-name
|
||||
@@ -365,8 +364,8 @@ class TestLibraryContentAnalytics(LibraryContentTest):
|
||||
Check that a LibraryContentModule analytics event was published by self.lc_block.
|
||||
"""
|
||||
self.assertTrue(self.publisher.called)
|
||||
self.assertTrue(len(self.publisher.call_args[0]), 3)
|
||||
_, event_name, event_data = self.publisher.call_args[0]
|
||||
self.assertTrue(len(self.publisher.call_args[0]), 3) # pylint: disable=unsubscriptable-object
|
||||
_, event_name, event_data = self.publisher.call_args[0] # pylint: disable=unsubscriptable-object
|
||||
self.assertEqual(event_name, "edx.librarycontentblock.content.{}".format(event_type))
|
||||
self.assertEqual(event_data["location"], six.text_type(self.lc_block.location))
|
||||
return event_data
|
||||
@@ -397,8 +396,6 @@ class TestLibraryContentAnalytics(LibraryContentTest):
|
||||
|
||||
# Now increase max_count so that one more child will be added:
|
||||
self.lc_block.max_count = 2
|
||||
# Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
|
||||
del self.lc_block._xmodule._selected_set
|
||||
children = self.lc_block.get_child_descriptors()
|
||||
self.assertEqual(len(children), 2)
|
||||
child, new_child = children if children[0].location == child.location else reversed(children)
|
||||
@@ -478,8 +475,6 @@ class TestLibraryContentAnalytics(LibraryContentTest):
|
||||
self.lc_block.get_child_descriptors() # This line is needed in the test environment or the change has no effect
|
||||
self.publisher.reset_mock() # Clear the "assigned" event that was just published.
|
||||
self.lc_block.max_count = 0
|
||||
# Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
|
||||
del self.lc_block._xmodule._selected_set
|
||||
|
||||
# Check that the event says that one block was removed, leaving no blocks left:
|
||||
children = self.lc_block.get_child_descriptors()
|
||||
@@ -497,8 +492,6 @@ class TestLibraryContentAnalytics(LibraryContentTest):
|
||||
# Start by assigning two blocks to the student:
|
||||
self.lc_block.get_child_descriptors() # This line is needed in the test environment or the change has no effect
|
||||
self.lc_block.max_count = 2
|
||||
# Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
|
||||
del self.lc_block._xmodule._selected_set
|
||||
initial_blocks_assigned = self.lc_block.get_child_descriptors()
|
||||
self.assertEqual(len(initial_blocks_assigned), 2)
|
||||
self.publisher.reset_mock() # Clear the "assigned" event that was just published.
|
||||
@@ -512,8 +505,6 @@ class TestLibraryContentAnalytics(LibraryContentTest):
|
||||
self.library.children = [keep_block_lib_usage_key]
|
||||
self.store.update_item(self.library, self.user_id)
|
||||
self.lc_block.refresh_children()
|
||||
# Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
|
||||
del self.lc_block._xmodule._selected_set
|
||||
|
||||
# Check that the event says that one block was removed, leaving one block left:
|
||||
children = self.lc_block.get_child_descriptors()
|
||||
|
||||
Reference in New Issue
Block a user