Commit 2e380c71 by Martyn James

Merge pull request #7435 from edx/mjames/SOL-499

Update course_publish event actions - Logged SOL-611 for follow up work 
parents 13b43a96 838060f3
...@@ -119,6 +119,7 @@ class BulkOpsRecord(object): ...@@ -119,6 +119,7 @@ class BulkOpsRecord(object):
""" """
def __init__(self): def __init__(self):
self._active_count = 0 self._active_count = 0
self.has_publish_item = False
@property @property
def active(self): def active(self):
...@@ -281,6 +282,15 @@ class BulkOperationsMixin(object): ...@@ -281,6 +282,15 @@ class BulkOperationsMixin(object):
""" """
return self._get_bulk_ops_record(course_key, ignore_case).active return self._get_bulk_ops_record(course_key, ignore_case).active
def send_bulk_published_signal(self, bulk_ops_record, course_id):
"""
Sends out the signal that items have been published from within this course.
"""
signal_handler = getattr(self, 'signal_handler', None)
if signal_handler and bulk_ops_record.has_publish_item:
signal_handler.send("course_published", course_key=course_id)
bulk_ops_record.has_publish_item = False
class EditInfo(object): class EditInfo(object):
""" """
...@@ -1299,6 +1309,23 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -1299,6 +1309,23 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
parent.children.append(item.location) parent.children.append(item.location)
self.update_item(parent, user_id) self.update_item(parent, user_id)
def _flag_publish_event(self, course_key):
"""
Wrapper around calls to fire the course_published 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:
course_key - course_key to which the signal applies
"""
signal_handler = getattr(self, 'signal_handler', None)
if signal_handler:
bulk_record = self._get_bulk_ops_record(course_key) if isinstance(self, BulkOperationsMixin) else None
if bulk_record and bulk_record.active:
bulk_record.has_publish_item = True
else:
signal_handler.send("course_published", course_key=course_key)
def only_xmodules(identifier, entry_points): def only_xmodules(identifier, entry_points):
"""Only use entry_points that are supplied by the xmodule package""" """Only use entry_points that are supplied by the xmodule package"""
......
...@@ -474,8 +474,8 @@ class MongoBulkOpsMixin(BulkOperationsMixin): ...@@ -474,8 +474,8 @@ class MongoBulkOpsMixin(BulkOperationsMixin):
if bulk_ops_record.dirty: if bulk_ops_record.dirty:
self.refresh_cached_metadata_inheritance_tree(course_id) self.refresh_cached_metadata_inheritance_tree(course_id)
if emit_signals and self.signal_handler: if emit_signals:
self.signal_handler.send("course_published", course_key=course_id) self.send_bulk_published_signal(bulk_ops_record, course_id)
bulk_ops_record.dirty = False # brand spanking clean now bulk_ops_record.dirty = False # brand spanking clean now
...@@ -1328,7 +1328,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -1328,7 +1328,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
else: else:
parent.children.insert(kwargs.get('position'), xblock.location) parent.children.insert(kwargs.get('position'), xblock.location)
self.update_item(parent, user_id) self.update_item(parent, user_id, child_update=True)
return xblock return xblock
......
...@@ -439,7 +439,15 @@ class DraftModuleStore(MongoModuleStore): ...@@ -439,7 +439,15 @@ class DraftModuleStore(MongoModuleStore):
# convert the subtree using the original item as the root # convert the subtree using the original item as the root
self._breadth_first(convert_item, [location]) self._breadth_first(convert_item, [location])
def update_item(self, xblock, user_id, allow_not_found=False, force=False, isPublish=False, **kwargs): def update_item(
self,
xblock,
user_id,
allow_not_found=False,
force=False,
isPublish=False,
child_update=False,
**kwargs):
""" """
See superclass doc. See superclass doc.
In addition to the superclass's behavior, this method converts the unit to draft if it's not In addition to the superclass's behavior, this method converts the unit to draft if it's not
...@@ -451,9 +459,8 @@ class DraftModuleStore(MongoModuleStore): ...@@ -451,9 +459,8 @@ class DraftModuleStore(MongoModuleStore):
if draft_loc.revision == MongoRevisionKey.published: if draft_loc.revision == MongoRevisionKey.published:
item = super(DraftModuleStore, self).update_item(xblock, user_id, allow_not_found) item = super(DraftModuleStore, self).update_item(xblock, user_id, allow_not_found)
course_key = xblock.location.course_key course_key = xblock.location.course_key
bulk_record = self._get_bulk_ops_record(course_key) if isPublish or (item.category in DIRECT_ONLY_CATEGORIES and not child_update):
if self.signal_handler and not bulk_record.active: self._flag_publish_event(course_key)
self.signal_handler.send("course_published", course_key=course_key)
return item return item
if not super(DraftModuleStore, self).has_item(draft_loc): if not super(DraftModuleStore, self).has_item(draft_loc):
...@@ -532,7 +539,8 @@ class DraftModuleStore(MongoModuleStore): ...@@ -532,7 +539,8 @@ class DraftModuleStore(MongoModuleStore):
parent_block = super(DraftModuleStore, self).get_item(parent_location) parent_block = super(DraftModuleStore, self).get_item(parent_location)
parent_block.children.remove(location) parent_block.children.remove(location)
parent_block.location = parent_location # ensure the location is with the correct revision parent_block.location = parent_location # ensure the location is with the correct revision
self.update_item(parent_block, user_id) self.update_item(parent_block, user_id, child_update=True)
self._flag_publish_event(location.course_key)
if is_item_direct_only or revision == ModuleStoreEnum.RevisionOption.all: if is_item_direct_only or revision == ModuleStoreEnum.RevisionOption.all:
as_functions = [as_draft, as_published] as_functions = [as_draft, as_published]
...@@ -728,8 +736,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -728,8 +736,7 @@ class DraftModuleStore(MongoModuleStore):
bulk_record.dirty = True bulk_record.dirty = True
self.collection.remove({'_id': {'$in': to_be_deleted}}) self.collection.remove({'_id': {'$in': to_be_deleted}})
if self.signal_handler and not bulk_record.active: self._flag_publish_event(course_key)
self.signal_handler.send("course_published", course_key=course_key)
# Now it's been published, add the object to the courseware search index so that it appears in search results # Now it's been published, add the object to the courseware search index so that it appears in search results
CoursewareSearchIndexer.do_publish_index(self, location) CoursewareSearchIndexer.do_publish_index(self, location)
...@@ -747,9 +754,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -747,9 +754,7 @@ class DraftModuleStore(MongoModuleStore):
self._convert_to_draft(location, user_id, delete_published=True) self._convert_to_draft(location, user_id, delete_published=True)
course_key = location.course_key course_key = location.course_key
bulk_record = self._get_bulk_ops_record(course_key) self._flag_publish_event(course_key)
if self.signal_handler and not bulk_record.active:
self.signal_handler.send("course_published", course_key=course_key)
def revert_to_published(self, location, user_id=None): def revert_to_published(self, location, user_id=None):
""" """
......
...@@ -268,9 +268,7 @@ class SplitBulkWriteMixin(BulkOperationsMixin): ...@@ -268,9 +268,7 @@ class SplitBulkWriteMixin(BulkOperationsMixin):
self.db_connection.update_course_index(bulk_write_record.index, from_index=bulk_write_record.initial_index) self.db_connection.update_course_index(bulk_write_record.index, from_index=bulk_write_record.initial_index)
if dirty and emit_signals: if dirty and emit_signals:
signal_handler = getattr(self, 'signal_handler', None) self.send_bulk_published_signal(bulk_write_record, course_key)
if signal_handler:
signal_handler.send("course_published", course_key=course_key)
def get_course_index(self, course_key, ignore_case=False): def get_course_index(self, course_key, ignore_case=False):
""" """
......
...@@ -201,6 +201,7 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli ...@@ -201,6 +201,7 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
] ]
) )
self._flag_publish_event(location.course_key)
for branch in branches_to_delete: for branch in branches_to_delete:
branched_location = location.for_branch(branch) branched_location = location.for_branch(branch)
parent_loc = self.get_parent_location(branched_location) parent_loc = self.get_parent_location(branched_location)
...@@ -356,6 +357,8 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli ...@@ -356,6 +357,8 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
blacklist=blacklist blacklist=blacklist
) )
self._flag_publish_event(location.course_key)
# Now it's been published, add the object to the courseware search index so that it appears in search results # Now it's been published, add the object to the courseware search index so that it appears in search results
CoursewareSearchIndexer.do_publish_index(self, location) CoursewareSearchIndexer.do_publish_index(self, location)
......
...@@ -1976,9 +1976,8 @@ class TestMixedModuleStore(CourseComparisonTest): ...@@ -1976,9 +1976,8 @@ class TestMixedModuleStore(CourseComparisonTest):
dest_store = self.store._get_modulestore_by_type(ModuleStoreEnum.Type.split) dest_store = self.store._get_modulestore_by_type(ModuleStoreEnum.Type.split)
self.assertCoursesEqual(source_store, source_course_key, dest_store, dest_course_id) self.assertCoursesEqual(source_store, source_course_key, dest_store, dest_course_id)
@skip("PLAT-449 XModule TestMixedModuleStore intermittent test failure")
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_publish_signal_firing(self, default): def test_course_publish_signal_direct_firing(self, default):
with MongoContentstoreBuilder().build() as contentstore: with MongoContentstoreBuilder().build() as contentstore:
self.store = MixedModuleStore( self.store = MixedModuleStore(
contentstore=contentstore, contentstore=contentstore,
...@@ -2016,22 +2015,29 @@ class TestMixedModuleStore(CourseComparisonTest): ...@@ -2016,22 +2015,29 @@ class TestMixedModuleStore(CourseComparisonTest):
self.store.publish(block.location, self.user_id) self.store.publish(block.location, self.user_id)
self.assertEqual(receiver.call_count, 3) self.assertEqual(receiver.call_count, 3)
# Test a draftable block type, which needs to be explicitly published. @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
receiver.reset_mock() def test_course_publish_signal_rerun_firing(self, default):
block = self.store.create_child(self.user_id, course.location, 'problem') with MongoContentstoreBuilder().build() as contentstore:
self.assertEqual(receiver.call_count, 1) self.store = MixedModuleStore(
contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance,
mappings={},
signal_handler=SignalHandler(MixedModuleStore),
**self.OPTIONS
)
self.addCleanup(self.store.close_all_connections)
self.store.update_item(block, self.user_id) with self.store.default_store(default):
self.assertEqual(receiver.call_count, 1) self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
self.store.publish(block.location, self.user_id) with mock_signal_receiver(SignalHandler.course_published) as receiver:
self.assertEqual(receiver.call_count, 2) self.assertEqual(receiver.call_count, 0)
self.store.unpublish(block.location, self.user_id) # Course creation and publication should fire the signal
self.assertEqual(receiver.call_count, 3) course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
self.assertEqual(receiver.call_count, 1)
self.store.delete_item(block.location, self.user_id) course_key = course.id
self.assertEqual(receiver.call_count, 4)
# Test course re-runs # Test course re-runs
receiver.reset_mock() receiver.reset_mock()
...@@ -2039,6 +2045,24 @@ class TestMixedModuleStore(CourseComparisonTest): ...@@ -2039,6 +2045,24 @@ class TestMixedModuleStore(CourseComparisonTest):
self.store.clone_course(course_key, dest_course_id, self.user_id) self.store.clone_course(course_key, dest_course_id, self.user_id)
self.assertEqual(receiver.call_count, 1) self.assertEqual(receiver.call_count, 1)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_publish_signal_import_firing(self, default):
with MongoContentstoreBuilder().build() as contentstore:
self.store = MixedModuleStore(
contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance,
mappings={},
signal_handler=SignalHandler(MixedModuleStore),
**self.OPTIONS
)
self.addCleanup(self.store.close_all_connections)
with self.store.default_store(default):
self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
with mock_signal_receiver(SignalHandler.course_published) as receiver:
self.assertEqual(receiver.call_count, 0)
# Test course imports # Test course imports
# Note: The signal is fired once when the course is created and # Note: The signal is fired once when the course is created and
# a second time after the actual data import. # a second time after the actual data import.
...@@ -2049,3 +2073,172 @@ class TestMixedModuleStore(CourseComparisonTest): ...@@ -2049,3 +2073,172 @@ class TestMixedModuleStore(CourseComparisonTest):
create_if_not_present=True, create_if_not_present=True,
) )
self.assertEqual(receiver.call_count, 2) self.assertEqual(receiver.call_count, 2)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_publish_signal_publish_firing(self, default):
with MongoContentstoreBuilder().build() as contentstore:
self.store = MixedModuleStore(
contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance,
mappings={},
signal_handler=SignalHandler(MixedModuleStore),
**self.OPTIONS
)
self.addCleanup(self.store.close_all_connections)
with self.store.default_store(default):
self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
with mock_signal_receiver(SignalHandler.course_published) as receiver:
self.assertEqual(receiver.call_count, 0)
# Course creation and publication should fire the signal
course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
self.assertEqual(receiver.call_count, 1)
# Test a draftable block type, which needs to be explicitly published, and nest it within the
# normal structure - this is important because some implementors change the parent when adding a
# non-published child; if parent is in DIRECT_ONLY_CATEGORIES then this should not fire the event
receiver.reset_mock()
section = self.store.create_item(self.user_id, course.id, 'chapter')
self.assertEqual(receiver.call_count, 1)
subsection = self.store.create_child(self.user_id, section.location, 'sequential')
self.assertEqual(receiver.call_count, 2)
# 'units' and 'blocks' are draftable types
receiver.reset_mock()
unit = self.store.create_child(self.user_id, subsection.location, 'vertical')
self.assertEqual(receiver.call_count, 0)
block = self.store.create_child(self.user_id, unit.location, 'problem')
self.assertEqual(receiver.call_count, 0)
self.store.update_item(block, self.user_id)
self.assertEqual(receiver.call_count, 0)
self.store.publish(unit.location, self.user_id)
self.assertEqual(receiver.call_count, 1)
self.store.unpublish(unit.location, self.user_id)
self.assertEqual(receiver.call_count, 2)
self.store.delete_item(unit.location, self.user_id)
self.assertEqual(receiver.call_count, 3)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_bulk_course_publish_signal_direct_firing(self, default):
with MongoContentstoreBuilder().build() as contentstore:
self.store = MixedModuleStore(
contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance,
mappings={},
signal_handler=SignalHandler(MixedModuleStore),
**self.OPTIONS
)
self.addCleanup(self.store.close_all_connections)
with self.store.default_store(default):
self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
with mock_signal_receiver(SignalHandler.course_published) as receiver:
self.assertEqual(receiver.call_count, 0)
# Course creation and publication should fire the signal
course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
self.assertEqual(receiver.call_count, 1)
course_key = course.id
# Test non-draftable block types. No signals should be received until
receiver.reset_mock()
with self.store.bulk_operations(course_key):
categories = DIRECT_ONLY_CATEGORIES
for block_type in categories:
log.debug('Testing with block type %s', block_type)
block = self.store.create_item(self.user_id, course_key, block_type)
self.assertEqual(receiver.call_count, 0)
block.display_name = block_type
self.store.update_item(block, self.user_id)
self.assertEqual(receiver.call_count, 0)
self.store.publish(block.location, self.user_id)
self.assertEqual(receiver.call_count, 0)
self.assertEqual(receiver.call_count, 1)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_bulk_course_publish_signal_publish_firing(self, default):
with MongoContentstoreBuilder().build() as contentstore:
self.store = MixedModuleStore(
contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance,
mappings={},
signal_handler=SignalHandler(MixedModuleStore),
**self.OPTIONS
)
self.addCleanup(self.store.close_all_connections)
with self.store.default_store(default):
self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
with mock_signal_receiver(SignalHandler.course_published) as receiver:
self.assertEqual(receiver.call_count, 0)
# Course creation and publication should fire the signal
course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
self.assertEqual(receiver.call_count, 1)
course_key = course.id
# Test a draftable block type, which needs to be explicitly published, and nest it within the
# normal structure - this is important because some implementors change the parent when adding a
# non-published child; if parent is in DIRECT_ONLY_CATEGORIES then this should not fire the event
receiver.reset_mock()
with self.store.bulk_operations(course_key):
section = self.store.create_item(self.user_id, course_key, 'chapter')
self.assertEqual(receiver.call_count, 0)
subsection = self.store.create_child(self.user_id, section.location, 'sequential')
self.assertEqual(receiver.call_count, 0)
# 'units' and 'blocks' are draftable types
unit = self.store.create_child(self.user_id, subsection.location, 'vertical')
self.assertEqual(receiver.call_count, 0)
block = self.store.create_child(self.user_id, unit.location, 'problem')
self.assertEqual(receiver.call_count, 0)
self.store.update_item(block, self.user_id)
self.assertEqual(receiver.call_count, 0)
self.store.publish(unit.location, self.user_id)
self.assertEqual(receiver.call_count, 0)
self.store.unpublish(unit.location, self.user_id)
self.assertEqual(receiver.call_count, 0)
self.store.delete_item(unit.location, self.user_id)
self.assertEqual(receiver.call_count, 0)
self.assertEqual(receiver.call_count, 1)
# Test editing draftable block type without publish
receiver.reset_mock()
with self.store.bulk_operations(course_key):
unit = self.store.create_child(self.user_id, subsection.location, 'vertical')
self.assertEqual(receiver.call_count, 0)
block = self.store.create_child(self.user_id, unit.location, 'problem')
self.assertEqual(receiver.call_count, 0)
self.store.publish(unit.location, self.user_id)
self.assertEqual(receiver.call_count, 0)
self.assertEqual(receiver.call_count, 1)
receiver.reset_mock()
with self.store.bulk_operations(course_key):
self.assertEqual(receiver.call_count, 0)
unit.display_name = "Change this unit"
self.store.update_item(unit, self.user_id)
self.assertEqual(receiver.call_count, 0)
self.assertEqual(receiver.call_count, 0)
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