Commit 2265889e by Davorin Sego

In content library transformer handle the cases where the old selected modules…

In content library transformer handle the cases where the old selected modules are no longer valid and update the user's set of selected modules.
parent b56166cb
...@@ -135,84 +135,116 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule): ...@@ -135,84 +135,116 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
any particular student. any particular student.
""" """
def _publish_event(self, event_name, result, **kwargs): @classmethod
""" Helper method to publish an event for analytics purposes """ def make_selection(cls, selected, children, max_count, mode, location, lib_tools, emit):
event_data = { """
"location": unicode(self.location), Dynamically selects block_ids indicating which of the possible children are displayed to the current user.
"result": result,
"previous_count": getattr(self, "_last_event_result_count", len(self.selected)), Arguments:
"max_count": self.max_count, selected - list of (block_type, block_id) tuples assigned to this student
} children - children of this block
event_data.update(kwargs) max_count - number of components to display to each student
self.runtime.publish(self, "edx.librarycontentblock.content.{}".format(event_name), event_data) mode - how content is drawn from the library
self._last_event_result_count = len(result) # pylint: disable=attribute-defined-outside-init location -
lib_tools - instance of LibraryToolsService
def selected_children(self): emit - function to emit events
"""
Returns a set() of block_ids indicating which of the possible children Returns:
have been selected to display to the current user. a set of (block_type, block_id) tuples used to record
which random/first set of matching blocks was selected per 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 def publish_event(event_name, result, **kwargs):
actual BlockUsageLocators, it is necessary to use self.children, """ Helper method to publish an event for analytics purposes """
because the block_ids alone do not specify the block type. event_data = {
""" "location": unicode(location),
if hasattr(self, "_selected_set"): "result": result,
# Already done: "previous_count": last_event_result_count,
return self._selected_set # pylint: disable=access-member-before-definition "max_count": max_count,
}
selected = set(tuple(k) for k in self.selected) # set of (block_type, block_id) tuples assigned to this student event_data.update(kwargs)
emit("edx.librarycontentblock.content.{}".format(event_name), event_data)
lib_tools = self.runtime.service(self, 'library_tools')
format_block_keys = lambda keys: lib_tools.create_block_analytics_summary(self.location.course_key, keys) last_event_result_count = len(selected)
selected = set(tuple(k) for k in selected) # set of (block_type, block_id) tuples assigned to this student
format_block_keys = lambda keys: lib_tools.create_block_analytics_summary(location.course_key, keys)
# Determine which of our children we will show: # Determine which of our children we will show:
valid_block_keys = set([(c.block_type, c.block_id) for c in self.children]) # pylint: disable=no-member valid_block_keys = set([(c.block_type, c.block_id) for c in children]) # pylint: disable=no-member
# Remove any selected blocks that are no longer valid: # Remove any selected blocks that are no longer valid:
invalid_block_keys = (selected - valid_block_keys) invalid_block_keys = (selected - valid_block_keys)
if invalid_block_keys: if invalid_block_keys:
selected -= invalid_block_keys selected -= invalid_block_keys
# Publish an event for analytics purposes: # Publish an event for analytics purposes:
# reason "invalid" means deleted from library or a different library is now being used. # reason "invalid" means deleted from library or a different library is now being used.
self._publish_event( publish_event(
"removed", "removed",
result=format_block_keys(selected), result=format_block_keys(selected),
removed=format_block_keys(invalid_block_keys), removed=format_block_keys(invalid_block_keys),
reason="invalid" reason="invalid"
) )
# If max_count has been decreased, we may have to drop some previously selected blocks: # If max_count has been decreased, we may have to drop some previously selected blocks:
overlimit_block_keys = set() overlimit_block_keys = set()
while len(selected) > self.max_count: while len(selected) > max_count:
overlimit_block_keys.add(selected.pop()) overlimit_block_keys.add(selected.pop())
if overlimit_block_keys: if overlimit_block_keys:
# Publish an event for analytics purposes: # Publish an event for analytics purposes:
self._publish_event( publish_event(
"removed", "removed",
result=format_block_keys(selected), result=format_block_keys(selected),
removed=format_block_keys(overlimit_block_keys), removed=format_block_keys(overlimit_block_keys),
reason="overlimit" reason="overlimit"
) )
# Do we have enough blocks now? # Do we have enough blocks now?
num_to_add = self.max_count - len(selected) num_to_add = max_count - len(selected)
added_block_keys = None
if num_to_add > 0: if num_to_add > 0:
added_block_keys = None
# We need to select [more] blocks to display to this user: # We need to select [more] blocks to display to this user:
pool = valid_block_keys - selected pool = valid_block_keys - selected
if self.mode == "random": if mode == "random":
num_to_add = min(len(pool), num_to_add) num_to_add = min(len(pool), num_to_add)
added_block_keys = set(random.sample(pool, num_to_add)) added_block_keys = set(random.sample(pool, num_to_add))
# We now have the correct n random children to show for this user. # We now have the correct n random children to show for this user.
else: else:
raise NotImplementedError("Unsupported mode.") raise NotImplementedError("Unsupported mode.")
selected |= added_block_keys selected |= added_block_keys
if added_block_keys: if added_block_keys:
# Publish an event for analytics purposes: # Publish an event for analytics purposes:
self._publish_event( publish_event(
"assigned", "assigned",
result=format_block_keys(selected), result=format_block_keys(selected),
added=format_block_keys(added_block_keys) added=format_block_keys(added_block_keys)
) )
return selected
def selected_children(self):
"""
Returns a set() 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
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
lib_tools = self.runtime.service(self, 'library_tools')
emit = lambda *args: self.runtime.publish(self, *args)
selected = self.make_selection(
self.selected, self.children, self.max_count, "random", self.location, lib_tools, emit
)
# Save our selections to the user state, to ensure consistency: # Save our selections to the user state, to ensure consistency:
self.selected = list(selected) # TODO: this doesn't save from the LMS "Progress" page. self.selected = list(selected) # TODO: this doesn't save from the LMS "Progress" page.
# Cache the results # Cache the results
......
...@@ -6,6 +6,10 @@ from courseware.access import _has_access_to_course ...@@ -6,6 +6,10 @@ from courseware.access import _has_access_to_course
from courseware.models import StudentModule from courseware.models import StudentModule
from opaque_keys.edx.locator import BlockUsageLocator from opaque_keys.edx.locator import BlockUsageLocator
from openedx.core.lib.block_cache.transformer import BlockStructureTransformer from openedx.core.lib.block_cache.transformer import BlockStructureTransformer
from xmodule.library_content_module import LibraryContentModule
from xmodule.library_tools import LibraryToolsService
from xmodule.modulestore.django import modulestore
from eventtracking import tracker
class ContentLibraryTransformer(BlockStructureTransformer): class ContentLibraryTransformer(BlockStructureTransformer):
...@@ -47,6 +51,9 @@ class ContentLibraryTransformer(BlockStructureTransformer): ...@@ -47,6 +51,9 @@ class ContentLibraryTransformer(BlockStructureTransformer):
Returns: Returns:
dict[UsageKey: dict] dict[UsageKey: dict]
""" """
block_structure.request_xblock_fields('mode')
block_structure.request_xblock_fields('max_count')
# For each block check if block is library_content. # For each block check if block is library_content.
# If library_content add children array to content_library_children field # If library_content add children array to content_library_children field
for block_key in block_structure.topological_traversal(): for block_key in block_structure.topological_traversal():
...@@ -63,6 +70,22 @@ class ContentLibraryTransformer(BlockStructureTransformer): ...@@ -63,6 +70,22 @@ class ContentLibraryTransformer(BlockStructureTransformer):
user_info(object) user_info(object)
block_structure (BlockStructureCollectedData) block_structure (BlockStructureCollectedData)
""" """
store = modulestore()
lib_tools = LibraryToolsService(store)
def build_key(block_type, block_id):
"""
Helper method to build a BlockUsageLocator for user_info.course_key
"""
return BlockUsageLocator(user_info.course_key, block_type, block_id)
def update_selection(selected, children, max_count, mode, location):
"""
Helper method to update library content selection
"""
return LibraryContentModule.make_selection(
selected, children, max_count, mode, location, lib_tools, tracker.emit
)
def check_child_removal(block_key): def check_child_removal(block_key):
""" """
...@@ -82,19 +105,25 @@ class ContentLibraryTransformer(BlockStructureTransformer): ...@@ -82,19 +105,25 @@ class ContentLibraryTransformer(BlockStructureTransformer):
library_children = block_structure.get_transformer_block_data(block_key, self, 'content_library_children') library_children = block_structure.get_transformer_block_data(block_key, self, 'content_library_children')
if library_children: if library_children:
children.extend(library_children) children.extend(library_children)
selected = []
mode = block_structure.get_xblock_field(block_key, 'mode')
max_count = block_structure.get_xblock_field(block_key, 'max_count')
# Retrieve "selected" json from LMS MySQL database. # Retrieve "selected" json from LMS MySQL database.
modules = self._get_selected_modules(user_info.user, user_info.course_key, block_key) modules = self._get_selected_modules(user_info.user, user_info.course_key, block_key)
for module in modules: for module in modules:
module_state = module.state module_state = module.state
state_dict = json.loads(module_state) state_dict = json.loads(module_state)
# Check all selected entries for this user on selected library. # Check all selected entries for this user on selected library.
# Add all selected to selected_children list. # Add all selected to selected list.
for state in state_dict['selected']: for state in state_dict['selected']:
usage_key = BlockUsageLocator( usage_key = build_key(state[0], state[1])
user_info.course_key, block_type=state[0], block_id=state[1]
)
if usage_key in library_children: if usage_key in library_children:
selected_children.append(usage_key) selected.append((state[0], state[1]))
# update selected
selected = update_selection(selected, children, max_count, mode, block_key)
selected_children.extend([build_key(s[0], s[1]) for s in selected])
# Check and remove all non-selected children from course structure. # Check and remove all non-selected children from course structure.
block_structure.remove_block_if( block_structure.remove_block_if(
......
...@@ -127,10 +127,15 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase): ...@@ -127,10 +127,15 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase):
transformers={self.transformer} transformers={self.transformer}
) )
self.assertEqual( # Should dynamically assign a block to student
set(trans_block_structure.get_block_keys()), trans_keys = set(trans_block_structure.get_block_keys())
self.get_block_key_set(self.blocks, 'course', 'chapter1', 'lesson1', 'vertical1', 'library_content1') block_key_set = self.get_block_key_set(self.blocks, 'course', 'chapter1', 'lesson1', 'vertical1', 'library_content1')
) for key in block_key_set:
self.assertIn(key, trans_keys)
vertical2_selected = self.get_block_key_set(self.blocks, 'vertical2').pop() in trans_keys
vertical3_selected = self.get_block_key_set(self.blocks, 'vertical3').pop() in trans_keys
self.assertTrue(vertical2_selected or vertical3_selected)
# Check course structure again, with mocked selected modules for a user. # Check course structure again, with mocked selected modules for a user.
with mock.patch( with mock.patch(
......
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