Commit 6da7bff1 by Calen Pennington

Allow test_mixed_modulestore to be run without django setup

parent 92eadf1a
...@@ -315,7 +315,8 @@ class BulkOperationsMixin(object): ...@@ -315,7 +315,8 @@ class BulkOperationsMixin(object):
Sends out the signal that items have been published from within this course. Sends out the signal that items have been published from within this course.
""" """
if self.signal_handler and bulk_ops_record.has_publish_item: if self.signal_handler and bulk_ops_record.has_publish_item:
self.signal_handler.send("course_published", course_key=course_id) # We remove the branch, because publishing always means copying from draft to published
self.signal_handler.send("course_published", course_key=course_id.for_branch(None))
bulk_ops_record.has_publish_item = False bulk_ops_record.has_publish_item = False
def send_bulk_library_updated_signal(self, bulk_ops_record, library_id): def send_bulk_library_updated_signal(self, bulk_ops_record, library_id):
...@@ -1345,22 +1346,6 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -1345,22 +1346,6 @@ 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
"""
if self.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:
self.signal_handler.send("course_published", course_key=course_key)
def _flag_library_updated_event(self, library_key): def _flag_library_updated_event(self, library_key):
""" """
Wrapper around calls to fire the library_updated signal Wrapper around calls to fire the library_updated signal
......
...@@ -5,7 +5,7 @@ This module provides an abstraction for Module Stores that support Draft and Pub ...@@ -5,7 +5,7 @@ This module provides an abstraction for Module Stores that support Draft and Pub
import threading import threading
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from contextlib import contextmanager from contextlib import contextmanager
from . import ModuleStoreEnum from . import ModuleStoreEnum, BulkOperationsMixin
# Things w/ these categories should never be marked as version=DRAFT # Things w/ these categories should never be marked as version=DRAFT
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
...@@ -62,7 +62,7 @@ class BranchSettingMixin(object): ...@@ -62,7 +62,7 @@ class BranchSettingMixin(object):
return self.default_branch_setting_func() return self.default_branch_setting_func()
class ModuleStoreDraftAndPublished(BranchSettingMixin): class ModuleStoreDraftAndPublished(BranchSettingMixin, BulkOperationsMixin):
""" """
A mixin for a read-write database backend that supports two branches, Draft and Published, with A mixin for a read-write database backend that supports two branches, Draft and Published, with
options to prefer Draft and fallback to Published. options to prefer Draft and fallback to Published.
...@@ -112,6 +112,23 @@ class ModuleStoreDraftAndPublished(BranchSettingMixin): ...@@ -112,6 +112,23 @@ class ModuleStoreDraftAndPublished(BranchSettingMixin):
""" """
raise NotImplementedError raise NotImplementedError
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
"""
if self.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:
# We remove the branch, because publishing always means copying from draft to published
self.signal_handler.send("course_published", course_key=course_key.for_branch(None))
class UnsupportedRevisionError(ValueError): class UnsupportedRevisionError(ValueError):
""" """
......
...@@ -13,7 +13,13 @@ from time import time ...@@ -13,7 +13,13 @@ from time import time
# Import this just to export it # Import this just to export it
from pymongo.errors import DuplicateKeyError # pylint: disable=unused-import from pymongo.errors import DuplicateKeyError # pylint: disable=unused-import
from django.core.cache import get_cache, InvalidCacheBackendError
try:
from django.core.cache import get_cache, InvalidCacheBackendError
DJANGO_AVAILABLE = True
except ImportError:
DJANGO_AVAILABLE = False
import dogstats_wrapper as dog_stats_api import dogstats_wrapper as dog_stats_api
from contracts import check, new_contract from contracts import check, new_contract
...@@ -216,15 +222,16 @@ class CourseStructureCache(object): ...@@ -216,15 +222,16 @@ class CourseStructureCache(object):
for set and get. for set and get.
""" """
def __init__(self): def __init__(self):
self.no_cache_found = False self.cache = None
try: if DJANGO_AVAILABLE:
self.cache = get_cache('course_structure_cache') try:
except InvalidCacheBackendError: self.cache = get_cache('course_structure_cache')
self.no_cache_found = True except InvalidCacheBackendError:
pass
def get(self, key, course_context=None): def get(self, key, course_context=None):
"""Pull the compressed, pickled struct data from cache and deserialize.""" """Pull the compressed, pickled struct data from cache and deserialize."""
if self.no_cache_found: if self.cache is None:
return None return None
with TIMER.timer("CourseStructureCache.get", course_context) as tagger: with TIMER.timer("CourseStructureCache.get", course_context) as tagger:
...@@ -245,7 +252,7 @@ class CourseStructureCache(object): ...@@ -245,7 +252,7 @@ class CourseStructureCache(object):
def set(self, key, structure, course_context=None): def set(self, key, structure, course_context=None):
"""Given a structure, will pickle, compress, and write to cache.""" """Given a structure, will pickle, compress, and write to cache."""
if self.no_cache_found: if self.cache is None:
return None return None
with TIMER.timer("CourseStructureCache.set", course_context) as tagger: with TIMER.timer("CourseStructureCache.set", course_context) as tagger:
......
...@@ -9,14 +9,13 @@ import itertools ...@@ -9,14 +9,13 @@ import itertools
import mimetypes import mimetypes
from uuid import uuid4 from uuid import uuid4
from contextlib import contextmanager from contextlib import contextmanager
from mock import patch from mock import patch, Mock, call
# Mixed modulestore depends on django, so we'll manually configure some django settings # Mixed modulestore depends on django, so we'll manually configure some django settings
# before importing the module # before importing the module
# TODO remove this import and the configuration -- xmodule should not depend on django! # TODO remove this import and the configuration -- xmodule should not depend on django!
from django.conf import settings from django.conf import settings
# This import breaks this test file when run separately. Needs to be fixed! (PLAT-449) # This import breaks this test file when run separately. Needs to be fixed! (PLAT-449)
from mock_django import mock_signal_receiver
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
import pymongo import pymongo
from pytz import UTC from pytz import UTC
...@@ -31,7 +30,6 @@ from xmodule.contentstore.content import StaticContent ...@@ -31,7 +30,6 @@ from xmodule.contentstore.content import StaticContent
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.xml_importer import import_course_from_xml from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.xml_exporter import export_course_to_xml from xmodule.modulestore.xml_exporter import export_course_to_xml
from xmodule.modulestore.django import SignalHandler
if not settings.configured: if not settings.configured:
settings.configure() settings.configure()
...@@ -2127,56 +2125,57 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): ...@@ -2127,56 +2125,57 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
def test_bulk_operations_signal_firing(self, default): def test_bulk_operations_signal_firing(self, default):
""" Signals should be fired right before bulk_operations() exits. """ """ Signals should be fired right before bulk_operations() exits. """
with MongoContentstoreBuilder().build() as contentstore: with MongoContentstoreBuilder().build() as contentstore:
signal_handler = Mock(name='signal_handler')
self.store = MixedModuleStore( self.store = MixedModuleStore(
contentstore=contentstore, contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance, create_modulestore_instance=create_modulestore_instance,
mappings={}, mappings={},
signal_handler=SignalHandler(MixedModuleStore), signal_handler=signal_handler,
**self.OPTIONS **self.OPTIONS
) )
self.addCleanup(self.store.close_all_connections) self.addCleanup(self.store.close_all_connections)
with self.store.default_store(default): with self.store.default_store(default):
with mock_signal_receiver(SignalHandler.course_published) as receiver: signal_handler.send.assert_not_called()
self.assertEqual(receiver.call_count, 0)
# Course creation and publication should fire the signal # Course creation and publication should fire the signal
course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id) course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_published', course_key=course.id)
receiver.reset_mock() signal_handler.reset_mock()
course_key = course.id course_key = course.id
def _clear_bulk_ops_record(course_key): # pylint: disable=unused-argument def _clear_bulk_ops_record(course_key): # pylint: disable=unused-argument
""" """
Check if the signal has been fired. Check if the signal has been fired.
The course_published signal fires before the _clear_bulk_ops_record. The course_published signal fires before the _clear_bulk_ops_record.
""" """
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_published', course_key=course.id)
with patch.object( with patch.object(
self.store.thread_cache.default_store, '_clear_bulk_ops_record', wraps=_clear_bulk_ops_record self.store.thread_cache.default_store, '_clear_bulk_ops_record', wraps=_clear_bulk_ops_record
) as mock_clear_bulk_ops_record: ) as mock_clear_bulk_ops_record:
with self.store.bulk_operations(course_key): with self.store.bulk_operations(course_key):
categories = DIRECT_ONLY_CATEGORIES categories = DIRECT_ONLY_CATEGORIES
for block_type in categories: for block_type in categories:
self.store.create_item(self.user_id, course_key, block_type) self.store.create_item(self.user_id, course_key, block_type)
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
self.assertEqual(mock_clear_bulk_ops_record.call_count, 1) self.assertEqual(mock_clear_bulk_ops_record.call_count, 1)
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_published', course_key=course.id)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_publish_signal_direct_firing(self, default): def test_course_publish_signal_direct_firing(self, default):
with MongoContentstoreBuilder().build() as contentstore: with MongoContentstoreBuilder().build() as contentstore:
signal_handler = Mock(name='signal_handler')
self.store = MixedModuleStore( self.store = MixedModuleStore(
contentstore=contentstore, contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance, create_modulestore_instance=create_modulestore_instance,
mappings={}, mappings={},
signal_handler=SignalHandler(MixedModuleStore), signal_handler=signal_handler,
**self.OPTIONS **self.OPTIONS
) )
self.addCleanup(self.store.close_all_connections) self.addCleanup(self.store.close_all_connections)
...@@ -2184,38 +2183,40 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): ...@@ -2184,38 +2183,40 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
with self.store.default_store(default): with self.store.default_store(default):
self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler) self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
with mock_signal_receiver(SignalHandler.course_published) as receiver: signal_handler.send.assert_not_called()
self.assertEqual(receiver.call_count, 0)
# Course creation and publication should fire the signal # Course creation and publication should fire the signal
course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id) course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_published', course_key=course.id)
course_key = course.id course_key = course.id
# Test non-draftable block types. The block should be published with every change. # Test non-draftable block types. The block should be published with every change.
categories = DIRECT_ONLY_CATEGORIES categories = DIRECT_ONLY_CATEGORIES
for block_type in categories: for block_type in categories:
log.debug('Testing with block type %s', block_type) log.debug('Testing with block type %s', block_type)
receiver.reset_mock() signal_handler.reset_mock()
block = self.store.create_item(self.user_id, course_key, block_type) block = self.store.create_item(self.user_id, course_key, block_type)
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_published', course_key=course.id)
block.display_name = block_type signal_handler.reset_mock()
self.store.update_item(block, self.user_id) block.display_name = block_type
self.assertEqual(receiver.call_count, 2) self.store.update_item(block, self.user_id)
signal_handler.send.assert_called_with('course_published', course_key=course.id)
self.store.publish(block.location, self.user_id) signal_handler.reset_mock()
self.assertEqual(receiver.call_count, 3) self.store.publish(block.location, self.user_id)
signal_handler.send.assert_called_with('course_published', course_key=course.id)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_publish_signal_rerun_firing(self, default): def test_course_publish_signal_rerun_firing(self, default):
with MongoContentstoreBuilder().build() as contentstore: with MongoContentstoreBuilder().build() as contentstore:
signal_handler = Mock(name='signal_handler')
self.store = MixedModuleStore( self.store = MixedModuleStore(
contentstore=contentstore, contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance, create_modulestore_instance=create_modulestore_instance,
mappings={}, mappings={},
signal_handler=SignalHandler(MixedModuleStore), signal_handler=signal_handler,
**self.OPTIONS **self.OPTIONS
) )
self.addCleanup(self.store.close_all_connections) self.addCleanup(self.store.close_all_connections)
...@@ -2223,30 +2224,30 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): ...@@ -2223,30 +2224,30 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
with self.store.default_store(default): with self.store.default_store(default):
self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler) self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
with mock_signal_receiver(SignalHandler.course_published) as receiver: signal_handler.send.assert_not_called()
self.assertEqual(receiver.call_count, 0)
# Course creation and publication should fire the signal # Course creation and publication should fire the signal
course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id) course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_published', course_key=course.id)
course_key = course.id course_key = course.id
# Test course re-runs # Test course re-runs
receiver.reset_mock() signal_handler.reset_mock()
dest_course_id = self.store.make_course_key("org.other", "course.other", "run.other") dest_course_id = self.store.make_course_key("org.other", "course.other", "run.other")
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) signal_handler.send.assert_called_with('course_published', course_key=dest_course_id)
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json) @patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_publish_signal_import_firing(self, default, _from_json): def test_course_publish_signal_import_firing(self, default, _from_json):
with MongoContentstoreBuilder().build() as contentstore: with MongoContentstoreBuilder().build() as contentstore:
signal_handler = Mock(name='signal_handler')
self.store = MixedModuleStore( self.store = MixedModuleStore(
contentstore=contentstore, contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance, create_modulestore_instance=create_modulestore_instance,
mappings={}, mappings={},
signal_handler=SignalHandler(MixedModuleStore), signal_handler=signal_handler,
**self.OPTIONS **self.OPTIONS
) )
self.addCleanup(self.store.close_all_connections) self.addCleanup(self.store.close_all_connections)
...@@ -2254,28 +2255,32 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): ...@@ -2254,28 +2255,32 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
with self.store.default_store(default): with self.store.default_store(default):
self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler) self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
with mock_signal_receiver(SignalHandler.course_published) as receiver: signal_handler.send.assert_not_called()
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. import_course_from_xml(
receiver.reset_mock() self.store, self.user_id, DATA_DIR, ['toy'], load_error_modules=False,
import_course_from_xml( static_content_store=contentstore,
self.store, self.user_id, DATA_DIR, ['toy'], load_error_modules=False, create_if_not_present=True,
static_content_store=contentstore, )
create_if_not_present=True, signal_handler.send.assert_has_calls([
) call('pre_publish', course_key=self.store.make_course_key('edX', 'toy', '2012_Fall')),
self.assertEqual(receiver.call_count, 2) call('course_published', course_key=self.store.make_course_key('edX', 'toy', '2012_Fall')),
call('pre_publish', course_key=self.store.make_course_key('edX', 'toy', '2012_Fall')),
call('course_published', course_key=self.store.make_course_key('edX', 'toy', '2012_Fall')),
])
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_publish_signal_publish_firing(self, default): def test_course_publish_signal_publish_firing(self, default):
with MongoContentstoreBuilder().build() as contentstore: with MongoContentstoreBuilder().build() as contentstore:
signal_handler = Mock(name='signal_handler')
self.store = MixedModuleStore( self.store = MixedModuleStore(
contentstore=contentstore, contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance, create_modulestore_instance=create_modulestore_instance,
mappings={}, mappings={},
signal_handler=SignalHandler(MixedModuleStore), signal_handler=signal_handler,
**self.OPTIONS **self.OPTIONS
) )
self.addCleanup(self.store.close_all_connections) self.addCleanup(self.store.close_all_connections)
...@@ -2283,51 +2288,55 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): ...@@ -2283,51 +2288,55 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
with self.store.default_store(default): with self.store.default_store(default):
self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler) self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
with mock_signal_receiver(SignalHandler.course_published) as receiver: signal_handler.send.assert_not_called()
self.assertEqual(receiver.call_count, 0)
# Course creation and publication should fire the signal # Course creation and publication should fire the signal
course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id) course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_published', course_key=course.id)
# Test a draftable block type, which needs to be explicitly published, and nest it within the # 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 # 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 # non-published child; if parent is in DIRECT_ONLY_CATEGORIES then this should not fire the event
receiver.reset_mock() signal_handler.reset_mock()
section = self.store.create_item(self.user_id, course.id, 'chapter') section = self.store.create_item(self.user_id, course.id, 'chapter')
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_published', course_key=course.id)
subsection = self.store.create_child(self.user_id, section.location, 'sequential') signal_handler.reset_mock()
self.assertEqual(receiver.call_count, 2) subsection = self.store.create_child(self.user_id, section.location, 'sequential')
signal_handler.send.assert_called_with('course_published', course_key=course.id)
# 'units' and 'blocks' are draftable types # 'units' and 'blocks' are draftable types
receiver.reset_mock() signal_handler.reset_mock()
unit = self.store.create_child(self.user_id, subsection.location, 'vertical') unit = self.store.create_child(self.user_id, subsection.location, 'vertical')
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
block = self.store.create_child(self.user_id, unit.location, 'problem') block = self.store.create_child(self.user_id, unit.location, 'problem')
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
self.store.update_item(block, self.user_id) self.store.update_item(block, self.user_id)
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
self.store.publish(unit.location, self.user_id) signal_handler.reset_mock()
self.assertEqual(receiver.call_count, 1) self.store.publish(unit.location, self.user_id)
signal_handler.send.assert_called_with('course_published', course_key=course.id)
self.store.unpublish(unit.location, self.user_id) signal_handler.reset_mock()
self.assertEqual(receiver.call_count, 2) self.store.unpublish(unit.location, self.user_id)
signal_handler.send.assert_called_with('course_published', course_key=course.id)
self.store.delete_item(unit.location, self.user_id) signal_handler.reset_mock()
self.assertEqual(receiver.call_count, 3) self.store.delete_item(unit.location, self.user_id)
signal_handler.send.assert_called_with('course_published', course_key=course.id)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_bulk_course_publish_signal_direct_firing(self, default): def test_bulk_course_publish_signal_direct_firing(self, default):
with MongoContentstoreBuilder().build() as contentstore: with MongoContentstoreBuilder().build() as contentstore:
signal_handler = Mock(name='signal_handler')
self.store = MixedModuleStore( self.store = MixedModuleStore(
contentstore=contentstore, contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance, create_modulestore_instance=create_modulestore_instance,
mappings={}, mappings={},
signal_handler=SignalHandler(MixedModuleStore), signal_handler=signal_handler,
**self.OPTIONS **self.OPTIONS
) )
self.addCleanup(self.store.close_all_connections) self.addCleanup(self.store.close_all_connections)
...@@ -2335,41 +2344,41 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): ...@@ -2335,41 +2344,41 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
with self.store.default_store(default): with self.store.default_store(default):
self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler) self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
with mock_signal_receiver(SignalHandler.course_published) as receiver: signal_handler.send.assert_not_called()
self.assertEqual(receiver.call_count, 0)
# Course creation and publication should fire the signal # Course creation and publication should fire the signal
course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id) course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_published', course_key=course.id)
course_key = course.id course_key = course.id
# Test non-draftable block types. No signals should be received until # Test non-draftable block types. No signals should be received until
receiver.reset_mock() signal_handler.reset_mock()
with self.store.bulk_operations(course_key): with self.store.bulk_operations(course_key):
categories = DIRECT_ONLY_CATEGORIES categories = DIRECT_ONLY_CATEGORIES
for block_type in categories: for block_type in categories:
log.debug('Testing with block type %s', block_type) log.debug('Testing with block type %s', block_type)
block = self.store.create_item(self.user_id, course_key, block_type) block = self.store.create_item(self.user_id, course_key, block_type)
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
block.display_name = block_type block.display_name = block_type
self.store.update_item(block, self.user_id) self.store.update_item(block, self.user_id)
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
self.store.publish(block.location, self.user_id) self.store.publish(block.location, self.user_id)
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_published', course_key=course.id)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_bulk_course_publish_signal_publish_firing(self, default): def test_bulk_course_publish_signal_publish_firing(self, default):
with MongoContentstoreBuilder().build() as contentstore: with MongoContentstoreBuilder().build() as contentstore:
signal_handler = Mock(name='signal_handler')
self.store = MixedModuleStore( self.store = MixedModuleStore(
contentstore=contentstore, contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance, create_modulestore_instance=create_modulestore_instance,
mappings={}, mappings={},
signal_handler=SignalHandler(MixedModuleStore), signal_handler=signal_handler,
**self.OPTIONS **self.OPTIONS
) )
self.addCleanup(self.store.close_all_connections) self.addCleanup(self.store.close_all_connections)
...@@ -2377,74 +2386,74 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): ...@@ -2377,74 +2386,74 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
with self.store.default_store(default): with self.store.default_store(default):
self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler) self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
with mock_signal_receiver(SignalHandler.course_published) as receiver: signal_handler.send.assert_not_called()
self.assertEqual(receiver.call_count, 0)
# Course creation and publication should fire the signal # Course creation and publication should fire the signal
course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id) course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_published', course_key=course.id)
course_key = course.id course_key = course.id
# Test a draftable block type, which needs to be explicitly published, and nest it within the # 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 # 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 # non-published child; if parent is in DIRECT_ONLY_CATEGORIES then this should not fire the event
receiver.reset_mock() signal_handler.reset_mock()
with self.store.bulk_operations(course_key): with self.store.bulk_operations(course_key):
section = self.store.create_item(self.user_id, course_key, 'chapter') section = self.store.create_item(self.user_id, course_key, 'chapter')
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
subsection = self.store.create_child(self.user_id, section.location, 'sequential') subsection = self.store.create_child(self.user_id, section.location, 'sequential')
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
# 'units' and 'blocks' are draftable types
unit = self.store.create_child(self.user_id, subsection.location, 'vertical')
signal_handler.send.assert_not_called()
# 'units' and 'blocks' are draftable types block = self.store.create_child(self.user_id, unit.location, 'problem')
unit = self.store.create_child(self.user_id, subsection.location, 'vertical') signal_handler.send.assert_not_called()
self.assertEqual(receiver.call_count, 0)
block = self.store.create_child(self.user_id, unit.location, 'problem') self.store.update_item(block, self.user_id)
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
self.store.update_item(block, self.user_id) self.store.publish(unit.location, self.user_id)
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
self.store.publish(unit.location, self.user_id) self.store.unpublish(unit.location, self.user_id)
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
self.store.unpublish(unit.location, self.user_id) self.store.delete_item(unit.location, self.user_id)
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
self.store.delete_item(unit.location, self.user_id) signal_handler.send.assert_called_with('course_published', course_key=course.id)
self.assertEqual(receiver.call_count, 0)
self.assertEqual(receiver.call_count, 1) # Test editing draftable block type without publish
signal_handler.reset_mock()
with self.store.bulk_operations(course_key):
unit = self.store.create_child(self.user_id, subsection.location, 'vertical')
signal_handler.send.assert_not_called()
block = self.store.create_child(self.user_id, unit.location, 'problem')
signal_handler.send.assert_not_called()
self.store.publish(unit.location, self.user_id)
signal_handler.send.assert_not_called()
signal_handler.send.assert_called_with('course_published', course_key=course.id)
# Test editing draftable block type without publish signal_handler.reset_mock()
receiver.reset_mock() with self.store.bulk_operations(course_key):
with self.store.bulk_operations(course_key): signal_handler.send.assert_not_called()
unit = self.store.create_child(self.user_id, subsection.location, 'vertical') unit.display_name = "Change this unit"
self.assertEqual(receiver.call_count, 0) self.store.update_item(unit, self.user_id)
block = self.store.create_child(self.user_id, unit.location, 'problem') signal_handler.send.assert_not_called()
self.assertEqual(receiver.call_count, 0) signal_handler.send.assert_not_called()
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)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_deleted_signal(self, default): def test_course_deleted_signal(self, default):
with MongoContentstoreBuilder().build() as contentstore: with MongoContentstoreBuilder().build() as contentstore:
signal_handler = Mock(name='signal_handler')
self.store = MixedModuleStore( self.store = MixedModuleStore(
contentstore=contentstore, contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance, create_modulestore_instance=create_modulestore_instance,
mappings={}, mappings={},
signal_handler=SignalHandler(MixedModuleStore), signal_handler=signal_handler,
**self.OPTIONS **self.OPTIONS
) )
self.addCleanup(self.store.close_all_connections) self.addCleanup(self.store.close_all_connections)
...@@ -2452,18 +2461,17 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): ...@@ -2452,18 +2461,17 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
with self.store.default_store(default): with self.store.default_store(default):
self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler) self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
with mock_signal_receiver(SignalHandler.course_deleted) as receiver: signal_handler.send.assert_not_called()
self.assertEqual(receiver.call_count, 0)
# Create a course # Create a course
course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id) course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
course_key = course.id course_key = course.id
# Delete the course # Delete the course
course = self.store.delete_course(course_key, self.user_id) course = self.store.delete_course(course_key, self.user_id)
# Verify that the signal was emitted # Verify that the signal was emitted
self.assertEqual(receiver.call_count, 1) signal_handler.send.assert_called_with('course_deleted', course_key=course_key)
@ddt.ddt @ddt.ddt
......
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