Added code to fire library_updated signal when library is updated

This commit is contained in:
E. Kolpakov
2015-04-08 17:07:09 +03:00
parent bd891c21db
commit 77cc80f6b6
7 changed files with 110 additions and 33 deletions

View File

@@ -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)
return cls._do_reindex(modulestore, library_key)

View File

@@ -1,4 +1,4 @@
""" 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
"""

View File

@@ -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)
LOGGER.debug('Search indexing successful for library %s', library_id)

View File

@@ -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)
self._perform_test_using_store(store_type, self._test_exception)