This is a secret
' + child_module.runtime = system + child_module.get_html.return_value = u'This is a secret
' + child_module.student_view.return_value = Fragment(child_module.get_html.return_value) child_module.displayable_items = lambda: [child_module] module_map = {source_descriptor: source_module, child_descriptor: child_module} system.get_module = lambda descriptor: module_map[descriptor] diff --git a/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py b/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py index 0369fffe8e..8ddb074d11 100644 --- a/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py +++ b/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py @@ -9,6 +9,7 @@ import copy from xmodule.crowdsource_hinter import CrowdsourceHinterModule from xmodule.vertical_module import VerticalModule, VerticalDescriptor from xblock.field_data import DictFieldData +from xblock.fragment import Fragment from . import get_test_system @@ -209,14 +210,16 @@ class FakeChild(object): A fake Xmodule. """ def __init__(self): - self.system = Mock() - self.system.ajax_url = 'this/is/a/fake/ajax/url' + self.runtime = get_test_system() + self.runtime.ajax_url = 'this/is/a/fake/ajax/url' + self.student_view = Mock(return_value=Fragment(self.get_html())) + self.save = Mock() def get_html(self): """ Return a fake html string. """ - return 'This is supposed to be test html.' + return u'This is supposed to be test html.' class CrowdsourceHinterTest(unittest.TestCase): @@ -238,7 +241,7 @@ class CrowdsourceHinterTest(unittest.TestCase): """ return [FakeChild()] mock_module.get_display_items = fake_get_display_items - out_html = mock_module.get_html() + out_html = mock_module.runtime.render(mock_module, None, 'student_view').content self.assertTrue('This is supposed to be test html.' in out_html) self.assertTrue('this/is/a/fake/ajax/url' in out_html) @@ -255,7 +258,7 @@ class CrowdsourceHinterTest(unittest.TestCase): """ return [] mock_module.get_display_items = fake_get_display_items - out_html = mock_module.get_html() + out_html = mock_module.runtime.render(mock_module, None, 'student_view').content self.assertTrue('Error in loading crowdsourced hinter' in out_html) @unittest.skip("Needs to be finished.") @@ -266,8 +269,7 @@ class CrowdsourceHinterTest(unittest.TestCase): NOT WORKING RIGHT NOW """ mock_module = VerticalWithModulesFactory.create() - out_html = mock_module.get_html() - print out_html + out_html = mock_module.runtime.render(mock_module, None, 'student_view').content self.assertTrue('Test numerical problem.' in out_html) self.assertTrue('Another test numerical problem.' in out_html) diff --git a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py index ef81a9a26b..948b4f6e18 100644 --- a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py +++ b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py @@ -131,7 +131,7 @@ class TestStudentView(TestXBlockWrapper): # it generates the same thing from student_view that it does from get_html def check_student_view_leaf_node(self, descriptor_cls): xmodule = self.leaf_module(descriptor_cls) - assert_equal(xmodule.get_html(), xmodule.student_view(None).content) + assert_equal(xmodule.get_html(), xmodule.runtime.render(xmodule, None, 'student_view').content) # Test that for all container XModule Descriptors, @@ -152,7 +152,7 @@ class TestStudentView(TestXBlockWrapper): # as it does using get_html def check_student_view_container_node_xmodules_only(self, descriptor_cls): xmodule = self.container_module(descriptor_cls, 2) - assert_equal(xmodule.get_html(), xmodule.student_view(None).content) + assert_equal(xmodule.get_html(), xmodule.runtime.render(xmodule, None, 'student_view').content) # Check that when an xmodule is generated from descriptor_cls # with mixed xmodule and xblock children, it generates the same html from student_view @@ -183,7 +183,7 @@ class TestStudioView(TestXBlockWrapper): raise SkipTest(descriptor_cls.__name__ + "is not editable in studio") descriptor = self.leaf_descriptor(descriptor_cls) - assert_equal(descriptor.get_html(), descriptor.studio_view(None).content) + assert_equal(descriptor.get_html(), descriptor.runtime.render(descriptor, None, 'studio_view').content) # Test that for all of the Descriptors listed in CONTAINER_XMODULES @@ -206,7 +206,7 @@ class TestStudioView(TestXBlockWrapper): raise SkipTest(descriptor_cls.__name__ + "is not editable in studio") descriptor = self.container_descriptor(descriptor_cls) - assert_equal(descriptor.get_html(), descriptor.studio_view(None).content) + assert_equal(descriptor.get_html(), descriptor.runtime.render(descriptor, None, 'studio_view').content) # Check that when a descriptor is generated from descriptor_cls # with mixed xmodule and xblock children, it generates the same html from studio_view diff --git a/common/lib/xmodule/xmodule/timelimit_module.py b/common/lib/xmodule/xmodule/timelimit_module.py index a5de35cfc2..70d3d9dba9 100644 --- a/common/lib/xmodule/xmodule/timelimit_module.py +++ b/common/lib/xmodule/xmodule/timelimit_module.py @@ -15,6 +15,8 @@ log = logging.getLogger(__name__) class TimeLimitFields(object): + has_children = True + beginning_at = Float(help="The time this timer was started", scope=Scope.user_state) ending_at = Float(help="The time this timer will end", scope=Scope.user_state) accomodation_code = String(help="A code indicating accommodations to be given the student", scope=Scope.user_state) @@ -31,8 +33,6 @@ class TimeLimitModule(TimeLimitFields, XModule): def __init__(self, *args, **kwargs): XModule.__init__(self, *args, **kwargs) - self.rendered = False - # For a timed activity, we are only interested here # in time-related accommodations, and these should be disjoint. # (For proctored exams, it is possible to have multiple accommodations @@ -85,8 +85,13 @@ class TimeLimitModule(TimeLimitFields, XModule): return int((self.ending_at - time()) * 1000) def get_html(self): - self.render() - return self.content + # assumes there is one and only one child, so it only renders the first child + children = self.get_display_items() + if children: + child = children[0] + return self.runtime.render_child(child, None, 'student_view').content + else: + return u"" def get_progress(self): ''' Return the total progress, adding total done and total available. @@ -101,16 +106,6 @@ class TimeLimitModule(TimeLimitFields, XModule): def handle_ajax(self, _dispatch, _data): raise NotFoundError('Unexpected dispatch type') - def render(self): - if self.rendered: - return - # assumes there is one and only one child, so it only renders the first child - children = self.get_display_items() - if children: - child = children[0] - self.content = child.get_html() - self.rendered = True - def get_icon_class(self): children = self.get_children() if children: diff --git a/common/lib/xmodule/xmodule/vertical_module.py b/common/lib/xmodule/xmodule/vertical_module.py index 610d180c11..2d6bfaa633 100644 --- a/common/lib/xmodule/xmodule/vertical_module.py +++ b/common/lib/xmodule/xmodule/vertical_module.py @@ -23,7 +23,7 @@ class VerticalModule(VerticalFields, XModule): if self.contents is None: self.contents = [{ 'id': child.id, - 'content': child.get_html() + 'content': self.runtime.render_child(child, None, 'student_view').content } for child in self.get_display_items()] return self.system.render_template('vert_module.html', { diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index ef37843371..6b3e3b67c2 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -657,7 +657,38 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): return Fragment(self.get_html()) -class DescriptorSystem(Runtime): +class ConfigurableFragmentWrapper(object): # pylint: disable=abstract-method + """ + Runtime mixin that allows for composition of many `wrap_child` wrappers + """ + def __init__(self, wrappers=None, **kwargs): + """ + :param wrappers: A list of wrappers, where each wrapper is: + + def wrapper(block, view, frag, context): + ... + return wrapped_frag + """ + super(ConfigurableFragmentWrapper, self).__init__(**kwargs) + if wrappers is not None: + self.wrappers = wrappers + else: + self.wrappers = [] + + def wrap_child(self, block, view, frag, context): + """ + See :func:`Runtime.wrap_child` + """ + for wrapper in self.wrappers: + frag = wrapper(block, view, frag, context) + + return frag + + +class DescriptorSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abstract-method + """ + Base class for :class:`Runtime`s to be used with :class:`XModuleDescriptor`s + """ def __init__(self, load_item, resources_fs, error_tracker, **kwargs): """ @@ -750,7 +781,7 @@ class XMLParsingSystem(DescriptorSystem): self.policy = policy -class ModuleSystem(Runtime): +class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abstract-method """ This is an abstraction such that x_modules can function independent of the courseware (e.g. import into other types of courseware, LMS, diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index f86bef29ad..3c1fe4e0e7 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -25,7 +25,7 @@ from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.x_module import ModuleSystem -from xmodule_modifiers import replace_course_urls, replace_jump_to_id_urls, replace_static_urls, add_histogram, wrap_xmodule, save_module # pylint: disable=F0401 +from xmodule_modifiers import replace_course_urls, replace_jump_to_id_urls, replace_static_urls, add_histogram, wrap_xmodule import static_replace from psychometrics.psychoanalyze import make_psychometrics_data_update_handler @@ -334,10 +334,46 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours dog_stats_api.increment("lms.courseware.question_answered", tags=tags) + # Build a list of wrapping functions that will be applied in order + # to the Fragment content coming out of the xblocks that are about to be rendered. + block_wrappers = [] + + # Wrap the output display in a single div to allow for the XModule + # javascript to be bound correctly + if wrap_xmodule_display is True: + block_wrappers.append(partial(wrap_xmodule, 'xmodule_display.html')) + # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory # that the xml was loaded from + # Rewrite urls beginning in /static to point to course-specific content + block_wrappers.append(partial( + replace_static_urls, + getattr(descriptor, 'data_dir', None), + course_id=course_id, + static_asset_path=static_asset_path or descriptor.static_asset_path + )) + + # Allow URLs of the form '/course/' refer to the root of multicourse directory + # hierarchy of this course + block_wrappers.append(partial(replace_course_urls, course_id)) + + # this will rewrite intra-courseware links (/jump_to_id/