Commit e65730b9 by Calen Pennington

Rationalize all cases around direct-only categories (both as blocks and parents) and delete

parent ac507c3d
......@@ -3036,9 +3036,9 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
Delete the orphan and any of its descendants which no longer have parents.
"""
if len(self._get_parents_from_structure(orphan, structure)) == 0:
for child in structure['blocks'][orphan].fields.get('children', []):
orphan_data = structure['blocks'].pop(orphan)
for child in orphan_data.fields.get('children', []):
self._delete_if_true_orphan(BlockKey(*child), structure)
del structure['blocks'][orphan]
@contract(returns=BlockData)
def _new_block(self, user_id, category, block_fields, definition_id, new_id, raw=False, block_defaults=None):
......
......@@ -189,41 +189,40 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
currently only provided by contentstore.views.item.orphan_handler
Otherwise, raises a ValueError.
"""
allowed_revisions = [
None,
ModuleStoreEnum.RevisionOption.published_only,
ModuleStoreEnum.RevisionOption.all
]
if revision not in allowed_revisions:
raise UnsupportedRevisionError(allowed_revisions)
autopublish_parent = False
with self.bulk_operations(location.course_key):
if isinstance(location, LibraryUsageLocator):
branches_to_delete = [ModuleStoreEnum.BranchName.library] # Libraries don't yet have draft/publish support
elif revision == ModuleStoreEnum.RevisionOption.published_only:
branches_to_delete = [ModuleStoreEnum.BranchName.published]
elif location.category in DIRECT_ONLY_CATEGORIES:
branches_to_delete = [ModuleStoreEnum.BranchName.published, ModuleStoreEnum.BranchName.draft]
elif revision == ModuleStoreEnum.RevisionOption.all:
branches_to_delete = [ModuleStoreEnum.BranchName.published, ModuleStoreEnum.BranchName.draft]
else:
if revision == ModuleStoreEnum.RevisionOption.published_only:
branches_to_delete = [ModuleStoreEnum.BranchName.published]
elif revision is None:
branches_to_delete = [ModuleStoreEnum.BranchName.draft]
if location.category in DIRECT_ONLY_CATEGORIES:
branches_to_delete.append(ModuleStoreEnum.BranchName.published)
else:
raise UnsupportedRevisionError(
[
None,
ModuleStoreEnum.RevisionOption.published_only,
ModuleStoreEnum.RevisionOption.all
]
parent_loc = self.get_parent_location(location.for_branch(ModuleStoreEnum.BranchName.draft))
autopublish_parent = (
not skip_auto_publish and
parent_loc is not None and
parent_loc.block_type in DIRECT_ONLY_CATEGORIES
)
self._flag_publish_event(location.course_key)
for branch in branches_to_delete:
branched_location = location.for_branch(branch)
parent_loc = self.get_parent_location(branched_location)
super(DraftVersioningModuleStore, self).delete_item(branched_location, user_id)
# publish parent w/o child if deleted element is direct only (not based on type of parent)
# publish vertical to behave more like the old mongo/draft modulestore - TNL-2593
if (
branch == ModuleStoreEnum.BranchName.draft and
branched_location.block_type in (DIRECT_ONLY_CATEGORIES + ['vertical']) and
parent_loc and
not skip_auto_publish
):
# will publish if its not an orphan
if autopublish_parent:
self.publish(parent_loc.version_agnostic(), user_id, blacklist=EXCLUDE_ALL, **kwargs)
def _map_revision_to_branch(self, key, revision=None):
......
......@@ -91,19 +91,19 @@ class CommonMixedModuleStoreSetup(CourseComparisonTest):
OPTIONS = {
'stores': [
{
'NAME': 'draft',
'NAME': ModuleStoreEnum.Type.mongo,
'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options
},
{
'NAME': 'split',
'NAME': ModuleStoreEnum.Type.split,
'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options
},
{
'NAME': 'xml',
'NAME': ModuleStoreEnum.Type.xml,
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': {
'data_dir': DATA_DIR,
......@@ -289,8 +289,11 @@ class CommonMixedModuleStoreSetup(CourseComparisonTest):
self.xml_chapter_location = self.course_locations[self.XML_COURSEID1].replace(
category='chapter', name='Overview'
)
self._create_course(self.course_locations[self.MONGO_COURSEID].course_key)
self.assertEquals(default, self.store.get_modulestore_type(self.course.id))
@ddt.ddt
@attr('mongo')
......@@ -298,7 +301,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
"""
Tests of the MixedModulestore interface methods.
"""
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_get_modulestore_type(self, default_ms):
"""
Make sure we get back the store type we expect for given mappings
......@@ -310,16 +313,15 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertEqual(self.store.get_modulestore_type(
self._course_key_from_string(self.XML_COURSEID2)), ModuleStoreEnum.Type.xml
)
mongo_ms_type = ModuleStoreEnum.Type.mongo if default_ms == 'draft' else ModuleStoreEnum.Type.split
self.assertEqual(self.store.get_modulestore_type(
self._course_key_from_string(self.MONGO_COURSEID)), mongo_ms_type
self._course_key_from_string(self.MONGO_COURSEID)), default_ms
)
# try an unknown mapping, it should be the 'default' store
self.assertEqual(self.store.get_modulestore_type(
SlashSeparatedCourseKey('foo', 'bar', '2012_Fall')), mongo_ms_type
SlashSeparatedCourseKey('foo', 'bar', '2012_Fall')), default_ms
)
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_get_modulestore_cache(self, default_ms):
"""
Make sure we cache discovered course mappings
......@@ -354,7 +356,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# problem: One lookup to locate an item that exists
# fake: one w/ wildcard version
# split has one lookup for the course and then one for the course items
@ddt.data(('draft', [1, 1], 0), ('split', [2, 2], 0))
@ddt.data((ModuleStoreEnum.Type.mongo, [1, 1], 0), (ModuleStoreEnum.Type.split, [2, 2], 0))
@ddt.unpack
def test_has_item(self, default_ms, max_find, max_send):
self.initdb(default_ms)
......@@ -382,7 +384,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# split:
# problem: active_versions, structure
# non-existent problem: ditto
@ddt.data(('draft', [3, 2], 0), ('split', [2, 2], 0))
@ddt.data((ModuleStoreEnum.Type.mongo, [3, 2], 0), (ModuleStoreEnum.Type.split, [2, 2], 0))
@ddt.unpack
def test_get_item(self, default_ms, max_find, max_send):
self.initdb(default_ms)
......@@ -410,7 +412,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# wildcard query, 6! load pertinent items for inheritance calls, load parents, course root fetch (why)
# Split:
# active_versions (with regex), structure, and spurious active_versions refetch
@ddt.data(('draft', 14, 0), ('split', 3, 0))
@ddt.data((ModuleStoreEnum.Type.mongo, 14, 0), (ModuleStoreEnum.Type.split, 3, 0))
@ddt.unpack
def test_get_items(self, default_ms, max_find, max_send):
self.initdb(default_ms)
......@@ -438,7 +440,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# sends: update problem and then each ancestor up to course (edit info)
# split: active_versions, definitions (calculator field), structures
# 2 sends to update index & structure (note, it would also be definition if a content field changed)
@ddt.data(('draft', 7, 5), ('split', 3, 2))
@ddt.data((ModuleStoreEnum.Type.mongo, 7, 5), (ModuleStoreEnum.Type.split, 3, 2))
@ddt.unpack
def test_update_item(self, default_ms, max_find, max_send):
"""
......@@ -463,7 +465,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertEqual(problem.max_attempts, 2, "Update didn't persist")
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_has_changes_direct_only(self, default_ms):
"""
Tests that has_changes() returns false when a new xblock in a direct only category is checked
......@@ -484,7 +486,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertFalse(self.store.has_changes(test_course))
self.assertFalse(self.store.has_changes(chapter))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_has_changes(self, default_ms):
"""
Tests that has_changes() only returns true when changes are present
......@@ -519,7 +521,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
component = self.store.publish(component.location, self.user_id)
self.assertFalse(self.store.has_changes(component))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_unit_stuck_in_draft_mode(self, default_ms):
"""
After revert_to_published() the has_changes() should return false if draft has no changes
......@@ -551,7 +553,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
component = self.store.publish(component.location, self.user_id)
self.assertFalse(self.store.has_changes(component))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_unit_stuck_in_published_mode(self, default_ms):
"""
After revert_to_published() the has_changes() should return true if draft has changes
......@@ -588,7 +590,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# Verify that changes are present
self.assertTrue(self.store.has_changes(component))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_unit_stuck_in_published_mode_after_delete(self, default_ms):
"""
Test that a unit does not get stuck in published mode
......@@ -631,7 +633,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
vertical = self.store.get_item(vertical.location)
self.assertTrue(self._has_changes(vertical.location))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_publish_automatically_after_delete_unit(self, default_ms):
"""
Check that sequential publishes automatically after deleting a unit
......@@ -674,7 +676,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
return locations
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_has_changes_ancestors(self, default_ms):
"""
Tests that has_changes() returns true on ancestors when a child is changed
......@@ -704,7 +706,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
for key in locations:
self.assertFalse(self._has_changes(locations[key]))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_has_changes_publish_ancestors(self, default_ms):
"""
Tests that has_changes() returns false after a child is published only if all children are unchanged
......@@ -741,7 +743,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertFalse(self._has_changes(locations['grandparent']))
self.assertFalse(self._has_changes(locations['parent']))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_has_changes_add_remove_child(self, default_ms):
"""
Tests that has_changes() returns true for the parent when a child with changes is added
......@@ -774,7 +776,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertFalse(self._has_changes(locations['grandparent']))
self.assertFalse(self._has_changes(locations['parent']))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_has_changes_non_direct_only_children(self, default_ms):
"""
Tests that has_changes() returns true after editing the child of a vertical (both not direct only categories).
......@@ -808,7 +810,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertTrue(self._has_changes(child.location))
@ddt.data(*itertools.product(
('draft', 'split'),
(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split),
(ModuleStoreEnum.Branch.draft_preferred, ModuleStoreEnum.Branch.published_only)
))
@ddt.unpack
......@@ -840,14 +842,14 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# Split
# Find: active_versions, 2 structures (published & draft), definition (unnecessary)
# Sends: updated draft and published structures and active_versions
@ddt.data(('draft', 7, 2), ('split', 4, 3))
@ddt.data((ModuleStoreEnum.Type.mongo, 7, 2), (ModuleStoreEnum.Type.split, 3, 3))
@ddt.unpack
def test_delete_item(self, default_ms, max_find, max_send):
"""
Delete should reject on r/o db and work on r/w one
"""
self.initdb(default_ms)
if default_ms == 'draft' and mongo_uses_error_check(self.store):
if default_ms == ModuleStoreEnum.Type.mongo and mongo_uses_error_check(self.store):
max_find += 1
# r/o try deleting the chapter (is here to ensure it can't be deleted)
......@@ -872,7 +874,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# Split:
# queries: active_versions, draft and published structures, definition (unnecessary)
# sends: update published (why?), draft, and active_versions
@ddt.data(('draft', 9, 2), ('split', 4, 3))
@ddt.data((ModuleStoreEnum.Type.mongo, 9, 2), (ModuleStoreEnum.Type.split, 4, 3))
@ddt.unpack
def test_delete_private_vertical(self, default_ms, max_find, max_send):
"""
......@@ -880,7 +882,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
behavioral properties which this deletion test gets at.
"""
self.initdb(default_ms)
if default_ms == 'draft' and mongo_uses_error_check(self.store):
if default_ms == ModuleStoreEnum.Type.mongo and mongo_uses_error_check(self.store):
max_find += 1
# create and delete a private vertical with private children
private_vert = self.store.create_child(
......@@ -925,7 +927,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# Split:
# find: active_version & structure (cached)
# send: update structure and active_versions
@ddt.data(('draft', 4, 1), ('split', 2, 2))
@ddt.data((ModuleStoreEnum.Type.mongo, 4, 1), (ModuleStoreEnum.Type.split, 2, 2))
@ddt.unpack
def test_delete_draft_vertical(self, default_ms, max_find, max_send):
"""
......@@ -955,7 +957,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
private_leaf.display_name = 'change me'
private_leaf = self.store.update_item(private_leaf, self.user_id)
# test succeeds if delete succeeds w/o error
if default_ms == 'draft' and mongo_uses_error_check(self.store):
if default_ms == ModuleStoreEnum.Type.mongo and mongo_uses_error_check(self.store):
max_find += 1
with check_mongo_calls(max_find, max_send):
self.store.delete_item(private_leaf.location, self.user_id)
......@@ -968,7 +970,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# 1) wildcard split search,
# 2-4) active_versions, structure, definition (s/b lazy; so, unnecessary)
# 5) wildcard draft mongo which has none
@ddt.data(('draft', 3, 0), ('split', 5, 0))
@ddt.data((ModuleStoreEnum.Type.mongo, 3, 0), (ModuleStoreEnum.Type.split, 5, 0))
@ddt.unpack
def test_get_courses(self, default_ms, max_find, max_send):
self.initdb(default_ms)
......@@ -987,7 +989,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
published_courses = self.store.get_courses(remove_branch=True)
self.assertEquals([c.id for c in draft_courses], [c.id for c in published_courses])
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_create_child_detached_tabs(self, default_ms):
"""
test 'create_child' method with a detached category ('static_tab')
......@@ -1012,7 +1014,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
"""
Test that the xml modulestore only loaded the courses from the maps.
"""
self.initdb('draft')
self.initdb(ModuleStoreEnum.Type.mongo)
xml_store = self.store._get_modulestore_by_type(ModuleStoreEnum.Type.xml) # pylint: disable=protected-access
courses = xml_store.get_courses()
self.assertEqual(len(courses), 2)
......@@ -1026,7 +1028,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
"""
Test that the xml modulestore doesn't allow write ops.
"""
self.initdb('draft')
self.initdb(ModuleStoreEnum.Type.mongo)
xml_store = self.store._get_modulestore_by_type(ModuleStoreEnum.Type.xml) # pylint: disable=protected-access
# the important thing is not which exception it raises but that it raises an exception
with self.assertRaises(AttributeError):
......@@ -1034,7 +1036,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# draft is 2: find out which ms owns course, get item
# split: active_versions, structure, definition (to load course wiki string)
@ddt.data(('draft', 2, 0), ('split', 3, 0))
@ddt.data((ModuleStoreEnum.Type.mongo, 2, 0), (ModuleStoreEnum.Type.split, 3, 0))
@ddt.unpack
def test_get_course(self, default_ms, max_find, max_send):
"""
......@@ -1049,7 +1051,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
course = self.store.get_item(self.course_locations[self.XML_COURSEID1])
self.assertEqual(course.id, self.course_locations[self.XML_COURSEID1].course_key)
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_get_library(self, default_ms):
"""
Test that create_library and get_library work regardless of the default modulestore.
......@@ -1074,7 +1076,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# still only 2)
# Draft: get_parent
# Split: active_versions, structure
@ddt.data(('draft', 1, 0), ('split', 2, 0))
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 0), (ModuleStoreEnum.Type.split, 2, 0))
@ddt.unpack
def test_get_parent_locations(self, default_ms, max_find, max_send):
"""
......@@ -1100,7 +1102,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.store.get_parent_location(child_location, revision=revision)
)
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_get_parent_locations_moved_child(self, default_ms):
self.initdb(default_ms)
self._create_block_hierarchy()
......@@ -1151,7 +1153,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
(child_to_move_location, new_parent_published_location, ModuleStoreEnum.RevisionOption.published_only),
])
@ddt.data('draft')
@ddt.data(ModuleStoreEnum.Type.mongo)
def test_get_parent_locations_deleted_child(self, default_ms):
self.initdb(default_ms)
self._create_block_hierarchy()
......@@ -1182,7 +1184,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
(child_to_delete_location, None, ModuleStoreEnum.RevisionOption.published_only),
])
@ddt.data('draft')
@ddt.data(ModuleStoreEnum.Type.mongo)
def test_get_parent_location_draft(self, default_ms):
"""
Test that "get_parent_location" method returns first published parent
......@@ -1225,7 +1227,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# 8-9. get vertical, compute inheritance
# 10-11. get other vertical_x1b (why?) and compute inheritance
# Split: active_versions & structure
@ddt.data(('draft', [12, 3], 0), ('split', [2, 2], 0))
@ddt.data((ModuleStoreEnum.Type.mongo, [12, 3], 0), (ModuleStoreEnum.Type.split, [2, 2], 0))
@ddt.unpack
def test_path_to_location(self, default_ms, num_finds, num_sends):
"""
......@@ -1276,7 +1278,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
with the toy and simple courses loaded.
"""
# only needs course_locations set
self.initdb('draft')
self.initdb(ModuleStoreEnum.Type.mongo)
course_key = self.course_locations[self.XML_COURSEID1].course_key
video_key = course_key.make_usage_key('video', 'Welcome')
chapter_key = course_key.make_usage_key('chapter', 'Overview')
......@@ -1310,7 +1312,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertEqual(5, navigation_index("5_2"))
self.assertEqual(7, navigation_index("7_3_5_6_"))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_revert_to_published_root_draft(self, default_ms):
"""
Test calling revert_to_published on draft vertical.
......@@ -1342,7 +1344,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertBlocksEqualByFields(reverted_parent, published_parent)
self.assertFalse(self._has_changes(self.vertical_x1a))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_revert_to_published_root_published(self, default_ms):
"""
Test calling revert_to_published on a published vertical with a draft child.
......@@ -1362,7 +1364,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
reverted_problem = self.store.get_item(self.problem_x1a_1)
self.assertEqual(orig_display_name, reverted_problem.display_name)
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_revert_to_published_no_draft(self, default_ms):
"""
Test calling revert_to_published on vertical with no draft content does nothing.
......@@ -1377,7 +1379,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertBlocksEqualByFields(orig_vertical, reverted_vertical)
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_revert_to_published_no_published(self, default_ms):
"""
Test calling revert_to_published on vertical with no published version errors.
......@@ -1387,7 +1389,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
with self.assertRaises(InvalidVersionError):
self.store.revert_to_published(self.vertical_x1a, self.user_id)
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_revert_to_published_direct_only(self, default_ms):
"""
Test calling revert_to_published on a direct-only item is a no-op.
......@@ -1402,7 +1404,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# Draft: get all items which can be or should have parents
# Split: active_versions, structure
@ddt.data(('draft', 1, 0), ('split', 2, 0))
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 0), (ModuleStoreEnum.Type.split, 2, 0))
@ddt.unpack
def test_get_orphans(self, default_ms, max_find, max_send):
"""
......@@ -1440,7 +1442,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
found_orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
self.assertItemsEqual(found_orphans, orphan_locations)
@ddt.data('draft')
@ddt.data(ModuleStoreEnum.Type.mongo)
def test_get_non_orphan_parents(self, default_ms):
"""
Test finding non orphan parents from many possible parents.
......@@ -1502,7 +1504,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
with self.assertRaises(ReferentialIntegrityError):
self.store.get_parent_location(self.problem_x1a_1)
@ddt.data('draft')
@ddt.data(ModuleStoreEnum.Type.mongo)
def test_create_item_from_parent_location(self, default_ms):
"""
Test a code path missed by the above: passing an old-style location as parent but no
......@@ -1518,7 +1520,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
self.assertEqual(len(orphans), 0, "unexpected orphans: {}".format(orphans))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_create_item_populates_edited_info(self, default_ms):
self.initdb(default_ms)
block = self.store.create_item(
......@@ -1529,7 +1531,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertEqual(self.user_id, block.edited_by)
self.assertGreater(datetime.datetime.now(UTC), block.edited_on)
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_create_item_populates_subtree_edited_info(self, default_ms):
self.initdb(default_ms)
block = self.store.create_item(
......@@ -1542,7 +1544,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# Draft: wildcard search of draft and split
# Split: wildcard search of draft and split
@ddt.data(('draft', 2, 0), ('split', 2, 0))
@ddt.data((ModuleStoreEnum.Type.mongo, 2, 0), (ModuleStoreEnum.Type.split, 2, 0))
@ddt.unpack
def test_get_courses_for_wiki(self, default_ms, max_find, max_send):
"""
......@@ -1580,14 +1582,14 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# Sends:
# - insert structure
# - write index entry
@ddt.data(('draft', 2, 6), ('split', 3, 2))
@ddt.data((ModuleStoreEnum.Type.mongo, 2, 6), (ModuleStoreEnum.Type.split, 3, 2))
@ddt.unpack
def test_unpublish(self, default_ms, max_find, max_send):
"""
Test calling unpublish
"""
self.initdb(default_ms)
if default_ms == 'draft' and mongo_uses_error_check(self.store):
if default_ms == ModuleStoreEnum.Type.mongo and mongo_uses_error_check(self.store):
max_find += 1
self._create_block_hierarchy()
......@@ -1618,7 +1620,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# Draft: specific query for revision None
# Split: active_versions, structure
@ddt.data(('draft', 1, 0), ('split', 2, 0))
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 0), (ModuleStoreEnum.Type.split, 2, 0))
@ddt.unpack
def test_has_published_version(self, default_ms, max_find, max_send):
"""
......@@ -1659,7 +1661,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertTrue(self.store.has_changes(item))
self.assertTrue(self.store.has_published_version(item))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_update_edit_info_ancestors(self, default_ms):
"""
Tests that edited_on, edited_by, subtree_edited_on, and subtree_edited_by are set correctly during update
......@@ -1735,7 +1737,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# Verify that others have unchanged edit info
check_node(sibling.location, None, after_create, self.user_id, None, after_create, self.user_id)
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_update_edit_info(self, default_ms):
"""
Tests that edited_on and edited_by are set correctly during an update
......@@ -1765,7 +1767,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertLess(old_edited_on, updated_component.edited_on)
self.assertEqual(updated_component.edited_by, edit_user)
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_update_published_info(self, default_ms):
"""
Tests that published_on and published_by are set correctly
......@@ -1799,7 +1801,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.assertLessEqual(old_time, updated_component.published_on)
self.assertEqual(updated_component.published_by, publish_user)
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_auto_publish(self, default_ms):
"""
Test that the correct things have been published automatically
......@@ -1869,7 +1871,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
chapter = self.store.get_item(chapter.location.for_branch(None))
self.assertTrue(self.store.has_published_version(chapter))
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_get_courses_for_wiki_shared(self, default_ms):
"""
Test two courses sharing the same wiki
......@@ -1924,7 +1926,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
wiki_courses
)
@ddt.data('draft', 'split')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_branch_setting(self, default_ms):
"""
Test the branch_setting context manager
......@@ -2473,6 +2475,119 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
# Verify that the signal was emitted
signal_handler.send.assert_called_with('course_deleted', course_key=course_key)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_delete_published_item_orphans(self, default_store):
"""
Tests delete published item dont create any oprhans in course
"""
self.initdb(default_store)
course_locator = self.course.id
chapter = self.store.create_child(
self.user_id, self.course.location, 'chapter', block_id='section_one'
)
sequential = self.store.create_child(
self.user_id, chapter.location, 'sequential', block_id='subsection_one'
)
vertical = self.store.create_child(
self.user_id, sequential.location, 'vertical', block_id='moon_unit'
)
problem = self.store.create_child(
self.user_id, vertical.location, 'problem', block_id='problem'
)
self.store.publish(chapter.location, self.user_id)
# Verify that there are no changes
self.assertFalse(self._has_changes(chapter.location))
self.assertFalse(self._has_changes(sequential.location))
self.assertFalse(self._has_changes(vertical.location))
self.assertFalse(self._has_changes(problem.location))
# No orphans in course
course_orphans = self.store.get_orphans(course_locator)
self.assertEqual(len(course_orphans), 0)
self.store.delete_item(vertical.location, self.user_id)
# No orphans in course after delete, except
# in old mongo, which still creates orphans
course_orphans = self.store.get_orphans(course_locator)
if default_store == ModuleStoreEnum.Type.mongo:
self.assertEqual(len(course_orphans), 1)
else:
self.assertEqual(len(course_orphans), 0)
course_locator_publish = course_locator.for_branch(ModuleStoreEnum.BranchName.published)
# No published oprhans after delete, except
# in old mongo, which still creates orphans
course_publish_orphans = self.store.get_orphans(course_locator_publish)
if default_store == ModuleStoreEnum.Type.mongo:
self.assertEqual(len(course_publish_orphans), 1)
else:
self.assertEqual(len(course_publish_orphans), 0)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_delete_draft_item_orphans(self, default_store):
"""
Tests delete draft item create no orphans in course
"""
self.initdb(default_store)
course_locator = self.course.id
chapter = self.store.create_child(
self.user_id, self.course.location, 'chapter', block_id='section_one'
)
sequential = self.store.create_child(
self.user_id, chapter.location, 'sequential', block_id='subsection_one'
)
vertical = self.store.create_child(
self.user_id, sequential.location, 'vertical', block_id='moon_unit'
)
problem = self.store.create_child(
self.user_id, vertical.location, 'problem', block_id='problem'
)
self.store.publish(chapter.location, self.user_id)
# Verify that there are no changes
self.assertFalse(self._has_changes(chapter.location))
self.assertFalse(self._has_changes(sequential.location))
self.assertFalse(self._has_changes(vertical.location))
self.assertFalse(self._has_changes(problem.location))
# No orphans in course
course_orphans = self.store.get_orphans(course_locator)
self.assertEqual(len(course_orphans), 0)
problem.display_name = 'changed'
problem = self.store.update_item(problem, self.user_id)
self.assertTrue(self._has_changes(vertical.location))
self.assertTrue(self._has_changes(problem.location))
self.store.delete_item(vertical.location, self.user_id)
# No orphans in course after delete, except
# in old mongo, which still creates them
course_orphans = self.store.get_orphans(course_locator)
if default_store == ModuleStoreEnum.Type.mongo:
self.assertEqual(len(course_orphans), 1)
else:
self.assertEqual(len(course_orphans), 0)
course_locator_publish = course_locator.for_branch(ModuleStoreEnum.BranchName.published)
# No published orphans after delete, except
# in old mongo, which still creates them
course_publish_orphans = self.store.get_orphans(course_locator_publish)
if default_store == ModuleStoreEnum.Type.mongo:
self.assertEqual(len(course_publish_orphans), 1)
else:
self.assertEqual(len(course_publish_orphans), 0)
@ddt.ddt
@attr('mongo')
......
......@@ -1122,20 +1122,7 @@ class ElementalDeleteItemTests(DraftPublishedOpBaseTestSetup):
self.assertOLXIsPublishedOnly(block_list_to_delete)
self.delete_item(block_list_to_delete, revision=revision)
self._check_for_item_deletion(block_list_to_delete, result)
# MODULESTORE_DIFFERENCE
if self.is_split_modulestore:
# Split:
if revision == ModuleStoreEnum.RevisionOption.published_only:
# If deleting published_only items, the children that are drafts remain.
self.assertOLXIsDraftOnly(block_list_children)
else:
self.assertOLXIsDeleted(block_list_children)
elif self.is_old_mongo_modulestore:
# Old Mongo:
# If deleting draft_only or both items, the drafts will be deleted.
self.assertOLXIsDeleted(block_list_children)
else:
raise Exception("Must test either Old Mongo or Split modulestore!")
@ddt.data(*itertools.product(
MODULESTORE_SETUPS,
......@@ -1176,20 +1163,7 @@ class ElementalDeleteItemTests(DraftPublishedOpBaseTestSetup):
self.delete_item(block_list_to_delete, revision=revision)
self._check_for_item_deletion(block_list_to_delete, result)
self.assertOLXIsDeleted(autopublished_children)
# MODULESTORE_DIFFERENCE
if self.is_split_modulestore:
# Split:
if revision == ModuleStoreEnum.RevisionOption.published_only:
# If deleting published_only items, the children that are drafts remain.
self.assertOLXIsDraftOnly(block_list_draft_children)
else:
self.assertOLXIsDeleted(block_list_draft_children)
elif self.is_old_mongo_modulestore:
# Old Mongo:
# If deleting draft_only or both items, the drafts will be deleted.
self.assertOLXIsDeleted(block_list_draft_children)
else:
raise Exception("Must test either Old Mongo or Split modulestore!")
@ddt.ddt
......
......@@ -3,6 +3,7 @@ Tests of modulestore semantics: How do the interfaces methods of ModuleStore rel
"""
import ddt
import itertools
from collections import namedtuple
from xmodule.modulestore.tests.utils import (
......@@ -51,45 +52,130 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
modulestore=self.store
)
def assertBlockDoesntExist(self):
with self.assertRaises(ItemNotFoundError):
self.store.get_item(self.block_usage_key, revision=ModuleStoreEnum.RevisionOption.published_only)
with self.assertRaises(ItemNotFoundError):
self.store.get_item(self.block_usage_key, revision=ModuleStoreEnum.RevisionOption.draft_only)
def assertBlockDoesntExist(self, block_usage_key, draft=None):
"""
Verify that loading ``block_usage_key`` raises an ItemNotFoundError.
Arguments:
block_usage_key: The xblock to check.
draft (optional): If omitted, verify both published and draft branches.
If True, verify only the draft branch. If False, verify only the
published branch.
"""
if draft is None or draft:
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
with self.assertRaises(ItemNotFoundError):
self.store.get_item(self.block_usage_key)
self.store.get_item(block_usage_key)
if draft is None or not draft:
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only):
with self.assertRaises(ItemNotFoundError):
self.store.get_item(self.block_usage_key)
self.store.get_item(block_usage_key)
def assertBlockHasContent(self, test_data, content):
def assertBlockHasContent(self, block_usage_key, field_name, content, draft=None):
"""
Assert that the block ``block_usage_key`` has the value ``content`` for ``field_name``
when it is loaded.
Arguments:
block_usage_key: The xblock to check.
field_name (string): The name of the field to check.
content: The value to assert is in the field.
draft (optional): If omitted, verify both published and draft branches.
If True, verify only the draft branch. If False, verify only the
published branch.
"""
if draft is None or not draft:
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only):
target_block = self.store.get_item(
self.block_usage_key,
revision=ModuleStoreEnum.RevisionOption.published_only
block_usage_key,
)
self.assertEquals(content, target_block.fields[field_name].read_from(target_block))
self.assertEquals(content, target_block.fields[test_data.field_name].read_from(target_block))
if draft is None or draft:
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
target_block = self.store.get_item(
self.block_usage_key,
revision=ModuleStoreEnum.RevisionOption.draft_only
block_usage_key,
)
self.assertEquals(content, target_block.fields[test_data.field_name].read_from(target_block))
self.assertEquals(content, target_block.fields[field_name].read_from(target_block))
def assertParentOf(self, parent_usage_key, child_usage_key, draft=None):
"""
Assert that the block ``parent_usage_key`` has ``child_usage_key`` listed
as one of its ``.children``.
Arguments:
parent_usage_key: The xblock to check as a parent.
child_usage_key: The xblock to check as a child.
draft (optional): If omitted, verify both published and draft branches.
If True, verify only the draft branch. If False, verify only the
published branch.
"""
if draft is None or not draft:
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only):
parent_block = self.store.get_item(
parent_usage_key,
)
self.assertIn(child_usage_key, parent_block.children)
if draft is None or draft:
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
target_block = self.store.get_item(
self.block_usage_key,
parent_block = self.store.get_item(
parent_usage_key,
)
self.assertEquals(content, target_block.fields[test_data.field_name].read_from(target_block))
self.assertIn(child_usage_key, parent_block.children)
def assertNotParentOf(self, parent_usage_key, child_usage_key, draft=None):
"""
Assert that the block ``parent_usage_key`` does not have ``child_usage_key`` listed
as one of its ``.children``.
Arguments:
parent_usage_key: The xblock to check as a parent.
child_usage_key: The xblock to check as a child.
draft (optional): If omitted, verify both published and draft branches.
If True, verify only the draft branch. If False, verify only the
published branch.
"""
if draft is None or not draft:
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only):
target_block = self.store.get_item(
self.block_usage_key,
parent_block = self.store.get_item(
parent_usage_key,
)
self.assertEquals(content, target_block.fields[test_data.field_name].read_from(target_block))
self.assertNotIn(child_usage_key, parent_block.children)
if draft is None or draft:
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
parent_block = self.store.get_item(
parent_usage_key,
)
self.assertNotIn(child_usage_key, parent_block.children)
def assertCoursePointsToBlock(self, block_usage_key, draft=None):
"""
Assert that the context course for the test has ``block_usage_key`` listed
as one of its ``.children``.
Arguments:
block_usage_key: The xblock to check.
draft (optional): If omitted, verify both published and draft branches.
If True, verify only the draft branch. If False, verify only the
published branch.
"""
self.assertParentOf(self.course.scope_ids.usage_id, block_usage_key, draft=draft)
def assertCourseDoesntPointToBlock(self, block_usage_key, draft=None):
"""
Assert that the context course for the test does not have ``block_usage_key`` listed
as one of its ``.children``.
Arguments:
block_usage_key: The xblock to check.
draft (optional): If omitted, verify both published and draft branches.
If True, verify only the draft branch. If False, verify only the
published branch.
"""
self.assertNotParentOf(self.course.scope_ids.usage_id, block_usage_key, draft=draft)
def is_detached(self, block_type):
"""
......@@ -109,9 +195,10 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
and then verify that it was created successfully, and is visible in
both published and draft branches.
"""
self.block_usage_key = self.course.id.make_usage_key(block_type, 'test_block')
block_usage_key = self.course.id.make_usage_key(block_type, 'test_block')
self.assertBlockDoesntExist()
self.assertBlockDoesntExist(block_usage_key)
self.assertCourseDoesntPointToBlock(block_usage_key)
test_data = self.DATA_FIELDS[block_type]
......@@ -122,8 +209,8 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
block = self.store.create_xblock(
self.course.runtime,
self.course.id,
self.block_usage_key.block_type,
block_id=self.block_usage_key.block_id
block_usage_key.block_type,
block_id=block_usage_key.block_id
)
block.fields[test_data.field_name].write_to(block, initial_field_value)
self.store.update_item(block, ModuleStoreEnum.UserID.test, allow_not_found=True)
......@@ -132,18 +219,24 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
user_id=ModuleStoreEnum.UserID.test,
parent_usage_key=self.course.scope_ids.usage_id,
block_type=block_type,
block_id=self.block_usage_key.block_id,
block_id=block_usage_key.block_id,
fields={test_data.field_name: initial_field_value},
)
self.assertBlockHasContent(test_data, initial_field_value)
if self.is_detached(block_type):
self.assertCourseDoesntPointToBlock(block_usage_key)
else:
self.assertCoursePointsToBlock(block_usage_key)
self.assertBlockHasContent(block_usage_key, test_data.field_name, initial_field_value)
return block_usage_key
@ddt.data(*TESTABLE_BLOCK_TYPES)
def test_update(self, block_type):
self._do_create(block_type)
block_usage_key = self._do_create(block_type)
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
block = self.store.get_item(self.block_usage_key)
block = self.store.get_item(block_usage_key)
test_data = self.DATA_FIELDS[block_type]
......@@ -154,16 +247,81 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
self.store.update_item(block, ModuleStoreEnum.UserID.test, allow_not_found=True)
self.assertBlockHasContent(test_data, updated_field_value)
self.assertBlockHasContent(block_usage_key, test_data.field_name, updated_field_value)
@ddt.data(*TESTABLE_BLOCK_TYPES)
def test_delete(self, block_type):
self._do_create(block_type)
block_usage_key = self._do_create(block_type)
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
self.store.delete_item(block_usage_key, ModuleStoreEnum.UserID.test)
self.assertCourseDoesntPointToBlock(block_usage_key)
self.assertBlockDoesntExist(block_usage_key)
@ddt.data(*itertools.product(['chapter', 'sequential'], [True, False]))
@ddt.unpack
def test_delete_child(self, block_type, child_published):
block_usage_key = self.course.id.make_usage_key(block_type, 'test_block')
child_usage_key = self.course.id.make_usage_key('html', 'test_child')
self.assertCourseDoesntPointToBlock(block_usage_key)
self.assertBlockDoesntExist(block_usage_key)
self.assertBlockDoesntExist(child_usage_key)
test_data = self.DATA_FIELDS[block_type]
child_data = '<div>child value</div>'
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
self.store.create_child(
user_id=ModuleStoreEnum.UserID.test,
parent_usage_key=self.course.scope_ids.usage_id,
block_type=block_type,
block_id=block_usage_key.block_id,
fields={test_data.field_name: test_data.initial},
)
self.store.create_child(
user_id=ModuleStoreEnum.UserID.test,
parent_usage_key=block_usage_key,
block_type=child_usage_key.block_type,
block_id=child_usage_key.block_id,
fields={'data': child_data},
)
if child_published:
self.store.publish(child_usage_key, ModuleStoreEnum.UserID.test)
self.assertCoursePointsToBlock(block_usage_key)
if child_published:
self.assertParentOf(block_usage_key, child_usage_key)
else:
self.assertParentOf(block_usage_key, child_usage_key, draft=True)
# N.B. whether the direct-only parent block points to the child in the publish branch
# is left as undefined behavior
self.assertBlockHasContent(block_usage_key, test_data.field_name, test_data.initial)
if child_published:
self.assertBlockHasContent(child_usage_key, 'data', child_data)
else:
self.assertBlockHasContent(child_usage_key, 'data', child_data, draft=True)
self.assertBlockDoesntExist(child_usage_key, draft=False)
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
self.store.delete_item(self.block_usage_key, ModuleStoreEnum.UserID.test)
self.store.delete_item(child_usage_key, ModuleStoreEnum.UserID.test)
self.assertCoursePointsToBlock(block_usage_key)
self.assertNotParentOf(block_usage_key, child_usage_key)
self.assertBlockDoesntExist()
if child_published and self.store.get_modulestore_type(self.course.id) == ModuleStoreEnum.Type.mongo:
# N.B. This block is being left as an orphan in old-mongo. This test will
# fail when that is fixed. At that time, this condition should just be removed,
# as SplitMongo and OldMongo will have the same semantics.
self.assertBlockHasContent(child_usage_key, 'data', child_data)
else:
self.assertBlockDoesntExist(child_usage_key)
class TestSplitDirectOnlyCategorySemantics(DirectOnlyCategorySemantics):
......
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