diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index ffbcd68d18..6de67226df 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -45,7 +45,7 @@ from contentstore.views.helpers import is_unit, xblock_studio_url, xblock_primar from contentstore.views.preview import get_preview_fragment from edxmako.shortcuts import render_to_string from models.settings.course_grading import CourseGradingModel -from cms.lib.xblock.runtime import handler_url, local_resource_url, get_asides +from cms.lib.xblock.runtime import handler_url, local_resource_url, applicable_aside_types from opaque_keys.edx.keys import UsageKey, CourseKey __all__ = ['orphan_handler', 'xblock_handler', 'xblock_view_handler', 'xblock_outline_handler'] @@ -64,7 +64,7 @@ ALWAYS = lambda x: True # TODO: Remove this code when Runtimes are no longer created by modulestores xmodule.x_module.descriptor_global_handler_url = handler_url xmodule.x_module.descriptor_global_local_resource_url = local_resource_url -xmodule.x_module.descriptor_global_get_asides = get_asides +xmodule.x_module.descriptor_global_applicable_aside_types = applicable_aside_types def hash_resource(resource): diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index a67b6019b6..4dc7e07422 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -95,7 +95,7 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method def local_resource_url(self, block, uri): return local_resource_url(block, uri) - def get_asides(self, block): + def applicable_aside_types(self, block): # TODO: Implement this to enable XBlockAsides on previews in Studio return [] diff --git a/cms/lib/xblock/runtime.py b/cms/lib/xblock/runtime.py index bd2d533ad1..5048f422b3 100644 --- a/cms/lib/xblock/runtime.py +++ b/cms/lib/xblock/runtime.py @@ -35,12 +35,9 @@ def local_resource_url(block, uri): }) -def get_asides(block): # pylint: disable=unused-argument +def applicable_aside_types(block): # pylint: disable=unused-argument """ - Return all of the asides which might be decorating this `block`. - - Arguments: - block (:class:`.XBlock`): The block to render retrieve asides for. + Get the application-relative list of aside types for this type of block. """ # TODO: Implement this method to make XBlockAsides for editing views in Studio return [] diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index ef14ddfe28..7a81430918 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -136,7 +136,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # snippets that will be included in the middle of pages. @classmethod - def load_definition(cls, xml_object, system, location): + def load_definition(cls, xml_object, system, location, id_generator): '''Load a descriptor from the specified xml_object: If there is a filename attribute, load it as a string, and @@ -145,6 +145,12 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): If there is not a filename attribute, the definition is the body of the xml_object, without the root tag (do not want in the middle of a page) + + Args: + xml_object: an lxml.etree._Element containing the definition to load + system: the modulestore system or runtime which caches data + location: the usage id for the block--used to compute the filename if none in the xml_object + id_generator: used by other impls of this method to generate the usage_id ''' filename = xml_object.get('filename') if filename is None: diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/base.py b/common/lib/xmodule/xmodule/modulestore/mongo/base.py index c6c0387d93..04123f0579 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo/base.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo/base.py @@ -1275,8 +1275,10 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo """ jsonfields = {} for field_name, field in xblock.fields.iteritems(): - if (field.scope == scope and field.is_set_on(xblock)): - if isinstance(field, Reference): + if field.scope == scope and field.is_set_on(xblock): + if field.scope == Scope.parent: + continue + elif isinstance(field, Reference): jsonfields[field_name] = unicode(field.read_from(xblock)) elif isinstance(field, ReferenceList): jsonfields[field_name] = [ diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_asides.py b/common/lib/xmodule/xmodule/modulestore/tests/test_asides.py new file mode 100644 index 0000000000..5aa86cb59b --- /dev/null +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_asides.py @@ -0,0 +1,52 @@ +""" +Tests for Asides +""" +from xblock.core import XBlockAside +from xblock.fields import Scope, String +from xblock.fragment import Fragment +from unittest import TestCase +from xmodule.modulestore.tests.test_cross_modulestore_import_export import XmlModulestoreBuilder +from mock import patch + + +class AsideTestType(XBlockAside): + """ + Test Aside type + """ + FRAG_CONTENT = u"

Aside rendered

" + + content = String(default="default_content", scope=Scope.content) + data_field = String(default="default_data", scope=Scope.settings) + + @XBlockAside.aside_for('student_view') + def student_view_aside(self, block, context): # pylint: disable=unused-argument + """Add to the student view""" + return Fragment(self.FRAG_CONTENT) + + +class TestAsidesXmlStore(TestCase): + """ + Test Asides sourced from xml store + """ + @patch('xmodule.x_module.descriptor_global_applicable_aside_types', lambda block: ['test_aside']) + @XBlockAside.register_temp_plugin(AsideTestType, 'test_aside') + def test_xml_aside(self): + """ + Check that the xml modulestore read in all the asides with their values + """ + with XmlModulestoreBuilder().build(course_ids=['edX/aside_test/2012_Fall']) as store: + 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, "{} aside data".format(category)) + self.assertEqual(asides[0].content, "{} Aside".format(category.capitalize())) + + for child in block.get_children(): + check_block(child) + + check_block(store.get_course(store.make_course_key('edX', "aside_test", "2012_Fall"))) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_libraries.py b/common/lib/xmodule/xmodule/modulestore/tests/test_libraries.py index 1447b02384..0e61e325be 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_libraries.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_libraries.py @@ -203,6 +203,6 @@ class TestLibraries(MixedSplitTestCase): message = u"Hello world" hello_render = lambda _, context: Fragment(message) with patch('xmodule.html_module.HtmlDescriptor.author_view', hello_render, create=True): - with patch('xmodule.x_module.descriptor_global_get_asides', lambda block: []): + with patch('xmodule.x_module.descriptor_global_applicable_aside_types', lambda block: []): result = library.render(AUTHOR_VIEW, context) self.assertIn(message, result.content) diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 0007d57574..a836597c00 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -22,7 +22,6 @@ from xmodule.x_module import XMLParsingSystem, policy_key, OpaqueKeyReader, Asid from xmodule.modulestore.xml_exporter import DEFAULT_CONTENT_FIELDS from xmodule.modulestore import ModuleStoreEnum, ModuleStoreReadBase from xmodule.tabs import CourseTabList -from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location from opaque_keys.edx.locator import CourseLocator @@ -33,7 +32,7 @@ from xblock.runtime import DictKeyValueStore from .exceptions import ItemNotFoundError from .inheritance import compute_inherited_metadata, inheriting_field_data -from xblock.fields import ScopeIds, Reference, ReferenceList, ReferenceValueDict +from xblock.fields import ScopeIds edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False, remove_comments=True, remove_blank_text=True) @@ -171,9 +170,9 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): make_name_unique(xml_data) - descriptor = create_block_from_xml( - etree.tostring(xml_data, encoding='unicode'), - self, + descriptor = self.xblock_from_node( + xml_data, + None, # parent_id id_manager, ) except Exception as err: # pylint: disable=broad-except @@ -279,71 +278,6 @@ class CourseLocationManager(OpaqueKeyReader, AsideKeyGenerator): return usage_id -def _make_usage_key(course_key, value): - """ - Makes value into a UsageKey inside the specified course. - If value is already a UsageKey, returns that. - """ - if isinstance(value, UsageKey): - return value - return course_key.make_usage_key_from_deprecated_string(value) - - -def _convert_reference_fields_to_keys(xblock): # pylint: disable=invalid-name - """ - Find all fields of type reference and convert the payload into UsageKeys - """ - course_key = xblock.scope_ids.usage_id.course_key - - for field in xblock.fields.itervalues(): - if field.is_set_on(xblock): - field_value = getattr(xblock, field.name) - if isinstance(field, Reference): - setattr(xblock, field.name, _make_usage_key(course_key, field_value)) - elif isinstance(field, ReferenceList): - setattr(xblock, field.name, [_make_usage_key(course_key, ele) for ele in field_value]) - elif isinstance(field, ReferenceValueDict): - for key, subvalue in field_value.iteritems(): - assert isinstance(subvalue, basestring) - field_value[key] = _make_usage_key(course_key, subvalue) - setattr(xblock, field.name, field_value) - - -def create_block_from_xml(xml_data, system, id_generator): - """ - Create an XBlock instance from XML data. - - Args: - xml_data (string): A string containing valid xml. - system (XMLParsingSystem): The :class:`.XMLParsingSystem` used to connect the block - to the outside world. - id_generator (IdGenerator): An :class:`~xblock.runtime.IdGenerator` that - will be used to construct the usage_id and definition_id for the block. - - Returns: - XBlock: The fully instantiated :class:`~xblock.core.XBlock`. - - """ - node = etree.fromstring(xml_data) - raw_class = system.load_block_type(node.tag) - xblock_class = system.mixologist.mix(raw_class) - - # leave next line commented out - useful for low-level debugging - # log.debug('[create_block_from_xml] tag=%s, class=%s' % (node.tag, xblock_class)) - - block_type = node.tag - url_name = node.get('url_name') - def_id = id_generator.create_definition(block_type, url_name) - usage_id = id_generator.create_usage(def_id) - - scope_ids = ScopeIds(None, block_type, def_id, usage_id) - xblock = xblock_class.parse_xml(node, system, scope_ids, id_generator) - - _convert_reference_fields_to_keys(xblock) - - return xblock - - class ParentTracker(object): """A simple class to factor out the logic for tracking location parent pointers.""" def __init__(self): diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 39a61bd342..4b946e4bb5 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -480,6 +480,8 @@ def _import_module_and_update_references( fields = {} for field_name, field in module.fields.iteritems(): if field.is_set_on(module): + if field.scope == Scope.parent: + continue if isinstance(field, Reference): fields[field_name] = _convert_reference_fields_to_new_namespace(field.read_from(module)) elif isinstance(field, ReferenceList): diff --git a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py index 647a92f27c..8bc094f66d 100644 --- a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py +++ b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py @@ -262,7 +262,7 @@ 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, + You can create an actual test case by inheriting from this class and UnitTest, and implement skip_if_invalid and check_property. """ @@ -289,6 +289,8 @@ class XBlockWrapperTestMixin(object): mocked_course = Mock() modulestore = Mock() modulestore.get_course.return_value = mocked_course + # pylint: disable=no-member + descriptor.runtime.id_reader.get_definition_id = Mock(return_value='a') descriptor.runtime.modulestore = modulestore self.check_property(descriptor) @@ -299,6 +301,8 @@ class XBlockWrapperTestMixin(object): descriptor_cls, fields = cls_and_fields self.skip_if_invalid(descriptor_cls) descriptor = ContainerModuleFactory(descriptor_cls=descriptor_cls, depth=2, **fields) + # pylint: disable=no-member + descriptor.runtime.id_reader.get_definition_id = Mock(return_value='a') self.check_property(descriptor) # Test that when an xmodule is generated from descriptor_cls @@ -347,7 +351,9 @@ class TestStudioView(XBlockWrapperTestMixin, TestCase): """ Assert that studio_view and get_html render the same. """ - self.assertEqual(descriptor.get_html(), descriptor.render(STUDIO_VIEW).content) + html = descriptor.get_html() + rendered_content = descriptor.render(STUDIO_VIEW).content + self.assertEqual(html, rendered_content) class TestXModuleHandler(TestCase): diff --git a/common/lib/xmodule/xmodule/tests/xml/__init__.py b/common/lib/xmodule/xmodule/tests/xml/__init__.py index 534d3d3d4c..c6ec34785c 100644 --- a/common/lib/xmodule/xmodule/tests/xml/__init__.py +++ b/common/lib/xmodule/xmodule/tests/xml/__init__.py @@ -2,12 +2,13 @@ Xml parsing tests for XModules """ import pprint +from lxml import etree from mock import Mock from unittest import TestCase from xmodule.x_module import XMLParsingSystem, policy_key from xmodule.mako_module import MakoDescriptorSystem -from xmodule.modulestore.xml import create_block_from_xml, CourseLocationManager +from xmodule.modulestore.xml import CourseLocationManager from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location from xblock.runtime import KvsFieldData, DictKeyValueStore @@ -40,9 +41,9 @@ class InMemorySystem(XMLParsingSystem, MakoDescriptorSystem): # pylint: disable def process_xml(self, xml): # pylint: disable=method-hidden """Parse `xml` as an XBlock, and add it to `self._descriptors`""" - descriptor = create_block_from_xml( - xml, - self, + descriptor = self.xblock_from_node( + etree.fromstring(xml), + None, CourseLocationManager(self.course_id), ) self._descriptors[descriptor.location.to_deprecated_string()] = descriptor diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index a9b093590d..b7f7e10fe5 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -18,7 +18,6 @@ Examples of html5 videos for manual testing: import copy import json import logging -import os.path from collections import OrderedDict from operator import itemgetter @@ -30,14 +29,13 @@ from django.conf import settings from xblock.fields import ScopeIds from xblock.runtime import KvsFieldData -from xmodule.exceptions import NotFoundError from xmodule.modulestore.inheritance import InheritanceKeyValueStore, own_metadata from xmodule.x_module import XModule, module_attr from xmodule.editing_module import TabsEditingDescriptor from xmodule.raw_module import EmptyDataRawDescriptor from xmodule.xml_module import is_pointer_tag, name_to_pathname, deserialize_field -from .transcripts_utils import Transcript, VideoTranscriptsMixin +from .transcripts_utils import VideoTranscriptsMixin from .video_utils import create_youtube_string, get_video_from_cdn from .video_xfields import VideoFields from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers @@ -300,7 +298,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler super(VideoDescriptor, self).__init__(*args, **kwargs) # For backwards compatibility -- if we've got XML data, parse it out and set the metadata fields if self.data: - field_data = self._parse_video_xml(self.data) + field_data = self._parse_video_xml(etree.fromstring(self.data)) self._field_data.set_many(self, field_data) del self.data @@ -406,8 +404,9 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler usage_id = id_generator.create_usage(definition_id) if is_pointer_tag(xml_object): filepath = cls._format_filepath(xml_object.tag, name_to_pathname(url_name)) - xml_data = etree.tostring(cls.load_file(filepath, system.resources_fs, usage_id)) - field_data = cls._parse_video_xml(xml_data) + xml_object = cls.load_file(filepath, system.resources_fs, usage_id) + system.parse_asides(xml_object, definition_id, usage_id, id_generator) + field_data = cls._parse_video_xml(xml_object) kvs = InheritanceKeyValueStore(initial_values=field_data) field_data = KvsFieldData(kvs) video = system.construct_xblock_from_class( @@ -543,12 +542,11 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler return ret @classmethod - def _parse_video_xml(cls, xml_data): + def _parse_video_xml(cls, xml): """ Parse video fields out of xml_data. The fields are set if they are present in the XML. """ - xml = etree.fromstring(xml_data) field_data = {} # Convert between key types for certain attributes -- diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 5b96721153..e660211fd5 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -15,8 +15,9 @@ from pkg_resources import ( from webob import Response from webob.multidict import MultiDict -from xblock.core import XBlock -from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String, Dict +from xblock.core import XBlock, XBlockAside +from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String, Dict, ScopeIds, Reference, \ + ReferenceList, ReferenceValueDict from xblock.fragment import Fragment from xblock.runtime import Runtime, IdReader, IdGenerator from xmodule.fields import RelativeTime @@ -852,6 +853,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): """ Interpret the parsed XML in `node`, creating an XModuleDescriptor. """ + # It'd be great to not reserialize and deserialize the xml xml = etree.tostring(node) block = cls.from_xml(xml, runtime, id_generator) return block @@ -1131,14 +1133,13 @@ def descriptor_global_local_resource_url(block, uri): # pylint: disable=invalid raise NotImplementedError("Applications must monkey-patch this function before using local_resource_url for studio_view") -# This function exists to give applications (LMS/CMS) a place to monkey-patch until -# we can refactor modulestore to split out the FieldData half of its interface from -# the Runtime part of its interface. This function matches the Runtime.get_asides interface -def descriptor_global_get_asides(block): # pylint: disable=unused-argument +# pylint: disable=invalid-name +def descriptor_global_applicable_aside_types(block): # pylint: disable=unused-argument """ - See :meth:`xblock.runtime.Runtime.get_asides`. + See :meth:`xblock.runtime.Runtime.applicable_aside_types`. """ - raise NotImplementedError("Applications must monkey-patch this function before using get_asides from a DescriptorSystem.") + raise NotImplementedError("Applications must monkey-patch this function before using applicable_aside_types" + " from a DescriptorSystem.") class MetricsMixin(object): @@ -1315,18 +1316,18 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p # global function that the application can override. return descriptor_global_local_resource_url(block, uri) - def get_asides(self, block): + def applicable_aside_types(self, block): """ - See :meth:`xblock.runtime.Runtime:get_asides` for documentation. + See :meth:`xblock.runtime.Runtime:applicable_aside_types` for documentation. """ if getattr(block, 'xmodule_runtime', None) is not None: - return block.xmodule_runtime.get_asides(block) + return block.xmodule_runtime.applicable_aside_types(block) else: # Currently, Modulestore is responsible for instantiating DescriptorSystems # This means that LMS/CMS don't have a way to define a subclass of DescriptorSystem # that implements the correct get_asides. So, for now, instead, we will reference a # global function that the application can override. - return descriptor_global_get_asides(block) + return descriptor_global_applicable_aside_types(block) def resource_url(self, resource): """ @@ -1357,6 +1358,103 @@ class XMLParsingSystem(DescriptorSystem): super(XMLParsingSystem, self).__init__(**kwargs) self.process_xml = process_xml + def _usage_id_from_node(self, node, parent_id, id_generator=None): + """Create a new usage id from an XML dom node. + + Args: + node (lxml.etree.Element): The DOM node to interpret. + parent_id: The usage ID of the parent block + id_generator (IdGenerator): The :class:`.IdGenerator` to use + for creating ids + Returns: + UsageKey: the usage key for the new xblock + """ + return self.xblock_from_node(node, parent_id, id_generator).scope_ids.usage_id + + def xblock_from_node(self, node, parent_id, id_generator=None): + """ + Create an XBlock instance from XML data. + + Args: + xml_data (string): A string containing valid xml. + system (XMLParsingSystem): The :class:`.XMLParsingSystem` used to connect the block + to the outside world. + id_generator (IdGenerator): An :class:`~xblock.runtime.IdGenerator` that + will be used to construct the usage_id and definition_id for the block. + + Returns: + XBlock: The fully instantiated :class:`~xblock.core.XBlock`. + + """ + id_generator = id_generator or self.id_generator + # leave next line commented out - useful for low-level debugging + # log.debug('[_usage_id_from_node] tag=%s, class=%s' % (node.tag, xblock_class)) + + block_type = node.tag + # remove xblock-family from elements + node.attrib.pop('xblock-family', None) + + url_name = node.get('url_name') # difference from XBlock.runtime + def_id = id_generator.create_definition(block_type, url_name) + usage_id = id_generator.create_usage(def_id) + + keys = ScopeIds(None, block_type, def_id, usage_id) + block_class = self.mixologist.mix(self.load_block_type(block_type)) + + self.parse_asides(node, def_id, usage_id, id_generator) + + block = block_class.parse_xml(node, self, keys, id_generator) + self._convert_reference_fields_to_keys(block) # difference from XBlock.runtime + block.parent = parent_id + block.save() + + return block + + def parse_asides(self, node, def_id, usage_id, id_generator): + """pull the asides out of the xml payload and instantiate them""" + aside_children = [] + for child in node.iterchildren(): + # get xblock-family from node + xblock_family = child.attrib.pop('xblock-family', None) + if xblock_family: + xblock_family = self._family_id_to_superclass(xblock_family) + if issubclass(xblock_family, XBlockAside): + aside_children.append(child) + # now process them & remove them from the xml payload + for child in aside_children: + self._aside_from_xml(child, def_id, usage_id, id_generator) + node.remove(child) + + def _make_usage_key(self, course_key, value): + """ + Makes value into a UsageKey inside the specified course. + If value is already a UsageKey, returns that. + """ + if isinstance(value, UsageKey): + return value + return course_key.make_usage_key_from_deprecated_string(value) + + def _convert_reference_fields_to_keys(self, xblock): # pylint: disable=invalid-name + """ + Find all fields of type reference and convert the payload into UsageKeys + """ + course_key = xblock.scope_ids.usage_id.course_key + + for field in xblock.fields.itervalues(): + if field.is_set_on(xblock): + field_value = getattr(xblock, field.name) + if field_value is None: + continue + elif isinstance(field, Reference): + setattr(xblock, field.name, self._make_usage_key(course_key, field_value)) + elif isinstance(field, ReferenceList): + setattr(xblock, field.name, [self._make_usage_key(course_key, ele) for ele in field_value]) + elif isinstance(field, ReferenceValueDict): + for key, subvalue in field_value.iteritems(): + assert isinstance(subvalue, basestring) + field_value[key] = self._make_usage_key(course_key, subvalue) + setattr(xblock, field.name, field_value) + class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylint: disable=abstract-method """ diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index dc9a3931da..1f7973c188 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -181,10 +181,18 @@ class XmlDescriptor(XModuleDescriptor): raise Exception, msg, sys.exc_info()[2] @classmethod - def load_definition(cls, xml_object, system, def_id): - '''Load a descriptor definition from the specified xml_object. + def load_definition(cls, xml_object, system, def_id, id_generator): + ''' + Load a descriptor definition from the specified xml_object. Subclasses should not need to override this except in special - cases (e.g. html module)''' + cases (e.g. html module) + + Args: + xml_object: an lxml.etree._Element containing the definition to load + system: the modulestore system (aka, runtime) which accesses data and provides access to services + def_id: the definition id for the block--used to compute the usage id and asides ids + id_generator: used to generate the usage_id + ''' # VS[compat] -- the filename attr should go away once everything is # converted. (note: make sure html files still work once this goes away) @@ -208,6 +216,8 @@ class XmlDescriptor(XModuleDescriptor): break definition_xml = cls.load_file(filepath, system.resources_fs, def_id) + usage_id = id_generator.create_usage(def_id) + system.parse_asides(definition_xml, def_id, usage_id, id_generator) # Add the attributes from the pointer node definition_xml.attrib.update(xml_object.attrib) @@ -281,11 +291,12 @@ class XmlDescriptor(XModuleDescriptor): # read the actual definition file--named using url_name.replace(':','/') filepath = cls._format_filepath(xml_object.tag, name_to_pathname(url_name)) definition_xml = cls.load_file(filepath, system.resources_fs, def_id) + system.parse_asides(definition_xml, def_id, usage_id, id_generator) else: definition_xml = xml_object filepath = None - definition, children = cls.load_definition(definition_xml, system, def_id) # note this removes metadata + definition, children = cls.load_definition(definition_xml, system, def_id, id_generator) # note this removes metadata # VS[compat] -- make Ike's github preview links work in both old and # new file layouts diff --git a/common/test/data/aside/README.md b/common/test/data/aside/README.md new file mode 100644 index 0000000000..59ab392ed3 --- /dev/null +++ b/common/test/data/aside/README.md @@ -0,0 +1 @@ +This is a very very simple course, useful for initial debugging of processing code. diff --git a/common/test/data/aside/course.xml b/common/test/data/aside/course.xml new file mode 100644 index 0000000000..f04994e343 --- /dev/null +++ b/common/test/data/aside/course.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/common/test/data/aside/course/2012_Fall.xml b/common/test/data/aside/course/2012_Fall.xml new file mode 100644 index 0000000000..e8534abf4d --- /dev/null +++ b/common/test/data/aside/course/2012_Fall.xml @@ -0,0 +1,13 @@ + + Course Aside + + + Chapter Aside + + Sequential Aside + + Html Aside + + + + diff --git a/common/test/data/aside/html/toyhtml.html b/common/test/data/aside/html/toyhtml.html new file mode 100644 index 0000000000..85fa34d71d --- /dev/null +++ b/common/test/data/aside/html/toyhtml.html @@ -0,0 +1 @@ +Sample \ No newline at end of file diff --git a/common/test/data/aside/html/toyhtml.xml b/common/test/data/aside/html/toyhtml.xml new file mode 100644 index 0000000000..321253e441 --- /dev/null +++ b/common/test/data/aside/html/toyhtml.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/common/test/data/aside/info/handouts.html b/common/test/data/aside/info/handouts.html new file mode 100644 index 0000000000..85fa34d71d --- /dev/null +++ b/common/test/data/aside/info/handouts.html @@ -0,0 +1 @@ +Sample \ No newline at end of file diff --git a/common/test/data/aside/policies/2012_Fall.json b/common/test/data/aside/policies/2012_Fall.json new file mode 100644 index 0000000000..c3ba2f869e --- /dev/null +++ b/common/test/data/aside/policies/2012_Fall.json @@ -0,0 +1,23 @@ +{ + "course/2012_Fall": { + "graceperiod": "2 days 5 hours 59 minutes 59 seconds", + "start": "2015-07-17T12:00", + "display_name": "Toy Course", + "graded": "true", + }, + "tabs": [ + {"type": "courseware"}, + {"type": "course_info", "name": "Course Info"}, + {"type": "static_tab", "url_slug": "syllabus", "name": "Syllabus"}, + {"type": "static_tab", "url_slug": "resources", "name": "Resources"}, + {"type": "discussion", "name": "Discussion"}, + {"type": "wiki", "name": "Wiki"}, + {"type": "progress", "name": "Progress"} + ], + "chapter/Overview": { + "display_name": "Overview" + }, + "html/secret:toylab": { + "display_name": "Toy lab" + }, +} diff --git a/common/test/data/aside/policies/assets.json b/common/test/data/aside/policies/assets.json new file mode 100644 index 0000000000..c051b44f81 --- /dev/null +++ b/common/test/data/aside/policies/assets.json @@ -0,0 +1,10 @@ +{ + "textbook.pdf":{ + "contentType":"text/pdf", + "displayname":"textbook.pdf", + "locked":false, + "filename":"/c4x/edx/toy/asset/textbook.pdf", + "import_path":null, + "thumbnail_location":null + } +} diff --git a/common/test/data/aside/static/another_static.txt b/common/test/data/aside/static/another_static.txt new file mode 100644 index 0000000000..83e560736d --- /dev/null +++ b/common/test/data/aside/static/another_static.txt @@ -0,0 +1,17 @@ + + _ + | | + _____ ____ _ _ __ ___ _ __ | | ___ + / _ \ \/ / _` | '_ ` _ \| '_ \| |/ _ \ +| __/> < (_| | | | | | | |_) | | __/ + \___/_/\_\__,_|_| |_| |_| .__/|_|\___| + | | + |_| + _ _ _ + | | | | (_) + ___| |_ __ _| |_ _ ___ + / __| __/ _` | __| |/ __| + \__ \ || (_| | |_| | (__ + |___/\__\__,_|\__|_|\___| + + diff --git a/common/test/data/aside/static/handouts/sample_handout.txt b/common/test/data/aside/static/handouts/sample_handout.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/test/data/aside/static/just_a_test.jpg b/common/test/data/aside/static/just_a_test.jpg new file mode 100644 index 0000000000..6bb7f377a0 Binary files /dev/null and b/common/test/data/aside/static/just_a_test.jpg differ diff --git a/common/test/data/aside/static/sample_static.txt b/common/test/data/aside/static/sample_static.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/test/data/aside/static/textbook.pdf b/common/test/data/aside/static/textbook.pdf new file mode 100644 index 0000000000..e6e7a031ce Binary files /dev/null and b/common/test/data/aside/static/textbook.pdf differ diff --git a/common/test/data/aside/tabs/resources.html b/common/test/data/aside/tabs/resources.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/test/data/aside/tabs/syllabus.html b/common/test/data/aside/tabs/syllabus.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/test/data/toy/html/secret/toylab.html b/common/test/data/toy/html/secret/toylab.html index b2a4599cc6..760482c4a0 100644 --- a/common/test/data/toy/html/secret/toylab.html +++ b/common/test/data/toy/html/secret/toylab.html @@ -1,6 +1,5 @@ Lab 2A: Superposition Experiment -<<<<<<< Updated upstream

Isn't the toy course great?

Let's add some markup that uses non-ascii characters. @@ -8,6 +7,3 @@ For example, we should be able to write words like encyclopædia, or foreig Looking beyond latin-1, we should handle math symbols: πr² ≤ ∞. And it shouldn't matter if we use entities or numeric codes — Ω ≠ π ≡ Ω ≠ π.

-======= -

Isn't the toy course great? — ≤

->>>>>>> Stashed changes diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 492558d274..6499886398 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -324,9 +324,9 @@ def _section_data_download(course, access): return section_data -def null_get_asides(block): # pylint: disable=unused-argument +def null_applicable_aside_types(block): # pylint: disable=unused-argument """ - get_aside method for monkey-patching into descriptor_global_get_asides + get_aside method for monkey-patching into descriptor_global_applicable_aside_types while rendering an HtmlDescriptor for email text editing. This returns an empty list. """ @@ -337,8 +337,8 @@ def _section_send_email(course, access): """ Provide data for the corresponding bulk email section """ course_key = course.id - # Monkey-patch descriptor_global_get_asides to return no asides for the duration of this render - with patch('xmodule.x_module.descriptor_global_get_asides', null_get_asides): + # Monkey-patch descriptor_global_applicable_aside_types to return no asides for the duration of this render + with patch('xmodule.x_module.descriptor_global_applicable_aside_types', null_applicable_aside_types): # This HtmlDescriptor is only being used to generate a nice text editor. html_module = HtmlDescriptor( course.system, diff --git a/lms/djangoapps/lms_xblock/runtime.py b/lms/djangoapps/lms_xblock/runtime.py index 1cad66d8d5..c319370e99 100644 --- a/lms/djangoapps/lms_xblock/runtime.py +++ b/lms/djangoapps/lms_xblock/runtime.py @@ -226,7 +226,7 @@ class LmsModuleSystem(LmsHandlerUrls, ModuleSystem): # pylint: disable=abstract extra_data, ) - def get_asides(self, block): + def applicable_aside_types(self, block): """ Return all of the asides which might be decorating this `block`. @@ -242,8 +242,4 @@ class LmsModuleSystem(LmsHandlerUrls, ModuleSystem): # pylint: disable=abstract if block.scope_ids.block_type in config.disabled_blocks.split(): return [] - return [ - self.get_aside_of_type(block, aside_type) - for aside_type, __ - in XBlockAside.load_classes() - ] + return super(LmsModuleSystem, self).applicable_aside_types()