diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index c167c25422..6949472011 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -272,12 +272,20 @@ class BulkOperationsMixin(object): dirty = self._end_outermost_bulk_operation(bulk_ops_record, structure_key) - self._clear_bulk_ops_record(structure_key) + # The bulk op has ended. However, the signal tasks below still need to use the + # built-up bulk op information (if the signals trigger tasks in the same thread). + # So re-nest until the signals are sent. + bulk_ops_record.nest() if emit_signals and dirty: self.send_bulk_published_signal(bulk_ops_record, structure_key) self.send_bulk_library_updated_signal(bulk_ops_record, structure_key) + # Signals are sent. Now unnest and clear the bulk op for good. + bulk_ops_record.unnest() + + self._clear_bulk_ops_record(structure_key) + def _is_in_bulk_operation(self, course_key, ignore_case=False): """ Return whether a bulk operation is active on `course_key`. diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py index b41bf268d6..dfafc920e1 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py @@ -2015,8 +2015,11 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): course_key = course.id def _clear_bulk_ops_record(course_key): # pylint: disable=unused-argument - """ Check if the signal has been fired. """ - self.assertEqual(receiver.call_count, 0) + """ + Check if the signal has been fired. + The course_published signal fires before the _clear_bulk_ops_record. + """ + self.assertEqual(receiver.call_count, 1) with patch.object( self.store.thread_cache.default_store, '_clear_bulk_ops_record', wraps=_clear_bulk_ops_record diff --git a/openedx/core/djangoapps/content/course_structures/tasks.py b/openedx/core/djangoapps/content/course_structures/tasks.py index f621cbe25f..5ed989be33 100644 --- a/openedx/core/djangoapps/content/course_structures/tasks.py +++ b/openedx/core/djangoapps/content/course_structures/tasks.py @@ -13,38 +13,40 @@ def _generate_course_structure(course_key): """ Generates a course structure dictionary for the specified course. """ - course = modulestore().get_course(course_key, depth=None) - blocks_stack = [course] - blocks_dict = {} - while blocks_stack: - curr_block = blocks_stack.pop() - children = curr_block.get_children() if curr_block.has_children else [] - key = unicode(curr_block.scope_ids.usage_id) - block = { - "usage_key": key, - "block_type": curr_block.category, - "display_name": curr_block.display_name, - "children": [unicode(child.scope_ids.usage_id) for child in children] + with modulestore().bulk_operations(course_key): + course = modulestore().get_course(course_key, depth=None) + blocks_stack = [course] + blocks_dict = {} + while blocks_stack: + curr_block = blocks_stack.pop() + children = curr_block.get_children() if curr_block.has_children else [] + key = unicode(curr_block.scope_ids.usage_id) + block = { + "usage_key": key, + "block_type": curr_block.category, + "display_name": curr_block.display_name, + "children": [unicode(child.scope_ids.usage_id) for child in children] + } + + # Retrieve these attributes separately so that we can fail gracefully + # if the block doesn't have the attribute. + attrs = (('graded', False), ('format', None)) + for attr, default in attrs: + if hasattr(curr_block, attr): + block[attr] = getattr(curr_block, attr, default) + else: + log.warning('Failed to retrieve %s attribute of block %s. Defaulting to %s.', attr, key, default) + block[attr] = default + + blocks_dict[key] = block + + # Add this blocks children to the stack so that we can traverse them as well. + blocks_stack.extend(children) + return { + "root": unicode(course.scope_ids.usage_id), + "blocks": blocks_dict } - # Retrieve these attributes separately so that we can fail gracefully if the block doesn't have the attribute. - attrs = (('graded', False), ('format', None)) - for attr, default in attrs: - if hasattr(curr_block, attr): - block[attr] = getattr(curr_block, attr, default) - else: - log.warning('Failed to retrieve %s attribute of block %s. Defaulting to %s.', attr, key, default) - block[attr] = default - - blocks_dict[key] = block - - # Add this blocks children to the stack so that we can traverse them as well. - blocks_stack.extend(children) - return { - "root": unicode(course.scope_ids.usage_id), - "blocks": blocks_dict - } - @task(name=u'openedx.core.djangoapps.content.course_structures.tasks.update_course_structure') def update_course_structure(course_key):