Commit 7764f364 by Braden MacDonald

Fix two content library analytics issues (SOL-521)

parent 8a1877f0
...@@ -133,6 +133,19 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule): ...@@ -133,6 +133,19 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
as children of this block, but only a subset of those children are shown to as children of this block, but only a subset of those children are shown to
any particular student. 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): def selected_children(self):
""" """
Returns a set() of block_ids indicating which of the possible children Returns a set() of block_ids indicating which of the possible children
...@@ -149,22 +162,10 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule): ...@@ -149,22 +162,10 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
return self._selected_set # pylint: disable=access-member-before-definition 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 selected = set(tuple(k) for k in self.selected) # set of (block_type, block_id) tuples assigned to this student
previous_count = len(selected)
lib_tools = self.runtime.service(self, 'library_tools') lib_tools = self.runtime.service(self, 'library_tools')
format_block_keys = lambda keys: lib_tools.create_block_analytics_summary(self.location.course_key, keys) format_block_keys = lambda keys: lib_tools.create_block_analytics_summary(self.location.course_key, keys)
def publish_event(event_name, **kwargs):
""" Publish an event for analytics purposes """
event_data = {
"location": unicode(self.location),
"result": format_block_keys(selected),
"previous_count": previous_count,
"max_count": self.max_count,
}
event_data.update(kwargs)
self.runtime.publish(self, "edx.librarycontentblock.content.{}".format(event_name), event_data)
# 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 self.children]) # pylint: disable=no-member
# Remove any selected blocks that are no longer valid: # Remove any selected blocks that are no longer valid:
...@@ -173,14 +174,24 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule): ...@@ -173,14 +174,24 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
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.
publish_event("removed", removed=format_block_keys(invalid_block_keys), reason="invalid") self._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: # 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) > self.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:
publish_event("removed", removed=format_block_keys(overlimit_block_keys), reason="overlimit") self._publish_event(
"removed",
result=format_block_keys(selected),
removed=format_block_keys(overlimit_block_keys),
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 = self.max_count - len(selected)
if num_to_add > 0: if num_to_add > 0:
...@@ -196,7 +207,11 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule): ...@@ -196,7 +207,11 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
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:
publish_event("assigned", added=format_block_keys(added_block_keys)) self._publish_event(
"assigned",
result=format_block_keys(selected),
added=format_block_keys(added_block_keys)
)
# 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
......
...@@ -327,6 +327,8 @@ class EditInfo(object): ...@@ -327,6 +327,8 @@ class EditInfo(object):
# User ID which changed this XBlock last. # User ID which changed this XBlock last.
self.edited_by = edit_info.get('edited_by', None) self.edited_by = edit_info.get('edited_by', None)
# If this block has been copied from a library using copy_from_template,
# these fields point to the original block in the library, for analytics.
self.original_usage = edit_info.get('original_usage', None) self.original_usage = edit_info.get('original_usage', None)
self.original_usage_version = edit_info.get('original_usage_version', None) self.original_usage_version = edit_info.get('original_usage_version', None)
......
...@@ -2286,9 +2286,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): ...@@ -2286,9 +2286,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
new_block_info.fields = existing_block_info.fields # Preserve any existing overrides new_block_info.fields = existing_block_info.fields # Preserve any existing overrides
if 'children' in new_block_info.defaults: if 'children' in new_block_info.defaults:
del new_block_info.defaults['children'] # Will be set later del new_block_info.defaults['children'] # Will be set later
# ALERT! Why was this 'block_id' stored here? Nothing else stores block_id
# in the block data. Was this a harmless error?
#new_block_info['block_id'] = new_block_key.id
new_block_info.edit_info = existing_block_info.edit_info new_block_info.edit_info = existing_block_info.edit_info
new_block_info.edit_info.previous_version = new_block_info.edit_info.update_version new_block_info.edit_info.previous_version = new_block_info.edit_info.update_version
new_block_info.edit_info.update_version = dest_structure['_id'] new_block_info.edit_info.update_version = dest_structure['_id']
...@@ -2922,6 +2920,10 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): ...@@ -2922,6 +2920,10 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
raw=True, raw=True,
block_defaults=new_block.defaults block_defaults=new_block.defaults
) )
# Extend the block's new edit_info with any extra edit_info fields from the source (e.g. original_usage):
for key, val in new_block.edit_info.to_storable().iteritems():
if getattr(destination_block.edit_info, key) is None:
setattr(destination_block.edit_info, key, val)
# introduce new edit info field for tracing where copied/published blocks came # introduce new edit info field for tracing where copied/published blocks came
destination_block.edit_info.source_version = new_block.edit_info.update_version destination_block.edit_info.source_version = new_block.edit_info.update_version
......
...@@ -12,6 +12,7 @@ from xblock.runtime import Runtime as VanillaRuntime ...@@ -12,6 +12,7 @@ from xblock.runtime import Runtime as VanillaRuntime
from xmodule.library_content_module import ANY_CAPA_TYPE_VALUE, LibraryContentDescriptor from xmodule.library_content_module import ANY_CAPA_TYPE_VALUE, LibraryContentDescriptor
from xmodule.library_tools import LibraryToolsService from xmodule.library_tools import LibraryToolsService
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.factories import LibraryFactory, CourseFactory from xmodule.modulestore.tests.factories import LibraryFactory, CourseFactory
from xmodule.modulestore.tests.utils import MixedSplitTestCase from xmodule.modulestore.tests.utils import MixedSplitTestCase
from xmodule.tests import get_test_system from xmodule.tests import get_test_system
...@@ -49,13 +50,13 @@ class LibraryContentTest(MixedSplitTestCase): ...@@ -49,13 +50,13 @@ class LibraryContentTest(MixedSplitTestCase):
""" """
Bind a module (part of self.course) so we can access student-specific data. Bind a module (part of self.course) so we can access student-specific data.
""" """
module_system = get_test_system(course_id=self.course.location.course_key) module_system = get_test_system(course_id=module.location.course_key)
module_system.descriptor_runtime = module.runtime._descriptor_system # pylint: disable=protected-access module_system.descriptor_runtime = module.runtime._descriptor_system # pylint: disable=protected-access
module_system._services['library_tools'] = self.tools # pylint: disable=protected-access module_system._services['library_tools'] = self.tools # pylint: disable=protected-access
def get_module(descriptor): def get_module(descriptor):
"""Mocks module_system get_module function""" """Mocks module_system get_module function"""
sub_module_system = get_test_system(course_id=self.course.location.course_key) sub_module_system = get_test_system(course_id=module.location.course_key)
sub_module_system.get_module = get_module sub_module_system.get_module = get_module
sub_module_system.descriptor_runtime = descriptor._runtime # pylint: disable=protected-access sub_module_system.descriptor_runtime = descriptor._runtime # pylint: disable=protected-access
descriptor.bind_for_student(sub_module_system, descriptor._field_data, self.user_id) # pylint: disable=protected-access descriptor.bind_for_student(sub_module_system, descriptor._field_data, self.user_id) # pylint: disable=protected-access
...@@ -324,6 +325,17 @@ class TestLibraryContentAnalytics(LibraryContentTest): ...@@ -324,6 +325,17 @@ class TestLibraryContentAnalytics(LibraryContentTest):
self.assertEqual(event_data["previous_count"], 1) self.assertEqual(event_data["previous_count"], 1)
self.assertEqual(event_data["max_count"], 2) self.assertEqual(event_data["max_count"], 2)
def test_assigned_event_published(self):
"""
Same as test_assigned_event but uses the published branch
"""
self.store.publish(self.course.location, self.user_id)
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only):
self.lc_block = self.store.get_item(self.lc_block.location)
self._bind_course_module(self.lc_block)
self.lc_block.xmodule_runtime.publish = self.publisher
self.test_assigned_event()
def test_assigned_descendants(self): def test_assigned_descendants(self):
""" """
Test the "assigned" event emitted includes descendant block information. Test the "assigned" event emitted includes descendant block information.
......
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