Commit 3a973d42 by Braden MacDonald

Unit tests for library content analytics

parent 3973317a
...@@ -119,6 +119,7 @@ class MixedSplitTestCase(TestCase): ...@@ -119,6 +119,7 @@ class MixedSplitTestCase(TestCase):
extra.update(kwargs) extra.update(kwargs)
return ItemFactory.create( return ItemFactory.create(
category=category, category=category,
parent=parent_block,
parent_location=parent_block.location, parent_location=parent_block.location,
modulestore=self.store, modulestore=self.store,
**extra **extra
......
...@@ -5,22 +5,22 @@ Basic unit tests for LibraryContentModule ...@@ -5,22 +5,22 @@ Basic unit tests for LibraryContentModule
Higher-level tests are in `cms/djangoapps/contentstore/tests/test_libraries.py`. Higher-level tests are in `cms/djangoapps/contentstore/tests/test_libraries.py`.
""" """
from bson.objectid import ObjectId from bson.objectid import ObjectId
from mock import patch from mock import Mock, patch
from opaque_keys.edx.locator import LibraryLocator from opaque_keys.edx.locator import LibraryLocator
from unittest import TestCase from unittest import TestCase
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.runtime import Runtime as VanillaRuntime from xblock.runtime import Runtime as VanillaRuntime
from xmodule.x_module import AUTHOR_VIEW
from xmodule.library_content_module import ( from xmodule.library_content_module import (
LibraryVersionReference, LibraryList, ANY_CAPA_TYPE_VALUE, LibraryContentDescriptor LibraryVersionReference, LibraryList, ANY_CAPA_TYPE_VALUE, LibraryContentDescriptor
) )
from xmodule.modulestore.tests.factories import LibraryFactory, CourseFactory, ItemFactory from xmodule.library_tools import LibraryToolsService
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
from xmodule.validation import StudioValidationMessage from xmodule.validation import StudioValidationMessage
from xmodule.x_module import AUTHOR_VIEW
dummy_render = lambda block, _: Fragment(block.data) # pylint: disable=invalid-name dummy_render = lambda block, _: Fragment(block.data) # pylint: disable=invalid-name
...@@ -32,46 +32,21 @@ class LibraryContentTest(MixedSplitTestCase): ...@@ -32,46 +32,21 @@ class LibraryContentTest(MixedSplitTestCase):
def setUp(self): def setUp(self):
super(LibraryContentTest, self).setUp() super(LibraryContentTest, self).setUp()
self.tools = LibraryToolsService(self.store)
self.library = LibraryFactory.create(modulestore=self.store) self.library = LibraryFactory.create(modulestore=self.store)
self.lib_blocks = [ self.lib_blocks = [
ItemFactory.create( self.make_block("html", self.library, data="Hello world from block {}".format(i))
category="html",
parent_location=self.library.location,
user_id=self.user_id,
publish_item=False,
metadata={"data": "Hello world from block {}".format(i), },
modulestore=self.store,
)
for i in range(1, 5) for i in range(1, 5)
] ]
self.course = CourseFactory.create(modulestore=self.store) self.course = CourseFactory.create(modulestore=self.store)
self.chapter = ItemFactory.create( self.chapter = self.make_block("chapter", self.course)
category="chapter", self.sequential = self.make_block("sequential", self.chapter)
parent_location=self.course.location, self.vertical = self.make_block("vertical", self.sequential)
user_id=self.user_id, self.lc_block = self.make_block(
modulestore=self.store, "library_content",
) self.vertical,
self.sequential = ItemFactory.create( max_count=1,
category="sequential", source_libraries=[LibraryVersionReference(self.library.location.library_key)]
parent_location=self.chapter.location,
user_id=self.user_id,
modulestore=self.store,
)
self.vertical = ItemFactory.create(
category="vertical",
parent_location=self.sequential.location,
user_id=self.user_id,
modulestore=self.store,
)
self.lc_block = ItemFactory.create(
category="library_content",
parent_location=self.vertical.location,
user_id=self.user_id,
modulestore=self.store,
metadata={
'max_count': 1,
'source_libraries': [LibraryVersionReference(self.library.location.library_key)]
}
) )
def _bind_course_module(self, module): def _bind_course_module(self, module):
...@@ -80,6 +55,7 @@ class LibraryContentTest(MixedSplitTestCase): ...@@ -80,6 +55,7 @@ class LibraryContentTest(MixedSplitTestCase):
""" """
module_system = get_test_system(course_id=self.course.location.course_key) module_system = get_test_system(course_id=self.course.location.course_key)
module_system.descriptor_runtime = module.runtime module_system.descriptor_runtime = module.runtime
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"""
...@@ -92,6 +68,11 @@ class LibraryContentTest(MixedSplitTestCase): ...@@ -92,6 +68,11 @@ class LibraryContentTest(MixedSplitTestCase):
module_system.get_module = get_module module_system.get_module = get_module
module.xmodule_runtime = module_system module.xmodule_runtime = module_system
class TestLibraryContentModule(LibraryContentTest):
"""
Basic unit tests for LibraryContentModule
"""
def _get_capa_problem_type_xml(self, *args): def _get_capa_problem_type_xml(self, *args):
""" Helper function to create empty CAPA problem definition """ """ Helper function to create empty CAPA problem definition """
problem = "<problem>" problem = "<problem>"
...@@ -111,20 +92,8 @@ class LibraryContentTest(MixedSplitTestCase): ...@@ -111,20 +92,8 @@ class LibraryContentTest(MixedSplitTestCase):
["coderesponse", "optionresponse"] ["coderesponse", "optionresponse"]
] ]
for problem_type in problem_types: for problem_type in problem_types:
ItemFactory.create( self.make_block("problem", self.library, data=self._get_capa_problem_type_xml(*problem_type))
category="problem",
parent_location=self.library.location,
user_id=self.user_id,
publish_item=False,
data=self._get_capa_problem_type_xml(*problem_type),
modulestore=self.store,
)
class TestLibraryContentModule(LibraryContentTest):
"""
Basic unit tests for LibraryContentModule
"""
def test_lib_content_block(self): def test_lib_content_block(self):
""" """
Test that blocks from a library are copied and added as children Test that blocks from a library are copied and added as children
...@@ -338,3 +307,167 @@ class TestLibraryList(TestCase): ...@@ -338,3 +307,167 @@ class TestLibraryList(TestCase):
lib_list = LibraryList() lib_list = LibraryList()
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
lib_list.from_json(["Not-a-library-key,whatever"]) lib_list.from_json(["Not-a-library-key,whatever"])
class TestLibraryContentAnalytics(LibraryContentTest):
"""
Test analytics features of LibraryContentModule
"""
def setUp(self):
super(TestLibraryContentAnalytics, self).setUp()
self.publisher = Mock()
self.lc_block.refresh_children()
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
def _assert_event_was_published(self, event_type):
"""
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.assertEqual(event_name, "edx.librarycontentblock.content.{}".format(event_type))
self.assertEqual(event_data["location"], unicode(self.lc_block.location))
return event_data
def test_assigned_event(self):
"""
Test the "assigned" event emitted when a student is assigned specific blocks.
"""
# In the beginning was the lc_block and it assigned one child to the student:
child = self.lc_block.get_child_descriptors()[0]
child_lib_location, child_lib_version = self.store.get_block_original_usage(child.location)
self.assertIsInstance(child_lib_version, ObjectId)
event_data = self._assert_event_was_published("assigned")
block_info = {
"usage_key": unicode(child.location),
"original_usage_key": unicode(child_lib_location),
"original_usage_version": unicode(child_lib_version),
"descendants": [],
}
self.assertEqual(event_data, {
"location": unicode(self.lc_block.location),
"added": [block_info],
"result": [block_info],
})
self.publisher.reset_mock()
# Now increase max_count so that one more child will be added:
self.lc_block.max_count = 2
del self.lc_block._xmodule._selected_set # Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
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)
event_data = self._assert_event_was_published("assigned")
self.assertEqual(event_data["added"][0]["usage_key"], unicode(new_child.location))
self.assertEqual(len(event_data["result"]), 2)
def test_assigned_descendants(self):
"""
Test the "assigned" event emitted includes descendant block information.
"""
# Replace the blocks in the library with a block that has descendants:
with self.store.bulk_operations(self.library.location.library_key):
self.library.children = []
main_vertical = self.make_block("vertical", self.library)
inner_vertical = self.make_block("vertical", main_vertical)
html_block = self.make_block("html", inner_vertical)
problem_block = self.make_block("problem", inner_vertical)
self.lc_block.refresh_children()
# Reload lc_block and set it up for a student:
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
# Get the keys of each of our blocks, as they appear in the course:
course_usage_main_vertical = self.lc_block.children[0]
course_usage_inner_vertical = self.store.get_item(course_usage_main_vertical).children[0]
inner_vertical_in_course = self.store.get_item(course_usage_inner_vertical)
course_usage_html = inner_vertical_in_course.children[0]
course_usage_problem = inner_vertical_in_course.children[1]
# Trigger a publish event:
self.lc_block.get_child_descriptors()
event_data = self._assert_event_was_published("assigned")
for block_list in (event_data["added"], event_data["result"]):
self.assertEqual(len(block_list), 1) # The main_vertical is the only root block added, and is the only result.
self.assertEqual(block_list[0]["usage_key"], unicode(course_usage_main_vertical))
# Check that "descendants" is a flat, unordered list of all of main_vertical's descendants:
descendants_expected = {}
for lib_key, course_usage_key in (
(inner_vertical.location, course_usage_inner_vertical),
(html_block.location, course_usage_html),
(problem_block.location, course_usage_problem),
):
descendants_expected[unicode(course_usage_key)] = {
"usage_key": unicode(course_usage_key),
"original_usage_key": unicode(lib_key),
"original_usage_version": unicode(self.store.get_block_original_usage(course_usage_key)[1]),
}
self.assertEqual(len(block_list[0]["descendants"]), len(descendants_expected))
for descendant in block_list[0]["descendants"]:
self.assertEqual(descendant, descendants_expected.get(descendant["usage_key"]))
def test_removed_overlimit(self):
"""
Test the "removed" event emitted when we un-assign blocks previously assigned to a student.
We go from one blocks assigned to none because max_count has been decreased.
"""
# Decrease max_count to 1, causing the block to be overlimit:
self.lc_block.get_child_descriptors() # We must call an XModule method before we can change max_count - otherwise the change has no effect
self.publisher.reset_mock() # Clear the "assigned" event that was just published.
self.lc_block.max_count = 0
del self.lc_block._xmodule._selected_set # Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
# Check that the event says that one block was removed, leaving no blocks left:
children = self.lc_block.get_child_descriptors()
self.assertEqual(len(children), 0)
event_data = self._assert_event_was_published("removed")
self.assertEqual(len(event_data["removed"]), 1)
self.assertEqual(event_data["result"], [])
self.assertEqual(event_data["reason"], "overlimit")
def test_removed_invalid(self):
"""
Test the "removed" event emitted when we un-assign blocks previously assigned to a student.
We go from two blocks assigned, to one because the others have been deleted from the library.
"""
# Start by assigning two blocks to the student:
self.lc_block.get_child_descriptors() # We must call an XModule method before we can change max_count - otherwise the change has no effect
self.lc_block.max_count = 2
del self.lc_block._xmodule._selected_set # Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
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.
# Now make sure that one of the assigned blocks will have to be un-assigned.
# To cause an "invalid" event, we delete all blocks from the content library except for one of the two already assigned to the student:
keep_block_key = initial_blocks_assigned[0].location
keep_block_lib_usage_key, keep_block_lib_version = self.store.get_block_original_usage(keep_block_key)
deleted_block_key = initial_blocks_assigned[1].location
self.library.children = [keep_block_lib_usage_key]
self.store.update_item(self.library, self.user_id)
self.lc_block.refresh_children()
del self.lc_block._xmodule._selected_set # Clear the cache (only needed because we skip saving/re-loading the block) pylint: disable=protected-access
# Check that the event says that one block was removed, leaving one block left:
children = self.lc_block.get_child_descriptors()
self.assertEqual(len(children), 1)
event_data = self._assert_event_was_published("removed")
self.assertEqual(event_data["removed"], [{
"usage_key": unicode(deleted_block_key),
"original_usage_key": None, # Note: original_usage_key info is sadly unavailable because the block has been deleted so that info can no longer be retrieved
"original_usage_version": None,
"descendants": [],
}])
self.assertEqual(event_data["result"], [{
"usage_key": unicode(keep_block_key),
"original_usage_key": unicode(keep_block_lib_usage_key),
"original_usage_version": unicode(keep_block_lib_version),
"descendants": [],
}])
self.assertEqual(event_data["reason"], "invalid")
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