diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py index dbe4a21f03c5871563c959634d4c1aa65ff0c479..63cad4a8922b06cf5e7a092bad469eb04d0dde5b 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py @@ -3095,6 +3095,210 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup): chapter_aside2 = new_chapter2.runtime.get_asides(new_chapter2)[0] self.assertEqual('another one value', chapter_aside2.data_field) + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) + @XBlockAside.register_temp_plugin(AsideTestType, 'test_aside') + @patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types', + lambda self, block: ['test_aside']) + def test_export_course_with_asides(self, default_store): + if default_store == ModuleStoreEnum.Type.mongo: + raise SkipTest("asides not supported in old mongo") + with MongoContentstoreBuilder().build() as contentstore: + self.store = MixedModuleStore( + contentstore=contentstore, + create_modulestore_instance=create_modulestore_instance, + mappings={}, + **self.OPTIONS + ) + self.addCleanup(self.store.close_all_connections) + with self.store.default_store(default_store): + dest_course_key = self.store.make_course_key('edX', "aside_test", "2012_Fall") + dest_course_key2 = self.store.make_course_key('edX', "aside_test_2", "2012_Fall_2") + + courses = import_course_from_xml( + self.store, + self.user_id, + DATA_DIR, + ['aside'], + load_error_modules=False, + static_content_store=contentstore, + target_id=dest_course_key, + create_if_not_present=True, + ) + + def update_block_aside(block): + """ + Check whether block has the expected aside w/ its fields and then recurse to the block's children + """ + asides = block.runtime.get_asides(block) + asides[0].data_field = ''.join(['Exported data_field ', asides[0].data_field]) + asides[0].content = ''.join(['Exported content ', asides[0].content]) + + self.store.update_item(block, self.user_id, asides=[asides[0]]) + + for child in block.get_children(): + update_block_aside(child) + + update_block_aside(courses[0]) + + # export course to xml + top_level_export_dir = 'exported_source_course_with_asides' + export_course_to_xml( + self.store, + contentstore, + dest_course_key, + self.export_dir, + top_level_export_dir, + ) + + # and restore the new one from the exported xml + courses2 = import_course_from_xml( + self.store, + self.user_id, + self.export_dir, + source_dirs=[top_level_export_dir], + static_content_store=contentstore, + target_id=dest_course_key2, + create_if_not_present=True, + raise_on_failure=True, + ) + + self.assertEquals(1, len(courses2)) + + # check that the imported blocks have the right asides and values + def check_block(block): + """ + Check whether block has the expected aside w/ its fields and then recurse to the block's children + """ + asides = block.runtime.get_asides(block) + + self.assertEqual(len(asides), 1, "Found {} asides but expected only test_aside".format(asides)) + self.assertIsInstance(asides[0], AsideTestType) + category = block.scope_ids.block_type + self.assertEqual(asides[0].data_field, "Exported data_field {} aside data".format(category)) + self.assertEqual(asides[0].content, "Exported content {} Aside".format(category.capitalize())) + + for child in block.get_children(): + check_block(child) + + check_block(courses2[0]) + + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) + @XBlockAside.register_temp_plugin(AsideTestType, 'test_aside') + @patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types', + lambda self, block: ['test_aside']) + def test_export_course_after_creating_new_items_with_asides(self, default_store): # pylint: disable=too-many-statements + if default_store == ModuleStoreEnum.Type.mongo: + raise SkipTest("asides not supported in old mongo") + with MongoContentstoreBuilder().build() as contentstore: + self.store = MixedModuleStore( + contentstore=contentstore, + create_modulestore_instance=create_modulestore_instance, + mappings={}, + **self.OPTIONS + ) + self.addCleanup(self.store.close_all_connections) + with self.store.default_store(default_store): + dest_course_key = self.store.make_course_key('edX', "aside_test", "2012_Fall") + dest_course_key2 = self.store.make_course_key('edX', "aside_test_2", "2012_Fall_2") + + courses = import_course_from_xml( + self.store, + self.user_id, + DATA_DIR, + ['aside'], + load_error_modules=False, + static_content_store=contentstore, + target_id=dest_course_key, + create_if_not_present=True, + ) + + # create new chapter and modify aside for it + new_chapter_display_name = 'New Chapter' + new_chapter = self.store.create_child(self.user_id, courses[0].location, 'chapter', 'new_chapter') + new_chapter.display_name = new_chapter_display_name + asides = new_chapter.runtime.get_asides(new_chapter) + + self.assertEqual(len(asides), 1, "Found {} asides but expected only test_aside".format(asides)) + chapter_aside = asides[0] + self.assertIsInstance(chapter_aside, AsideTestType) + chapter_aside.data_field = 'new value' + self.store.update_item(new_chapter, self.user_id, asides=[chapter_aside]) + + # create new problem and modify aside for it + sequence = courses[0].get_children()[0].get_children()[0] + new_problem_display_name = 'New Problem' + new_problem = self.store.create_child(self.user_id, sequence.location, 'problem', 'new_problem') + new_problem.display_name = new_problem_display_name + asides = new_problem.runtime.get_asides(new_problem) + + self.assertEqual(len(asides), 1, "Found {} asides but expected only test_aside".format(asides)) + problem_aside = asides[0] + self.assertIsInstance(problem_aside, AsideTestType) + problem_aside.data_field = 'new problem value' + problem_aside.content = 'new content value' + self.store.update_item(new_problem, self.user_id, asides=[problem_aside]) + + # export course to xml + top_level_export_dir = 'exported_source_course_with_asides' + export_course_to_xml( + self.store, + contentstore, + dest_course_key, + self.export_dir, + top_level_export_dir, + ) + + # and restore the new one from the exported xml + courses2 = import_course_from_xml( + self.store, + self.user_id, + self.export_dir, + source_dirs=[top_level_export_dir], + static_content_store=contentstore, + target_id=dest_course_key2, + create_if_not_present=True, + raise_on_failure=True, + ) + + self.assertEquals(1, len(courses2)) + + # check that aside for the new chapter was exported/imported properly + chapters = courses2[0].get_children() + self.assertEquals(2, len(chapters)) + self.assertIn(new_chapter_display_name, [item.display_name for item in chapters]) + + found = False + for child in chapters: + if new_chapter.display_name == child.display_name: + found = True + asides = child.runtime.get_asides(child) + self.assertEqual(len(asides), 1) + child_aside = asides[0] + self.assertIsInstance(child_aside, AsideTestType) + self.assertEquals(child_aside.data_field, 'new value') + break + + self.assertTrue(found, "new_chapter not found") + + # check that aside for the new problem was exported/imported properly + sequence_children = courses2[0].get_children()[0].get_children()[0].get_children() + self.assertEquals(2, len(sequence_children)) + self.assertIn(new_problem_display_name, [item.display_name for item in sequence_children]) + + found = False + for child in sequence_children: + if new_problem.display_name == child.display_name: + found = True + asides = child.runtime.get_asides(child) + self.assertEqual(len(asides), 1) + child_aside = asides[0] + self.assertIsInstance(child_aside, AsideTestType) + self.assertEquals(child_aside.data_field, 'new problem value') + self.assertEquals(child_aside.content, 'new content value') + break + + self.assertTrue(found, "new_chapter not found") + @ddt.ddt @attr('mongo') diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index 7e6d439a01524faa45b94fb70f5b9f1dea9a4736..d7a7fe7cdb4cb4a2f69c43bf2ae14472a48619b1 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -5,6 +5,7 @@ import os import sys from lxml import etree +from xblock.core import XML_NAMESPACES from xblock.fields import Dict, Scope, ScopeIds from xblock.runtime import KvsFieldData from xmodule.x_module import XModuleDescriptor, DEPRECATION_VSCOMPAT_EVENT @@ -428,6 +429,12 @@ class XmlParserMixin(object): """ # Get the definition xml_object = self.definition_to_xml(self.runtime.export_fs) + for aside in self.runtime.get_asides(self): + if aside.needs_serialization(): + aside_node = etree.Element("unknown_root", nsmap=XML_NAMESPACES) + aside.add_xml_to_node(aside_node) + xml_object.append(aside_node) + self.clean_metadata_from_xml(xml_object) # Set the tag on both nodes so we get the file path right. diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index bf5b98c622f9e05b538f0d9d78a4713f0e3c8df8..bd90fbec23dc4fe41c1284cb21906d56c95ef64b 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -69,7 +69,7 @@ git+https://github.com/edx/rfc6266.git@v0.0.5-edx#egg=rfc6266==0.0.5-edx git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002 # Our libraries: -git+https://github.com/edx/XBlock.git@xblock-0.4.7#egg=XBlock==0.4.7 +git+https://github.com/edx/XBlock.git@xblock-0.4.8#egg=XBlock==0.4.8 -e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail -e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool -e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1