Make XmlModuleStore handle asides
PLAT-221
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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 []
|
||||
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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 <html> 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:
|
||||
|
||||
@@ -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] = [
|
||||
|
||||
52
common/lib/xmodule/xmodule/modulestore/tests/test_asides.py
Normal file
52
common/lib/xmodule/xmodule/modulestore/tests/test_asides.py
Normal file
@@ -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"<p>Aside rendered</p>"
|
||||
|
||||
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")))
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 --
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
1
common/test/data/aside/README.md
Normal file
1
common/test/data/aside/README.md
Normal file
@@ -0,0 +1 @@
|
||||
This is a very very simple course, useful for initial debugging of processing code.
|
||||
1
common/test/data/aside/course.xml
Normal file
1
common/test/data/aside/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="aside_test" url_name="2012_Fall"/>
|
||||
13
common/test/data/aside/course/2012_Fall.xml
Normal file
13
common/test/data/aside/course/2012_Fall.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<course course_image="just_a_test.jpg">
|
||||
<test_aside xblock-family='xblock_asides.v1' data_field='course aside data'>Course Aside</test_aside>
|
||||
<textbook title="Textbook" book_url="https://s3.amazonaws.com/edx-textbooks/guttag_computation_v3/"/>
|
||||
<chapter url_name="Overview">
|
||||
<test_aside xblock-family='xblock_asides.v1' data_field='chapter aside data'>Chapter Aside</test_aside>
|
||||
<sequential url_name="Toy_Videos">
|
||||
<test_aside xblock-family='xblock_asides.v1' data_field='sequential aside data'>Sequential Aside</test_aside>
|
||||
<html url_name="toyhtml">
|
||||
<test_aside xblock-family='xblock_asides.v1' data_field='html aside data'>Html Aside</test_aside>
|
||||
</html>
|
||||
</sequential>
|
||||
</chapter>
|
||||
</course>
|
||||
1
common/test/data/aside/html/toyhtml.html
Normal file
1
common/test/data/aside/html/toyhtml.html
Normal file
@@ -0,0 +1 @@
|
||||
<a href='/static/handouts/sample_handout.txt'>Sample</a>
|
||||
1
common/test/data/aside/html/toyhtml.xml
Normal file
1
common/test/data/aside/html/toyhtml.xml
Normal file
@@ -0,0 +1 @@
|
||||
<html filename="toyhtml.html"/>
|
||||
1
common/test/data/aside/info/handouts.html
Normal file
1
common/test/data/aside/info/handouts.html
Normal file
@@ -0,0 +1 @@
|
||||
<a href='/static/handouts/sample_handout.txt'>Sample</a>
|
||||
23
common/test/data/aside/policies/2012_Fall.json
Normal file
23
common/test/data/aside/policies/2012_Fall.json
Normal file
@@ -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"
|
||||
},
|
||||
}
|
||||
10
common/test/data/aside/policies/assets.json
Normal file
10
common/test/data/aside/policies/assets.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
17
common/test/data/aside/static/another_static.txt
Normal file
17
common/test/data/aside/static/another_static.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
_
|
||||
| |
|
||||
_____ ____ _ _ __ ___ _ __ | | ___
|
||||
/ _ \ \/ / _` | '_ ` _ \| '_ \| |/ _ \
|
||||
| __/> < (_| | | | | | | |_) | | __/
|
||||
\___/_/\_\__,_|_| |_| |_| .__/|_|\___|
|
||||
| |
|
||||
|_|
|
||||
_ _ _
|
||||
| | | | (_)
|
||||
___| |_ __ _| |_ _ ___
|
||||
/ __| __/ _` | __| |/ __|
|
||||
\__ \ || (_| | |_| | (__
|
||||
|___/\__\__,_|\__|_|\___|
|
||||
|
||||
|
||||
BIN
common/test/data/aside/static/just_a_test.jpg
Normal file
BIN
common/test/data/aside/static/just_a_test.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 547 B |
0
common/test/data/aside/static/sample_static.txt
Normal file
0
common/test/data/aside/static/sample_static.txt
Normal file
BIN
common/test/data/aside/static/textbook.pdf
Normal file
BIN
common/test/data/aside/static/textbook.pdf
Normal file
Binary file not shown.
0
common/test/data/aside/tabs/resources.html
Normal file
0
common/test/data/aside/tabs/resources.html
Normal file
0
common/test/data/aside/tabs/syllabus.html
Normal file
0
common/test/data/aside/tabs/syllabus.html
Normal file
@@ -1,6 +1,5 @@
|
||||
<b>Lab 2A: Superposition Experiment</b>
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
<p>Isn't the toy course great?</p>
|
||||
|
||||
<p>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 — Ω ≠ π ≡ Ω ≠ π.
|
||||
</p>
|
||||
=======
|
||||
<p>Isn't the toy course great? — ≤</p>
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user