Commit ba971db1 by Calen Pennington

Merge pull request #8594 from cpennington/pass-xblock-parents

Pass xblock parents
parents 754eb9af a25f91da
...@@ -919,7 +919,7 @@ class CourseModule(CourseFields, SequenceModule): # pylint: disable=abstract-me ...@@ -919,7 +919,7 @@ class CourseModule(CourseFields, SequenceModule): # pylint: disable=abstract-me
""" """
class CourseDescriptor(CourseFields, LicenseMixin, SequenceDescriptor): class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
""" """
The descriptor for the course XModule The descriptor for the course XModule
""" """
......
...@@ -80,7 +80,18 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor): ...@@ -80,7 +80,18 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor):
return u'' return u''
@classmethod @classmethod
def _construct(cls, system, contents, error_msg, location): def _construct(cls, system, contents, error_msg, location, for_parent=None):
"""
Build a new ErrorDescriptor. using ``system``.
Arguments:
system (:class:`DescriptorSystem`): The :class:`DescriptorSystem` used
to construct the XBlock that had an error.
contents (unicode): An encoding of the content of the xblock that had an error.
error_msg (unicode): A message describing the error.
location (:class:`UsageKey`): The usage key of the XBlock that had an error.
for_parent (:class:`XBlock`): Optional. The parent of this error block.
"""
if error_msg is None: if error_msg is None:
# this string is not marked for translation because we don't have # this string is not marked for translation because we don't have
...@@ -110,6 +121,7 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor): ...@@ -110,6 +121,7 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor):
# real scope keys # real scope keys
ScopeIds(None, 'error', location, location), ScopeIds(None, 'error', location, location),
field_data, field_data,
for_parent=for_parent,
) )
def get_context(self): def get_context(self):
...@@ -139,6 +151,7 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor): ...@@ -139,6 +151,7 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor):
str(descriptor), str(descriptor),
error_msg, error_msg,
location=descriptor.location, location=descriptor.location,
for_parent=descriptor.get_parent() if descriptor.has_cached_parent else None
) )
@classmethod @classmethod
......
...@@ -223,7 +223,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -223,7 +223,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
self.course_id = course_key self.course_id = course_key
self.cached_metadata = cached_metadata self.cached_metadata = cached_metadata
def load_item(self, location): def load_item(self, location, for_parent=None): # pylint: disable=method-hidden
""" """
Return an XModule instance for the specified location Return an XModule instance for the specified location
""" """
...@@ -292,7 +292,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -292,7 +292,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
field_data = KvsFieldData(kvs) field_data = KvsFieldData(kvs)
scope_ids = ScopeIds(None, category, location, location) scope_ids = ScopeIds(None, category, location, location)
module = self.construct_xblock_from_class(class_, scope_ids, field_data) module = self.construct_xblock_from_class(class_, scope_ids, field_data, for_parent=for_parent)
if self.cached_metadata is not None: if self.cached_metadata is not None:
# parent container pointers don't differentiate between draft and non-draft # parent container pointers don't differentiate between draft and non-draft
# so when we do the lookup, we should do so with a non-draft location # so when we do the lookup, we should do so with a non-draft location
...@@ -883,7 +883,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -883,7 +883,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
apply_cached_metadata=bool, apply_cached_metadata=bool,
using_descriptor_system="None|CachingDescriptorSystem" using_descriptor_system="None|CachingDescriptorSystem"
) )
def _load_item(self, course_key, item, data_cache, apply_cached_metadata=True, using_descriptor_system=None): def _load_item(self, course_key, item, data_cache,
apply_cached_metadata=True, using_descriptor_system=None, for_parent=None):
""" """
Load an XModuleDescriptor from item, using the children stored in data_cache Load an XModuleDescriptor from item, using the children stored in data_cache
...@@ -898,6 +899,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -898,6 +899,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
purposes. purposes.
using_descriptor_system (CachingDescriptorSystem): The existing CachingDescriptorSystem using_descriptor_system (CachingDescriptorSystem): The existing CachingDescriptorSystem
to add data to, and to load the XBlocks from. to add data to, and to load the XBlocks from.
for_parent (:class:`XBlock`): The parent of the XBlock being loaded.
""" """
course_key = self.fill_in_run(course_key) course_key = self.fill_in_run(course_key)
location = Location._from_deprecated_son(item['location'], course_key.run) location = Location._from_deprecated_son(item['location'], course_key.run)
...@@ -942,9 +944,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -942,9 +944,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
system.module_data.update(data_cache) system.module_data.update(data_cache)
system.cached_metadata.update(cached_metadata) system.cached_metadata.update(cached_metadata)
return system.load_item(location) return system.load_item(location, for_parent=for_parent)
def _load_items(self, course_key, items, depth=0, using_descriptor_system=None): def _load_items(self, course_key, items, depth=0, using_descriptor_system=None, for_parent=None):
""" """
Load a list of xmodules from the data in items, with children cached up Load a list of xmodules from the data in items, with children cached up
to specified depth to specified depth
...@@ -960,7 +962,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -960,7 +962,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
item, item,
data_cache, data_cache,
using_descriptor_system=using_descriptor_system, using_descriptor_system=using_descriptor_system,
apply_cached_metadata=self._should_apply_cached_metadata(item, depth) apply_cached_metadata=self._should_apply_cached_metadata(item, depth),
for_parent=for_parent,
) )
for item in items for item in items
] ]
...@@ -1078,7 +1081,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -1078,7 +1081,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
except ItemNotFoundError: except ItemNotFoundError:
return False return False
def get_item(self, usage_key, depth=0, using_descriptor_system=None): def get_item(self, usage_key, depth=0, using_descriptor_system=None, for_parent=None, **kwargs):
""" """
Returns an XModuleDescriptor instance for the item at location. Returns an XModuleDescriptor instance for the item at location.
...@@ -1101,7 +1104,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -1101,7 +1104,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
usage_key.course_key, usage_key.course_key,
[item], [item],
depth, depth,
using_descriptor_system=using_descriptor_system using_descriptor_system=using_descriptor_system,
for_parent=for_parent,
)[0] )[0]
return module return module
...@@ -1293,6 +1297,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -1293,6 +1297,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
# so we use the location for both. # so we use the location for both.
ScopeIds(None, block_type, location, location), ScopeIds(None, block_type, location, location),
dbmodel, dbmodel,
for_parent=kwargs.get('for_parent'),
) )
if fields is not None: if fields is not None:
for key, value in fields.iteritems(): for key, value in fields.iteritems():
...@@ -1341,11 +1346,16 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -1341,11 +1346,16 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
block_id: a unique identifier for the new item. If not supplied, block_id: a unique identifier for the new item. If not supplied,
a new identifier will be generated a new identifier will be generated
""" """
xblock = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, **kwargs)
# attach to parent if given # attach to parent if given
if 'detached' not in xblock._class_tags: parent = None
if parent_usage_key is not None:
parent = self.get_item(parent_usage_key) parent = self.get_item(parent_usage_key)
kwargs.setdefault('for_parent', parent)
xblock = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, **kwargs)
if parent is not None and 'detached' not in xblock._class_tags:
# Originally added to support entrance exams (settings.FEATURES.get('ENTRANCE_EXAMS')) # Originally added to support entrance exams (settings.FEATURES.get('ENTRANCE_EXAMS'))
if kwargs.get('position') is None: if kwargs.get('position') is None:
parent.children.append(xblock.location) parent.children.append(xblock.location)
......
...@@ -82,12 +82,14 @@ class DraftModuleStore(MongoModuleStore): ...@@ -82,12 +82,14 @@ class DraftModuleStore(MongoModuleStore):
""" """
def get_published(): def get_published():
return wrap_draft(super(DraftModuleStore, self).get_item( return wrap_draft(super(DraftModuleStore, self).get_item(
usage_key, depth=depth, using_descriptor_system=using_descriptor_system usage_key, depth=depth, using_descriptor_system=using_descriptor_system,
for_parent=kwargs.get('for_parent'),
)) ))
def get_draft(): def get_draft():
return wrap_draft(super(DraftModuleStore, self).get_item( return wrap_draft(super(DraftModuleStore, self).get_item(
as_draft(usage_key), depth=depth, using_descriptor_system=using_descriptor_system as_draft(usage_key), depth=depth, using_descriptor_system=using_descriptor_system,
for_parent=kwargs.get('for_parent')
)) ))
# return the published version if ModuleStoreEnum.RevisionOption.published_only is requested # return the published version if ModuleStoreEnum.RevisionOption.published_only is requested
......
...@@ -227,6 +227,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -227,6 +227,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
class_, class_,
ScopeIds(None, block_key.type, definition_id, block_locator), ScopeIds(None, block_key.type, definition_id, block_locator),
field_data, field_data,
for_parent=kwargs.get('for_parent')
) )
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
log.warning("Failed to load descriptor", exc_info=True) log.warning("Failed to load descriptor", exc_info=True)
......
...@@ -2,12 +2,15 @@ ...@@ -2,12 +2,15 @@
Factories for use in tests of XBlocks. Factories for use in tests of XBlocks.
""" """
import functools
import inspect import inspect
import pprint import pprint
import pymongo.message
import threading import threading
from uuid import uuid4 import traceback
from collections import defaultdict
from decorator import contextmanager from decorator import contextmanager
import pymongo.message from uuid import uuid4
from factory import Factory, Sequence, lazy_attribute_sequence, lazy_attribute from factory import Factory, Sequence, lazy_attribute_sequence, lazy_attribute
from factory.containers import CyclicDefinitionError from factory.containers import CyclicDefinitionError
...@@ -320,47 +323,160 @@ def check_number_of_calls(object_with_method, method_name, maximum_calls, minimu ...@@ -320,47 +323,160 @@ def check_number_of_calls(object_with_method, method_name, maximum_calls, minimu
return check_sum_of_calls(object_with_method, [method_name], maximum_calls, minimum_calls) return check_sum_of_calls(object_with_method, [method_name], maximum_calls, minimum_calls)
class StackTraceCounter(object):
"""
A class that counts unique stack traces underneath a particular stack frame.
"""
def __init__(self, stack_depth, include_arguments=True):
"""
Arguments:
stack_depth (int): The number of stack frames above this constructor to capture.
include_arguments (bool): Whether to store the arguments that are passed
when capturing a stack trace.
"""
self.include_arguments = include_arguments
self._top_of_stack = traceback.extract_stack(limit=stack_depth)[0]
if self.include_arguments:
self._stacks = defaultdict(lambda: defaultdict(int))
else:
self._stacks = defaultdict(int)
def capture_stack(self, args, kwargs):
"""
Record the stack frames starting at the caller of this method, and
ending at the top of the stack as defined by the ``stack_depth``.
Arguments:
args: The positional arguments to capture at this stack frame
kwargs: The keyword arguments to capture at this stack frame
"""
# pylint: disable=broad-except
stack = traceback.extract_stack()[:-2]
if self._top_of_stack in stack:
stack = stack[stack.index(self._top_of_stack):]
if self.include_arguments:
safe_args = []
for arg in args:
try:
safe_args.append(repr(arg))
except Exception as exc:
safe_args.append('<un-repr-able value: {}'.format(exc))
safe_kwargs = {}
for key, kwarg in kwargs.items():
try:
safe_kwargs[key] = repr(kwarg)
except Exception as exc:
safe_kwargs[key] = '<un-repr-able value: {}'.format(exc)
self._stacks[tuple(stack)][tuple(safe_args), tuple(safe_kwargs.items())] += 1
else:
self._stacks[tuple(stack)] += 1
@property
def total_calls(self):
"""
Return the total number of stacks recorded.
"""
return sum(self.stack_calls(stack) for stack in self._stacks)
def stack_calls(self, stack):
"""
Return the number of calls to the supplied ``stack``.
"""
if self.include_arguments:
return sum(self._stacks[stack].values())
else:
return self._stacks[stack]
def __iter__(self):
"""
Iterate over all unique captured stacks.
"""
return iter(sorted(self._stacks.keys(), key=lambda stack: (self.stack_calls(stack), stack), reverse=True))
def __getitem__(self, stack):
"""
Return the set of captured calls with the supplied stack.
"""
return self._stacks[stack]
@classmethod
def capture_call(cls, func, stack_depth, include_arguments=True):
"""
A decorator that wraps ``func``, and captures each call to ``func``,
recording the stack trace, and optionally the arguments that the function
is called with.
Arguments:
func: the function to wrap
stack_depth: how far up the stack to truncate the stored stack traces (
this is counted from the call to ``capture_call``, rather than calls
to the captured function).
"""
stacks = StackTraceCounter(stack_depth, include_arguments)
# pylint: disable=missing-docstring
@functools.wraps(func)
def capture(*args, **kwargs):
stacks.capture_stack(args, kwargs)
return func(*args, **kwargs)
capture.stack_counter = stacks
return capture
@contextmanager @contextmanager
def check_sum_of_calls(object_, methods, maximum_calls, minimum_calls=1): def check_sum_of_calls(object_, methods, maximum_calls, minimum_calls=1, include_arguments=True):
""" """
Instruments the given methods on the given object to verify that the total sum of calls made to the Instruments the given methods on the given object to verify that the total sum of calls made to the
methods falls between minumum_calls and maximum_calls. methods falls between minumum_calls and maximum_calls.
""" """
mocks = { mocks = {
method: Mock(wraps=getattr(object_, method)) method: StackTraceCounter.capture_call(
getattr(object_, method),
stack_depth=7,
include_arguments=include_arguments
)
for method in methods for method in methods
} }
if inspect.isclass(object_): with patch.multiple(object_, **mocks):
# If the object that we're intercepting methods on is a class, rather than a module,
# then we need to set the method to a real function, so that self gets passed to it,
# and then explicitly pass that self into the call to the mock
# pylint: disable=unnecessary-lambda,cell-var-from-loop
mock_kwargs = {
method: lambda self, *args, **kwargs: mocks[method](self, *args, **kwargs)
for method in methods
}
else:
mock_kwargs = mocks
with patch.multiple(object_, **mock_kwargs):
yield yield
call_count = sum(mock.call_count for mock in mocks.values()) call_count = sum(capture_fn.stack_counter.total_calls for capture_fn in mocks.values())
# Assertion errors don't handle multi-line values, so pretty-print to std-out instead # Assertion errors don't handle multi-line values, so pretty-print to std-out instead
if not minimum_calls <= call_count <= maximum_calls: if not minimum_calls <= call_count <= maximum_calls:
calls = { messages = ["Expected between {} and {} calls, {} were made.\n\n".format(
method_name: mock.call_args_list
for method_name, mock in mocks.items()
}
print "Expected between {} and {} calls, {} were made. Calls: {}".format(
minimum_calls, minimum_calls,
maximum_calls, maximum_calls,
call_count, call_count,
pprint.pformat(calls), )]
) for method_name, capture_fn in mocks.items():
stack_counter = capture_fn.stack_counter
messages.append("{!r} was called {} times:\n".format(
method_name,
stack_counter.total_calls
))
for stack in stack_counter:
messages.append(" called {} times:\n\n".format(stack_counter.stack_calls(stack)))
messages.append(" " + " ".join(traceback.format_list(stack)))
messages.append("\n\n")
if include_arguments:
for (args, kwargs), count in stack_counter[stack].items():
messages.append(" called {} times with:\n".format(count))
messages.append(" args: {}\n".format(args))
messages.append(" kwargs: {}\n\n".format(dict(kwargs)))
print "".join(messages)
# verify the counter actually worked by ensuring we have counted greater than (or equal to) the minimum calls # verify the counter actually worked by ensuring we have counted greater than (or equal to) the minimum calls
assert_greater_equal(call_count, minimum_calls) assert_greater_equal(call_count, minimum_calls)
......
...@@ -60,7 +60,7 @@ DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor' ...@@ -60,7 +60,7 @@ DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
RENDER_TEMPLATE = lambda t_n, d, ctx=None, nsp='main': '' RENDER_TEMPLATE = lambda t_n, d, ctx=None, nsp='main': ''
class ReferenceTestXBlock(XBlock, XModuleMixin): class ReferenceTestXBlock(XModuleMixin):
""" """
Test xblock type to test the reference field types Test xblock type to test the reference field types
""" """
......
...@@ -101,7 +101,7 @@ def render_to_template_mock(*args): ...@@ -101,7 +101,7 @@ def render_to_template_mock(*args):
pass pass
class StubXBlock(XBlock, XModuleMixin, InheritanceMixin): class StubXBlock(XModuleMixin, InheritanceMixin):
""" """
Stub XBlock used for testing. Stub XBlock used for testing.
""" """
......
...@@ -250,9 +250,9 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -250,9 +250,9 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# TODO (vshnayder): we are somewhat architecturally confused in the loading code: # TODO (vshnayder): we are somewhat architecturally confused in the loading code:
# load_item should actually be get_instance, because it expects the course-specific # load_item should actually be get_instance, because it expects the course-specific
# policy to be loaded. For now, just add the course_id here... # policy to be loaded. For now, just add the course_id here...
def load_item(usage_key): def load_item(usage_key, for_parent=None):
"""Return the XBlock for the specified location""" """Return the XBlock for the specified location"""
return xmlstore.get_item(usage_key) return xmlstore.get_item(usage_key, for_parent=for_parent)
resources_fs = OSFS(xmlstore.data_dir / course_dir) resources_fs = OSFS(xmlstore.data_dir / course_dir)
......
...@@ -79,10 +79,14 @@ class ConditionalFactory(object): ...@@ -79,10 +79,14 @@ class ConditionalFactory(object):
child_descriptor.render = lambda view, context=None: descriptor_system.render(child_descriptor, view, context) child_descriptor.render = lambda view, context=None: descriptor_system.render(child_descriptor, view, context)
child_descriptor.location = source_location.replace(category='html', name='child') child_descriptor.location = source_location.replace(category='html', name='child')
descriptor_system.load_item = { def load_item(usage_id, for_parent=None): # pylint: disable=unused-argument
child_descriptor.location: child_descriptor, """Test-only implementation of load_item that simply returns static xblocks."""
source_location: source_descriptor return {
}.get child_descriptor.location: child_descriptor,
source_location: source_descriptor
}.get(usage_id)
descriptor_system.load_item = load_item
system.descriptor_runtime = descriptor_system system.descriptor_runtime = descriptor_system
......
...@@ -9,7 +9,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location ...@@ -9,7 +9,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW from xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW
from mock import MagicMock, Mock, patch from mock import MagicMock, Mock, patch
from xblock.runtime import Runtime, IdReader from xblock.runtime import Runtime, IdReader
from xblock.field_data import FieldData from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from xblock.test.tools import unabc from xblock.test.tools import unabc
...@@ -43,10 +43,11 @@ class TestErrorModule(SetupTestErrorModules): ...@@ -43,10 +43,11 @@ class TestErrorModule(SetupTestErrorModules):
self.assertIn(repr(self.valid_xml), context_repr) self.assertIn(repr(self.valid_xml), context_repr)
def test_error_module_from_descriptor(self): def test_error_module_from_descriptor(self):
descriptor = MagicMock([XModuleDescriptor], descriptor = MagicMock(
runtime=self.system, spec=XModuleDescriptor,
location=self.location, runtime=self.system,
_field_data=self.valid_xml) location=self.location,
)
error_descriptor = ErrorDescriptor.from_descriptor( error_descriptor = ErrorDescriptor.from_descriptor(
descriptor, self.error_msg) descriptor, self.error_msg)
...@@ -81,10 +82,11 @@ class TestNonStaffErrorModule(SetupTestErrorModules): ...@@ -81,10 +82,11 @@ class TestNonStaffErrorModule(SetupTestErrorModules):
self.assertNotIn(repr(self.valid_xml), context_repr) self.assertNotIn(repr(self.valid_xml), context_repr)
def test_error_module_from_descriptor(self): def test_error_module_from_descriptor(self):
descriptor = MagicMock([XModuleDescriptor], descriptor = MagicMock(
runtime=self.system, spec=XModuleDescriptor,
location=self.location, runtime=self.system,
_field_data=self.valid_xml) location=self.location,
)
error_descriptor = NonStaffErrorDescriptor.from_descriptor( error_descriptor = NonStaffErrorDescriptor.from_descriptor(
descriptor, self.error_msg) descriptor, self.error_msg)
...@@ -122,7 +124,7 @@ class TestErrorModuleConstruction(unittest.TestCase): ...@@ -122,7 +124,7 @@ class TestErrorModuleConstruction(unittest.TestCase):
def setUp(self): def setUp(self):
# pylint: disable=abstract-class-instantiated # pylint: disable=abstract-class-instantiated
super(TestErrorModuleConstruction, self).setUp() super(TestErrorModuleConstruction, self).setUp()
field_data = Mock(spec=FieldData) field_data = DictFieldData({})
self.descriptor = BrokenDescriptor( self.descriptor = BrokenDescriptor(
TestRuntime(Mock(spec=IdReader), field_data), TestRuntime(Mock(spec=IdReader), field_data),
field_data, field_data,
......
...@@ -49,7 +49,7 @@ class InMemorySystem(XMLParsingSystem, MakoDescriptorSystem): # pylint: disable ...@@ -49,7 +49,7 @@ class InMemorySystem(XMLParsingSystem, MakoDescriptorSystem): # pylint: disable
self._descriptors[descriptor.location.to_deprecated_string()] = descriptor self._descriptors[descriptor.location.to_deprecated_string()] = descriptor
return descriptor return descriptor
def load_item(self, location): # pylint: disable=method-hidden def load_item(self, location, for_parent=None): # pylint: disable=method-hidden, unused-argument
"""Return the descriptor loaded for `location`""" """Return the descriptor loaded for `location`"""
return self._descriptors[location.to_deprecated_string()] return self._descriptors[location.to_deprecated_string()]
......
...@@ -86,7 +86,7 @@ log = logging.getLogger(__name__) ...@@ -86,7 +86,7 @@ log = logging.getLogger(__name__)
_ = lambda text: text _ = lambda text: text
class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, XModule): class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, XModule, LicenseMixin):
""" """
XML source example: XML source example:
<video show_captions="true" <video show_captions="true"
...@@ -332,7 +332,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, ...@@ -332,7 +332,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
@XBlock.wants("request_cache") @XBlock.wants("request_cache")
@XBlock.wants("settings") @XBlock.wants("settings")
class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers, class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers,
TabsEditingDescriptor, EmptyDataRawDescriptor): TabsEditingDescriptor, EmptyDataRawDescriptor, LicenseMixin):
""" """
Descriptor for `VideoModule`. Descriptor for `VideoModule`.
""" """
......
...@@ -12,7 +12,7 @@ from xmodule.mixin import LicenseMixin ...@@ -12,7 +12,7 @@ from xmodule.mixin import LicenseMixin
_ = lambda text: text _ = lambda text: text
class VideoFields(LicenseMixin): class VideoFields(object):
"""Fields for `VideoModule` and `VideoDescriptor`.""" """Fields for `VideoModule` and `VideoDescriptor`."""
display_name = String( display_name = String(
help=_("The name students see. This name appears in the course ribbon and as a header for the video."), help=_("The name students see. This name appears in the course ribbon and as a header for the video."),
......
...@@ -20,7 +20,7 @@ from lazy import lazy ...@@ -20,7 +20,7 @@ from lazy import lazy
from xblock.core import XBlock, XBlockAside from xblock.core import XBlock, XBlockAside
from xblock.fields import ( from xblock.fields import (
Scope, Integer, Float, List, XBlockMixin, Scope, Integer, Float, List,
String, Dict, ScopeIds, Reference, ReferenceList, String, Dict, ScopeIds, Reference, ReferenceList,
ReferenceValueDict, UserScope ReferenceValueDict, UserScope
) )
...@@ -265,7 +265,7 @@ class XModuleFields(object): ...@@ -265,7 +265,7 @@ class XModuleFields(object):
) )
class XModuleMixin(XModuleFields, XBlockMixin): class XModuleMixin(XModuleFields, XBlock):
""" """
Fields and methods used by XModules internally. Fields and methods used by XModules internally.
...@@ -295,7 +295,6 @@ class XModuleMixin(XModuleFields, XBlockMixin): ...@@ -295,7 +295,6 @@ class XModuleMixin(XModuleFields, XBlockMixin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.xmodule_runtime = None self.xmodule_runtime = None
self._child_instances = None
super(XModuleMixin, self).__init__(*args, **kwargs) super(XModuleMixin, self).__init__(*args, **kwargs)
...@@ -424,39 +423,45 @@ class XModuleMixin(XModuleFields, XBlockMixin): ...@@ -424,39 +423,45 @@ class XModuleMixin(XModuleFields, XBlockMixin):
else: else:
return [self.display_name_with_default] return [self.display_name_with_default]
def get_children(self, usage_key_filter=None): def get_children(self, usage_id_filter=None, usage_key_filter=None): # pylint: disable=arguments-differ
"""Returns a list of XBlock instances for the children of """Returns a list of XBlock instances for the children of
this module""" this module"""
if not self.has_children: # Be backwards compatible with callers using usage_key_filter
return [] if usage_id_filter is None and usage_key_filter is not None:
usage_id_filter = usage_key_filter
if self._child_instances is None: return [
self._child_instances = [] # pylint: disable=attribute-defined-outside-init child
for child_loc in self.children: for child
# Skip if it doesn't satisfy the filter function in super(XModuleMixin, self).get_children(usage_id_filter)
if usage_key_filter and not usage_key_filter(child_loc): if child is not None
continue ]
try:
child = self.runtime.get_block(child_loc)
if child is None:
continue
child.runtime.export_fs = self.runtime.export_fs def get_child(self, usage_id):
except ItemNotFoundError: """
log.warning(u'Unable to load item {loc}, skipping'.format(loc=child_loc)) Return the child XBlock identified by ``usage_id``, or ``None`` if there
dog_stats_api.increment( is an error while retrieving the block.
"xmodule.item_not_found_error", """
tags=[ try:
u"course_id:{}".format(child_loc.course_key), child = super(XModuleMixin, self).get_child(usage_id)
u"block_type:{}".format(child_loc.block_type), except ItemNotFoundError:
u"parent_block_type:{}".format(self.location.block_type), log.warning(u'Unable to load item %s, skipping', usage_id)
] dog_stats_api.increment(
) "xmodule.item_not_found_error",
continue tags=[
self._child_instances.append(child) u"course_id:{}".format(usage_id.course_key),
u"block_type:{}".format(usage_id.block_type),
u"parent_block_type:{}".format(self.location.block_type),
]
)
return None
if child is None:
return None
return self._child_instances child.runtime.export_fs = self.runtime.export_fs
return child
def get_required_module_descriptors(self): def get_required_module_descriptors(self):
"""Returns a list of XModuleDescriptor instances upon which this module depends, but are """Returns a list of XModuleDescriptor instances upon which this module depends, but are
...@@ -573,7 +578,7 @@ class XModuleMixin(XModuleFields, XBlockMixin): ...@@ -573,7 +578,7 @@ class XModuleMixin(XModuleFields, XBlockMixin):
self.scope_ids = self.scope_ids._replace(user_id=user_id) self.scope_ids = self.scope_ids._replace(user_id=user_id)
# Clear out any cached instantiated children. # Clear out any cached instantiated children.
self._child_instances = None self.clear_child_cache()
# Clear out any cached field data scoped to the old user. # Clear out any cached field data scoped to the old user.
for field in self.fields.values(): for field in self.fields.values():
...@@ -737,7 +742,7 @@ module_runtime_attr = partial(ProxyAttribute, 'xmodule_runtime') # pylint: disa ...@@ -737,7 +742,7 @@ module_runtime_attr = partial(ProxyAttribute, 'xmodule_runtime') # pylint: disa
@XBlock.needs("i18n") @XBlock.needs("i18n")
class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-method class XModule(HTMLSnippet, XModuleMixin): # pylint: disable=abstract-method
""" Implements a generic learning module. """ Implements a generic learning module.
Subclasses must at a minimum provide a definition for get_html in order Subclasses must at a minimum provide a definition for get_html in order
...@@ -767,7 +772,6 @@ class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-me ...@@ -767,7 +772,6 @@ class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-me
# Set the descriptor first so that we can proxy to it # Set the descriptor first so that we can proxy to it
self.descriptor = descriptor self.descriptor = descriptor
self._loaded_children = None
self._runtime = None self._runtime = None
super(XModule, self).__init__(*args, **kwargs) super(XModule, self).__init__(*args, **kwargs)
self.runtime.xmodule_instance = self self.runtime.xmodule_instance = self
...@@ -820,6 +824,19 @@ class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-me ...@@ -820,6 +824,19 @@ class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-me
response_data = self.handle_ajax(suffix, request_post) response_data = self.handle_ajax(suffix, request_post)
return Response(response_data, content_type='application/json') return Response(response_data, content_type='application/json')
def get_child(self, usage_id):
if usage_id in self._child_cache:
return self._child_cache[usage_id]
# Take advantage of the children cache that the descriptor might have
child_descriptor = self.descriptor.get_child(usage_id)
child_block = None
if child_descriptor is not None:
child_block = self.system.get_module(child_descriptor)
self._child_cache[usage_id] = child_block
return child_block
def get_child_descriptors(self): def get_child_descriptors(self):
""" """
Returns the descriptors of the child modules Returns the descriptors of the child modules
...@@ -932,7 +949,7 @@ class ResourceTemplates(object): ...@@ -932,7 +949,7 @@ class ResourceTemplates(object):
@XBlock.needs("i18n") @XBlock.needs("i18n")
class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): class XModuleDescriptor(HTMLSnippet, ResourceTemplates, XModuleMixin):
""" """
An XModuleDescriptor is a specification for an element of a course. This An XModuleDescriptor is a specification for an element of a course. This
could be a problem, an organizational element (a group of content), or a could be a problem, an organizational element (a group of content), or a
...@@ -1103,6 +1120,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -1103,6 +1120,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
descriptor=self, descriptor=self,
scope_ids=self.scope_ids, scope_ids=self.scope_ids,
field_data=self._field_data, field_data=self._field_data,
for_parent=self.get_parent() if self.has_cached_parent else None
) )
self.xmodule_runtime.xmodule_instance.save() self.xmodule_runtime.xmodule_instance.save()
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
...@@ -1340,9 +1358,9 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p ...@@ -1340,9 +1358,9 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p
else: else:
self.get_policy = lambda u: {} self.get_policy = lambda u: {}
def get_block(self, usage_id): def get_block(self, usage_id, for_parent=None):
"""See documentation for `xblock.runtime:Runtime.get_block`""" """See documentation for `xblock.runtime:Runtime.get_block`"""
return self.load_item(usage_id) return self.load_item(usage_id, for_parent=for_parent)
def get_field_provenance(self, xblock, field): def get_field_provenance(self, xblock, field):
""" """
...@@ -1681,8 +1699,8 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin ...@@ -1681,8 +1699,8 @@ class ModuleSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylin
assert self.xmodule_instance is not None assert self.xmodule_instance is not None
return self.handler_url(self.xmodule_instance, 'xmodule_handler', '', '').rstrip('/?') return self.handler_url(self.xmodule_instance, 'xmodule_handler', '', '').rstrip('/?')
def get_block(self, block_id): def get_block(self, block_id, for_parent=None):
return self.get_module(self.descriptor_runtime.get_block(block_id)) return self.get_module(self.descriptor_runtime.get_block(block_id, for_parent=for_parent))
def resource_url(self, resource): def resource_url(self, resource):
raise NotImplementedError("edX Platform doesn't currently implement XBlock resource urls") raise NotImplementedError("edX Platform doesn't currently implement XBlock resource urls")
......
...@@ -143,7 +143,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, ...@@ -143,7 +143,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin,
with self.assertNumQueries(queries): with self.assertNumQueries(queries):
with check_mongo_calls(reads): with check_mongo_calls(reads):
with check_sum_of_calls(XBlock, ['__init__'], xblocks): with check_sum_of_calls(XBlock, ['__init__'], xblocks, xblocks, include_arguments=False):
self.grade_course(self.course) self.grade_course(self.course)
@ddt.data(*itertools.product(('no_overrides', 'ccx'), range(1, 4), (True, False))) @ddt.data(*itertools.product(('no_overrides', 'ccx'), range(1, 4), (True, False)))
...@@ -173,18 +173,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): ...@@ -173,18 +173,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
TEST_DATA = { TEST_DATA = {
# (providers, course_width, enable_ccx): # of sql queries, # of mongo queries, # of xblocks # (providers, course_width, enable_ccx): # of sql queries, # of mongo queries, # of xblocks
('no_overrides', 1, True): (26, 7, 19), ('no_overrides', 1, True): (26, 7, 14),
('no_overrides', 2, True): (134, 7, 131), ('no_overrides', 2, True): (134, 7, 85),
('no_overrides', 3, True): (594, 7, 537), ('no_overrides', 3, True): (594, 7, 336),
('ccx', 1, True): (26, 7, 47), ('ccx', 1, True): (26, 7, 14),
('ccx', 2, True): (134, 7, 455), ('ccx', 2, True): (134, 7, 85),
('ccx', 3, True): (594, 7, 2037), ('ccx', 3, True): (594, 7, 336),
('no_overrides', 1, False): (26, 7, 19), ('no_overrides', 1, False): (26, 7, 14),
('no_overrides', 2, False): (134, 7, 131), ('no_overrides', 2, False): (134, 7, 85),
('no_overrides', 3, False): (594, 7, 537), ('no_overrides', 3, False): (594, 7, 336),
('ccx', 1, False): (26, 7, 19), ('ccx', 1, False): (26, 7, 14),
('ccx', 2, False): (134, 7, 131), ('ccx', 2, False): (134, 7, 85),
('ccx', 3, False): (594, 7, 537), ('ccx', 3, False): (594, 7, 336),
} }
......
...@@ -560,7 +560,12 @@ class CourseBlocksAndNavigation(ListAPIView): ...@@ -560,7 +560,12 @@ class CourseBlocksAndNavigation(ListAPIView):
) )
# verify the user has access to this block # verify the user has access to this block
if not has_access(request_info.request.user, 'load', block_info.block, course_key=request_info.course.id): if (block_info.block is None or not has_access(
request_info.request.user,
'load',
block_info.block,
course_key=request_info.course.id
)):
return return
# add the block's value to the result # add the block's value to the result
......
...@@ -610,11 +610,11 @@ class TestTOC(ModuleStoreTestCase): ...@@ -610,11 +610,11 @@ class TestTOC(ModuleStoreTestCase):
# Split makes 6 queries to load the course to depth 2: # Split makes 6 queries to load the course to depth 2:
# - load the structure # - load the structure
# - load 5 definitions # - load 5 definitions
# Split makes 6 queries to render the toc: # Split makes 5 queries to render the toc:
# - it loads the active version at the start of the bulk operation # - it loads the active version at the start of the bulk operation
# - it loads 5 definitions, because it instantiates the a CourseModule and 4 VideoModules # - it loads 4 definitions, because it instantiates 4 VideoModules
# each of which access a Scope.content field in __init__ # each of which access a Scope.content field in __init__
@ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 6)) @ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 5))
@ddt.unpack @ddt.unpack
def test_toc_toy_from_chapter(self, default_ms, setup_finds, setup_sends, toc_finds): def test_toc_toy_from_chapter(self, default_ms, setup_finds, setup_sends, toc_finds):
with self.store.default_store(default_ms): with self.store.default_store(default_ms):
...@@ -634,9 +634,10 @@ class TestTOC(ModuleStoreTestCase): ...@@ -634,9 +634,10 @@ class TestTOC(ModuleStoreTestCase):
'format': '', 'due': None, 'active': False}], 'format': '', 'due': None, 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) 'url_name': 'secret:magic', 'display_name': 'secret:magic'}])
course = self.store.get_course(self.toy_course.id, depth=2)
with check_mongo_calls(toc_finds): with check_mongo_calls(toc_finds):
actual = render.toc_for_course( actual = render.toc_for_course(
self.request, self.toy_course, self.chapter, None, self.field_data_cache self.request, course, self.chapter, None, self.field_data_cache
) )
for toc_section in expected: for toc_section in expected:
self.assertIn(toc_section, actual) self.assertIn(toc_section, actual)
...@@ -648,11 +649,11 @@ class TestTOC(ModuleStoreTestCase): ...@@ -648,11 +649,11 @@ class TestTOC(ModuleStoreTestCase):
# Split makes 6 queries to load the course to depth 2: # Split makes 6 queries to load the course to depth 2:
# - load the structure # - load the structure
# - load 5 definitions # - load 5 definitions
# Split makes 2 queries to render the toc: # Split makes 5 queries to render the toc:
# - it loads the active version at the start of the bulk operation # - it loads the active version at the start of the bulk operation
# - it loads 5 definitions, because it instantiates the a CourseModule and 4 VideoModules # - it loads 4 definitions, because it instantiates 4 VideoModules
# each of which access a Scope.content field in __init__ # each of which access a Scope.content field in __init__
@ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 6)) @ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 5))
@ddt.unpack @ddt.unpack
def test_toc_toy_from_section(self, default_ms, setup_finds, setup_sends, toc_finds): def test_toc_toy_from_section(self, default_ms, setup_finds, setup_sends, toc_finds):
with self.store.default_store(default_ms): with self.store.default_store(default_ms):
......
...@@ -32,7 +32,7 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c ...@@ -32,7 +32,7 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c
-e git+https://github.com/jazkarta/ccx-keys.git@e6b03704b1bb97c1d2f31301ecb4e3a687c536ea#egg=ccx-keys -e git+https://github.com/jazkarta/ccx-keys.git@e6b03704b1bb97c1d2f31301ecb4e3a687c536ea#egg=ccx-keys
# Our libraries: # Our libraries:
-e git+https://github.com/edx/XBlock.git@e1831fa86bff778ffe1308e00d8ed51b26f7c047#egg=XBlock -e git+https://github.com/edx/XBlock.git@74fdc5a361f48e5596acf3846ca3790a33a05253#egg=XBlock
-e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail -e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail
-e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool -e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool
-e git+https://github.com/edx/event-tracking.git@0.2.0#egg=event-tracking -e git+https://github.com/edx/event-tracking.git@0.2.0#egg=event-tracking
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment