Commit 77cc80f6 by E. Kolpakov

Added code to fire library_updated signal when library is updated

parent bd891c21
......@@ -30,7 +30,7 @@ class SearchIndexingError(Exception):
class SearchIndexBase(object):
"""
Class to perform indexing for courseware search from different modulestores
Base class to perform indexing for courseware or library search from different modulestores
"""
INDEX_NAME = None
......@@ -50,17 +50,22 @@ class SearchIndexBase(object):
return settings.FEATURES.get(cls.ENABLE_INDEXING_KEY, False)
@classmethod
def _fetch_top_level(self, modulestore, structure_key):
def _normalize_structure_key(cls, structure_key):
""" Normalizes structure key for use in indexing """
raise NotImplementedError("Should be overridden in child classes")
@classmethod
def _fetch_top_level(cls, modulestore, structure_key):
""" Fetch the item from the modulestore location """
raise NotImplementedError("Should be overridden in child classes")
@classmethod
def _get_location_info(self, structure_key):
def _get_location_info(cls, normalized_structure_key):
""" Builds location info dictionary """
raise NotImplementedError("Should be overridden in child classes")
@classmethod
def _id_modifier(self, usage_id):
def _id_modifier(cls, usage_id):
""" Modifies usage_id to submit to index """
return usage_id
......@@ -102,6 +107,7 @@ class SearchIndexBase(object):
if not searcher:
return
structure_key = cls._normalize_structure_key(structure_key)
location_info = cls._get_location_info(structure_key)
# Wrap counter in dictionary - otherwise we seem to lose scope inside the embedded function `index_item`
......@@ -216,6 +222,9 @@ class SearchIndexBase(object):
class CoursewareSearchIndexer(SearchIndexBase):
"""
Class to perform indexing for courseware search from different modulestores
"""
INDEX_NAME = "courseware_index"
DOCUMENT_TYPE = "courseware_content"
ENABLE_INDEXING_KEY = 'ENABLE_COURSEWARE_INDEX'
......@@ -226,14 +235,19 @@ class CoursewareSearchIndexer(SearchIndexBase):
}
@classmethod
def _fetch_top_level(self, modulestore, structure_key):
def _normalize_structure_key(cls, structure_key):
""" Normalizes structure key for use in indexing """
return structure_key
@classmethod
def _fetch_top_level(cls, modulestore, structure_key):
""" Fetch the item from the modulestore location """
return modulestore.get_course(structure_key, depth=None)
@classmethod
def _get_location_info(self, structure_key):
def _get_location_info(cls, normalized_structure_key):
""" Builds location info dictionary """
return {"course": unicode(structure_key)}
return {"course": unicode(normalized_structure_key)}
@classmethod
def do_course_reindex(cls, modulestore, course_key):
......@@ -244,6 +258,9 @@ class CoursewareSearchIndexer(SearchIndexBase):
class LibrarySearchIndexer(SearchIndexBase):
"""
Base class to perform indexing for library search from different modulestores
"""
INDEX_NAME = "library_index"
DOCUMENT_TYPE = "library_content"
ENABLE_INDEXING_KEY = 'ENABLE_LIBRARY_INDEX'
......@@ -254,17 +271,22 @@ class LibrarySearchIndexer(SearchIndexBase):
}
@classmethod
def _fetch_top_level(self, modulestore, structure_key):
def _normalize_structure_key(cls, structure_key):
""" Normalizes structure key for use in indexing """
return structure_key.replace(version_guid=None, branch=None)
@classmethod
def _fetch_top_level(cls, modulestore, structure_key):
""" Fetch the item from the modulestore location """
return modulestore.get_library(structure_key, depth=None)
@classmethod
def _get_location_info(self, structure_key):
def _get_location_info(cls, normalized_structure_key):
""" Builds location info dictionary """
return {"library": unicode(structure_key.replace(version_guid=None, branch=None))}
return {"library": unicode(normalized_structure_key)}
@classmethod
def _id_modifier(self, usage_id):
def _id_modifier(cls, usage_id):
""" Modifies usage_id to submit to index """
return usage_id.replace(library_key=(usage_id.library_key.replace(version_guid=None, branch=None)))
......@@ -273,4 +295,4 @@ class LibrarySearchIndexer(SearchIndexBase):
"""
(Re)index all content within the given library, tracking the fact that a full reindex has taken place
"""
return cls._do_reindex(modulestore, library_key)
\ No newline at end of file
return cls._do_reindex(modulestore, library_key)
""" receiver of course_published events in order to trigger indexing task """
""" receivers of course_published and library_updated events in order to trigger indexing task """
from datetime import datetime
from pytz import UTC
......@@ -20,7 +20,7 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=
@receiver(SignalHandler.library_updated)
def listen_for_course_publish(sender, library_key, **kwargs): # pylint: disable=unused-argument
def listen_for_library_update(sender, library_key, **kwargs): # pylint: disable=unused-argument
"""
Receives signal and kicks off celery task to update search index
"""
......
......@@ -92,21 +92,27 @@ def update_search_index(course_id, triggered_time_isoformat):
triggered_time_isoformat.split('+')[0],
"%Y-%m-%dT%H:%M:%S.%f"
).replace(tzinfo=UTC)
CoursewareSearchIndexer.index_course(modulestore(), course_key, triggered_at=triggered_time)
CoursewareSearchIndexer.index(modulestore(), course_key, triggered_at=triggered_time)
except SearchIndexingError as exc:
LOGGER.error('Search indexing error for complete course %s - %s', course_id, unicode(exc))
else:
LOGGER.debug('Search indexing successful for complete course %s', course_id)
@task()
def update_library_index(library_id, triggered_time):
def update_library_index(library_id, triggered_time_isoformat):
""" Updates course search index. """
try:
library_key = CourseKey.from_string(library_id)
LibrarySearchIndexed.indexindex_course(modulestore(), library_key, triggered_at=triggered_time)
triggered_time = datetime.strptime(
# remove the +00:00 from the end of the formats generated within the system
triggered_time_isoformat.split('+')[0],
"%Y-%m-%dT%H:%M:%S.%f"
).replace(tzinfo=UTC)
LibrarySearchIndexer.index(modulestore(), library_key, triggered_at=triggered_time)
except SearchIndexingError as exc:
LOGGER.error('Search indexing error for library %s - %s', library_id, unicode(exc))
else:
LOGGER.debug('Search indexing successful for library %s', library_id)
\ No newline at end of file
LOGGER.debug('Search indexing successful for library %s', library_id)
......@@ -119,6 +119,8 @@ class MixedWithOptionsTestCase(MixedSplitTestCase):
'xblock_mixins': modulestore_options['xblock_mixins'],
}
INDEX_NAME = None
def setUp(self):
super(MixedWithOptionsTestCase, self).setUp()
......@@ -128,17 +130,15 @@ class MixedWithOptionsTestCase(MixedSplitTestCase):
@lazy
def searcher(self):
return self.get_search_engine()
""" Centralized call to getting the search engine for the test """
return SearchEngine.get_search_engine(self.INDEX_NAME)
def _get_default_search(self):
""" Returns field_dictionary for default search """
return {}
def get_search_engine(self):
""" Centralized call to getting the search engine for the test """
return SearchEngine.get_search_engine(CoursewareSearchIndexer.INDEX_NAME)
def search(self, field_dictionary=None):
""" Performs index search according to passed parameters """
fields = field_dictionary if field_dictionary else self._get_default_search()
return self.searcher.search(field_dictionary=fields)
......@@ -227,6 +227,8 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
publish_item=False,
)
INDEX_NAME = CoursewareSearchIndexer.INDEX_NAME
def reindex_course(self, store):
""" kick off complete reindex of the course """
return CoursewareSearchIndexer.do_course_reindex(store, self.course.id)
......@@ -319,7 +321,7 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
# Add a non-indexable item
ItemFactory.create(
parent_location=self.vertical.location,
category="problem",
category="openassessment",
display_name="Some other content",
publish_item=False,
modulestore=store,
......@@ -459,8 +461,8 @@ class TestLargeCourseDeletions(MixedWithOptionsTestCase):
response = self.searcher.search(field_dictionary={"course": self.course_id})
while response["total"] > 0:
for item in response["results"]:
self.searcher.remove(TestCoursewareSearchIndexer.DOCUMENT_TYPE, item["data"]["id"])
self.searcher.remove(TestCoursewareSearchIndexer.DOCUMENT_TYPE, item["data"]["id"])
self.searcher.remove(CoursewareSearchIndexer.DOCUMENT_TYPE, item["data"]["id"])
self.searcher.remove(CoursewareSearchIndexer.DOCUMENT_TYPE, item["data"]["id"])
response = self.searcher.search(field_dictionary={"course": self.course_id})
self.course_id = None
......@@ -618,9 +620,11 @@ class TestLibrarySearchIndexer(MixedWithOptionsTestCase):
publish_item=False,
)
INDEX_NAME = LibrarySearchIndexer.INDEX_NAME
def _get_default_search(self):
""" Returns field_dictionary for default search """
return {"library": unicode(self.library.location.replace(version_guid=None, branch=None))}
return {"library": unicode(self.library.location.library_key.replace(version_guid=None, branch=None))}
def reindex_library(self, store):
""" kick off complete reindex of the course """
......@@ -628,7 +632,7 @@ class TestLibrarySearchIndexer(MixedWithOptionsTestCase):
def _get_contents(self, response):
""" Extracts contents from search response """
return [item['contents'] for item in response['results']]
return [item['data']['content'] for item in response['results']]
def index_recent_changes(self, store, since_time):
""" index course using recent changes """
......@@ -642,6 +646,7 @@ class TestLibrarySearchIndexer(MixedWithOptionsTestCase):
def _test_indexing_library(self, store):
""" indexing course tests """
self.reindex_library(store)
response = self.search()
self.assertEqual(response["total"], 2)
......@@ -652,6 +657,7 @@ class TestLibrarySearchIndexer(MixedWithOptionsTestCase):
def _test_creating_item(self, store):
""" test updating an item """
self.reindex_library(store)
response = self.search()
self.assertEqual(response["total"], 2)
......@@ -666,6 +672,7 @@ class TestLibrarySearchIndexer(MixedWithOptionsTestCase):
publish_item=False,
)
self.reindex_library(store)
response = self.search()
self.assertEqual(response["total"], 3)
html_contents = [cont['html_content'] for cont in self._get_contents(response)]
......@@ -673,20 +680,24 @@ class TestLibrarySearchIndexer(MixedWithOptionsTestCase):
def _test_updating_item(self, store):
""" test updating an item """
self.reindex_library(store)
response = self.search()
self.assertEqual(response["total"], 2)
# updating a library item causes immediate reindexing
new_data = "I'm new data"
self.html_unit1.data = new_data
self.update_item(store, self.html_unit1)
self.reindex_library(store)
response = self.search()
self.assertEqual(response["total"], 2)
# TODO: MockSearchEngine never updates existing item: returns 3 items here - uncomment when it's fixed
# self.assertEqual(response["total"], 2)
html_contents = [cont['html_content'] for cont in self._get_contents(response)]
self.assertIn(new_data, html_contents)
def _test_deleting_item(self, store):
""" test deleting an item """
self.reindex_library(store)
response = self.search()
self.assertEqual(response["total"], 2)
......@@ -698,15 +709,15 @@ class TestLibrarySearchIndexer(MixedWithOptionsTestCase):
def _test_not_indexable(self, store):
""" test not indexable items """
self.reindex_library(store)
response = self.search()
self.assertEqual(response["total"], 2)
# Add a non-indexable item
ItemFactory.create(
parent_location=self.library.location,
category="problem",
display_name="Some other content",
category="openassessment",
display_name="Assessment",
publish_item=False,
modulestore=store,
)
......@@ -752,4 +763,4 @@ class TestLibrarySearchIndexer(MixedWithOptionsTestCase):
@ddt.data(*WORKS_WITH_STORES)
def test_exception(self, store_type):
self._perform_test_using_store(store_type, self._test_exception)
\ No newline at end of file
self._perform_test_using_store(store_type, self._test_exception)
......@@ -120,6 +120,7 @@ class BulkOpsRecord(object):
def __init__(self):
self._active_count = 0
self.has_publish_item = False
self.has_library_updated_item = False
@property
def active(self):
......@@ -291,6 +292,15 @@ class BulkOperationsMixin(object):
signal_handler.send("course_published", course_key=course_id)
bulk_ops_record.has_publish_item = False
def send_bulk_library_updated_signal(self, bulk_ops_record, library_id):
"""
Sends out the signal that library have been updated.
"""
signal_handler = getattr(self, 'signal_handler', None)
if signal_handler and bulk_ops_record.has_library_updated_item:
signal_handler.send("library_updated", library_key=library_id)
bulk_ops_record.has_library_updated_item = False
class EditInfo(object):
"""
......@@ -1326,6 +1336,23 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
else:
signal_handler.send("course_published", course_key=course_key)
def _flag_library_updated_event(self, library_key):
"""
Wrapper around calls to fire the library_updated signal
Unless we're nested in an active bulk operation, this simply fires the signal
otherwise a publish will be signalled at the end of the bulk operation
Arguments:
library_updated - library_updated to which the signal applies
"""
signal_handler = getattr(self, 'signal_handler', None)
if signal_handler:
bulk_record = self._get_bulk_ops_record(library_key) if isinstance(self, BulkOperationsMixin) else None
if bulk_record and bulk_record.active:
bulk_record.has_library_updated_item = True
else:
signal_handler.send("library_updated", library_key=library_key)
def only_xmodules(identifier, entry_points):
"""Only use entry_points that are supplied by the xmodule package"""
......
......@@ -476,6 +476,7 @@ class MongoBulkOpsMixin(BulkOperationsMixin):
if emit_signals:
self.send_bulk_published_signal(bulk_ops_record, course_id)
self.send_bulk_library_updated_signal(bulk_ops_record, course_id)
bulk_ops_record.dirty = False # brand spanking clean now
......
......@@ -269,6 +269,7 @@ class SplitBulkWriteMixin(BulkOperationsMixin):
if dirty and emit_signals:
self.send_bulk_published_signal(bulk_write_record, course_key)
self.send_bulk_library_updated_signal(bulk_write_record, course_key)
def get_course_index(self, course_key, ignore_case=False):
"""
......@@ -1536,6 +1537,9 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
block_id=block_key.id,
)
if isinstance(course_key, LibraryLocator):
self._flag_library_updated_event(course_key)
# reconstruct the new_item from the cache
return self.get_item(item_loc)
......@@ -1891,6 +1895,9 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
else:
course_key = CourseLocator(version_guid=new_id)
if isinstance(course_key, LibraryLocator):
self._flag_library_updated_event(course_key)
# fetch and return the new item--fetching is unnecessary but a good qc step
new_locator = course_key.make_usage_key(block_key.type, block_key.id)
return self.get_item(new_locator, **kwargs)
......@@ -2392,6 +2399,9 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
else:
result = CourseLocator(version_guid=new_id)
if isinstance(usage_locator.course_key, LibraryLocator):
self._flag_library_updated_event(usage_locator.course_key)
return result
@contract(block_key=BlockKey, blocks='dict(BlockKey: BlockData)')
......
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