Commit e0f6b1a2 by Calen Pennington

Merge pull request #2221 from cpennington/xblock-xml-serialization

Enable (and test) xml export of pure XBlocks
parents a60abede db17bf4c
...@@ -149,7 +149,7 @@ class ABTestDescriptor(ABTestFields, RawDescriptor, XmlDescriptor): ...@@ -149,7 +149,7 @@ class ABTestDescriptor(ABTestFields, RawDescriptor, XmlDescriptor):
for child_loc in group: for child_loc in group:
child = self.system.load_item(child_loc) child = self.system.load_item(child_loc)
group_elem.append(etree.fromstring(child.export_to_xml(resource_fs))) self.runtime.add_block_as_child_node(child, group_elem)
return xml_object return xml_object
......
...@@ -222,9 +222,9 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor): ...@@ -222,9 +222,9 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):
show_tag_list = [] show_tag_list = []
for child in xml_object: for child in xml_object:
if child.tag == 'show': if child.tag == 'show':
location = ConditionalDescriptor.parse_sources(child, system) locations = ConditionalDescriptor.parse_sources(child, system)
children.extend(location) children.extend(locations)
show_tag_list.extend(location.url()) # pylint: disable=no-member show_tag_list.extend(location.url() for location in locations) # pylint: disable=no-member
else: else:
try: try:
descriptor = system.process_xml(etree.tostring(child)) descriptor = system.process_xml(etree.tostring(child))
...@@ -244,6 +244,5 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor): ...@@ -244,6 +244,5 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):
tag_name='show', sources=location) tag_name='show', sources=location)
xml_object.append(etree.fromstring(show_str)) xml_object.append(etree.fromstring(show_str))
else: else:
xml_object.append( self.runtime.add_block_as_child_node(child, xml_object)
etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object return xml_object
...@@ -400,6 +400,5 @@ class CrowdsourceHinterDescriptor(CrowdsourceHinterFields, RawDescriptor): ...@@ -400,6 +400,5 @@ class CrowdsourceHinterDescriptor(CrowdsourceHinterFields, RawDescriptor):
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
xml_object = etree.Element('crowdsource_hinter') xml_object = etree.Element('crowdsource_hinter')
for child in self.get_children(): for child in self.get_children():
xml_object.append( self.runtime.add_block_as_child_node(child, xml_object)
etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object return xml_object
...@@ -3,6 +3,7 @@ Methods for exporting course data to XML ...@@ -3,6 +3,7 @@ Methods for exporting course data to XML
""" """
import logging import logging
import lxml.etree
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from fs.osfs import OSFS from fs.osfs import OSFS
...@@ -57,11 +58,13 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d ...@@ -57,11 +58,13 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
course = modulestore.get_course(course_id) course = modulestore.get_course(course_id)
fs = OSFS(root_dir) fs = OSFS(root_dir)
export_fs = fs.makeopendir(course_dir) export_fs = course.runtime.export_fs = fs.makeopendir(course_dir)
root = lxml.etree.Element('unknown')
course.add_xml_to_node(root)
xml = course.export_to_xml(export_fs)
with export_fs.open('course.xml', 'w') as course_xml: with export_fs.open('course.xml', 'w') as course_xml:
course_xml.write(xml) lxml.etree.ElementTree(root).write(course_xml)
# export the static assets # export the static assets
policies_dir = export_fs.makeopendir('policies') policies_dir = export_fs.makeopendir('policies')
...@@ -112,7 +115,9 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d ...@@ -112,7 +115,9 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
sequential = modulestore.get_item(Location(parent_locs[0])) sequential = modulestore.get_item(Location(parent_locs[0]))
index = sequential.children.index(draft_vertical.location.url()) index = sequential.children.index(draft_vertical.location.url())
draft_vertical.xml_attributes['index_in_children_list'] = str(index) draft_vertical.xml_attributes['index_in_children_list'] = str(index)
draft_vertical.export_to_xml(draft_course_dir) draft_vertical.runtime.export_fs = draft_course_dir
node = lxml.etree.Element('unknown')
draft_vertical.add_xml_to_node(node)
def export_extra_content(export_fs, modulestore, course_id, course_location, category_type, dirname, file_suffix=''): def export_extra_content(export_fs, modulestore, course_id, course_location, category_type, dirname, file_suffix=''):
......
...@@ -100,8 +100,7 @@ class RandomizeDescriptor(RandomizeFields, SequenceDescriptor): ...@@ -100,8 +100,7 @@ class RandomizeDescriptor(RandomizeFields, SequenceDescriptor):
xml_object = etree.Element('randomize') xml_object = etree.Element('randomize')
for child in self.get_children(): for child in self.get_children():
xml_object.append( self.runtime.add_block_as_child_node(child, xml_object)
etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object return xml_object
def has_dynamic_children(self): def has_dynamic_children(self):
......
...@@ -150,6 +150,5 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor): ...@@ -150,6 +150,5 @@ class SequenceDescriptor(SequenceFields, MakoModuleDescriptor, XmlDescriptor):
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
xml_object = etree.Element('sequential') xml_object = etree.Element('sequential')
for child in self.get_children(): for child in self.get_children():
xml_object.append( self.runtime.add_block_as_child_node(child, xml_object)
etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object return xml_object
...@@ -2,19 +2,25 @@ ...@@ -2,19 +2,25 @@
Tests of XML export Tests of XML export
""" """
from datetime import datetime, timedelta, tzinfo import ddt
from tempfile import mkdtemp import lxml.etree
import unittest
import shutil
from textwrap import dedent
import mock import mock
import os
import pytz import pytz
import shutil
import tarfile
import unittest
import uuid
from datetime import datetime, timedelta, tzinfo
from fs.osfs import OSFS from fs.osfs import OSFS
from path import path from path import path
import uuid from tempfile import mkdtemp
import tarfile from textwrap import dedent
import os
from xblock.core import XBlock
from xblock.fields import String, Scope, Integer
from xblock.test.tools import blocks_are_equivalent
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
...@@ -23,6 +29,7 @@ from xmodule.modulestore.xml_exporter import ( ...@@ -23,6 +29,7 @@ from xmodule.modulestore.xml_exporter import (
) )
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR
from xmodule.tests.helpers import directories_equal from xmodule.tests.helpers import directories_equal
from xmodule.x_module import XModuleMixin
def strip_filenames(descriptor): def strip_filenames(descriptor):
...@@ -43,6 +50,16 @@ def strip_filenames(descriptor): ...@@ -43,6 +50,16 @@ def strip_filenames(descriptor):
descriptor.save() descriptor.save()
class PureXBlock(XBlock):
"""Class for testing pure XBlocks."""
has_children = True
field1 = String(default="something", scope=Scope.user_state)
field2 = Integer(scope=Scope.user_state)
@ddt.ddt
class RoundTripTestCase(unittest.TestCase): class RoundTripTestCase(unittest.TestCase):
""" """
Check that our test courses roundtrip properly. Check that our test courses roundtrip properly.
...@@ -51,8 +68,25 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -51,8 +68,25 @@ class RoundTripTestCase(unittest.TestCase):
Thus we make sure that export and import work properly. Thus we make sure that export and import work properly.
""" """
def setUp(self):
self.maxDiff = None
self.temp_dir = mkdtemp()
self.addCleanup(shutil.rmtree, self.temp_dir)
@mock.patch('xmodule.course_module.requests.get') @mock.patch('xmodule.course_module.requests.get')
def check_export_roundtrip(self, data_dir, course_dir, mock_get): @ddt.data(
"toy",
"simple",
"conditional_and_poll",
"self_assessment",
"graphic_slider_tool",
"test_exam_registration",
"word_cloud",
"pure_xblock",
)
@XBlock.register_temp_plugin(PureXBlock, 'pure')
def test_export_roundtrip(self, course_dir, mock_get):
# Patch network calls to retrieve the textbook TOC # Patch network calls to retrieve the textbook TOC
mock_get.return_value.text = dedent(""" mock_get.return_value.text = dedent("""
...@@ -64,11 +98,11 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -64,11 +98,11 @@ class RoundTripTestCase(unittest.TestCase):
root_dir = path(self.temp_dir) root_dir = path(self.temp_dir)
print("Copying test course to temp dir {0}".format(root_dir)) print("Copying test course to temp dir {0}".format(root_dir))
data_dir = path(data_dir) data_dir = path(DATA_DIR)
shutil.copytree(data_dir / course_dir, root_dir / course_dir) shutil.copytree(data_dir / course_dir, root_dir / course_dir)
print("Starting import") print("Starting import")
initial_import = XMLModuleStore(root_dir, course_dirs=[course_dir]) initial_import = XMLModuleStore(root_dir, course_dirs=[course_dir], xblock_mixins=(XModuleMixin,))
courses = initial_import.get_courses() courses = initial_import.get_courses()
self.assertEquals(len(courses), 1) self.assertEquals(len(courses), 1)
...@@ -78,14 +112,15 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -78,14 +112,15 @@ class RoundTripTestCase(unittest.TestCase):
# will still be there. # will still be there.
print("Starting export") print("Starting export")
fs = OSFS(root_dir) fs = OSFS(root_dir)
export_fs = fs.makeopendir(course_dir) initial_course.runtime.export_fs = fs.makeopendir(course_dir)
root = lxml.etree.Element('root')
xml = initial_course.export_to_xml(export_fs) initial_course.add_xml_to_node(root)
with export_fs.open('course.xml', 'w') as course_xml: with initial_course.runtime.export_fs.open('course.xml', 'w') as course_xml:
course_xml.write(xml) lxml.etree.ElementTree(root).write(course_xml)
print("Starting second import") print("Starting second import")
second_import = XMLModuleStore(root_dir, course_dirs=[course_dir]) second_import = XMLModuleStore(root_dir, course_dirs=[course_dir], xblock_mixins=(XModuleMixin,))
courses2 = second_import.get_courses() courses2 = second_import.get_courses()
self.assertEquals(len(courses2), 1) self.assertEquals(len(courses2), 1)
...@@ -98,48 +133,24 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -98,48 +133,24 @@ class RoundTripTestCase(unittest.TestCase):
strip_filenames(initial_course) strip_filenames(initial_course)
strip_filenames(exported_course) strip_filenames(exported_course)
self.assertEquals(initial_course, exported_course) self.assertTrue(blocks_are_equivalent(initial_course, exported_course))
self.assertEquals(initial_course.id, exported_course.id) self.assertEquals(initial_course.id, exported_course.id)
course_id = initial_course.id course_id = initial_course.id
print("Checking key equality") print("Checking key equality")
self.assertEquals(sorted(initial_import.modules[course_id].keys()), self.assertItemsEqual(
sorted(second_import.modules[course_id].keys())) initial_import.modules[course_id].keys(),
second_import.modules[course_id].keys()
)
print("Checking module equality") print("Checking module equality")
for location in initial_import.modules[course_id].keys(): for location in initial_import.modules[course_id].keys():
print("Checking", location) print("Checking", location)
self.assertEquals(initial_import.modules[course_id][location], self.assertTrue(blocks_are_equivalent(
second_import.modules[course_id][location]) initial_import.modules[course_id][location],
second_import.modules[course_id][location]
def setUp(self): ))
self.maxDiff = None
self.temp_dir = mkdtemp()
self.addCleanup(shutil.rmtree, self.temp_dir)
def test_toy_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "toy")
def test_simple_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "simple")
def test_conditional_and_poll_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "conditional_and_poll")
def test_selfassessment_roundtrip(self):
#Test selfassessment xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR, "self_assessment")
def test_graphicslidertool_roundtrip(self):
#Test graphicslidertool xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR, "graphic_slider_tool")
def test_exam_registration_roundtrip(self):
# Test exam_registration xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR, "test_exam_registration")
def test_word_cloud_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "word_cloud")
class TestEdxJsonEncoder(unittest.TestCase): class TestEdxJsonEncoder(unittest.TestCase):
......
...@@ -158,9 +158,9 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -158,9 +158,9 @@ class ImportTestCase(BaseCourseTestCase):
system = self.get_system() system = self.get_system()
descriptor = system.process_xml(bad_xml) descriptor = system.process_xml(bad_xml)
resource_fs = None node = etree.Element('unknown')
tag_xml = descriptor.export_to_xml(resource_fs) descriptor.add_xml_to_node(node)
re_import_descriptor = system.process_xml(tag_xml) re_import_descriptor = system.process_xml(etree.tostring(node))
self.assertEqual(re_import_descriptor.__class__.__name__, 'ErrorDescriptorWithMixins') self.assertEqual(re_import_descriptor.__class__.__name__, 'ErrorDescriptorWithMixins')
...@@ -182,12 +182,11 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -182,12 +182,11 @@ class ImportTestCase(BaseCourseTestCase):
descriptor = system.process_xml(xml_str_in) descriptor = system.process_xml(xml_str_in)
# export it # export it
resource_fs = None node = etree.Element('unknown')
xml_str_out = descriptor.export_to_xml(resource_fs) descriptor.add_xml_to_node(node)
# Now make sure the exported xml is a sequential # Now make sure the exported xml is a sequential
xml_out = etree.fromstring(xml_str_out) self.assertEqual(node.tag, 'sequential')
self.assertEqual(xml_out.tag, 'sequential')
def test_metadata_import_export(self): def test_metadata_import_export(self):
"""Two checks: """Two checks:
...@@ -221,19 +220,19 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -221,19 +220,19 @@ class ImportTestCase(BaseCourseTestCase):
) )
# Now export and check things # Now export and check things
resource_fs = MemoryFS() descriptor.runtime.export_fs = MemoryFS()
exported_xml = descriptor.export_to_xml(resource_fs) node = etree.Element('unknown')
descriptor.add_xml_to_node(node)
# Check that the exported xml is just a pointer # Check that the exported xml is just a pointer
print("Exported xml:", exported_xml) print("Exported xml:", etree.tostring(node))
pointer = etree.fromstring(exported_xml) self.assertTrue(is_pointer_tag(node))
self.assertTrue(is_pointer_tag(pointer))
# but it's a special case course pointer # but it's a special case course pointer
self.assertEqual(pointer.attrib['course'], COURSE) self.assertEqual(node.attrib['course'], COURSE)
self.assertEqual(pointer.attrib['org'], ORG) self.assertEqual(node.attrib['org'], ORG)
# Does the course still have unicorns? # Does the course still have unicorns?
with resource_fs.open('course/{url_name}.xml'.format(url_name=url_name)) as f: with descriptor.runtime.export_fs.open('course/{url_name}.xml'.format(url_name=url_name)) as f:
course_xml = etree.fromstring(f.read()) course_xml = etree.fromstring(f.read())
self.assertEqual(course_xml.attrib['unicorn'], 'purple') self.assertEqual(course_xml.attrib['unicorn'], 'purple')
...@@ -247,7 +246,7 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -247,7 +246,7 @@ class ImportTestCase(BaseCourseTestCase):
# Does the chapter tag now have a due attribute? # Does the chapter tag now have a due attribute?
# hardcoded path to child # hardcoded path to child
with resource_fs.open('chapter/ch.xml') as f: with descriptor.runtime.export_fs.open('chapter/ch.xml') as f:
chapter_xml = etree.fromstring(f.read()) chapter_xml = etree.fromstring(f.read())
self.assertEqual(chapter_xml.tag, 'chapter') self.assertEqual(chapter_xml.tag, 'chapter')
self.assertFalse('due' in chapter_xml.attrib) self.assertFalse('due' in chapter_xml.attrib)
......
...@@ -6,16 +6,27 @@ functionality ...@@ -6,16 +6,27 @@ functionality
# pylint: disable=protected-access # pylint: disable=protected-access
import webob import webob
from nose.tools import assert_equal, assert_is_instance # pylint: disable=E0611 import ddt
from unittest.case import SkipTest from factory import (
BUILD_STRATEGY,
Factory,
lazy_attribute,
LazyAttributeSequence,
post_generation,
SubFactory,
use_strategy,
)
from fs.memoryfs import MemoryFS
from lxml import etree
from mock import Mock from mock import Mock
from unittest.case import SkipTest, TestCase
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.x_module import ModuleSystem, XModule, XModuleDescriptor from xmodule.x_module import ModuleSystem, XModule, XModuleDescriptor, DescriptorSystem
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from xmodule.annotatable_module import AnnotatableDescriptor from xmodule.annotatable_module import AnnotatableDescriptor
from xmodule.capa_module import CapaDescriptor from xmodule.capa_module import CapaDescriptor
...@@ -37,30 +48,37 @@ from xmodule.vertical_module import VerticalDescriptor ...@@ -37,30 +48,37 @@ from xmodule.vertical_module import VerticalDescriptor
from xmodule.wrapper_module import WrapperDescriptor from xmodule.wrapper_module import WrapperDescriptor
from xmodule.tests import get_test_descriptor_system, get_test_system from xmodule.tests import get_test_descriptor_system, get_test_system
LEAF_XMODULES = (
AnnotatableDescriptor, # A dictionary that maps specific XModuleDescriptor classes without children
CapaDescriptor, # to a list of sample field values to test with.
CombinedOpenEndedDescriptor, # TODO: Add more types of sample data
DiscussionDescriptor, LEAF_XMODULES = {
GraphicalSliderToolDescriptor, AnnotatableDescriptor: [{}],
HtmlDescriptor, CapaDescriptor: [{}],
PeerGradingDescriptor, CombinedOpenEndedDescriptor: [{}],
PollDescriptor, DiscussionDescriptor: [{}],
WordCloudDescriptor, GraphicalSliderToolDescriptor: [{}],
HtmlDescriptor: [{}],
PeerGradingDescriptor: [{}],
PollDescriptor: [{'display_name': 'Poll Display Name'}],
WordCloudDescriptor: [{}],
# This is being excluded because it has dependencies on django # This is being excluded because it has dependencies on django
#VideoDescriptor, #VideoDescriptor,
) }
CONTAINER_XMODULES = ( # A dictionary that maps specific XModuleDescriptor classes with children
ConditionalDescriptor, # to a list of sample field values to test with.
CourseDescriptor, # TODO: Add more types of sample data
CrowdsourceHinterDescriptor, CONTAINER_XMODULES = {
RandomizeDescriptor, ConditionalDescriptor: [{}],
SequenceDescriptor, CourseDescriptor: [{}],
VerticalDescriptor, CrowdsourceHinterDescriptor: [{}],
WrapperDescriptor, RandomizeDescriptor: [{}],
) SequenceDescriptor: [{}],
VerticalDescriptor: [{}],
WrapperDescriptor: [{}],
}
# These modules are editable in studio yet # These modules are editable in studio yet
NOT_STUDIO_EDITABLE = ( NOT_STUDIO_EDITABLE = (
...@@ -69,189 +87,213 @@ NOT_STUDIO_EDITABLE = ( ...@@ -69,189 +87,213 @@ NOT_STUDIO_EDITABLE = (
PollDescriptor PollDescriptor
) )
class TestXBlockWrapper(object):
"""Helper methods used in test case classes below."""
@property def flatten(class_dict):
def leaf_module_runtime(self): """
return get_test_system() Flatten a dict from cls -> [fields, ...] and yields values of the form (cls, fields)
for each entry in the dictionary value.
"""
for cls, fields_list in class_dict.items():
for fields in fields_list:
yield (cls, fields)
@use_strategy(BUILD_STRATEGY)
class ModuleSystemFactory(Factory):
FACTORY_FOR = ModuleSystem
def leaf_descriptor(self, descriptor_cls): @classmethod
location = Location('i4x://org/course/category/name') def _build(cls, target_class, *args, **kwargs):
runtime = get_test_descriptor_system() return get_test_system(*args, **kwargs)
return runtime.construct_xblock_from_class(
descriptor_cls,
ScopeIds(None, descriptor_cls.__name__, location, location), @use_strategy(BUILD_STRATEGY)
DictFieldData({}), class DescriptorSystemFactory(Factory):
) FACTORY_FOR = DescriptorSystem
def leaf_module(self, descriptor_cls): @classmethod
"""Returns a descriptor that is ready to proxy as an xmodule""" def _build(cls, target_class, *args, **kwargs):
descriptor = self.leaf_descriptor(descriptor_cls) return get_test_descriptor_system(*args, **kwargs)
descriptor.xmodule_runtime = self.leaf_module_runtime
return descriptor
def container_module_runtime(self, depth):
runtime = self.leaf_module_runtime class LeafModuleRuntimeFactory(ModuleSystemFactory):
pass
class ContainerModuleRuntimeFactory(ModuleSystemFactory):
@post_generation
def depth(self, create, depth, **kwargs):
if depth == 0: if depth == 0:
runtime.get_module.side_effect = lambda x: self.leaf_module(HtmlDescriptor) self.get_module.side_effect = lambda x: LeafModuleFactory(descriptor_cls=HtmlDescriptor)
else: else:
runtime.get_module.side_effect = lambda x: self.container_module(VerticalDescriptor, depth - 1) self.get_module.side_effect = lambda x: ContainerModuleFactory(descriptor_cls=VerticalDescriptor, depth=depth-1)
runtime.position = 2
return runtime
def container_descriptor(self, descriptor_cls, depth): @post_generation
"""Return an instance of `descriptor_cls` with `depth` levels of children""" def position(self, create, position=2, **kwargs):
location = Location('i4x://org/course/category/name') self.position = position
runtime = get_test_descriptor_system()
class ContainerDescriptorRuntimeFactory(DescriptorSystemFactory):
@post_generation
def depth(self, create, depth, **kwargs):
if depth == 0: if depth == 0:
runtime.load_item.side_effect = lambda x: self.leaf_module(HtmlDescriptor) self.load_item.side_effect = lambda x: LeafModuleFactory(descriptor_cls=HtmlDescriptor)
else: else:
runtime.load_item.side_effect = lambda x: self.container_module(VerticalDescriptor, depth - 1) self.load_item.side_effect = lambda x: ContainerModuleFactory(descriptor_cls=VerticalDescriptor, depth=depth-1)
return runtime.construct_xblock_from_class(
descriptor_cls,
ScopeIds(None, descriptor_cls.__name__, location, location),
DictFieldData({
'children': range(3)
}),
)
def container_module(self, descriptor_cls, depth): @post_generation
"""Returns a descriptor that is ready to proxy as an xmodule""" def position(self, create, position=2, **kwargs):
descriptor = self.container_descriptor(descriptor_cls, depth) self.position = position
descriptor.xmodule_runtime = self.container_module_runtime(depth)
return descriptor
class TestStudentView(TestXBlockWrapper):
# Test that for all of the leaf XModule Descriptors, @use_strategy(BUILD_STRATEGY)
# the student_view wrapper returns the same thing in its content class LeafDescriptorFactory(Factory):
# as get_html returns FACTORY_FOR = XModuleDescriptor
def test_student_view_leaf_node(self):
for descriptor_cls in LEAF_XMODULES:
yield self.check_student_view_leaf_node, descriptor_cls
# Check that when an xmodule is instantiated from descriptor_cls runtime = SubFactory(DescriptorSystemFactory)
# it generates the same thing from student_view that it does from get_html url_name = LazyAttributeSequence('{.block_type}_{}'.format)
def check_student_view_leaf_node(self, descriptor_cls):
if descriptor_cls.module_class.student_view != XModule.student_view: @lazy_attribute
raise SkipTest(descriptor_cls.__name__ + " implements student_view") def location(self):
return Location('i4x://org/course/category/{}'.format(self.url_name))
descriptor = self.leaf_module(descriptor_cls) @lazy_attribute
assert_equal( def block_type(self):
descriptor._xmodule.get_html(), return self.descriptor_cls.__name__
descriptor.render('student_view').content
)
# Test that for all container XModule Descriptors, @lazy_attribute
# their corresponding XModule renders the same thing using student_view def definition_id(self):
# as it does using get_html, under the following conditions: return self.location
# a) All of its descendents are xmodules
# b) Some of its descendents are xmodules and some are xblocks
# c) All of its descendents are xblocks
def test_student_view_container_node(self):
for descriptor_cls in CONTAINER_XMODULES:
yield self.check_student_view_container_node_xmodules_only, descriptor_cls
yield self.check_student_view_container_node_mixed, descriptor_cls
yield self.check_student_view_container_node_xblocks_only, descriptor_cls
# Check that when an xmodule is generated from descriptor_cls
# with only xmodule children, it generates the same html from student_view
# as it does using get_html
def check_student_view_container_node_xmodules_only(self, descriptor_cls):
if descriptor_cls.module_class.student_view != XModule.student_view: @lazy_attribute
raise SkipTest(descriptor_cls.__name__ + " implements student_view") def usage_id(self):
return self.location
descriptor = self.container_module(descriptor_cls, 2) @classmethod
assert_equal( def _build(cls, target_class, *args, **kwargs):
descriptor._xmodule.get_html(), runtime = kwargs.pop('runtime')
descriptor.render('student_view').content desc_cls = kwargs.pop('descriptor_cls')
block_type = kwargs.pop('block_type')
def_id = kwargs.pop('definition_id')
usage_id = kwargs.pop('usage_id')
block = runtime.construct_xblock_from_class(
desc_cls,
ScopeIds(None, block_type, def_id, usage_id),
DictFieldData(dict(**kwargs))
) )
block.save()
return block
# Check that when an xmodule is generated from descriptor_cls
# with mixed xmodule and xblock children, it generates the same html from student_view
# as it does using get_html
def check_student_view_container_node_mixed(self, descriptor_cls):
raise SkipTest("XBlock support in XDescriptor not yet fully implemented")
# Check that when an xmodule is generated from descriptor_cls class LeafModuleFactory(LeafDescriptorFactory):
# with only xblock children, it generates the same html from student_view
# as it does using get_html
def check_student_view_container_node_xblocks_only(self, descriptor_cls):
raise SkipTest("XBlock support in XModules not yet fully implemented")
@post_generation
def xmodule_runtime(self, create, xmodule_runtime, **kwargs):
if xmodule_runtime is None:
xmodule_runtime = LeafModuleRuntimeFactory()
class TestStudioView(TestXBlockWrapper): self.xmodule_runtime = xmodule_runtime
# Test that for all of the Descriptors listed in LEAF_XMODULES,
# the studio_view wrapper returns the same thing in its content
# as get_html returns
def test_studio_view_leaf_node(self):
for descriptor_cls in LEAF_XMODULES:
yield self.check_studio_view_leaf_node, descriptor_cls
# Check that when a descriptor is instantiated from descriptor_cls class ContainerDescriptorFactory(LeafDescriptorFactory):
# it generates the same thing from studio_view that it does from get_html runtime = SubFactory(ContainerDescriptorRuntimeFactory)
def check_studio_view_leaf_node(self, descriptor_cls): children = range(3)
if descriptor_cls in NOT_STUDIO_EDITABLE:
raise SkipTest(descriptor_cls.__name__ + " is not editable in studio")
class ContainerModuleFactory(LeafModuleFactory):
@lazy_attribute
def xmodule_runtime(self):
return ContainerModuleRuntimeFactory(depth=self.depth)
if descriptor_cls.studio_view != XModuleDescriptor.studio_view:
raise SkipTest(descriptor_cls.__name__ + " implements studio_view")
descriptor = self.leaf_descriptor(descriptor_cls) @ddt.ddt
assert_equal(descriptor.get_html(), descriptor.render('studio_view').content) class XBlockWrapperTestMixin(object):
"""
This is a mixin for building tests of the implementation of the XBlock
api by wrapping XModule native functions.
You can creat an actual test case by inheriting from this class and UnitTest,
and implement skip_if_invalid and check_property.
"""
# Test that for all of the Descriptors listed in CONTAINER_XMODULES def skip_if_invalid(self, descriptor_cls):
# render the same thing using studio_view as they do using get_html, under the following conditions: """
# a) All of its descendants are xmodules Raise SkipTest if this descriptor_cls shouldn't be tested.
# b) Some of its descendants are xmodules and some are xblocks """
# c) All of its descendants are xblocks pass
def test_studio_view_container_node(self):
for descriptor_cls in CONTAINER_XMODULES:
yield self.check_studio_view_container_node_xmodules_only, descriptor_cls
yield self.check_studio_view_container_node_mixed, descriptor_cls
yield self.check_studio_view_container_node_xblocks_only, descriptor_cls
def check_property(self, descriptor):
raise SkipTest("check_property not defined")
# Check that when a descriptor is generated from descriptor_cls # Test that for all of the leaf XModule Descriptors,
# with only xmodule children, it generates the same html from studio_view # the test property holds
# as it does using get_html @ddt.data(*flatten(LEAF_XMODULES))
def check_studio_view_container_node_xmodules_only(self, descriptor_cls): def test_leaf_node(self, cls_and_fields):
if descriptor_cls in NOT_STUDIO_EDITABLE: descriptor_cls, fields = cls_and_fields
raise SkipTest(descriptor_cls.__name__ + "is not editable in studio") self.skip_if_invalid(descriptor_cls)
descriptor = LeafModuleFactory(descriptor_cls=descriptor_cls, **fields)
self.check_property(descriptor)
# Test that when an xmodule is generated from descriptor_cls
# with only xmodule children, the test property holds
@ddt.data(*flatten(CONTAINER_XMODULES))
def test_container_node_xmodules_only(self, cls_and_fields):
descriptor_cls, fields = cls_and_fields
self.skip_if_invalid(descriptor_cls)
descriptor = ContainerModuleFactory(descriptor_cls=descriptor_cls, depth=2, **fields)
self.check_property(descriptor)
# Test that when an xmodule is generated from descriptor_cls
# with mixed xmodule and xblock children, the test property holds
@ddt.data(*flatten(CONTAINER_XMODULES))
def test_container_node_mixed(self, cls_and_fields):
raise SkipTest("XBlock support in XDescriptor not yet fully implemented")
if descriptor_cls.studio_view != XModuleDescriptor.studio_view: # Test that when an xmodule is generated from descriptor_cls
raise SkipTest(descriptor_cls.__name__ + " implements studio_view") # with only xblock children, the test property holds
@ddt.data(*flatten(CONTAINER_XMODULES))
def test_container_node_xblocks_only(self, cls_and_fields):
raise SkipTest("XBlock support in XModules not yet fully implemented")
descriptor = self.container_descriptor(descriptor_cls, 2)
assert_equal(descriptor.get_html(), descriptor.render('studio_view').content)
# Check that when a descriptor is generated from descriptor_cls class TestStudentView(XBlockWrapperTestMixin, TestCase):
# with mixed xmodule and xblock children, it generates the same html from studio_view """
# as it does using get_html This tests that student_view and XModule.get_html produce the same results.
def check_studio_view_container_node_mixed(self, descriptor_cls): """
if descriptor_cls in NOT_STUDIO_EDITABLE: def skip_if_invalid(self, descriptor_cls):
raise SkipTest(descriptor_cls.__name__ + "is not editable in studio") if descriptor_cls.module_class.student_view != XModule.student_view:
raise SkipTest(descriptor_cls.__name__ + " implements student_view")
def check_property(self, descriptor):
"""
Assert that both student_view and get_html render the same.
"""
self.assertEqual(
descriptor._xmodule.get_html(),
descriptor.render('student_view').content
)
raise SkipTest("XBlock support in XDescriptor not yet fully implemented")
# Check that when a descriptor is generated from descriptor_cls class TestStudioView(XBlockWrapperTestMixin, TestCase):
# with only xblock children, it generates the same html from studio_view """
# as it does using get_html This tests that studio_view and XModuleDescriptor.get_html produce the same results
def check_studio_view_container_node_xblocks_only(self, descriptor_cls): """
def skip_if_invalid(self, descriptor_cls):
if descriptor_cls in NOT_STUDIO_EDITABLE: if descriptor_cls in NOT_STUDIO_EDITABLE:
raise SkipTest(descriptor_cls.__name__ + "is not editable in studio") raise SkipTest(descriptor_cls.__name__ + " is not editable in studio")
raise SkipTest("XBlock support in XModules not yet fully implemented") if descriptor_cls.studio_view != XModuleDescriptor.studio_view:
raise SkipTest(descriptor_cls.__name__ + " implements studio_view")
def check_property(self, descriptor):
"""
Assert that studio_view and get_html render the same.
"""
self.assertEqual(descriptor.get_html(), descriptor.render('studio_view').content)
class TestXModuleHandler(TestXBlockWrapper):
class TestXModuleHandler(TestCase):
""" """
Tests that the xmodule_handler function correctly wraps handle_ajax Tests that the xmodule_handler function correctly wraps handle_ajax
""" """
...@@ -271,5 +313,27 @@ class TestXModuleHandler(TestXBlockWrapper): ...@@ -271,5 +313,27 @@ class TestXModuleHandler(TestXBlockWrapper):
def test_xmodule_handler_return_value(self): def test_xmodule_handler_return_value(self):
response = self.module.xmodule_handler(self.request) response = self.module.xmodule_handler(self.request)
assert_is_instance(response, webob.Response) self.assertIsInstance(response, webob.Response)
assert_equal(response.body, '{}') self.assertEqual(response.body, '{}')
class TestXmlExport(XBlockWrapperTestMixin, TestCase):
"""
This tests that XModuleDescriptor.export_to_xml and add_xml_to_node produce the same results.
"""
def skip_if_invalid(self, descriptor_cls):
if descriptor_cls.add_xml_to_node != XModuleDescriptor.add_xml_to_node:
raise SkipTest(descriptor_cls.__name__ + " implements add_xml_to_node")
def check_property(self, descriptor):
xmodule_api_fs = MemoryFS()
xblock_api_fs = MemoryFS()
descriptor.runtime.export_fs = xblock_api_fs
xblock_node = etree.Element('unknown')
descriptor.add_xml_to_node(xblock_node)
xmodule_node = etree.fromstring(descriptor.export_to_xml(xmodule_api_fs))
self.assertEquals(list(xmodule_api_fs.walk()), list(xblock_api_fs.walk()))
self.assertEquals(etree.tostring(xmodule_node), etree.tostring(xblock_node))
...@@ -222,6 +222,7 @@ class XModuleMixin(XBlockMixin): ...@@ -222,6 +222,7 @@ class XModuleMixin(XBlockMixin):
for child_loc in self.children: for child_loc in self.children:
try: try:
child = self.runtime.get_block(child_loc) child = self.runtime.get_block(child_loc)
child.runtime.export_fs = self.runtime.export_fs
except ItemNotFoundError: except ItemNotFoundError:
log.exception(u'Unable to load item {loc}, skipping'.format(loc=child_loc)) log.exception(u'Unable to load item {loc}, skipping'.format(loc=child_loc))
continue continue
...@@ -685,6 +686,21 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -685,6 +686,21 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
""" """
raise NotImplementedError('Modules must implement from_xml to be parsable from xml') raise NotImplementedError('Modules must implement from_xml to be parsable from xml')
def add_xml_to_node(self, node):
"""
Export this :class:`XModuleDescriptor` as XML, by setting attributes on the provided
`node`.
"""
xml_string = self.export_to_xml(self.runtime.export_fs)
exported_node = etree.fromstring(xml_string)
node.tag = exported_node.tag
node.text = exported_node.text
node.tail = exported_node.tail
for key, value in exported_node.items():
node.set(key, value)
node.extend(list(exported_node))
def export_to_xml(self, resource_fs): def export_to_xml(self, resource_fs):
""" """
Returns an xml string representing this module, and all modules Returns an xml string representing this module, and all modules
...@@ -926,6 +942,9 @@ class DescriptorSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable ...@@ -926,6 +942,9 @@ class DescriptorSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable
""" """
super(DescriptorSystem, self).__init__(**kwargs) super(DescriptorSystem, self).__init__(**kwargs)
# This is used by XModules to write out separate files during xml export
self.export_fs = None
self.load_item = load_item self.load_item = load_item
self.resources_fs = resources_fs self.resources_fs = resources_fs
self.error_tracker = error_tracker self.error_tracker = error_tracker
...@@ -996,6 +1015,11 @@ class DescriptorSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable ...@@ -996,6 +1015,11 @@ class DescriptorSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable
def publish(self, block, event): def publish(self, block, event):
raise NotImplementedError("edX Platform doesn't currently implement XBlock publish") raise NotImplementedError("edX Platform doesn't currently implement XBlock publish")
def add_block_as_child_node(self, block, node):
child = etree.SubElement(node, "unknown")
child.set('url_name', block.url_name)
block.add_xml_to_node(child)
class XMLParsingSystem(DescriptorSystem): class XMLParsingSystem(DescriptorSystem):
def __init__(self, process_xml, **kwargs): def __init__(self, process_xml, **kwargs):
......
...@@ -363,6 +363,10 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -363,6 +363,10 @@ class XmlDescriptor(XModuleDescriptor):
resource_fs is a pyfilesystem object (from the fs package) resource_fs is a pyfilesystem object (from the fs package)
""" """
# Set up runtime.export_fs so that it's available through future
# uses of the pure xblock add_xml_to_node api
self.runtime.export_fs = resource_fs
# Get the definition # Get the definition
xml_object = self.definition_to_xml(resource_fs) xml_object = self.definition_to_xml(resource_fs)
self.clean_metadata_from_xml(xml_object) self.clean_metadata_from_xml(xml_object)
...@@ -377,12 +381,11 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -377,12 +381,11 @@ class XmlDescriptor(XModuleDescriptor):
val = serialize_field(self._field_data.get(self, attr)) val = serialize_field(self._field_data.get(self, attr))
try: try:
xml_object.set(attr, val) xml_object.set(attr, val)
except Exception, e: except Exception:
logging.exception( logging.exception(
u'Failed to serialize metadata attribute %s with value %s in module %s. This could mean data loss!!! Exception: %s', u'Failed to serialize metadata attribute %s with value %s in module %s. This could mean data loss!!!',
attr, val, self.url_name, e attr, val, self.url_name
) )
pass
for key, value in self.xml_attributes.items(): for key, value in self.xml_attributes.items():
if key not in self.metadata_to_strip: if key not in self.metadata_to_strip:
......
<course org="edX" course="pure_xblock" url_name="2012_Fall">
<pure field1="a.field1" url_name="pure0">
<vertical url_name="vert0">
<pure field2="10" url_name="pure1"/>
<pure field2="20" url_name="pure2"/>
</vertical>
</pure>
</course>
\ No newline at end of file
import lxml.etree
import os import os
import sys import sys
import traceback import traceback
...@@ -30,9 +31,11 @@ def export(course, export_dir): ...@@ -30,9 +31,11 @@ def export(course, export_dir):
' May clobber/confuse things'.format(dir=export_dir)) ' May clobber/confuse things'.format(dir=export_dir))
try: try:
xml = course.export_to_xml(fs) course.runtime.export_fs = fs
root = lxml.etree.Element('root')
course.add_xml_to_node(root)
with fs.open('course.xml', mode='w') as f: with fs.open('course.xml', mode='w') as f:
f.write(xml) root.write(f)
return True return True
except: except:
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
-e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk -e git+https://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# Our libraries: # Our libraries:
-e git+https://github.com/edx/XBlock.git@de92d3bf798699a6bbd06b54012ef15934c41ac0#egg=XBlock -e git+https://github.com/edx/XBlock.git@3830ee50015b460fad63ff3b71f77bf1a2684195#egg=XBlock
-e git+https://github.com/edx/codejail.git@e3d98f9455#egg=codejail -e git+https://github.com/edx/codejail.git@e3d98f9455#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.2.9#egg=diff_cover -e git+https://github.com/edx/diff-cover.git@v0.2.9#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool -e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool
......
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