Difficulty selectbox in Studio (based on new XBlockAside functionality). Include:

- adaptation asides to be imported from the XML
- updating SplitMongo to handle XBlockAsides (CRUD operations)
- updating Studio to handle XBlockAsides handler calls
- updating xblock/core.js to properly init XBlockAsides JavaScript
This commit is contained in:
Dmitry Viskov
2016-02-15 22:38:09 +03:00
parent d481768571
commit 209ddc700d
23 changed files with 1135 additions and 110 deletions

View File

@@ -428,7 +428,8 @@ class BlockData(object):
'block_type': self.block_type,
'definition': self.definition,
'defaults': self.defaults,
'edit_info': self.edit_info.to_storable()
'asides': self.get_asides(),
'edit_info': self.edit_info.to_storable(),
}
def from_storable(self, block_data):
@@ -449,9 +450,21 @@ class BlockData(object):
# blocks are copied from a library to a course)
self.defaults = block_data.get('defaults', {})
# Additional field data that stored in connected XBlockAsides
self.asides = block_data.get('asides', {})
# EditInfo object containing all versioning/editing data.
self.edit_info = EditInfo(**block_data.get('edit_info', {}))
def get_asides(self):
"""
For the situations if block_data has no asides attribute
(in case it was taken from memcache)
"""
if not hasattr(self, 'asides'):
self.asides = {} # pylint: disable=attribute-defined-outside-init
return self.asides
def __repr__(self):
# pylint: disable=bad-continuation, redundant-keyword-arg
return ("{classname}(fields={self.fields}, "
@@ -459,17 +472,19 @@ class BlockData(object):
"definition={self.definition}, "
"definition_loaded={self.definition_loaded}, "
"defaults={self.defaults}, "
"asides={asides}, "
"edit_info={self.edit_info})").format(
self=self,
classname=self.__class__.__name__,
asides=self.get_asides()
) # pylint: disable=bad-continuation
def __eq__(self, block_data):
"""
Two BlockData objects are equal iff all their attributes are equal.
"""
attrs = ['fields', 'block_type', 'definition', 'defaults', 'edit_info']
return all(getattr(self, attr) == getattr(block_data, attr) for attr in attrs)
attrs = ['fields', 'block_type', 'definition', 'defaults', 'asides', 'edit_info']
return all(getattr(self, attr, None) == getattr(block_data, attr, None) for attr in attrs)
def __neq__(self, block_data):
"""

View File

@@ -95,6 +95,40 @@ def strip_key(func):
return inner
def prepare_asides(func):
"""
A decorator to handle optional asides param
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""
Supported kwargs:
asides - list with connected asides data for the passed block
"""
if 'asides' in kwargs:
kwargs['asides'] = prepare_asides_to_store(kwargs['asides'])
return func(*args, **kwargs)
return wrapper
def prepare_asides_to_store(asides_source):
"""
Convert Asides Xblocks objects to the list of dicts (to store this information in MongoDB)
"""
asides = None
if asides_source:
asides = []
for asd in asides_source:
aside_fields = {}
for asd_field_key, asd_field_val in asd.fields.iteritems():
aside_fields[asd_field_key] = asd_field_val.read_from(asd)
asides.append({
'aside_type': asd.scope_ids.block_type,
'fields': aside_fields
})
return asides
class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
"""
ModuleStore knows how to route requests to the right persistence ms
@@ -687,6 +721,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
))
@strip_key
@prepare_asides
def create_item(self, user_id, course_key, block_type, block_id=None, fields=None, **kwargs):
"""
Creates and saves a new item in a course.
@@ -707,6 +742,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
return modulestore.create_item(user_id, course_key, block_type, block_id=block_id, fields=fields, **kwargs)
@strip_key
@prepare_asides
def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, **kwargs):
"""
Creates and saves a new xblock that is a child of the specified block
@@ -727,6 +763,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
return modulestore.create_child(user_id, parent_usage_key, block_type, block_id=block_id, fields=fields, **kwargs)
@strip_key
@prepare_asides
def import_xblock(self, user_id, course_key, block_type, block_id, fields=None, runtime=None, **kwargs):
"""
See :py:meth `ModuleStoreDraftAndPublished.import_xblock`
@@ -734,7 +771,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
Defer to the course's modulestore if it supports this method
"""
store = self._verify_modulestore_support(course_key, 'import_xblock')
return store.import_xblock(user_id, course_key, block_type, block_id, fields, runtime)
return store.import_xblock(user_id, course_key, block_type, block_id, fields, runtime, **kwargs)
@strip_key
def copy_from_template(self, source_keys, dest_key, user_id, **kwargs):
@@ -745,6 +782,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
return store.copy_from_template(source_keys, dest_key, user_id)
@strip_key
@prepare_asides
def update_item(self, xblock, user_id, allow_not_found=False, **kwargs):
"""
Update the xblock persisted to be the same as the given for all types of fields

View File

@@ -3,7 +3,7 @@ import logging
from contracts import contract, new_contract
from fs.osfs import OSFS
from lazy import lazy
from xblock.runtime import KvsFieldData
from xblock.runtime import KvsFieldData, KeyValueStore
from xblock.fields import ScopeIds
from xblock.core import XBlock
from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, LibraryLocator, DefinitionLocator
@@ -19,6 +19,7 @@ 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
from xmodule.x_module import XModuleMixin
log = logging.getLogger(__name__)
@@ -209,12 +210,26 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
parent = course_key.make_usage_key(parent_key.type, parent_key.id)
else:
parent = None
aside_fields = None
# for the situation if block_data has no asides attribute
# (in case it was taken from memcache)
try:
if block_data.asides:
aside_fields = {block_key.type: {}}
for aside in block_data.asides:
aside_fields[block_key.type].update(aside['fields'])
except AttributeError:
pass
try:
kvs = SplitMongoKVS(
definition_loader,
converted_fields,
converted_defaults,
parent=parent,
aside_fields=aside_fields,
field_decorator=kwargs.get('field_decorator')
)
@@ -338,3 +353,30 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
block_data.edit_info._subtree_edited_on = max_date
block_data.edit_info._subtree_edited_by = max_date_by
def get_aside_of_type(self, block, aside_type):
"""
See `runtime.Runtime.get_aside_of_type`
This override adds the field data from the block to the aside
"""
asides_cached = block.get_asides() if isinstance(block, XModuleMixin) else None
if asides_cached:
for aside in asides_cached:
if aside.scope_ids.block_type == aside_type:
return aside
new_aside = super(CachingDescriptorSystem, self).get_aside_of_type(block, aside_type)
new_aside._field_data = block._field_data # pylint: disable=protected-access
for key, _ in new_aside.fields.iteritems():
if isinstance(key, KeyValueStore.Key) and block._field_data.has(new_aside, key): # pylint: disable=protected-access
try:
value = block._field_data.get(new_aside, key) # pylint: disable=protected-access
except KeyError:
pass
else:
setattr(new_aside, key, value)
block.add_aside(new_aside)
return new_aside

View File

@@ -1604,11 +1604,8 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
serial += 1
@contract(returns='XBlock')
def create_item(
self, user_id, course_key, block_type, block_id=None,
definition_locator=None, fields=None,
force=False, **kwargs
):
def create_item(self, user_id, course_key, block_type, block_id=None, definition_locator=None, fields=None,
asides=None, force=False, **kwargs):
"""
Add a descriptor to persistence as an element
of the course. Return the resulting post saved version with populated locators.
@@ -1695,6 +1692,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
block_fields,
definition_locator.definition_id,
new_id,
asides=asides
))
self.update_structure(course_key, new_structure)
@@ -1723,7 +1721,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
# reconstruct the new_item from the cache
return self.get_item(item_loc)
def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, **kwargs):
def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, asides=None, **kwargs):
"""
Creates and saves a new xblock that as a child of the specified block
@@ -1738,10 +1736,12 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
a new identifier will be generated
fields (dict): A dictionary specifying initial values for some or all fields
in the newly created block
asides (dict): A dictionary specifying initial values for some or all aside fields
in the newly created block
"""
with self.bulk_operations(parent_usage_key.course_key):
xblock = self.create_item(
user_id, parent_usage_key.course_key, block_type, block_id=block_id, fields=fields,
user_id, parent_usage_key.course_key, block_type, block_id=block_id, fields=fields, asides=asides,
**kwargs)
# skip attach to parent if xblock has 'detached' tag
@@ -1986,10 +1986,8 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
partitioned_fields, descriptor.definition_locator, allow_not_found, force, **kwargs
) or descriptor
def _update_item_from_fields(
self, user_id, course_key, block_key, partitioned_fields,
definition_locator, allow_not_found, force, **kwargs
):
def _update_item_from_fields(self, user_id, course_key, block_key, partitioned_fields, # pylint: disable=too-many-statements
definition_locator, allow_not_found, force, asides=None, **kwargs):
"""
Broke out guts of update_item for short-circuited internal use only
"""
@@ -1999,7 +1997,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
for subfields in partitioned_fields.itervalues():
fields.update(subfields)
return self.create_item(
user_id, course_key, block_key.type, fields=fields, force=force
user_id, course_key, block_key.type, fields=fields, asides=asides, force=force
)
original_structure = self._lookup_course(course_key).structure
@@ -2011,9 +2009,8 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
fields = {}
for subfields in partitioned_fields.itervalues():
fields.update(subfields)
return self.create_item(
user_id, course_key, block_key.type, block_id=block_key.id, fields=fields, force=force,
)
return self.create_item(user_id, course_key, block_key.type, block_id=block_key.id, fields=fields,
asides=asides, force=force)
else:
raise ItemNotFoundError(course_key.make_usage_key(block_key.type, block_key.id))
@@ -2039,14 +2036,24 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
if is_updated:
settings['children'] = serialized_children
asides_data_to_update = None
if asides:
asides_data_to_update, asides_updated = self._get_asides_to_update_from_structure(original_structure,
block_key, asides)
else:
asides_updated = False
# if updated, rev the structure
if is_updated:
if is_updated or asides_updated:
new_structure = self.version_structure(course_key, original_structure, user_id)
block_data = self._get_block_from_structure(new_structure, block_key)
block_data.definition = definition_locator.definition_id
block_data.fields = settings
if asides_updated:
block_data.asides = asides_data_to_update
new_id = new_structure['_id']
# source_version records which revision a block was copied from. In this method, we're updating
@@ -3215,7 +3222,8 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
self._delete_if_true_orphan(BlockKey(*child), structure)
@contract(returns=BlockData)
def _new_block(self, user_id, category, block_fields, definition_id, new_id, raw=False, block_defaults=None):
def _new_block(self, user_id, category, block_fields, definition_id, new_id, raw=False,
asides=None, block_defaults=None):
"""
Create the core document structure for a block.
@@ -3226,10 +3234,13 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
"""
if not raw:
block_fields = self._serialize_fields(category, block_fields)
if not asides:
asides = []
document = {
'block_type': category,
'definition': definition_id,
'fields': block_fields,
'asides': asides,
'edit_info': {
'edited_on': datetime.datetime.now(UTC),
'edited_by': user_id,
@@ -3249,6 +3260,38 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
"""
return structure['blocks'].get(block_key)
@contract(block_key=BlockKey)
def _get_asides_to_update_from_structure(self, structure, block_key, asides):
"""
Get list of aside fields that should be updated/inserted
"""
block = self._get_block_from_structure(structure, block_key)
if asides:
updated = False
tmp_new_asides_data = {}
for asd in asides:
aside_type = asd['aside_type']
tmp_new_asides_data[aside_type] = asd
result_list = []
for i, aside in enumerate(block.asides):
if aside['aside_type'] in tmp_new_asides_data:
result_list.append(tmp_new_asides_data.pop(aside['aside_type']))
updated = True
else:
result_list.append(aside)
if tmp_new_asides_data:
for _, asd in tmp_new_asides_data.iteritems():
result_list.append(asd)
updated = True
return result_list, updated
else:
return block.asides, False
@contract(block_key=BlockKey, content=BlockData)
def _update_block_in_structure(self, structure, block_key, content):
"""

View File

@@ -137,7 +137,7 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
keys_to_check.extend(children)
return new_keys
def update_item(self, descriptor, user_id, allow_not_found=False, force=False, **kwargs):
def update_item(self, descriptor, user_id, allow_not_found=False, force=False, asides=None, **kwargs):
old_descriptor_locn = descriptor.location
descriptor.location = self._map_revision_to_branch(old_descriptor_locn)
emit_signals = descriptor.location.branch == ModuleStoreEnum.BranchName.published \
@@ -149,17 +149,15 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
user_id,
allow_not_found=allow_not_found,
force=force,
asides=asides,
**kwargs
)
self._auto_publish_no_children(item.location, item.location.category, user_id, **kwargs)
descriptor.location = old_descriptor_locn
return item
def create_item(
self, user_id, course_key, block_type, block_id=None,
definition_locator=None, fields=None,
force=False, skip_auto_publish=False, **kwargs
):
def create_item(self, user_id, course_key, block_type, block_id=None, # pylint: disable=too-many-statements
definition_locator=None, fields=None, asides=None, force=False, skip_auto_publish=False, **kwargs):
"""
See :py:meth `ModuleStoreDraftAndPublished.create_item`
"""
@@ -169,7 +167,7 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
with self.bulk_operations(course_key, emit_signals=emit_signals):
item = super(DraftVersioningModuleStore, self).create_item(
user_id, course_key, block_type, block_id=block_id,
definition_locator=definition_locator, fields=fields,
definition_locator=definition_locator, fields=fields, asides=asides,
force=force, **kwargs
)
if not skip_auto_publish:
@@ -178,13 +176,13 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
def create_child(
self, user_id, parent_usage_key, block_type, block_id=None,
fields=None, **kwargs
fields=None, asides=None, **kwargs
):
parent_usage_key = self._map_revision_to_branch(parent_usage_key)
with self.bulk_operations(parent_usage_key.course_key):
item = super(DraftVersioningModuleStore, self).create_child(
user_id, parent_usage_key, block_type, block_id=block_id,
fields=fields, **kwargs
fields=fields, asides=asides, **kwargs
)
# Publish both the child and the parent, if the child is a direct-only category
self._auto_publish_no_children(item.location, item.location.category, user_id, **kwargs)
@@ -552,14 +550,16 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
draft_course = course_key.for_branch(ModuleStoreEnum.BranchName.draft)
with self.branch_setting(ModuleStoreEnum.Branch.draft_preferred, draft_course):
# Importing the block and publishing the block links the draft & published blocks' version history.
draft_block = self.import_xblock(user_id, draft_course, block_type, block_id, fields, runtime)
draft_block = self.import_xblock(user_id, draft_course, block_type, block_id, fields,
runtime, **kwargs)
return self.publish(draft_block.location.version_agnostic(), user_id, blacklist=EXCLUDE_ALL, **kwargs)
# do the import
partitioned_fields = self.partition_fields_by_scope(block_type, fields)
course_key = self._map_revision_to_branch(course_key) # cast to branch_setting
return self._update_item_from_fields(
user_id, course_key, BlockKey(block_type, block_id), partitioned_fields, None, allow_not_found=True, force=True
user_id, course_key, BlockKey(block_type, block_id), partitioned_fields, None,
allow_not_found=True, force=True, **kwargs
) or self.get_item(new_usage_key)
def compute_published_info_internal(self, xblock):

View File

@@ -6,6 +6,7 @@ from xblock.exceptions import InvalidScopeError
from .definition_lazy_loader import DefinitionLazyLoader
from xmodule.modulestore.inheritance import InheritanceKeyValueStore
from opaque_keys.edx.locator import BlockUsageLocator
from xblock.core import XBlockAside
# id is a BlockUsageLocator, def_id is the definition's guid
SplitMongoKVSid = namedtuple('SplitMongoKVSid', 'id, def_id')
@@ -21,7 +22,7 @@ class SplitMongoKVS(InheritanceKeyValueStore):
VALID_SCOPES = (Scope.parent, Scope.children, Scope.settings, Scope.content)
@contract(parent="BlockUsageLocator | None")
def __init__(self, definition, initial_values, default_values, parent, field_decorator=None):
def __init__(self, definition, initial_values, default_values, parent, aside_fields=None, field_decorator=None):
"""
:param definition: either a lazyloader or definition id for the definition
@@ -42,34 +43,52 @@ class SplitMongoKVS(InheritanceKeyValueStore):
self.field_decorator = field_decorator
self.parent = parent
self.aside_fields = aside_fields if aside_fields else {}
def get(self, key):
# load the field, if needed
if key.field_name not in self._fields:
# parent undefined in editing runtime (I think)
if key.scope == Scope.parent:
return self.parent
if key.scope == Scope.children:
# didn't find children in _fields; so, see if there's a default
raise KeyError()
elif key.scope == Scope.settings:
# get default which may be the inherited value
raise KeyError()
elif key.scope == Scope.content:
if isinstance(self._definition, DefinitionLazyLoader):
self._load_definition()
else:
raise KeyError()
else:
if key.block_family == XBlockAside.entry_point:
if key.scope not in [Scope.settings, Scope.content]:
raise InvalidScopeError(key, self.VALID_SCOPES)
if key.field_name in self._fields:
field_value = self._fields[key.field_name]
if key.block_scope_id.block_type not in self.aside_fields:
# load the definition to see if it has the aside_fields
self._load_definition()
if key.block_scope_id.block_type not in self.aside_fields:
raise KeyError()
aside_fields = self.aside_fields[key.block_scope_id.block_type]
# load the field, if needed
if key.field_name not in aside_fields:
self._load_definition()
# return the "decorated" field value
return self.field_decorator(field_value)
if key.field_name in aside_fields:
return self.field_decorator(aside_fields[key.field_name])
return None
raise KeyError()
else:
# load the field, if needed
if key.field_name not in self._fields:
if key.scope == Scope.parent:
return self.parent
if key.scope == Scope.children:
# didn't find children in _fields; so, see if there's a default
raise KeyError()
elif key.scope == Scope.settings:
# get default which may be the inherited value
raise KeyError()
elif key.scope == Scope.content:
if isinstance(self._definition, DefinitionLazyLoader):
self._load_definition()
else:
raise KeyError()
else:
raise InvalidScopeError(key)
if key.field_name in self._fields:
field_value = self._fields[key.field_name]
# return the "decorated" field value
return self.field_decorator(field_value)
return None
def set(self, key, value):
# handle any special cases
@@ -78,17 +97,23 @@ class SplitMongoKVS(InheritanceKeyValueStore):
if key.scope == Scope.content:
self._load_definition()
# set the field
self._fields[key.field_name] = value
if key.block_family == XBlockAside.entry_point:
if key.scope == Scope.children:
raise InvalidScopeError(key)
# This function is currently incomplete: it doesn't handle side effects.
# To complete this function, here is some pseudocode for what should happen:
#
# if key.scope == Scope.children:
# remove inheritance from any exchildren
# add inheritance to any new children
# if key.scope == Scope.settings:
# if inheritable, push down to children
self.aside_fields.setdefault(key.block_scope_id.block_type, {})[key.field_name] = value
else:
# set the field
self._fields[key.field_name] = value
# This function is currently incomplete: it doesn't handle side effects.
# To complete this function, here is some pseudocode for what should happen:
#
# if key.scope == Scope.children:
# remove inheritance from any exchildren
# add inheritance to any new children
# if key.scope == Scope.settings:
# if inheritable, push down to children
def delete(self, key):
# handle any special cases
@@ -97,9 +122,17 @@ class SplitMongoKVS(InheritanceKeyValueStore):
if key.scope == Scope.content:
self._load_definition()
# delete the field value
if key.field_name in self._fields:
del self._fields[key.field_name]
if key.block_family == XBlockAside.entry_point:
if key.scope == Scope.children:
raise InvalidScopeError(key)
if key.block_scope_id.block_type in self.aside_fields \
and key.field_name in self.aside_fields[key.block_scope_id.block_type]:
del self.aside_fields[key.block_scope_id.block_type][key.field_name]
else:
# delete the field value
if key.field_name in self._fields:
del self._fields[key.field_name]
def has(self, key):
"""
@@ -111,9 +144,16 @@ class SplitMongoKVS(InheritanceKeyValueStore):
elif key.scope == Scope.parent:
return True
# it's not clear whether inherited values should return True. Right now they don't
# if someone changes it so that they do, then change any tests of field.name in xx._field_data
return key.field_name in self._fields
if key.block_family == XBlockAside.entry_point:
if key.scope == Scope.children:
return False
b_type = key.block_scope_id.block_type
return b_type in self.aside_fields and key.field_name in self.aside_fields[b_type]
else:
# it's not clear whether inherited values should return True. Right now they don't
# if someone changes it so that they do, then change any tests of field.name in xx._field_data
return key.field_name in self._fields
def default(self, key):
"""
@@ -134,5 +174,10 @@ class SplitMongoKVS(InheritanceKeyValueStore):
if persisted_definition is not None:
fields = self._definition.field_converter(persisted_definition.get('fields'))
self._fields.update(fields)
aside_fields_p = persisted_definition.get('aside_fields')
if aside_fields_p:
aside_fields = self._definition.field_converter(aside_fields_p)
for aside_type, fields in aside_fields.iteritems():
self.aside_fields.setdefault(aside_type, {}).update(fields)
# do we want to cache any of the edit_info?
self._definition = None # already loaded

View File

@@ -17,6 +17,7 @@ from mock import patch, Mock, call
from django.conf import settings
# This import breaks this test file when run separately. Needs to be fixed! (PLAT-449)
from nose.plugins.attrib import attr
from nose import SkipTest
import pymongo
from pytz import UTC
from shutil import rmtree
@@ -30,6 +31,12 @@ from xmodule.contentstore.content import StaticContent
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.xml_exporter import export_course_to_xml
from xmodule.modulestore.tests.test_asides import AsideTestType
from xblock.core import XBlockAside
from xblock.fields import Scope, String, ScopeIds
from xblock.fragment import Fragment
from xblock.runtime import DictKeyValueStore, KvsFieldData
from xblock.test.tools import TestRuntime
if not settings.configured:
settings.configure()
@@ -156,7 +163,7 @@ class CommonMixedModuleStoreSetup(CourseComparisonTest):
self.user_id = ModuleStoreEnum.UserID.test
# pylint: disable=invalid-name
def _create_course(self, course_key):
def _create_course(self, course_key, asides=None):
"""
Create a course w/ one item in the persistence store using the given course & item location.
"""
@@ -169,7 +176,8 @@ class CommonMixedModuleStoreSetup(CourseComparisonTest):
self.assertEqual(self.course.id, course_key)
# create chapter
chapter = self.store.create_child(self.user_id, self.course.location, 'chapter', block_id='Overview')
chapter = self.store.create_child(self.user_id, self.course.location, 'chapter',
block_id='Overview', asides=asides)
self.writable_chapter_location = chapter.location
def _create_block_hierarchy(self):
@@ -296,6 +304,36 @@ class CommonMixedModuleStoreSetup(CourseComparisonTest):
self.assertEquals(default, self.store.get_modulestore_type(self.course.id))
class AsideFoo(XBlockAside):
"""
Test xblock aside class
"""
FRAG_CONTENT = u"<p>Aside Foo rendered</p>"
field11 = String(default="aside1_default_value1", scope=Scope.content)
field12 = String(default="aside1_default_value2", 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 AsideBar(XBlockAside):
"""
Test xblock aside class
"""
FRAG_CONTENT = u"<p>Aside Bar rendered</p>"
field21 = String(default="aside2_default_value1", scope=Scope.content)
field22 = String(default="aside2_default_value2", 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)
@ddt.ddt
@attr('mongo')
class TestMixedModuleStore(CommonMixedModuleStoreSetup):
@@ -2978,3 +3016,307 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup):
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only, source_course_key):
component = self.store.get_item(unit.location)
self.assertEqual(component.display_name, updated_display_name)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
@XBlockAside.register_temp_plugin(AsideTestType, 'test_aside')
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
lambda self, block: ['test_aside'])
def test_aside_crud(self, default_store):
"""
Check that asides could be imported from XML and the modulestores handle asides crud
"""
if default_store == ModuleStoreEnum.Type.mongo:
raise SkipTest("asides not supported in old mongo")
with MongoContentstoreBuilder().build() as contentstore:
self.store = MixedModuleStore(
contentstore=contentstore,
create_modulestore_instance=create_modulestore_instance,
mappings={},
**self.OPTIONS
)
self.addCleanup(self.store.close_all_connections)
with self.store.default_store(default_store):
dest_course_key = self.store.make_course_key('edX', "aside_test", "2012_Fall")
courses = import_course_from_xml(
self.store, self.user_id, DATA_DIR, ['aside'],
load_error_modules=False,
static_content_store=contentstore,
target_id=dest_course_key,
create_if_not_present=True,
)
# check that the imported blocks have the right asides and values
def check_block(block):
"""
Check whether block has the expected aside w/ its fields and then recurse to the block's children
"""
asides = block.runtime.get_asides(block)
self.assertEqual(len(asides), 1, "Found {} asides but expected only test_aside".format(asides))
self.assertIsInstance(asides[0], AsideTestType)
category = block.scope_ids.block_type
self.assertEqual(asides[0].data_field, "{} aside data".format(category))
self.assertEqual(asides[0].content, "{} Aside".format(category.capitalize()))
for child in block.get_children():
check_block(child)
check_block(courses[0])
# create a new block and ensure its aside magically appears with the right fields
new_chapter = self.store.create_child(self.user_id, courses[0].location, 'chapter', 'new_chapter')
asides = new_chapter.runtime.get_asides(new_chapter)
self.assertEqual(len(asides), 1, "Found {} asides but expected only test_aside".format(asides))
chapter_aside = asides[0]
self.assertIsInstance(chapter_aside, AsideTestType)
self.assertFalse(
chapter_aside.fields['data_field'].is_set_on(chapter_aside),
"data_field says it's assigned to {}".format(chapter_aside.data_field)
)
self.assertFalse(
chapter_aside.fields['content'].is_set_on(chapter_aside),
"content says it's assigned to {}".format(chapter_aside.content)
)
# now update the values
chapter_aside.data_field = 'new value'
self.store.update_item(new_chapter, self.user_id, asides=[chapter_aside])
new_chapter = self.store.get_item(new_chapter.location)
chapter_aside = new_chapter.runtime.get_asides(new_chapter)[0]
self.assertEqual('new value', chapter_aside.data_field)
# update the values the second time
chapter_aside.data_field = 'another one value'
self.store.update_item(new_chapter, self.user_id, asides=[chapter_aside])
new_chapter2 = self.store.get_item(new_chapter.location)
chapter_aside2 = new_chapter2.runtime.get_asides(new_chapter2)[0]
self.assertEqual('another one value', chapter_aside2.data_field)
@ddt.ddt
@attr('mongo')
class TestAsidesWithMixedModuleStore(CommonMixedModuleStoreSetup):
"""
Tests of the MixedModulestore interface methods with XBlock asides.
"""
def setUp(self):
"""
Setup environment for testing
"""
super(TestAsidesWithMixedModuleStore, self).setUp()
key_store = DictKeyValueStore()
field_data = KvsFieldData(key_store)
self.runtime = TestRuntime(services={'field-data': field_data}) # pylint: disable=abstract-class-instantiated
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
@XBlockAside.register_temp_plugin(AsideFoo, 'test_aside1')
@XBlockAside.register_temp_plugin(AsideBar, 'test_aside2')
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
lambda self, block: ['test_aside1', 'test_aside2'])
def test_get_and_update_asides(self, default_store):
"""
Tests that connected asides could be stored, received and updated along with connected course items
"""
if default_store == ModuleStoreEnum.Type.mongo:
raise SkipTest("asides not supported in old mongo")
self.initdb(default_store)
block_type1 = 'test_aside1'
def_id = self.runtime.id_generator.create_definition(block_type1)
usage_id = self.runtime.id_generator.create_usage(def_id)
# the first aside item
aside1 = AsideFoo(scope_ids=ScopeIds('user', block_type1, def_id, usage_id), runtime=self.runtime)
aside1.field11 = 'new_value11'
aside1.field12 = 'new_value12'
block_type2 = 'test_aside2'
def_id = self.runtime.id_generator.create_definition(block_type1)
usage_id = self.runtime.id_generator.create_usage(def_id)
# the second aside item
aside2 = AsideBar(scope_ids=ScopeIds('user', block_type2, def_id, usage_id), runtime=self.runtime)
aside2.field21 = 'new_value21'
# create new item with two asides
published_xblock = self.store.create_item(
self.user_id,
self.course.id,
'vertical',
block_id='test_vertical',
asides=[aside1, aside2]
)
def _check_asides(asides, field11, field12, field21, field22):
""" Helper function to check asides """
self.assertEqual(len(asides), 2)
self.assertEqual({type(asides[0]), type(asides[1])}, {AsideFoo, AsideBar})
self.assertEqual(asides[0].field11, field11)
self.assertEqual(asides[0].field12, field12)
self.assertEqual(asides[1].field21, field21)
self.assertEqual(asides[1].field22, field22)
# get saved item and check asides
component = self.store.get_item(published_xblock.location)
asides = component.runtime.get_asides(component)
_check_asides(asides, 'new_value11', 'new_value12', 'new_value21', 'aside2_default_value2')
asides[0].field11 = 'other_value11'
# update the first aside item and check that it was stored correctly
self.store.update_item(component, self.user_id, asides=[asides[0]])
cached_asides = component.runtime.get_asides(component)
_check_asides(cached_asides, 'other_value11', 'new_value12', 'new_value21', 'aside2_default_value2')
new_component = self.store.get_item(published_xblock.location)
new_asides = new_component.runtime.get_asides(new_component)
_check_asides(new_asides, 'other_value11', 'new_value12', 'new_value21', 'aside2_default_value2')
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
@XBlockAside.register_temp_plugin(AsideFoo, 'test_aside1')
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
lambda self, block: ['test_aside1'])
def test_clone_course_with_asides(self, default_store):
"""
Tests that connected asides will be cloned together with the parent courses
"""
if default_store == ModuleStoreEnum.Type.mongo:
raise SkipTest("asides not supported in old mongo")
with MongoContentstoreBuilder().build() as contentstore:
# initialize the mixed modulestore
self._initialize_mixed(contentstore=contentstore, mappings={})
with self.store.default_store(default_store):
block_type1 = 'test_aside1'
def_id = self.runtime.id_generator.create_definition(block_type1)
usage_id = self.runtime.id_generator.create_usage(def_id)
aside1 = AsideFoo(scope_ids=ScopeIds('user', block_type1, def_id, usage_id), runtime=self.runtime)
aside1.field11 = 'test1'
source_course_key = self.store.make_course_key("org.source", "course.source", "run.source")
self._create_course(source_course_key, asides=[aside1])
dest_course_id = self.store.make_course_key("org.other", "course.other", "run.other")
self.store.clone_course(source_course_key, dest_course_id, self.user_id)
source_store = self.store._get_modulestore_by_type(default_store) # pylint: disable=protected-access
self.assertCoursesEqual(source_store, source_course_key, source_store, dest_course_id)
# after clone get connected aside and check that it was cloned correctly
actual_items = source_store.get_items(dest_course_id,
revision=ModuleStoreEnum.RevisionOption.published_only)
chapter_is_found = False
for block in actual_items:
if block.scope_ids.block_type == 'chapter':
asides = block.runtime.get_asides(block)
self.assertEqual(len(asides), 1)
self.assertEqual(asides[0].field11, 'test1')
self.assertEqual(asides[0].field12, 'aside1_default_value2')
chapter_is_found = True
break
self.assertTrue(chapter_is_found)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
@XBlockAside.register_temp_plugin(AsideFoo, 'test_aside1')
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
lambda self, block: ['test_aside1'])
def test_delete_item_with_asides(self, default_store):
"""
Tests that connected asides will be removed together with the connected items
"""
if default_store == ModuleStoreEnum.Type.mongo:
raise SkipTest("asides not supported in old mongo")
self.initdb(default_store)
block_type1 = 'test_aside1'
def_id = self.runtime.id_generator.create_definition(block_type1)
usage_id = self.runtime.id_generator.create_usage(def_id)
aside1 = AsideFoo(scope_ids=ScopeIds('user', block_type1, def_id, usage_id), runtime=self.runtime)
aside1.field11 = 'new_value11'
aside1.field12 = 'new_value12'
published_xblock = self.store.create_item(
self.user_id,
self.course.id,
'vertical',
block_id='test_vertical',
asides=[aside1]
)
asides = published_xblock.runtime.get_asides(published_xblock)
self.assertEquals(asides[0].field11, 'new_value11')
self.assertEquals(asides[0].field12, 'new_value12')
# remove item
self.store.delete_item(published_xblock.location, self.user_id)
# create item again
published_xblock2 = self.store.create_item(
self.user_id,
self.course.id,
'vertical',
block_id='test_vertical'
)
# check that aside has default values
asides2 = published_xblock2.runtime.get_asides(published_xblock2)
self.assertEquals(asides2[0].field11, 'aside1_default_value1')
self.assertEquals(asides2[0].field12, 'aside1_default_value2')
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 0), (ModuleStoreEnum.Type.split, 2, 0))
@XBlockAside.register_temp_plugin(AsideFoo, 'test_aside1')
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
lambda self, block: ['test_aside1'])
@ddt.unpack
def test_published_and_unpublish_item_with_asides(self, default_store, max_find, max_send):
"""
Tests that public/unpublish doesn't affect connected stored asides
"""
if default_store == ModuleStoreEnum.Type.mongo:
raise SkipTest("asides not supported in old mongo")
self.initdb(default_store)
block_type1 = 'test_aside1'
def_id = self.runtime.id_generator.create_definition(block_type1)
usage_id = self.runtime.id_generator.create_usage(def_id)
aside1 = AsideFoo(scope_ids=ScopeIds('user', block_type1, def_id, usage_id), runtime=self.runtime)
aside1.field11 = 'new_value11'
aside1.field12 = 'new_value12'
def _check_asides(item):
""" Helper function to check asides """
asides = item.runtime.get_asides(item)
self.assertEquals(asides[0].field11, 'new_value11')
self.assertEquals(asides[0].field12, 'new_value12')
# start off as Private
item = self.store.create_child(self.user_id, self.writable_chapter_location, 'problem',
'test_compute_publish_state', asides=[aside1])
item_location = item.location
with check_mongo_calls(max_find, max_send):
self.assertFalse(self.store.has_published_version(item))
_check_asides(item)
# Private -> Public
self.store.publish(item_location, self.user_id)
item = self.store.get_item(item_location)
self.assertTrue(self.store.has_published_version(item))
_check_asides(item)
# Public -> Private
self.store.unpublish(item_location, self.user_id)
item = self.store.get_item(item_location)
self.assertFalse(self.store.has_published_version(item))
_check_asides(item)

View File

@@ -6,6 +6,7 @@ import ddt
import itertools
from collections import namedtuple
from xmodule.course_module import CourseSummary
from mock import patch
from xmodule.modulestore.tests.utils import (
PureModulestoreTestCase, MongoModulestoreBuilder,
@@ -15,7 +16,10 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
from xblock.core import XBlock
from xblock.core import XBlock, XBlockAside
from xblock.fields import Scope, String
from xblock.runtime import DictKeyValueStore, KvsFieldData
from xblock.test.tools import TestRuntime
DETACHED_BLOCK_TYPES = dict(XBlock.load_tagged_classes('detached'))
@@ -26,6 +30,13 @@ TESTABLE_BLOCK_TYPES.discard('course')
TestField = namedtuple('TestField', ['field_name', 'initial', 'updated'])
class AsideTest(XBlockAside):
"""
Test xblock aside class
"""
content = String(default="content", scope=Scope.content)
@ddt.ddt
class DirectOnlyCategorySemantics(PureModulestoreTestCase):
"""
@@ -43,6 +54,8 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
'course_info': TestField('data', '<div>test data</div>', '<div>different test data</div>'),
}
ASIDE_DATA_FIELD = TestField('content', '<div>aside test data</div>', '<div>aside different test data</div>')
def setUp(self):
super(DirectOnlyCategorySemantics, self).setUp()
self.course = CourseFactory.create(
@@ -73,7 +86,8 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
with self.assertRaises(ItemNotFoundError):
self.store.get_item(block_usage_key)
def assertBlockHasContent(self, block_usage_key, field_name, content, draft=None):
def assertBlockHasContent(self, block_usage_key, field_name, content,
aside_field_name=None, aside_content=None, draft=None):
"""
Assert that the block ``block_usage_key`` has the value ``content`` for ``field_name``
when it is loaded.
@@ -82,6 +96,8 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
block_usage_key: The xblock to check.
field_name (string): The name of the field to check.
content: The value to assert is in the field.
aside_field_name (string): The name of the field to check (in connected xblock aside)
aside_content: The value to assert is in the xblock aside field.
draft (optional): If omitted, verify both published and draft branches.
If True, verify only the draft branch. If False, verify only the
published branch.
@@ -92,6 +108,10 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
block_usage_key,
)
self.assertEquals(content, target_block.fields[field_name].read_from(target_block))
if aside_field_name and aside_content:
aside = self._get_aside(target_block)
self.assertIsNotNone(aside)
self.assertEquals(aside_content, aside.fields[aside_field_name].read_from(aside))
if draft is None or draft:
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
@@ -99,6 +119,10 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
block_usage_key,
)
self.assertEquals(content, target_block.fields[field_name].read_from(target_block))
if aside_field_name and aside_content:
aside = self._get_aside(target_block)
self.assertIsNotNone(aside)
self.assertEquals(aside_content, aside.fields[aside_field_name].read_from(aside))
def assertParentOf(self, parent_usage_key, child_usage_key, draft=None):
"""
@@ -202,9 +226,29 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
def test_create(self, block_type):
self._do_create(block_type)
def _prepare_asides(self, scope_ids):
"""
Return list with connected aside xblocks
"""
key_store = DictKeyValueStore()
field_data = KvsFieldData(key_store)
aside = AsideTest(scope_ids=scope_ids, runtime=TestRuntime(services={'field-data': field_data})) # pylint: disable=abstract-class-instantiated
aside.fields[self.ASIDE_DATA_FIELD.field_name].write_to(aside, self.ASIDE_DATA_FIELD.initial)
return [aside]
def _get_aside(self, block):
"""
Return connected xblock aside
"""
for aside in block.runtime.get_asides(block):
if isinstance(aside, AsideTest):
return aside
return None
# This function is split out from the test_create method so that it can be called
# by other tests
def _do_create(self, block_type):
def _do_create(self, block_type, with_asides=False):
"""
Create a block of block_type (which should be a DIRECT_ONLY_CATEGORY),
and then verify that it was created successfully, and is visible in
@@ -228,21 +272,33 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
block_id=block_usage_key.block_id
)
block.fields[test_data.field_name].write_to(block, initial_field_value)
self.store.update_item(block, ModuleStoreEnum.UserID.test, allow_not_found=True)
asides = []
if with_asides:
asides = self._prepare_asides(self.course.scope_ids.usage_id)
self.store.update_item(block, ModuleStoreEnum.UserID.test, asides=asides, allow_not_found=True)
else:
block = self.store.create_child(
asides = []
if with_asides:
asides = self._prepare_asides(self.course.scope_ids.usage_id)
self.store.create_child(
user_id=ModuleStoreEnum.UserID.test,
parent_usage_key=self.course.scope_ids.usage_id,
block_type=block_type,
block_id=block_usage_key.block_id,
fields={test_data.field_name: initial_field_value},
asides=asides
)
if self.is_detached(block_type):
self.assertCourseDoesntPointToBlock(block_usage_key)
else:
self.assertCoursePointsToBlock(block_usage_key)
self.assertBlockHasContent(block_usage_key, test_data.field_name, initial_field_value)
if with_asides:
self.assertBlockHasContent(block_usage_key, test_data.field_name, initial_field_value,
self.ASIDE_DATA_FIELD.field_name, self.ASIDE_DATA_FIELD.initial)
else:
self.assertBlockHasContent(block_usage_key, test_data.field_name, initial_field_value)
return block_usage_key
@@ -354,6 +410,7 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase):
self.assertBlockDoesntExist(child_usage_key)
@ddt.ddt
class TestSplitDirectOnlyCategorySemantics(DirectOnlyCategorySemantics):
"""
Verify DIRECT_ONLY_CATEGORY semantics against the SplitMongoModulestore.
@@ -361,6 +418,32 @@ class TestSplitDirectOnlyCategorySemantics(DirectOnlyCategorySemantics):
MODULESTORE = SPLIT_MODULESTORE_SETUP
__test__ = True
@ddt.data(*TESTABLE_BLOCK_TYPES)
@XBlockAside.register_temp_plugin(AsideTest, 'test_aside')
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
lambda self, block: ['test_aside'])
def test_create_with_asides(self, block_type):
self._do_create(block_type, with_asides=True)
@ddt.data(*TESTABLE_BLOCK_TYPES)
@XBlockAside.register_temp_plugin(AsideTest, 'test_aside')
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
lambda self, block: ['test_aside'])
def test_update_asides(self, block_type):
block_usage_key = self._do_create(block_type, with_asides=True)
test_data = self.DATA_FIELDS[block_type]
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
block = self.store.get_item(block_usage_key)
aside = self._get_aside(block)
self.assertIsNotNone(aside)
aside.fields[self.ASIDE_DATA_FIELD.field_name].write_to(aside, self.ASIDE_DATA_FIELD.updated)
self.store.update_item(block, ModuleStoreEnum.UserID.test, allow_not_found=True, asides=[aside])
self.assertBlockHasContent(block_usage_key, test_data.field_name, test_data.initial,
self.ASIDE_DATA_FIELD.field_name, self.ASIDE_DATA_FIELD.updated)
class TestMongoDirectOnlyCategorySemantics(DirectOnlyCategorySemantics):
"""

View File

@@ -736,10 +736,11 @@ def _update_and_import_module(
)
fields = _update_module_references(module, source_course_id, dest_course_id)
asides = module.get_asides() if isinstance(module, XModuleMixin) else None
return store.import_xblock(
user_id, dest_course_id, module.location.category,
module.location.block_id, fields, runtime
module.location.block_id, fields, runtime, asides=asides
)

View File

@@ -287,7 +287,7 @@ class TestLibraryContentRender(LibraryContentTest):
"""
Rendering unit tests for LibraryContentModule
"""
def test_preivew_view(self):
def test_preview_view(self):
""" Test preview view rendering """
self.lc_block.refresh_children()
self.lc_block = self.store.get_item(self.lc_block.location)

View File

@@ -41,6 +41,7 @@ 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`"""
self.get_asides = Mock(return_value=[])
descriptor = self.xblock_from_node(
etree.fromstring(xml),
None,

View File

@@ -298,6 +298,7 @@ class XModuleMixin(XModuleFields, XBlock):
def __init__(self, *args, **kwargs):
self.xmodule_runtime = None
self._asides = []
super(XModuleMixin, self).__init__(*args, **kwargs)
@@ -382,6 +383,18 @@ class XModuleMixin(XModuleFields, XBlock):
"""
return self._field_data
def add_aside(self, aside):
"""
save connected asides
"""
self._asides.append(aside)
def get_asides(self):
"""
get the list of connected asides
"""
return self._asides
def get_explicitly_set_fields_by_scope(self, scope=Scope.content):
"""
Get a dictionary of the fields for the given scope which are set explicitly on this xblock. (Including
@@ -1194,7 +1207,7 @@ class ConfigurableFragmentWrapper(object):
"""
Runtime mixin that allows for composition of many `wrap_xblock` wrappers
"""
def __init__(self, wrappers=None, **kwargs):
def __init__(self, wrappers=None, wrappers_asides=None, **kwargs):
"""
:param wrappers: A list of wrappers, where each wrapper is:
@@ -1207,6 +1220,10 @@ class ConfigurableFragmentWrapper(object):
self.wrappers = wrappers
else:
self.wrappers = []
if wrappers_asides is not None:
self.wrappers_asides = wrappers_asides
else:
self.wrappers_asides = []
def wrap_xblock(self, block, view, frag, context):
"""
@@ -1217,6 +1234,15 @@ class ConfigurableFragmentWrapper(object):
return frag
def wrap_aside(self, block, aside, view, frag, context): # pylint: disable=unused-argument
"""
See :func:`Runtime.wrap_child`
"""
for wrapper in self.wrappers_asides:
frag = wrapper(aside, view, frag, context)
return frag
# 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
@@ -1524,13 +1550,19 @@ class XMLParsingSystem(DescriptorSystem):
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)
aside_children = self.parse_asides(node, def_id, usage_id, id_generator)
asides_tags = [x.tag for x in aside_children]
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()
asides = self.get_asides(block)
for asd in asides:
if asd.scope_ids.block_type in asides_tags:
block.add_aside(asd)
return block
def parse_asides(self, node, def_id, usage_id, id_generator):
@@ -1547,6 +1579,7 @@ class XMLParsingSystem(DescriptorSystem):
for child in aside_children:
self._aside_from_xml(child, def_id, usage_id, id_generator)
node.remove(child)
return aside_children
def _make_usage_key(self, course_key, value):
"""

View File

@@ -223,6 +223,7 @@ class XmlParserMixin(object):
if filename is None:
definition_xml = copy.deepcopy(xml_object)
filepath = ''
aside_children = []
else:
dog_stats_api.increment(
DEPRECATION_VSCOMPAT_EVENT,
@@ -250,7 +251,7 @@ class XmlParserMixin(object):
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)
aside_children = 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)
@@ -262,6 +263,9 @@ class XmlParserMixin(object):
definition['definition_metadata'] = definition_metadata
definition['filename'] = [filepath, filename]
if aside_children:
definition['aside_children'] = aside_children
return definition, children
@classmethod
@@ -333,6 +337,7 @@ class XmlParserMixin(object):
url_name = node.get('url_name', node.get('slug'))
def_id = id_generator.create_definition(node.tag, url_name)
usage_id = id_generator.create_usage(def_id)
aside_children = []
# VS[compat] -- detect new-style each-in-a-file mode
if is_pointer_tag(node):
@@ -340,7 +345,7 @@ class XmlParserMixin(object):
# read the actual definition file--named using url_name.replace(':','/')
filepath = cls._format_filepath(node.tag, name_to_pathname(url_name))
definition_xml = cls.load_file(filepath, runtime.resources_fs, def_id)
runtime.parse_asides(definition_xml, def_id, usage_id, id_generator)
aside_children = runtime.parse_asides(definition_xml, def_id, usage_id, id_generator)
else:
filepath = None
definition_xml = node
@@ -370,6 +375,10 @@ class XmlParserMixin(object):
log.debug('Error in loading metadata %r', dmdata, exc_info=True)
metadata['definition_metadata_err'] = str(err)
definition_aside_children = definition.pop('aside_children', None)
if definition_aside_children:
aside_children.extend(definition_aside_children)
# Set/override any metadata specified by policy
cls.apply_policy(metadata, runtime.get_policy(usage_id))
@@ -382,13 +391,22 @@ class XmlParserMixin(object):
kvs = InheritanceKeyValueStore(initial_values=field_data)
field_data = KvsFieldData(kvs)
return runtime.construct_xblock_from_class(
xblock = runtime.construct_xblock_from_class(
cls,
# We're loading a descriptor, so student_id is meaningless
ScopeIds(None, node.tag, def_id, usage_id),
field_data,
)
if aside_children:
asides_tags = [x.tag for x in aside_children]
asides = runtime.get_asides(xblock)
for asd in asides:
if asd.scope_ids.block_type in asides_tags:
xblock.add_aside(asd)
return xblock
@classmethod
def _format_filepath(cls, category, name):
return u'{category}/{name}.{ext}'.format(category=category,

View File

@@ -92,6 +92,10 @@
var requestToken = requestToken || $element.data('request-token');
var children = XBlock.initializeXBlocks($element, requestToken);
var asides = XBlock.initializeXBlockAsides($element, requestToken);
if (asides) {
children = children.concat(asides);
}
$element.prop('xblock_children', children);
return constructBlock(element, [initArgs(element)]);
@@ -132,8 +136,12 @@
* 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);
var asides = XBlock.initializeXBlockAsides(element, requestToken);
var xblocks = XBlock.initializeXBlocks(element, requestToken);
if (asides) {
xblocks = xblocks.concat(asides);
}
return xblocks;
}
};