Commit db0f9dfc by Nimisha Asthagiri

LMS-11354 Make Split has_changes check subtree.

parent 7eb3a22d
......@@ -236,18 +236,34 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
:param xblock: the block to check
:return: True if the draft and published versions differ
"""
def get_block(branch_name):
course_structure = self._lookup_course(xblock.location.course_key.for_branch(branch_name))['structure']
return self._get_block_from_structure(course_structure, xblock.location.block_id)
def get_course(branch_name):
return self._lookup_course(xblock.location.course_key.for_branch(branch_name))['structure']
draft_block = get_block(ModuleStoreEnum.BranchName.draft)
published_block = get_block(ModuleStoreEnum.BranchName.published)
if not published_block:
return True
def get_block(course_structure, block_id):
return self._get_block_from_structure(course_structure, block_id)
# check if the draft has changed since the published was created
return self._get_version(draft_block) != self._get_version(published_block)
draft_course = get_course(ModuleStoreEnum.BranchName.draft)
published_course = get_course(ModuleStoreEnum.BranchName.published)
def has_changes_subtree(block_id):
draft_block = get_block(draft_course, block_id)
published_block = get_block(published_course, block_id)
if not published_block:
return True
# check if the draft has changed since the published was created
if self._get_version(draft_block) != self._get_version(published_block):
return True
# check the children in the draft
if 'children' in draft_block.setdefault('fields', {}):
return any(
[has_changes_subtree(child_block_id) for child_block_id in draft_block['fields']['children']]
)
return False
return has_changes_subtree(xblock.location.block_id)
def publish(self, location, user_id, blacklist=None, **kwargs):
"""
......
......@@ -158,8 +158,14 @@ class TestMixedModuleStore(unittest.TestCase):
BlockInfo('problem_x1a_3', 'problem', 'Problem_x1a_3', []),
BlockInfo('html_x1a_1', 'html', 'HTML_x1a_1', []),
]
),
BlockInfo(
'vertical_x1b', 'vertical', 'Vertical_x1b', []
)
]
),
BlockInfo(
'sequential_x2', 'sequential', 'Sequential_x2', []
)
]
),
......@@ -441,6 +447,184 @@ class TestMixedModuleStore(unittest.TestCase):
component = self.store.publish(component.location, self.user_id)
self.assertFalse(self.store.has_changes(component))
def _has_changes(self, location):
return self.store.has_changes(self.store.get_item(location))
def setup_has_changes(self, default_ms):
"""
Common set up for has_changes tests below.
Returns a dictionary of useful location maps for testing.
"""
self.initdb(default_ms)
self._create_block_hierarchy()
locations = {
'grandparent': self.chapter_x,
'parent_sibling': self.sequential_x2,
'parent': self.sequential_x1,
'child_sibling': self.vertical_x1b,
'child': self.vertical_x1a,
}
# Publish the vertical units
self.store.publish(locations['parent_sibling'], self.user_id)
self.store.publish(locations['parent'], self.user_id)
return locations
@ddt.data('draft', 'split')
def test_has_changes_ancestors(self, default_ms):
"""
Tests that has_changes() returns true on ancestors when a child is changed
"""
locations = self.setup_has_changes(default_ms)
# Verify that there are no unpublished changes
for key in locations:
self.assertFalse(self._has_changes(locations[key]))
# Change the child
child = self.store.get_item(locations['child'])
child.display_name = 'Changed Display Name'
self.store.update_item(child, self.user_id)
# All ancestors should have changes, but not siblings
self.assertTrue(self._has_changes(locations['grandparent']))
self.assertTrue(self._has_changes(locations['parent']))
self.assertTrue(self._has_changes(locations['child']))
self.assertFalse(self._has_changes(locations['parent_sibling']))
self.assertFalse(self._has_changes(locations['child_sibling']))
# Publish the unit with changes
self.store.publish(locations['parent'], self.user_id)
# Verify that there are no unpublished changes
for key in locations:
self.assertFalse(self._has_changes(locations[key]))
@ddt.data('draft', '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
"""
locations = self.setup_has_changes(default_ms)
# Verify that there are no unpublished changes
for key in locations:
self.assertFalse(self._has_changes(locations[key]))
# Change both children
child = self.store.get_item(locations['child'])
child_sibling = self.store.get_item(locations['child_sibling'])
child.display_name = 'Changed Display Name'
child_sibling.display_name = 'Changed Display Name'
self.store.update_item(child, user_id=self.user_id)
self.store.update_item(child_sibling, user_id=self.user_id)
# Verify that ancestors have changes
self.assertTrue(self._has_changes(locations['grandparent']))
self.assertTrue(self._has_changes(locations['parent']))
# Publish one child
self.store.publish(locations['child_sibling'], self.user_id)
# Verify that ancestors still have changes
self.assertTrue(self._has_changes(locations['grandparent']))
self.assertTrue(self._has_changes(locations['parent']))
# Publish the other child
self.store.publish(locations['child'], self.user_id)
# Verify that ancestors now have no changes
self.assertFalse(self._has_changes(locations['grandparent']))
self.assertFalse(self._has_changes(locations['parent']))
@ddt.data('draft', '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
and false when that child is removed.
"""
locations = self.setup_has_changes(default_ms)
# Test that the ancestors don't have changes
self.assertFalse(self._has_changes(locations['grandparent']))
self.assertFalse(self._has_changes(locations['parent']))
# Create a new child and attach it to parent
self.store.create_child(
self.user_id,
locations['parent'],
'vertical',
block_id='new_child',
)
# Verify that the ancestors now have changes
self.assertTrue(self._has_changes(locations['grandparent']))
self.assertTrue(self._has_changes(locations['parent']))
# Remove the child from the parent
parent = self.store.get_item(locations['parent'])
parent.children = [locations['child'], locations['child_sibling']]
self.store.update_item(parent, user_id=self.user_id)
# Verify that ancestors now have no changes
self.assertFalse(self._has_changes(locations['grandparent']))
self.assertFalse(self._has_changes(locations['parent']))
@ddt.data('draft', '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).
"""
self.initdb(default_ms)
parent = self.store.create_item(
self.user_id,
self.course.id,
'vertical',
block_id='parent',
)
child = self.store.create_child(
self.user_id,
parent.location,
'html',
block_id='child',
)
self.store.publish(parent.location, self.user_id)
# Verify that there are no changes
self.assertFalse(self._has_changes(parent.location))
self.assertFalse(self._has_changes(child.location))
# Change the child
child.display_name = 'Changed Display Name'
self.store.update_item(child, user_id=self.user_id)
# Verify that both parent and child have changes
self.assertTrue(self._has_changes(parent.location))
self.assertTrue(self._has_changes(child.location))
@ddt.data('draft', 'split')
def test_has_changes_missing_child(self, default_ms):
"""
Tests that has_changes() does not throw an exception when a child doesn't exist.
"""
self.initdb(default_ms)
# Create the parent and point it to a fake child
parent = self.store.create_item(
self.user_id,
self.course.id,
'vertical',
block_id='parent',
)
parent.children += [self.course.id.make_usage_key('vertical', 'does_not_exist')]
parent = self.store.update_item(parent, self.user_id)
# Check the parent for changes should return True and not throw an exception
self.assertTrue(self.store.has_changes(parent))
# TODO: LMS-11220: Document why split find count is 4
# TODO: LMS-11220: Document why split send count is 3
@ddt.data(('draft', 8, 2), ('split', 4, 3))
......@@ -717,7 +901,7 @@ class TestMixedModuleStore(unittest.TestCase):
# TODO: LMS-11220: Document why draft send count is 5
# TODO: LMS-11220: Document why draft find count is [19, 6]
# TODO: LMS-11220: Document why split find count is [2, 2]
@ddt.data(('draft', [19, 6], 0), ('split', [2, 2], 0))
@ddt.data(('draft', [21, 6], 0), ('split', [2, 2], 0))
@ddt.unpack
def test_path_to_location(self, default_ms, num_finds, num_sends):
"""
......@@ -866,10 +1050,11 @@ class TestMixedModuleStore(unittest.TestCase):
"""
self.initdb(default_ms)
self._create_block_hierarchy()
num_children = len(self.store.get_item(self.sequential_x1).children)
self.store.revert_to_published(self.sequential_x1, self.user_id)
reverted_parent = self.store.get_item(self.sequential_x1)
# It does not discard the child vertical, even though that child is a draft (with no published version)
self.assertEqual(1, len(reverted_parent.children))
self.assertEqual(num_children, len(reverted_parent.children))
@ddt.data(('draft', 1, 0), ('split', 2, 0))
@ddt.unpack
......
......@@ -505,25 +505,6 @@ class TestMongoModuleStore(unittest.TestCase):
finally:
shutil.rmtree(root_dir)
def test_has_changes_missing_child(self):
"""
Tests that has_changes() returns False when a published parent points to a child that doesn't exist.
"""
location = Location('edX', 'toy', '2012_Fall', 'sequential', 'parent')
# Create the parent and point it to a fake child
parent = self.draft_store.create_item(
self.dummy_user,
location.course_key,
location.block_type,
block_id=location.block_id
)
parent.children += [Location('edX', 'toy', '2012_Fall', 'vertical', 'does_not_exist')]
parent = self.draft_store.update_item(parent, self.dummy_user)
# Check the parent for changes should return False and not throw an exception
self.assertFalse(self.draft_store.has_changes(parent))
def _create_test_tree(self, name, user_id=None):
"""
Creates and returns a tree with the following structure:
......@@ -573,142 +554,6 @@ class TestMongoModuleStore(unittest.TestCase):
return locations
def _has_changes(self, location):
""" Helper that returns True if location has changes, False otherwise """
store = self.draft_store
return store.has_changes(store.get_item(location))
def test_has_changes_ancestors(self):
"""
Tests that has_changes() returns true on ancestors when a child is changed
"""
locations = self._create_test_tree('has_changes_ancestors')
# Verify that there are no unpublished changes
for key in locations:
self.assertFalse(self._has_changes(locations[key]))
# Change the child
child = self.draft_store.get_item(locations['child'])
child.display_name = 'Changed Display Name'
self.draft_store.update_item(child, user_id=self.dummy_user)
# All ancestors should have changes, but not siblings
self.assertTrue(self._has_changes(locations['grandparent']))
self.assertTrue(self._has_changes(locations['parent']))
self.assertTrue(self._has_changes(locations['child']))
self.assertFalse(self._has_changes(locations['parent_sibling']))
self.assertFalse(self._has_changes(locations['child_sibling']))
# Publish the unit with changes
self.draft_store.publish(locations['parent'], self.dummy_user)
# Verify that there are no unpublished changes
for key in locations:
self.assertFalse(self._has_changes(locations[key]))
def test_has_changes_publish_ancestors(self):
"""
Tests that has_changes() returns false after a child is published only if all children are unchanged
"""
locations = self._create_test_tree('has_changes_publish_ancestors')
# Verify that there are no unpublished changes
for key in locations:
self.assertFalse(self._has_changes(locations[key]))
# Change both children
child = self.draft_store.get_item(locations['child'])
child_sibling = self.draft_store.get_item(locations['child_sibling'])
child.display_name = 'Changed Display Name'
child_sibling.display_name = 'Changed Display Name'
self.draft_store.update_item(child, user_id=self.dummy_user)
self.draft_store.update_item(child_sibling, user_id=self.dummy_user)
# Verify that ancestors have changes
self.assertTrue(self._has_changes(locations['grandparent']))
self.assertTrue(self._has_changes(locations['parent']))
# Publish one child
self.draft_store.publish(locations['child_sibling'], self.dummy_user)
# Verify that ancestors still have changes
self.assertTrue(self._has_changes(locations['grandparent']))
self.assertTrue(self._has_changes(locations['parent']))
# Publish the other child
self.draft_store.publish(locations['child'], self.dummy_user)
# Verify that ancestors now have no changes
self.assertFalse(self._has_changes(locations['grandparent']))
self.assertFalse(self._has_changes(locations['parent']))
def test_has_changes_add_remove_child(self):
"""
Tests that has_changes() returns true for the parent when a child with changes is added
and false when that child is removed.
"""
locations = self._create_test_tree('has_changes_add_remove_child')
# Test that the ancestors don't have changes
self.assertFalse(self._has_changes(locations['grandparent']))
self.assertFalse(self._has_changes(locations['parent']))
# Create a new child and attach it to parent
new_child_location = Location('edX', 'tree', 'has_changes_add_remove_child', 'vertical', 'new_child')
self.draft_store.create_child(
self.dummy_user,
locations['parent'],
new_child_location.block_type,
block_id=new_child_location.block_id
)
# Verify that the ancestors now have changes
self.assertTrue(self._has_changes(locations['grandparent']))
self.assertTrue(self._has_changes(locations['parent']))
# Remove the child from the parent
parent = self.draft_store.get_item(locations['parent'])
parent.children = [locations['child'], locations['child_sibling']]
self.draft_store.update_item(parent, user_id=self.dummy_user)
# Verify that ancestors now have no changes
self.assertFalse(self._has_changes(locations['grandparent']))
self.assertFalse(self._has_changes(locations['parent']))
def test_has_changes_non_direct_only_children(self):
"""
Tests that has_changes() returns true after editing the child of a vertical (both not direct only categories).
"""
parent_location = Location('edX', 'toy', '2012_Fall', 'vertical', 'parent')
child_location = Location('edX', 'toy', '2012_Fall', 'html', 'child')
parent = self.draft_store.create_item(
self.dummy_user,
parent_location.course_key,
parent_location.block_type,
block_id=parent_location.block_id
)
child = self.draft_store.create_child(
self.dummy_user,
parent_location,
child_location.block_type,
block_id=child_location.block_id
)
self.draft_store.publish(parent_location, self.dummy_user)
# Verify that there are no changes
self.assertFalse(self._has_changes(parent_location))
self.assertFalse(self._has_changes(child_location))
# Change the child
child.display_name = 'Changed Display Name'
self.draft_store.update_item(child, user_id=self.dummy_user)
# Verify that both parent and child have changes
self.assertTrue(self._has_changes(parent_location))
self.assertTrue(self._has_changes(child_location))
def test_migrate_published_info(self):
"""
Tests that blocks that were storing published_date and published_by through CMSBlockMixin are loaded correctly
......
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