Teach LMS how to render XBlockAsides

[PLAT-217]
This commit is contained in:
Calen Pennington
2014-12-05 13:39:53 -05:00
parent e20fe2b879
commit d919d2ae46
47 changed files with 669 additions and 183 deletions

View File

@@ -1,9 +1,13 @@
"""
Useful django models for implementing XBlock infrastructure in django.
"""
import warnings
from django.db import models
from django.core.exceptions import ValidationError
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.keys import CourseKey, UsageKey, BlockTypeKey
from opaque_keys.edx.locator import Locator
from south.modelsinspector import add_introspection_rules
@@ -91,7 +95,6 @@ class OpaqueKeyField(models.CharField):
super(OpaqueKeyField, self).__init__(*args, **kwargs)
def to_python(self, value):
if value is self.Empty or value is None:
return None
@@ -140,22 +143,38 @@ class OpaqueKeyField(models.CharField):
class CourseKeyField(OpaqueKeyField):
"""
A django Field that stores a CourseKey object as a string.
"""
description = "A CourseKey object, saved to the DB in the form of a string"
KEY_CLASS = CourseKey
class UsageKeyField(OpaqueKeyField):
"""
A django Field that stores a UsageKey object as a string.
"""
description = "A Location object, saved to the DB in the form of a string"
KEY_CLASS = UsageKey
class LocationKeyField(UsageKeyField):
"""
A django Field that stores a UsageKey object as a string.
"""
def __init__(self, *args, **kwargs):
warnings.warn("LocationKeyField is deprecated. Please use UsageKeyField instead.", stacklevel=2)
super(LocationKeyField, self).__init__(*args, **kwargs)
class BlockTypeKeyField(OpaqueKeyField):
"""
A django Field that stores a BlockTypeKey object as a string.
"""
description = "A BlockTypeKey object, saved to the DB in the form of a string."
KEY_CLASS = BlockTypeKey
add_introspection_rules([], ["^xmodule_django\.models\.CourseKeyField"])
add_introspection_rules([], ["^xmodule_django\.models\.LocationKeyField"])
add_introspection_rules([], ["^xmodule_django\.models\.UsageKeyField"])
add_introspection_rules([], [r"^xmodule_django\.models\.CourseKeyField"])
add_introspection_rules([], [r"^xmodule_django\.models\.LocationKeyField"])
add_introspection_rules([], [r"^xmodule_django\.models\.UsageKeyField"])

View File

@@ -72,7 +72,11 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer,
data = {}
data.update(extra_data)
css_classes = ['xblock', 'xblock-{}'.format(markupsafe.escape(view))]
css_classes = [
'xblock',
'xblock-{}'.format(markupsafe.escape(view))
]
if isinstance(block, (XModule, XModuleDescriptor)):
if view in PREVIEW_VIEWS:
@@ -90,9 +94,10 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer,
data['init'] = frag.js_init_fn
data['runtime-class'] = runtime_class
data['runtime-version'] = frag.js_init_version
data['block-type'] = block.scope_ids.block_type
data['usage-id'] = usage_id_serializer(block.scope_ids.usage_id)
data['request-token'] = request_token
data['block-type'] = block.scope_ids.block_type
data['usage-id'] = usage_id_serializer(block.scope_ids.usage_id)
data['request-token'] = request_token
if block.name:
data['name'] = block.name

View File

@@ -33,7 +33,7 @@ class Date(JSONField):
result = dateutil.parser.parse(field, default=self.PREVENT_DEFAULT_DAY_MON_SEED1)
result_other = dateutil.parser.parse(field, default=self.PREVENT_DEFAULT_DAY_MON_SEED2)
if result != result_other:
log.warning("Field {0} is missing month or day".format(self._name, field))
log.warning("Field {0} is missing month or day".format(self.name))
return None
if result.tzinfo is None:
result = result.replace(tzinfo=UTC)
@@ -59,7 +59,7 @@ class Date(JSONField):
return field
else:
msg = "Field {0} has bad value '{1}'".format(
self._name, field)
self.name, field)
raise TypeError(msg)
def to_json(self, value):
@@ -199,7 +199,7 @@ class RelativeTime(JSONField):
if isinstance(value, basestring):
return self.isotime_to_timedelta(value)
msg = "RelativeTime Field {0} has bad value '{1!r}'".format(self._name, value)
msg = "RelativeTime Field {0} has bad value '{1!r}'".format(self.name, value)
raise TypeError(msg)
def to_json(self, value):

View File

@@ -50,6 +50,7 @@ from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError, ReferentialIntegrityError
from xmodule.modulestore.inheritance import InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
from xmodule.modulestore.xml import CourseLocationManager
log = logging.getLogger(__name__)
@@ -173,6 +174,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
render_template: a function for rendering templates, as per
MakoDescriptorSystem
"""
id_manager = CourseLocationManager(course_key)
kwargs.setdefault('id_reader', id_manager)
kwargs.setdefault('id_generator', id_manager)
super(CachingDescriptorSystem, self).__init__(
field_data=None,
load_item=self.load_item,

View File

@@ -1,6 +1,7 @@
import sys
import logging
from contracts import contract, new_contract
from fs.osfs import OSFS
from lazy import lazy
from xblock.runtime import KvsFieldData
from xblock.fields import ScopeIds
@@ -8,13 +9,13 @@ from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, L
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
from ..exceptions import ItemNotFoundError
from .split_mongo_kvs import SplitMongoKVS
from fs.osfs import OSFS
from .definition_lazy_loader import DefinitionLazyLoader
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import inheriting_field_data, InheritanceMixin
from xmodule.modulestore.split_mongo import BlockKey, CourseEnvelope
from xmodule.modulestore.split_mongo.id_manager import SplitMongoIdManager
from xmodule.modulestore.split_mongo.definition_lazy_loader import DefinitionLazyLoader
from xmodule.modulestore.split_mongo.split_mongo_kvs import SplitMongoKVS
log = logging.getLogger(__name__)
@@ -54,6 +55,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
root = modulestore.fs_root / course_entry.structure['_id']
root.makedirs_p() # create directory if it doesn't exist
id_manager = SplitMongoIdManager(self)
kwargs.setdefault('id_reader', id_manager)
kwargs.setdefault('id_generator', id_manager)
super(CachingDescriptorSystem, self).__init__(
field_data=None,
load_item=self._load_item,

View File

@@ -0,0 +1,32 @@
"""
An implementation of IdReader and IdGenerator that manages ids for the SplitMongo storage
mechanism.
"""
from opaque_keys.edx.locator import LocalId, DefinitionLocator
from xmodule.x_module import OpaqueKeyReader, AsideKeyGenerator
from xmodule.modulestore.split_mongo import BlockKey
# TODO: Migrate split_mongo to use this class for all key mapping/creation.
class SplitMongoIdManager(OpaqueKeyReader, AsideKeyGenerator): # pylint: disable=abstract-method
"""
An IdManager that knows how to retrieve the DefinitionLocator, given
a usage_id and a :class:`.CachingDescriptorSystem`.
"""
def __init__(self, caching_descriptor_system):
self._cds = caching_descriptor_system
def get_definition_id(self, usage_id):
if isinstance(usage_id.block_id, LocalId):
# a LocalId indicates that this block hasn't been persisted yet, and is instead stored
# in-memory in the local_modules dictionary.
return self._cds.local_modules[usage_id].scope_ids.def_id
else:
block_key = BlockKey.from_usage_key(usage_id)
module_data = self._cds.get_module_data(block_key, usage_id.course_key)
if 'definition' in module_data:
return DefinitionLocator(usage_id.block_type, module_data['definition'])
else:
raise ValueError("All non-local blocks should have a definition specified")

View File

@@ -203,5 +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):
result = library.render(AUTHOR_VIEW, context)
with patch('xmodule.x_module.descriptor_global_get_asides', lambda block: []):
result = library.render(AUTHOR_VIEW, context)
self.assertIn(message, result.content)

View File

@@ -420,7 +420,7 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
assert_equals(len(course_locations), 1)
assert_in(SlashSeparatedCourseKey('edX', 'simple', '2012_Fall'), course_locations)
@Plugin.register_temp_plugin(ReferenceTestXBlock, 'ref_test')
@XBlock.register_temp_plugin(ReferenceTestXBlock, 'ref_test')
def test_reference_converters(self):
"""
Test that references types get deserialized correctly

View File

@@ -18,7 +18,7 @@ from contextlib import contextmanager
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import make_error_tracker, exc_info_to_str
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XMLParsingSystem, policy_key
from xmodule.x_module import XMLParsingSystem, policy_key, OpaqueKeyReader, AsideKeyGenerator
from xmodule.modulestore.xml_exporter import DEFAULT_CONTENT_FIELDS
from xmodule.modulestore import ModuleStoreEnum, ModuleStoreReadBase
from xmodule.tabs import CourseTabList
@@ -27,7 +27,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from opaque_keys.edx.locator import CourseLocator
from xblock.field_data import DictFieldData
from xblock.runtime import DictKeyValueStore, IdGenerator
from xblock.runtime import DictKeyValueStore
from .exceptions import ItemNotFoundError
@@ -64,7 +64,6 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
"""
self.unnamed = defaultdict(int) # category -> num of new url_names for that category
self.used_names = defaultdict(set) # category -> set of used url_names
id_generator = CourseLocationGenerator(course_id)
# cdodge: adding the course_id as passed in for later reference rather than having to recomine the org/course/url_name
self.course_id = course_id
@@ -175,7 +174,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
descriptor = create_block_from_xml(
etree.tostring(xml_data, encoding='unicode'),
self,
id_generator,
id_manager,
)
except Exception as err: # pylint: disable=broad-except
if not self.load_error_modules:
@@ -201,7 +200,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
descriptor = ErrorDescriptor.from_xml(
xml,
self,
id_generator,
id_manager,
err_msg
)
@@ -229,12 +228,16 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
resources_fs = OSFS(xmlstore.data_dir / course_dir)
id_manager = CourseLocationManager(course_id)
super(ImportSystem, self).__init__(
load_item=load_item,
resources_fs=resources_fs,
render_template=render_template,
error_tracker=error_tracker,
process_xml=process_xml,
id_generator=id_manager,
id_reader=id_manager,
**kwargs
)
@@ -245,12 +248,13 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
block.children.append(child_block.scope_ids.usage_id)
class CourseLocationGenerator(IdGenerator):
class CourseLocationManager(OpaqueKeyReader, AsideKeyGenerator):
"""
IdGenerator for Location-based definition ids and usage ids
based within a course
"""
def __init__(self, course_id):
super(CourseLocationManager, self).__init__()
self.course_id = course_id
self.autogen_ids = itertools.count(0)
@@ -263,6 +267,17 @@ class CourseLocationGenerator(IdGenerator):
slug = 'autogen_{}_{}'.format(block_type, self.autogen_ids.next())
return self.course_id.make_usage_key(block_type, slug)
def get_definition_id(self, usage_id):
"""Retrieve the definition that a usage is derived from.
Args:
usage_id: The id of the usage to query
Returns:
The `definition_id` the usage is derived from
"""
return usage_id
def _make_usage_key(course_key, value):
"""

View File

@@ -28,6 +28,7 @@ from xmodule.mako_module import MakoDescriptorSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.mongo.draft import DraftModuleStore
from xmodule.modulestore.xml import CourseLocationManager
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES, ModuleStoreDraftAndPublished
@@ -51,9 +52,16 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method
"""
ModuleSystem for testing
"""
def __init__(self, **kwargs):
id_manager = CourseLocationManager(kwargs['course_id'])
kwargs.setdefault('id_reader', id_manager)
kwargs.setdefault('id_generator', id_manager)
kwargs.setdefault('services', {}).setdefault('field-data', DictFieldData({}))
super(TestModuleSystem, self).__init__(**kwargs)
def handler_url(self, block, handler, suffix='', query='', thirdparty=False):
return '{usage_id}/{handler}{suffix}?{query}'.format(
usage_id=block.scope_ids.usage_id.to_deprecated_string(),
usage_id=unicode(block.scope_ids.usage_id),
handler=handler,
suffix=suffix,
query=query,
@@ -61,10 +69,14 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method
def local_resource_url(self, block, uri):
return 'resource/{usage_id}/{uri}'.format(
usage_id=block.scope_ids.usage_id.to_deprecated_string(),
usage_id=unicode(block.scope_ids.usage_id),
uri=uri,
)
# Disable XBlockAsides in most tests
def get_asides(self, block):
return []
def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')):
"""
@@ -113,13 +125,16 @@ def get_test_descriptor_system():
"""
Construct a test DescriptorSystem instance.
"""
field_data = DictFieldData({})
return MakoDescriptorSystem(
load_item=Mock(),
resources_fs=Mock(),
error_tracker=Mock(),
render_template=mock_render_template,
mixins=(InheritanceMixin, XModuleMixin),
field_data=DictFieldData({}),
field_data=field_data,
services={'field-data': field_data},
)
@@ -149,13 +164,8 @@ class LogicTest(unittest.TestCase):
raw_field_data = {}
def setUp(self):
class EmptyClass:
"""Empty object."""
url_name = ''
category = 'test'
self.system = get_test_system()
self.descriptor = EmptyClass()
self.descriptor = Mock(name="descriptor", url_name='', category='test')
self.xmodule_class = self.descriptor_class.module_class
usage_key = self.system.course_id.make_usage_key(self.descriptor.category, 'test_loc')

View File

@@ -8,7 +8,7 @@ from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xmodule.error_module import NonStaffErrorDescriptor
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore, CourseLocationGenerator
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore, CourseLocationManager
from xmodule.conditional_module import ConditionalDescriptor
from xmodule.tests import DATA_DIR, get_test_system, get_test_descriptor_system
from xmodule.x_module import STUDENT_VIEW
@@ -60,7 +60,7 @@ class ConditionalFactory(object):
source_descriptor = NonStaffErrorDescriptor.from_xml(
'some random xml data',
system,
id_generator=CourseLocationGenerator(SlashSeparatedCourseKey('edX', 'conditional_test', 'test_run')),
id_generator=CourseLocationManager(source_location.course_key),
error_msg='random error message'
)
else:

View File

@@ -4,7 +4,7 @@ Tests for ErrorModule and NonStaffErrorModule
import unittest
from xmodule.tests import get_test_system
from xmodule.error_module import ErrorDescriptor, ErrorModule, NonStaffErrorDescriptor
from xmodule.modulestore.xml import CourseLocationGenerator
from xmodule.modulestore.xml import CourseLocationManager
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW
from mock import MagicMock, Mock, patch
@@ -34,7 +34,7 @@ class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = ErrorDescriptor.from_xml(
self.valid_xml,
self.system,
CourseLocationGenerator(self.course_id),
CourseLocationManager(self.course_id),
self.error_msg
)
self.assertIsInstance(descriptor, ErrorDescriptor)
@@ -69,7 +69,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = NonStaffErrorDescriptor.from_xml(
self.valid_xml,
self.system,
CourseLocationGenerator(self.course_id)
CourseLocationManager(self.course_id)
)
self.assertIsInstance(descriptor, NonStaffErrorDescriptor)
@@ -77,7 +77,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = NonStaffErrorDescriptor.from_xml(
self.valid_xml,
self.system,
CourseLocationGenerator(self.course_id)
CourseLocationManager(self.course_id)
)
descriptor.xmodule_runtime = self.system
context_repr = self.system.render(descriptor, STUDENT_VIEW).content

View File

@@ -7,7 +7,7 @@ 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, CourseLocationGenerator
from xmodule.modulestore.xml import create_block_from_xml, CourseLocationManager
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xblock.runtime import KvsFieldData, DictKeyValueStore
@@ -43,7 +43,7 @@ class InMemorySystem(XMLParsingSystem, MakoDescriptorSystem): # pylint: disable
descriptor = create_block_from_xml(
xml,
self,
CourseLocationGenerator(self.course_id),
CourseLocationManager(self.course_id),
)
self._descriptors[descriptor.location.to_deprecated_string()] = descriptor
return descriptor

View File

@@ -18,12 +18,13 @@ from webob.multidict import MultiDict
from xblock.core import XBlock
from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String, Dict
from xblock.fragment import Fragment
from xblock.runtime import Runtime, IdReader
from xblock.runtime import Runtime, IdReader, IdGenerator
from xmodule.fields import RelativeTime
from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore.exceptions import ItemNotFoundError
from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.asides import AsideUsageKeyV1, AsideDefinitionKeyV1
from xmodule.exceptions import UndefinedContext
import dogstats_wrapper as dog_stats_api
@@ -65,7 +66,7 @@ class OpaqueKeyReader(IdReader):
Returns:
The `definition_id` the usage is derived from
"""
return usage_id.definition_key
raise NotImplementedError("Specific Modulestores must implement get_definition_id")
def get_block_type(self, def_id):
"""Retrieve the block_type of a particular definition
@@ -78,6 +79,91 @@ class OpaqueKeyReader(IdReader):
"""
return def_id.block_type
def get_usage_id_from_aside(self, aside_id):
"""
Retrieve the XBlock `usage_id` associated with this aside usage id.
Args:
aside_id: The usage id of the XBlockAside.
Returns:
The `usage_id` of the usage the aside is commenting on.
"""
return aside_id.usage_key
def get_definition_id_from_aside(self, aside_id):
"""
Retrieve the XBlock `definition_id` associated with this aside definition id.
Args:
aside_id: The usage id of the XBlockAside.
Returns:
The `definition_id` of the usage the aside is commenting on.
"""
return aside_id.definition_key
def get_aside_type_from_usage(self, aside_id):
"""
Retrieve the XBlockAside `aside_type` associated with this aside
usage id.
Args:
aside_id: The usage id of the XBlockAside.
Returns:
The `aside_type` of the aside.
"""
return aside_id.aside_type
def get_aside_type_from_definition(self, aside_id):
"""
Retrieve the XBlockAside `aside_type` associated with this aside
definition id.
Args:
aside_id: The definition id of the XBlockAside.
Returns:
The `aside_type` of the aside.
"""
return aside_id.aside_type
class AsideKeyGenerator(IdGenerator): # pylint: disable=abstract-method
"""
An :class:`.IdGenerator` that only provides facilities for constructing new XBlockAsides.
"""
def create_aside(self, definition_id, usage_id, aside_type):
"""
Make a new aside definition and usage ids, indicating an :class:`.XBlockAside` of type `aside_type`
commenting on an :class:`.XBlock` usage `usage_id`
Returns:
(aside_definition_id, aside_usage_id)
"""
def_key = AsideDefinitionKeyV1(definition_id, aside_type)
usage_key = AsideUsageKeyV1(usage_id, aside_type)
return (def_key, usage_key)
def create_usage(self, def_id):
"""Make a usage, storing its definition id.
Returns the newly-created usage id.
"""
raise NotImplementedError("Specific Modulestores must provide implementations of create_usage")
def create_definition(self, block_type, slug=None):
"""Make a definition, storing its block type.
If `slug` is provided, it is a suggestion that the definition id
incorporate the slug somehow.
Returns the newly-created definition id.
"""
raise NotImplementedError("Specific Modulestores must provide implementations of create_definition")
def dummy_track(_event_type, _event):
pass
@@ -160,6 +246,8 @@ class XModuleMixin(XBlockMixin):
Adding this Mixin to an :class:`XBlock` allows it to cooperate with old-style :class:`XModules`
"""
entry_point = "xmodule.v1"
# Attributes for inspection of the descriptor
# This indicates whether the xmodule is a problem-type.
@@ -526,6 +614,7 @@ class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-me
field_data: A dictionary-like object that maps field names to values
for those fields.
"""
# Set the descriptor first so that we can proxy to it
self.descriptor = descriptor
super(XModule, self).__init__(*args, **kwargs)
@@ -715,7 +804,6 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
create a problem, and can generate XModules (which do know about student
state).
"""
entry_point = "xmodule.v1"
module_class = XModule
# VS[compat]. Backwards compatibility code that can go away after
@@ -997,7 +1085,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
class ConfigurableFragmentWrapper(object): # pylint: disable=abstract-method
"""
Runtime mixin that allows for composition of many `wrap_child` wrappers
Runtime mixin that allows for composition of many `wrap_xblock` wrappers
"""
def __init__(self, wrappers=None, **kwargs):
"""
@@ -1013,7 +1101,7 @@ class ConfigurableFragmentWrapper(object): # pylint: disable=abstract-method
else:
self.wrappers = []
def wrap_child(self, block, view, frag, context):
def wrap_xblock(self, block, view, frag, context):
"""
See :func:`Runtime.wrap_child`
"""
@@ -1043,6 +1131,16 @@ 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
"""
See :meth:`xblock.runtime.Runtime.get_asides`.
"""
raise NotImplementedError("Applications must monkey-patch this function before using get_asides from a DescriptorSystem.")
class MetricsMixin(object):
"""
Mixin for adding metric logging for render and handle methods in the DescriptorSystem and ModuleSystem.
@@ -1137,7 +1235,9 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p
local_resource_url: an implementation of :meth:`xblock.runtime.Runtime.local_resource_url`
"""
super(DescriptorSystem, self).__init__(id_reader=OpaqueKeyReader(), **kwargs)
kwargs.setdefault('id_reader', OpaqueKeyReader())
kwargs.setdefault('id_generator', AsideKeyGenerator())
super(DescriptorSystem, self).__init__(**kwargs)
# This is used by XModules to write out separate files during xml export
self.export_fs = None
@@ -1215,6 +1315,19 @@ 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):
"""
See :meth:`xblock.runtime.Runtime:get_asides` for documentation.
"""
if getattr(block, 'xmodule_runtime', None) is not None:
return block.xmodule_runtime.get_asides(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)
def resource_url(self, resource):
"""
See :meth:`xblock.runtime.Runtime:resource_url` for documentation.
@@ -1335,7 +1448,9 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin
# Usage_store is unused, and field_data is often supplanted with an
# explicit field_data during construct_xblock.
super(ModuleSystem, self).__init__(id_reader=OpaqueKeyReader(), field_data=field_data, **kwargs)
kwargs.setdefault('id_reader', getattr(descriptor_runtime, 'id_reader', OpaqueKeyReader()))
kwargs.setdefault('id_generator', getattr(descriptor_runtime, 'id_generator', AsideKeyGenerator()))
super(ModuleSystem, self).__init__(field_data=field_data, **kwargs)
self.STATIC_URL = static_url
self.xqueue = xqueue
@@ -1373,6 +1488,9 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin
self.descriptor_runtime = descriptor_runtime
self.rebind_noauth_module_to_user = rebind_noauth_module_to_user
if user:
self.user_id = user.id
def get(self, attr):
""" provide uniform access to attributes (like etree)."""
return self.__dict__.get(attr)

View File

@@ -35,7 +35,7 @@ describe "XBlock", ->
window.initFnZ = jasmine.createSpy()
@fakeChildren = ['list', 'of', 'children']
spyOn(XBlock, 'initializeBlocks').andReturn(@fakeChildren)
spyOn(XBlock, 'initializeXBlocks').andReturn(@fakeChildren)
@vANode = $('#vA')[0]
@vZNode = $('#vZ')[0]
@@ -50,8 +50,8 @@ describe "XBlock", ->
expect(TestRuntime.vZ).toHaveBeenCalledWith()
it "loads the right init function", ->
expect(window.initFnA).toHaveBeenCalledWith(@runtimeA, @vANode)
expect(window.initFnZ).toHaveBeenCalledWith(@runtimeZ, @vZNode)
expect(window.initFnA).toHaveBeenCalledWith(@runtimeA, @vANode, {})
expect(window.initFnZ).toHaveBeenCalledWith(@runtimeZ, @vZNode, {})
it "loads when missing versions", ->
expect(@missingVersionBlock.element).toBe($('#missing-version'))
@@ -74,8 +74,8 @@ describe "XBlock", ->
expect(@missingInitBlock.element).toBe($('#missing-init')[0])
it "passes through the request token", ->
expect(XBlock.initializeBlocks).toHaveBeenCalledWith($(@vANode), 'req-token-a')
expect(XBlock.initializeBlocks).toHaveBeenCalledWith($(@vZNode), 'req-token-z')
expect(XBlock.initializeXBlocks).toHaveBeenCalledWith($(@vANode), 'req-token-a')
expect(XBlock.initializeXBlocks).toHaveBeenCalledWith($(@vZNode), 'req-token-z')
describe "initializeBlocks", ->

View File

@@ -1,57 +0,0 @@
@XBlock =
Runtime: {}
###
Initialize the javascript for a single xblock element, and for all of it's
xblock children that match requestToken. If requestToken is omitted, use the
data-request-token attribute from element, or use the request-tokens specified on
the children themselves.
###
initializeBlock: (element, requestToken) ->
$element = $(element)
requestToken = requestToken or $element.data('request-token')
children = @initializeBlocks($element, requestToken)
runtime = $element.data("runtime-class")
version = $element.data("runtime-version")
initFnName = $element.data("init")
$element.prop('xblock_children', children)
if runtime? and version? and initFnName?
runtime = new window[runtime]["v#{version}"]
initFn = window[initFnName]
if initFn.length > 2
initargs = $(".xblock_json_init_args", element)
if initargs.length == 0
console.log("Warning: XBlock expects data parameters")
data = JSON.parse(initargs.text())
block = initFn(runtime, element, data) ? {}
else
block = initFn(runtime, element) ? {}
block.runtime = runtime
else
elementTag = $('<div>').append($element.clone()).html();
console.log("Block #{elementTag} is missing data-runtime, data-runtime-version or data-init, and can't be initialized")
block = {}
block.element = element
block.name = $element.data("name")
block.type = $element.data("block-type")
$element.trigger("xblock-initialized")
$element.data("initialized", true)
$element.addClass("xblock-initialized")
block
###
Initialize all XBlocks inside element that were rendered with requestToken.
If requestToken is omitted, and element has a 'data-request-token' attribute, use that.
If neither is available, then use the request tokens of the immediateDescendent xblocks.
###
initializeBlocks: (element, requestToken) ->
requestToken = requestToken or $(element).data('request-token')
if requestToken
selector = ".xblock[data-request-token='#{requestToken}']"
else
selector = ".xblock"
$(element).immediateDescendents(selector).map((idx, elem) =>
@initializeBlock(elem, requestToken)
).toArray()

View File

@@ -0,0 +1,142 @@
(function($, JSON) {
'use strict';
function initializeBlockLikes(block_class, initializer, element, requestToken) {
var requestToken = requestToken || $(element).data('request-token');
if (requestToken) {
var selector = '.' + block_class + '[data-request-token="' + requestToken + '"]';
} else {
var selector = '.' + block_class;
}
return $(element).immediateDescendents(selector).map(function(idx, elem) {
return initializer(elem, requestToken);
}).toArray();
}
function elementRuntime(element) {
var $element = $(element);
var runtime = $element.data('runtime-class');
var version = $element.data('runtime-version');
var initFnName = $element.data('init');
if (runtime && version && initFnName) {
return new window[runtime]['v' + version];
} else {
if (!runtime || !version || !initFnName) {
var elementTag = $('<div>').append($element.clone()).html();
console.log('Block ' + elementTag + ' is missing data-runtime, data-runtime-version or data-init, and can\'t be initialized');
}
return null;
}
}
function initArgs(element) {
var initargs = $('.xblock_json_init_args', element).text();
return initargs ? JSON.parse(initargs) : {};
}
/**
* Construct an XBlock family object from an element. The constructor
* function is loaded from the 'data-init' attribute of the element.
* The constructor is called with the arguments 'runtime', 'element',
* and then all of 'block_args'.
*/
function constructBlock(element, block_args) {
var block;
var $element = $(element);
var runtime = elementRuntime(element);
block_args.unshift(element);
block_args.unshift(runtime);
if (runtime) {
block = (function() {
var initFn = window[$element.data('init')];
// This create a new constructor that can then apply() the block_args
// to the initFn.
function Block() {
return initFn.apply(this, block_args);
}
Block.prototype = initFn.prototype;
return new Block();
})();
block.runtime = runtime;
} else {
block = {};
}
block.element = element;
block.name = $element.data('name');
block.type = $element.data('block-type');
$element.trigger('xblock-initialized');
$element.data('initialized', true);
$element.addClass('xblock-initialized');
return block;
}
var XBlock = {
Runtime: {},
/**
* Initialize the javascript for a single xblock element, and for all of it's
* xblock children that match requestToken. If requestToken is omitted, use the
* data-request-token attribute from element, or use the request-tokens specified on
* the children themselves.
*/
initializeBlock: function(element, requestToken) {
var $element = $(element);
var requestToken = requestToken || $element.data('request-token');
var children = XBlock.initializeXBlocks($element, requestToken);
$element.prop('xblock_children', children);
return constructBlock(element, [initArgs(element)]);
},
/**
* Initialize the javascript for a single xblock aside element that matches requestToken.
* If requestToken is omitted, use the data-request-token attribute from element, or use
* the request-tokens specified on the children themselves.
*/
initializeAside: function(element, requestToken) {
var blockUsageId = $(element).data('block-id');
var blockElement = $(element).siblings('[data-usage-id="' + blockUsageId + '"]')[0];
return constructBlock(element, [blockElement, initArgs(element)]);
},
/**
* Initialize all XBlocks inside element that were rendered with requestToken.
* If requestToken is omitted, and element has a 'data-request-token' attribute, use that.
* If neither is available, then use the request tokens of the immediateDescendent xblocks.
*/
initializeXBlocks: function(element, requestToken) {
return initializeBlockLikes('xblock', XBlock.initializeBlock, element, requestToken);
},
/**
* Initialize all XBlockAsides inside element that were rendered with requestToken.
* If requestToken is omitted, and element has a 'data-request-token' attribute, use that.
* If neither is available, then use the request tokens of the immediateDescendent xblocks.
*/
initializeXBlockAsides: function(element, requestToken) {
return initializeBlockLikes('xblock_asides-v1', XBlock.initializeAside, element, requestToken);
},
/**
* Initialize all XBlock-family blocks inside element that were rendered with requestToken.
* If requestToken is omitted, and element has a 'data-request-token' attribute, use that.
* If neither is available, then use the request tokens of the immediateDescendent xblocks.
*/
initializeBlocks: function(element, requestToken) {
XBlock.initializeXBlockAsides(element, requestToken);
return XBlock.initializeXBlocks(element, requestToken);
}
};
this.XBlock = XBlock;
}).call(this, $, JSON);

View File

@@ -45,6 +45,7 @@ lib_paths:
# Paths to source JavaScript files
src_paths:
- js/xblock
- coffee/src
- js/src
- js/utils

View File

@@ -51,7 +51,7 @@ class ContainerPage(PageObject):
num_wrappers = len(self.q(css='{} [data-request-token="{}"]'.format(XBlockWrapper.BODY_SELECTOR, request_token)).results)
# Wait until all components have been loaded and marked as either initialized or failed.
# See:
# - common/static/coffee/src/xblock/core.coffee which adds the class "xblock-initialized"
# - common/static/js/xblock/core.js which adds the class "xblock-initialized"
# at the end of initializeBlock.
# - common/static/js/views/xblock.js which adds the class "xblock-initialization-failed"
# if the xblock threw an error while initializing.

View File

@@ -3,7 +3,7 @@
End-to-end tests for the LMS.
"""
from unittest import skip
from unittest import expectedFailure
from ..helpers import UniqueCourseTest
from ...pages.lms.auto_auth import AutoAuthPage
@@ -44,17 +44,6 @@ class XBlockAcidBase(UniqueCourseTest):
self.assertTrue(acid_block.scope_passed('preferences'))
self.assertTrue(acid_block.scope_passed('user_info'))
def test_acid_block(self):
"""
Verify that all expected acid block tests pass in the lms.
"""
self.course_info_page.visit()
self.tab_nav.go_to_tab('Courseware')
acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
self.validate_acid_block_view(acid_block)
class XBlockAcidNoChildTest(XBlockAcidBase):
"""
@@ -81,7 +70,15 @@ class XBlockAcidNoChildTest(XBlockAcidBase):
).install()
def test_acid_block(self):
super(XBlockAcidNoChildTest, self).test_acid_block()
"""
Verify that all expected acid block tests pass in the lms.
"""
self.course_info_page.visit()
self.tab_nav.go_to_tab('Courseware')
acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
self.validate_acid_block_view(acid_block)
class XBlockAcidChildTest(XBlockAcidBase):
@@ -129,3 +126,46 @@ class XBlockAcidChildTest(XBlockAcidBase):
acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
self.validate_acid_block_view(acid_block)
class XBlockAcidAsideTest(XBlockAcidBase):
"""
Tests of an AcidBlock with children
"""
__test__ = True
def setup_fixtures(self):
course_fix = CourseFixture(
self.course_info['org'],
self.course_info['number'],
self.course_info['run'],
self.course_info['display_name']
)
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
XBlockFixtureDesc('acid', 'Acid Block')
)
)
)
).install()
@expectedFailure
def test_acid_block(self):
"""
Verify that all expected acid block tests pass in the lms.
"""
self.course_info_page.visit()
self.tab_nav.go_to_tab('Courseware')
acid_aside = AcidView(self.browser, '.xblock_asides-v1-student_view[data-block-type=acid_aside]')
self.validate_acid_aside_view(acid_aside)
acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]')
self.validate_acid_block_view(acid_block)
def validate_acid_aside_view(self, acid_aside):
self.validate_acid_block_view(acid_aside)