Commit 4d825106 by Braden MacDonald Committed by E. Kolpakov

Fix bug w/ publishing ( + test), move copy_from_template tests to their own file

parent 55fb45fb
......@@ -2843,7 +2843,8 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
self._filter_blacklist(copy.copy(new_block['fields']), blacklist),
new_block['definition'],
destination_version,
raw=True
raw=True,
block_defaults=new_block.get('defaults')
)
# introduce new edit info field for tracing where copied/published blocks came
......@@ -2882,7 +2883,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
self._delete_if_true_orphan(BlockKey(*child), structure)
del structure['blocks'][orphan]
def _new_block(self, user_id, category, block_fields, definition_id, new_id, raw=False):
def _new_block(self, user_id, category, block_fields, definition_id, new_id, raw=False, block_defaults=None):
"""
Create the core document structure for a block.
......@@ -2893,7 +2894,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
"""
if not raw:
block_fields = self._serialize_fields(category, block_fields)
return {
document = {
'block_type': category,
'definition': definition_id,
'fields': block_fields,
......@@ -2904,6 +2905,9 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
'update_version': new_id
}
}
if block_defaults:
document['defaults'] = block_defaults
return document
@contract(block_key=BlockKey)
def _get_block_from_structure(self, structure, block_key):
......
......@@ -10,9 +10,8 @@ from mock import patch
from opaque_keys.edx.locator import LibraryLocator
from xblock.fragment import Fragment
from xblock.runtime import Runtime as VanillaRuntime
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError
from xmodule.modulestore.tests.factories import CourseFactory, LibraryFactory, ItemFactory, check_mongo_calls
from xmodule.modulestore.exceptions import DuplicateCourseError
from xmodule.modulestore.tests.factories import LibraryFactory, ItemFactory, check_mongo_calls
from xmodule.modulestore.tests.utils import MixedSplitTestCase
from xmodule.x_module import AUTHOR_VIEW
......@@ -218,146 +217,3 @@ class TestLibraries(MixedSplitTestCase):
modulestore=self.store,
)
self.assertFalse(self.store.has_published_version(block))
@ddt.ddt
class TestSplitCopyTemplate(MixedSplitTestCase):
"""
Test for split's copy_from_template method.
Currently it is only used for content libraries.
However for this test, we make sure it also works when copying from course to course.
"""
@ddt.data(
LibraryFactory,
CourseFactory,
)
def test_copy_from_template(self, source_type):
"""
Test that the behavior of copy_from_template() matches its docstring
"""
source_container = source_type.create(modulestore=self.store) # Either a library or a course
course = CourseFactory.create(modulestore=self.store)
# Add a vertical with a capa child to the source library/course:
vertical_block = ItemFactory.create(
category="vertical",
parent_location=source_container.location,
user_id=self.user_id,
publish_item=False,
modulestore=self.store,
)
problem_library_display_name = "Problem Library Display Name"
problem_block = ItemFactory.create(
category="problem",
parent_location=vertical_block.location,
user_id=self.user_id,
publish_item=False,
modulestore=self.store,
display_name=problem_library_display_name,
markdown="Problem markdown here"
)
if source_type == LibraryFactory:
source_container = self.store.get_library(source_container.location.library_key, remove_version=False, remove_branch=False)
else:
source_container = self.store.get_course(source_container.location.course_key, remove_version=False, remove_branch=False)
# Inherit the vertical and the problem from the library into the course:
source_keys = [source_container.children[0]]
new_blocks = self.store.copy_from_template(source_keys, dest_key=course.location, user_id=self.user_id)
self.assertEqual(len(new_blocks), 1)
course = self.store.get_course(course.location.course_key) # Reload from modulestore
self.assertEqual(len(course.children), 1)
vertical_block_course = self.store.get_item(course.children[0])
self.assertEqual(new_blocks[0], vertical_block_course.location)
problem_block_course = self.store.get_item(vertical_block_course.children[0])
self.assertEqual(problem_block_course.display_name, problem_library_display_name)
# Check that when capa modules are copied, their "markdown" fields (Scope.settings) are removed. (See note in split.py:copy_from_template())
self.assertIsNotNone(problem_block.markdown)
self.assertIsNone(problem_block_course.markdown)
# Override the display_name and weight:
new_display_name = "The Trouble with Tribbles"
new_weight = 20
problem_block_course.display_name = new_display_name
problem_block_course.weight = new_weight
self.store.update_item(problem_block_course, self.user_id)
# Test that "Any previously existing children of `dest_usage` that haven't been replaced/updated by this copy_from_template operation will be deleted."
extra_block = ItemFactory.create(
category="html",
parent_location=vertical_block_course.location,
user_id=self.user_id,
publish_item=False,
modulestore=self.store,
)
# Repeat the copy_from_template():
new_blocks2 = self.store.copy_from_template(source_keys, dest_key=course.location, user_id=self.user_id)
self.assertEqual(new_blocks, new_blocks2)
# Reload problem_block_course:
problem_block_course = self.store.get_item(problem_block_course.location)
self.assertEqual(problem_block_course.display_name, new_display_name)
self.assertEqual(problem_block_course.weight, new_weight)
# Ensure that extra_block was deleted:
vertical_block_course = self.store.get_item(new_blocks2[0])
self.assertEqual(len(vertical_block_course.children), 1)
with self.assertRaises(ItemNotFoundError):
self.store.get_item(extra_block.location)
def test_copy_from_template_auto_publish(self):
"""
Make sure that copy_from_template works with things like 'chapter' that
are always auto-published.
"""
source_course = CourseFactory.create(modulestore=self.store)
course = CourseFactory.create(modulestore=self.store)
make_block = lambda category, parent: ItemFactory.create(category=category, parent_location=parent.location, user_id=self.user_id, modulestore=self.store)
# Populate the course:
about = make_block("about", source_course)
chapter = make_block("chapter", source_course)
sequential = make_block("sequential", chapter)
# And three blocks that are NOT auto-published:
vertical = make_block("vertical", sequential)
make_block("problem", vertical)
html = make_block("html", source_course)
# Reload source_course since we need its branch and version to use copy_from_template:
source_course = self.store.get_course(source_course.location.course_key, remove_version=False, remove_branch=False)
# Inherit the vertical and the problem from the library into the course:
source_keys = [block.location for block in [about, chapter, html]]
block_keys = self.store.copy_from_template(source_keys, dest_key=course.location, user_id=self.user_id)
self.assertEqual(len(block_keys), len(source_keys))
# Build dict of the new blocks in 'course', keyed by category (which is a unique key in our case)
new_blocks = {}
block_keys = set(block_keys)
while block_keys:
key = block_keys.pop()
block = self.store.get_item(key)
new_blocks[block.category] = block
block_keys.update(set(getattr(block, "children", [])))
# Check that auto-publish blocks with no children are indeed published:
def published_version_exists(block):
""" Does a published version of block exist? """
try:
self.store.get_item(block.location.for_branch(ModuleStoreEnum.BranchName.published))
return True
except ItemNotFoundError:
return False
# Check that the auto-publish blocks have been published:
self.assertFalse(self.store.has_changes(new_blocks["about"]))
self.assertTrue(published_version_exists(new_blocks["chapter"])) # We can't use has_changes because it includes descendants
self.assertTrue(published_version_exists(new_blocks["sequential"])) # Ditto
# Check that non-auto-publish blocks and blocks with non-auto-publish descendants show changes:
self.assertTrue(self.store.has_changes(new_blocks["html"]))
self.assertTrue(self.store.has_changes(new_blocks["problem"]))
self.assertTrue(self.store.has_changes(new_blocks["chapter"])) # Will have changes since a child block has changes.
self.assertFalse(published_version_exists(new_blocks["vertical"])) # Verify that our published_version_exists works
"""
Tests for split's copy_from_template method.
Currently it is only used for content libraries.
However for these tests, we make sure it also works when copying from course to course.
"""
import ddt
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.tests.factories import CourseFactory, LibraryFactory
from xmodule.modulestore.tests.utils import MixedSplitTestCase
@ddt.ddt
class TestSplitCopyTemplate(MixedSplitTestCase):
"""
Test for split's copy_from_template method.
"""
@ddt.data(
LibraryFactory,
CourseFactory,
)
def test_copy_from_template(self, source_type):
"""
Test that the behavior of copy_from_template() matches its docstring
"""
source_container = source_type.create(modulestore=self.store) # Either a library or a course
course = CourseFactory.create(modulestore=self.store)
# Add a vertical with a capa child to the source library/course:
vertical_block = self.make_block("vertical", source_container)
problem_library_display_name = "Problem Library Display Name"
problem_block = self.make_block("problem", vertical_block, display_name=problem_library_display_name, markdown="Problem markdown here")
if source_type == LibraryFactory:
source_container = self.store.get_library(source_container.location.library_key, remove_version=False, remove_branch=False)
else:
source_container = self.store.get_course(source_container.location.course_key, remove_version=False, remove_branch=False)
# Inherit the vertical and the problem from the library into the course:
source_keys = [source_container.children[0]]
new_blocks = self.store.copy_from_template(source_keys, dest_key=course.location, user_id=self.user_id)
self.assertEqual(len(new_blocks), 1)
course = self.store.get_course(course.location.course_key) # Reload from modulestore
self.assertEqual(len(course.children), 1)
vertical_block_course = self.store.get_item(course.children[0])
self.assertEqual(new_blocks[0], vertical_block_course.location)
problem_block_course = self.store.get_item(vertical_block_course.children[0])
self.assertEqual(problem_block_course.display_name, problem_library_display_name)
# Check that when capa modules are copied, their "markdown" fields (Scope.settings) are removed. (See note in split.py:copy_from_template())
self.assertIsNotNone(problem_block.markdown)
self.assertIsNone(problem_block_course.markdown)
# Override the display_name and weight:
new_display_name = "The Trouble with Tribbles"
new_weight = 20
problem_block_course.display_name = new_display_name
problem_block_course.weight = new_weight
self.store.update_item(problem_block_course, self.user_id)
# Test that "Any previously existing children of `dest_usage` that haven't been replaced/updated by this copy_from_template operation will be deleted."
extra_block = self.make_block("html", vertical_block_course)
# Repeat the copy_from_template():
new_blocks2 = self.store.copy_from_template(source_keys, dest_key=course.location, user_id=self.user_id)
self.assertEqual(new_blocks, new_blocks2)
# Reload problem_block_course:
problem_block_course = self.store.get_item(problem_block_course.location)
self.assertEqual(problem_block_course.display_name, new_display_name)
self.assertEqual(problem_block_course.weight, new_weight)
# Ensure that extra_block was deleted:
vertical_block_course = self.store.get_item(new_blocks2[0])
self.assertEqual(len(vertical_block_course.children), 1)
with self.assertRaises(ItemNotFoundError):
self.store.get_item(extra_block.location)
def test_copy_from_template_publish(self):
"""
Test that copy_from_template's "defaults" data is not lost
when blocks are published.
"""
# Create a library with a problem:
source_library = LibraryFactory.create(modulestore=self.store)
display_name_expected = "CUSTOM Library Display Name"
self.make_block("problem", source_library, display_name=display_name_expected)
# Reload source_library since we need its branch and version to use copy_from_template:
source_library = self.store.get_library(source_library.location.library_key, remove_version=False, remove_branch=False)
# And a course with a vertical:
course = CourseFactory.create(modulestore=self.store)
self.make_block("vertical", course)
problem_key_in_course = self.store.copy_from_template(source_library.children, dest_key=course.location, user_id=self.user_id)[0]
# We do the following twice because different methods get used inside split modulestore on first vs. subsequent publish
for __ in range(0, 2):
# Publish:
self.store.publish(problem_key_in_course, self.user_id)
# Test that the defaults values are there.
problem_published = self.store.get_item(problem_key_in_course.for_branch(ModuleStoreEnum.BranchName.published))
self.assertEqual(problem_published.display_name, display_name_expected)
def test_copy_from_template_auto_publish(self):
"""
Make sure that copy_from_template works with things like 'chapter' that
are always auto-published.
"""
source_course = CourseFactory.create(modulestore=self.store)
course = CourseFactory.create(modulestore=self.store)
# Populate the course:
about = self.make_block("about", source_course)
chapter = self.make_block("chapter", source_course)
sequential = self.make_block("sequential", chapter)
# And three blocks that are NOT auto-published:
vertical = self.make_block("vertical", sequential)
self.make_block("problem", vertical)
html = self.make_block("html", source_course)
# Reload source_course since we need its branch and version to use copy_from_template:
source_course = self.store.get_course(source_course.location.course_key, remove_version=False, remove_branch=False)
# Inherit the vertical and the problem from the library into the course:
source_keys = [block.location for block in [about, chapter, html]]
block_keys = self.store.copy_from_template(source_keys, dest_key=course.location, user_id=self.user_id)
self.assertEqual(len(block_keys), len(source_keys))
# Build dict of the new blocks in 'course', keyed by category (which is a unique key in our case)
new_blocks = {}
block_keys = set(block_keys)
while block_keys:
key = block_keys.pop()
block = self.store.get_item(key)
new_blocks[block.category] = block
block_keys.update(set(getattr(block, "children", [])))
# Check that auto-publish blocks with no children are indeed published:
def published_version_exists(block):
""" Does a published version of block exist? """
try:
self.store.get_item(block.location.for_branch(ModuleStoreEnum.BranchName.published))
return True
except ItemNotFoundError:
return False
# Check that the auto-publish blocks have been published:
self.assertFalse(self.store.has_changes(new_blocks["about"]))
self.assertTrue(published_version_exists(new_blocks["chapter"])) # We can't use has_changes because it includes descendants
self.assertTrue(published_version_exists(new_blocks["sequential"])) # Ditto
# Check that non-auto-publish blocks and blocks with non-auto-publish descendants show changes:
self.assertTrue(self.store.has_changes(new_blocks["html"]))
self.assertTrue(self.store.has_changes(new_blocks["problem"]))
self.assertTrue(self.store.has_changes(new_blocks["chapter"])) # Will have changes since a child block has changes.
self.assertFalse(published_version_exists(new_blocks["vertical"])) # Verify that our published_version_exists works
......@@ -10,6 +10,7 @@ from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from xmodule.modulestore.edit_info import EditInfoMixin
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.modulestore.mixed import MixedModuleStore
from xmodule.modulestore.tests.factories import ItemFactory
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.tests import DATA_DIR
......@@ -108,3 +109,17 @@ class MixedSplitTestCase(TestCase):
)
self.addCleanup(self.store.close_all_connections)
self.addCleanup(self.store._drop_database) # pylint: disable=protected-access
def make_block(self, category, parent_block, **kwargs):
"""
Create a block of type `category` as a child of `parent_block`, in any
course or library. You can pass any field values as kwargs.
"""
extra = {"publish_item": False, "user_id": self.user_id}
extra.update(kwargs)
return ItemFactory.create(
category=category,
parent_location=parent_block.location,
modulestore=self.store,
**extra
)
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