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):
any particular student.
"""
def _publish_event(self, event_name, result, **kwargs):
""" Helper method to publish an event for analytics purposes """
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
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
selected = set(tuple(k) for k in self.selected) # set of (block_type, block_id) tuples assigned to this student
lib_tools = self.runtime.service(self, 'library_tools')
format_block_keys = lambda keys: lib_tools.create_block_analytics_summary(self.location.course_key, keys)
@classmethod
def make_selection(cls, selected, children, max_count, mode, location, lib_tools, emit):
"""
Dynamically selects block_ids indicating which of the possible children are displayed to the current user.
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
Returns:
a set of (block_type, block_id) tuples used to record
which random/first set of matching blocks was selected per user
"""
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)
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:
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:
invalid_block_keys = (selected - valid_block_keys)
if invalid_block_keys:
selected -= invalid_block_keys
# Publish an event for analytics purposes:
# reason "invalid" means deleted from library or a different library is now being used.
self._publish_event(
publish_event(
"removed",
result=format_block_keys(selected),
removed=format_block_keys(invalid_block_keys),
reason="invalid"
)
# If max_count has been decreased, we may have to drop some previously selected blocks:
overlimit_block_keys = set()
while len(selected) > self.max_count:
while len(selected) > max_count:
overlimit_block_keys.add(selected.pop())
if overlimit_block_keys:
# Publish an event for analytics purposes:
self._publish_event(
publish_event(
"removed",
result=format_block_keys(selected),
removed=format_block_keys(overlimit_block_keys),
reason="overlimit"
)
# 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:
added_block_keys = None
# We need to select [more] blocks to display to this user:
pool = valid_block_keys - selected
if self.mode == "random":
if mode == "random":
num_to_add = min(len(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.
else:
raise NotImplementedError("Unsupported mode.")
selected |= added_block_keys
if added_block_keys:
# Publish an event for analytics purposes:
self._publish_event(
publish_event(
"assigned",
result=format_block_keys(selected),
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:
self.selected = list(selected) # TODO: this doesn't save from the LMS "Progress" page.
# Cache the results
......
......@@ -6,6 +6,10 @@ from courseware.access import _has_access_to_course
from courseware.models import StudentModule
from opaque_keys.edx.locator import BlockUsageLocator
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):
......@@ -47,6 +51,9 @@ class ContentLibraryTransformer(BlockStructureTransformer):
Returns:
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.
# If library_content add children array to content_library_children field
for block_key in block_structure.topological_traversal():
......@@ -63,6 +70,22 @@ class ContentLibraryTransformer(BlockStructureTransformer):
user_info(object)
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):
"""
......@@ -82,19 +105,25 @@ class ContentLibraryTransformer(BlockStructureTransformer):
library_children = block_structure.get_transformer_block_data(block_key, self, 'content_library_children')
if 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.
modules = self._get_selected_modules(user_info.user, user_info.course_key, block_key)
for module in modules:
module_state = module.state
state_dict = json.loads(module_state)
# 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']:
usage_key = BlockUsageLocator(
user_info.course_key, block_type=state[0], block_id=state[1]
)
usage_key = build_key(state[0], state[1])
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.
block_structure.remove_block_if(
......
......@@ -127,10 +127,15 @@ class ContentLibraryTransformerTestCase(CourseStructureTestCase):
transformers={self.transformer}
)
self.assertEqual(
set(trans_block_structure.get_block_keys()),
self.get_block_key_set(self.blocks, 'course', 'chapter1', 'lesson1', 'vertical1', 'library_content1')
)
# Should dynamically assign a block to student
trans_keys = set(trans_block_structure.get_block_keys())
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.
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