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),
"result": result,
"previous_count": getattr(self, "_last_event_result_count", len(self.selected)),
"max_count": self.max_count,
}
event_data.update(kwargs)
self.runtime.publish(self, "edx.librarycontentblock.content.{}".format(event_name), event_data)
self._last_event_result_count = len(result) # pylint: disable=attribute-defined-outside-init
def selected_children(self):
""" """
Returns a set() of block_ids indicating which of the possible children Dynamically selects block_ids indicating which of the possible children are displayed to the current user.
have been selected to display to the current user.
This reads and updates the "selected" field, which has user_state scope. Arguments:
selected - list of (block_type, block_id) tuples assigned to this student
children - children of this block
max_count - number of components to display to each student
mode - how content is drawn from the library
location -
lib_tools - instance of LibraryToolsService
emit - function to emit events
Note: self.selected and the return value contain block_ids. To get Returns:
actual BlockUsageLocators, it is necessary to use self.children, a set of (block_type, block_id) tuples used to record
because the block_ids alone do not specify the block type. which random/first set of matching blocks was selected per user
""" """
if hasattr(self, "_selected_set"):
# Already done:
return self._selected_set # pylint: disable=access-member-before-definition
selected = set(tuple(k) for k in self.selected) # set of (block_type, block_id) tuples assigned to this student def publish_event(event_name, result, **kwargs):
""" Helper method to publish an event for analytics purposes """
event_data = {
"location": unicode(location),
"result": result,
"previous_count": last_event_result_count,
"max_count": max_count,
}
event_data.update(kwargs)
emit("edx.librarycontentblock.content.{}".format(event_name), event_data)
lib_tools = self.runtime.service(self, 'library_tools') last_event_result_count = len(selected)
format_block_keys = lambda keys: lib_tools.create_block_analytics_summary(self.location.course_key, keys) 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)
if num_to_add > 0:
added_block_keys = None added_block_keys = None
if num_to_add > 0:
# 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