Make XModuleDescriptor and XModule act as a single class

By transparently proxying between the XModuleDescriptor and the XModule,
and between their runtimes, we can make them act as a single class, so
that we can swap in an actual XBlock instead.
This commit is contained in:
Calen Pennington
2013-09-19 13:11:17 -04:00
parent 3cebb4eae2
commit 6b474724ac
44 changed files with 551 additions and 356 deletions

View File

@@ -95,11 +95,6 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor
"""
def preview_field_data(descriptor):
"Helper method to create a DbModel from a descriptor"
student_data = DbModel(SessionKeyValueStore(request))
return lms_field_data(descriptor._field_data, student_data)
course_id = get_course_for_item(descriptor.location).location.course_id
if descriptor.location.category == 'static_tab':
@@ -118,7 +113,6 @@ def preview_module_system(request, preview_id, descriptor):
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
user=request.user,
xmodule_field_data=preview_field_data,
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
mixins=settings.XBLOCK_MIXINS,
course_id=course_id,
@@ -136,7 +130,8 @@ def preview_module_system(request, preview_id, descriptor):
getattr(descriptor, 'data_dir', descriptor.location.course),
course_id=descriptor.location.org + '/' + descriptor.location.course + '/BOGUS_RUN_REPLACE_WHEN_AVAILABLE',
),
)
),
error_descriptor_class=ErrorDescriptor,
)
@@ -148,17 +143,12 @@ def load_preview_module(request, preview_id, descriptor):
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule(system)
return module
student_data = DbModel(SessionKeyValueStore(request))
descriptor.bind_for_student(
preview_module_system(request, preview_id, descriptor),
lms_field_data(descriptor._field_data, student_data), # pylint: disable=protected-access
)
return descriptor
def get_preview_html(request, descriptor, idx):

View File

@@ -134,7 +134,7 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a
return frag
block_id = block.id
if block.descriptor.has_score:
if block.has_score:
histogram = grade_histogram(block_id)
render_histogram = len(histogram) > 0
else:
@@ -142,7 +142,7 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a
render_histogram = False
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
[filepath, filename] = getattr(block.descriptor, 'xml_attributes', {}).get('filename', ['', None])
[filepath, filename] = getattr(block, 'xml_attributes', {}).get('filename', ['', None])
osfs = block.system.filestore
if filename is not None and osfs.exists(filename):
# if original, unmangled filename exists then use it (github
@@ -163,13 +163,13 @@ def add_histogram(user, block, view, frag, context): # pylint: disable=unused-a
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = datetime.datetime.now(UTC())
is_released = "unknown"
mstart = block.descriptor.start
mstart = block.start
if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
staff_context = {'fields': [(name, field.read_from(block)) for name, field in block.fields.items()],
'xml_attributes': getattr(block.descriptor, 'xml_attributes', {}),
'xml_attributes': getattr(block, 'xml_attributes', {}),
'location': block.location,
'xqa_key': block.xqa_key,
'source_file': source_file,

View File

@@ -15,7 +15,7 @@ from capa.responsetypes import StudentInputError, \
ResponseError, LoncapaProblemError
from capa.util import convert_files_to_filenames
from .progress import Progress
from xmodule.x_module import XModule
from xmodule.x_module import XModule, module_attr
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
@@ -1193,3 +1193,33 @@ class CapaDescriptor(CapaFields, RawDescriptor):
CapaDescriptor.force_save_button, CapaDescriptor.markdown,
CapaDescriptor.text_customization])
return non_editable_fields
# Proxy to CapaModule for access to any of its attributes
answer_available = module_attr('answer_available')
check_button_name = module_attr('check_button_name')
check_problem = module_attr('check_problem')
choose_new_seed = module_attr('choose_new_seed')
closed = module_attr('closed')
get_answer = module_attr('get_answer')
get_problem = module_attr('get_problem')
get_problem_html = module_attr('get_problem_html')
get_state_for_lcp = module_attr('get_state_for_lcp')
handle_input_ajax = module_attr('handle_input_ajax')
handle_problem_html_error = module_attr('handle_problem_html_error')
handle_ungraded_response = module_attr('handle_ungraded_response')
is_attempted = module_attr('is_attempted')
is_correct = module_attr('is_correct')
is_past_due = module_attr('is_past_due')
is_submitted = module_attr('is_submitted')
lcp = module_attr('lcp')
make_dict_of_responses = module_attr('make_dict_of_responses')
new_lcp = module_attr('new_lcp')
publish_grade = module_attr('publish_grade')
rescore_problem = module_attr('rescore_problem')
reset_problem = module_attr('reset_problem')
save_problem = module_attr('save_problem')
set_state_from_lcp = module_attr('set_state_from_lcp')
should_show_check_button = module_attr('should_show_check_button')
should_show_reset_button = module_attr('should_show_reset_button')
should_show_save_button = module_attr('should_show_save_button')
update_score = module_attr('update_score')

View File

@@ -496,7 +496,7 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
metadata_translations = {
'is_graded': 'graded',
'attempts': 'max_attempts',
}
}
def get_context(self):
_context = RawDescriptor.get_context(self)

View File

@@ -18,6 +18,7 @@ log = logging.getLogger('mitx.' + __name__)
class ConditionalFields(object):
has_children = True
show_tag_list = List(help="Poll answers", scope=Scope.content)

View File

@@ -115,15 +115,15 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
child = self.get_display_items()[0]
out = self.runtime.render_child(child, None, 'student_view').content
# The event listener uses the ajax url to find the child.
child_url = child.runtime.ajax_url
child_id = child.id
except IndexError:
out = u"Error in loading crowdsourced hinter - can't find child problem."
child_url = ''
child_id = ''
# Wrap the module in a <section>. This lets us pass data attributes to the javascript.
out += u'<section class="crowdsource-wrapper" data-url="{ajax_url}" data-child-url="{child_url}"> </section>'.format(
out += u'<section class="crowdsource-wrapper" data-url="{ajax_url}" data-child-id="{child_id}"> </section>'.format(
ajax_url=self.runtime.ajax_url,
child_url=child_url
child_id=child_id
)
return out

View File

@@ -77,7 +77,7 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor):
module_class = ErrorModule
def get_html(self):
return ''
return u''
@classmethod
def _construct(cls, system, contents, error_msg, location):

View File

@@ -1,8 +1,8 @@
<li id="vert-0" data-id="i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_def7a1142dd0">
<section class="xmodule_display xmodule_CrowdsourceHinterModule" data-type="Hinter" id="hinter-root">
<section class="xmodule_display xmodule_CapaModule" data-type="Problem" id="problem">
<section id="problem_i4x-Me-19_002-problem-Numerical_Input" class="problems-wrapper" data-problem-id="i4x://Me/19.002/problem/Numerical_Input" data-url="/courses/Me/19.002/Test/modx/i4x://Me/19.002/problem/Numerical_Input" data-progress_status="done" data-progress_detail="1/1">
@@ -44,7 +44,7 @@
<section class="crowdsource-wrapper" data-url="/courses/Me/19.002/Test/modx/i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_def7a1142dd0" data-child-url="/courses/Me/19.002/Test/modx/i4x://Me/19.002/problem/Numerical_Input" style="display: none;"> </section>
<section class="crowdsource-wrapper" data-url="/courses/Me/19.002/Test/modx/i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_def7a1142dd0" data-child-id="i4x://Me/19.002/problem/Numerical_Input" style="display: none;"> </section>
</section>

View File

@@ -139,12 +139,12 @@ describe 'Problem', ->
it 'log the problem_graded event, after the problem is done grading.', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
response =
response =
success: 'correct'
contents: 'mock grader response'
callback(response)
@problem.check()
expect(Logger.log).toHaveBeenCalledWith 'problem_graded', ['foo=1&bar=2', 'mock grader response'], @problem.url
expect(Logger.log).toHaveBeenCalledWith 'problem_graded', ['foo=1&bar=2', 'mock grader response'], @problem.id
it 'submit the answer for check', ->
spyOn $, 'postWithPrefix'

View File

@@ -248,7 +248,7 @@ class @Problem
@updateProgress response
else
@gentle_alert response.success
Logger.log 'problem_graded', [@answers, response.contents], @url
Logger.log 'problem_graded', [@answers, response.contents], @id
if not abort_submission
$.ajaxWithPrefix("#{@url}/problem_check", settings)
@@ -271,7 +271,7 @@ class @Problem
@el.removeClass 'showed'
else
@gentle_alert response.success
Logger.log 'problem_graded', [@answers, response.contents], @url
Logger.log 'problem_graded', [@answers, response.contents], @id
reset: =>
Logger.log 'problem_reset', @answers

View File

@@ -7,7 +7,7 @@ class @Hinter
constructor: (element) ->
@el = $(element).find('.crowdsource-wrapper')
@url = @el.data('url')
Logger.listen('problem_graded', @el.data('child-url'), @capture_problem)
Logger.listen('problem_graded', @el.data('child-id'), @capture_problem)
@render()
capture_problem: (event_type, data, element) =>

View File

@@ -81,7 +81,7 @@ class MongoKeyValueStore(InheritanceKeyValueStore):
else:
return self._data[key.field_name]
else:
raise InvalidScopeError(key.scope)
raise InvalidScopeError(key)
def set(self, key, value):
if key.scope == Scope.children:
@@ -94,7 +94,7 @@ class MongoKeyValueStore(InheritanceKeyValueStore):
else:
self._data[key.field_name] = value
else:
raise InvalidScopeError(key.scope)
raise InvalidScopeError(key)
def delete(self, key):
if key.scope == Scope.children:
@@ -108,7 +108,7 @@ class MongoKeyValueStore(InheritanceKeyValueStore):
else:
del self._data[key.field_name]
else:
raise InvalidScopeError(key.scope)
raise InvalidScopeError(key)
def has(self, key):
if key.scope in (Scope.children, Scope.parent):

View File

@@ -53,12 +53,12 @@ class SplitMongoKVS(InheritanceKeyValueStore):
raise KeyError()
else:
raise InvalidScopeError(key.scope)
raise InvalidScopeError(key)
def set(self, key, value):
# handle any special cases
if key.scope not in [Scope.children, Scope.settings, Scope.content]:
raise InvalidScopeError(key.scope)
raise InvalidScopeError(key)
if key.scope == Scope.content:
self._load_definition()
@@ -75,7 +75,7 @@ class SplitMongoKVS(InheritanceKeyValueStore):
def delete(self, key):
# handle any special cases
if key.scope not in [Scope.children, Scope.settings, Scope.content]:
raise InvalidScopeError(key.scope)
raise InvalidScopeError(key)
if key.scope == Scope.content:
self._load_definition()

View File

@@ -204,7 +204,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
descriptor.save()
return descriptor
render_template = lambda: ''
render_template = lambda template, context: u''
# TODO (vshnayder): we are somewhat architecturally confused in the loading code:
# 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...

View File

@@ -6,7 +6,7 @@ from lxml import etree
from datetime import datetime
from pkg_resources import resource_string
from .capa_module import ComplexEncoder
from .x_module import XModule
from .x_module import XModule, module_attr
from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
from .timeinfo import TimeInfo
@@ -106,7 +106,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
#We need to set the location here so the child modules can use it
self.runtime.set('location', self.location)
if (self.system.open_ended_grading_interface):
if (self.runtime.open_ended_grading_interface):
self.peer_gs = PeerGradingService(self.system.open_ended_grading_interface, self.system)
else:
self.peer_gs = MockPeerGradingService()
@@ -662,3 +662,19 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
return [self.system.load_item(self.link_to_location)]
else:
return []
# Proxy to PeerGradingModule so that external callers don't have to know if they're working
# with a module or a descriptor
closed = module_attr('closed')
get_instance_state = module_attr('get_instance_state')
get_next_submission = module_attr('get_next_submission')
is_student_calibrated = module_attr('is_student_calibrated')
peer_grading = module_attr('peer_grading')
peer_grading_closed = module_attr('peer_grading_closed')
peer_grading_problem = module_attr('peer_grading_problem')
peer_gs = module_attr('peer_gs')
query_data_for_location = module_attr('query_data_for_location')
save_calibration_essay = module_attr('save_calibration_essay')
save_grade = module_attr('save_grade')
show_calibration_essay = module_attr('show_calibration_essay')
_find_corresponding_module_for_location = module_attr('_find_corresponding_module_for_location')

View File

@@ -45,9 +45,6 @@ class SequenceModule(SequenceFields, XModule):
self.rendered = False
def get_instance_state(self):
return json.dumps({'position': self.position})
def get_html(self):
self.render()
return self.content

View File

@@ -9,6 +9,7 @@ Run like this:
import json
import os
import pprint
import unittest
from mock import Mock
@@ -18,6 +19,7 @@ from xblock.field_data import DictFieldData
from xmodule.x_module import ModuleSystem, XModuleDescriptor, XModuleMixin
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.error_module import ErrorDescriptor
# Location of common test DATA directory
@@ -54,18 +56,18 @@ def get_test_system(course_id=''):
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
render_template=lambda template, context: repr(context),
replace_urls=lambda html: str(html),
render_template=mock_render_template,
replace_urls=str,
user=Mock(is_staff=False),
filestore=Mock(),
debug=True,
hostname="edx.org",
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10, 'construct_callback' : Mock(side_effect="/")},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
xmodule_field_data=lambda descriptor: descriptor._field_data,
anonymous_student_id='student',
open_ended_grading_interface=open_ended_grading_interface,
course_id=course_id,
error_descriptor_class=ErrorDescriptor,
)
@@ -77,11 +79,21 @@ def get_test_descriptor_system():
load_item=Mock(),
resources_fs=Mock(),
error_tracker=Mock(),
render_template=lambda template, context: repr(context),
render_template=mock_render_template,
mixins=(InheritanceMixin, XModuleMixin),
)
def mock_render_template(*args, **kwargs):
"""
Pretty-print the args and kwargs.
Allows us to not depend on any actual template rendering mechanism,
while still returning a unicode object
"""
return pprint.pformat((args, kwargs)).decode()
class ModelsTest(unittest.TestCase):
def setUp(self):
pass

View File

@@ -350,11 +350,11 @@ class OpenEndedModuleTest(unittest.TestCase):
"""
Test storing answer with the open ended module.
"""
# Create a module with no state yet. Important that this start off as a blank slate.
test_module = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
saved_response = "Saved response."
submitted_response = "Submitted response."
@@ -753,28 +753,36 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
#Simulate a student saving an answer
html = module.handle_ajax("get_html", {})
module.save()
module.handle_ajax("save_answer", {"student_answer": self.answer})
module.save()
html = module.handle_ajax("get_html", {})
module.save()
#Mock a student submitting an assessment
assessment_dict = MockQueryDict()
assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
module.handle_ajax("save_assessment", assessment_dict)
module.save()
task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
rubric = module.handle_ajax("get_combined_rubric", {})
module.save()
#Move to the next step in the problem
module.handle_ajax("next_problem", {})
module.save()
self.assertEqual(module.current_task_number, 0)
html = module.get_html()
html = module.runtime.render(module, None, 'student_view').content
self.assertIsInstance(html, basestring)
rubric = module.handle_ajax("get_combined_rubric", {})
module.save()
self.assertIsInstance(rubric, basestring)
self.assertEqual(module.state, "assessing")
module.handle_ajax("reset", {})
module.save()
self.assertEqual(module.current_task_number, 0)
def test_open_ended_flow_correct(self):
@@ -789,31 +797,36 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
#Simulate a student saving an answer
module.handle_ajax("save_answer", {"student_answer": self.answer})
module.save()
status = module.handle_ajax("get_status", {})
module.save()
self.assertIsInstance(status, basestring)
#Mock a student submitting an assessment
assessment_dict = MockQueryDict()
assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
module.handle_ajax("save_assessment", assessment_dict)
module.save()
task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
#Move to the next step in the problem
try:
module.handle_ajax("next_problem", {})
module.save()
except GradingServiceError:
#This error is okay. We don't have a grading service to connect to!
pass
self.assertEqual(module.current_task_number, 1)
try:
module.get_html()
module.runtime.render(module, None, 'student_view')
except GradingServiceError:
#This error is okay. We don't have a grading service to connect to!
pass
#Try to get the rubric from the module
module.handle_ajax("get_combined_rubric", {})
module.save()
#Make a fake reply from the queue
queue_reply = {
@@ -832,22 +845,27 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
}
module.handle_ajax("check_for_score", {})
module.save()
#Update the module with the fake queue reply
module.handle_ajax("score_update", queue_reply)
module.save()
self.assertFalse(module.ready_to_reset)
self.assertEqual(module.current_task_number, 1)
#Get html and other data client will request
module.get_html()
module.runtime.render(module, None, 'student_view')
module.handle_ajax("skip_post_assessment", {})
module.save()
#Get all results
module.handle_ajax("get_combined_rubric", {})
module.save()
#reset the problem
module.handle_ajax("reset", {})
module.save()
self.assertEqual(module.state, "initial")
@@ -876,31 +894,37 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
"""
assessment = [0, 1]
module = self.get_module_from_location(self.problem_location, COURSE)
module.save()
#Simulate a student saving an answer
module.handle_ajax("save_answer", {"student_answer": self.answer})
module.save()
#Mock a student submitting an assessment
assessment_dict = MockQueryDict()
assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
module.handle_ajax("save_assessment", assessment_dict)
module.save()
task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
#Move to the next step in the problem
module.handle_ajax("next_problem", {})
module.save()
self.assertEqual(module.current_task_number, 0)
html = module.get_html()
self.assertTrue(isinstance(html, basestring))
html = module.runtime.render(module, None, 'student_view').content
self.assertIsInstance(html, basestring)
#Module should now be done
rubric = module.handle_ajax("get_combined_rubric", {})
self.assertTrue(isinstance(rubric, basestring))
module.save()
self.assertIsInstance(rubric, basestring)
self.assertEqual(module.state, "done")
#Try to reset, should fail because only 1 attempt is allowed
reset_data = json.loads(module.handle_ajax("reset", {}))
module.save()
self.assertEqual(reset_data['success'], False)
class OpenEndedModuleXmlImageUploadTest(unittest.TestCase, DummyModulestore):

View File

@@ -1,4 +1,3 @@
from ast import literal_eval
import json
import unittest
@@ -8,12 +7,11 @@ from mock import Mock, patch
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xblock.fragment import Fragment
from xmodule.error_module import NonStaffErrorDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from xmodule.conditional_module import ConditionalModule
from xmodule.tests import DATA_DIR, get_test_system
from xmodule.conditional_module import ConditionalDescriptor
from xmodule.tests import DATA_DIR, get_test_system, get_test_descriptor_system
ORG = 'test_org'
@@ -26,20 +24,15 @@ class DummySystem(ImportSystem):
def __init__(self, load_error_modules):
xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
course_id = "/".join([ORG, COURSE, 'test_run'])
course_dir = "test_dir"
policy = {}
error_tracker = Mock()
parent_tracker = Mock()
super(DummySystem, self).__init__(
xmlstore,
course_id,
course_dir,
policy,
error_tracker,
parent_tracker,
xmlstore=xmlstore,
course_id='/'.join([ORG, COURSE, 'test_run']),
course_dir='test_dir',
error_tracker=Mock(),
parent_tracker=Mock(),
load_error_modules=load_error_modules,
policy={},
)
def render_template(self, template, context):
@@ -59,52 +52,55 @@ class ConditionalFactory(object):
if the source_is_error_module flag is set, create a real ErrorModule for the source.
"""
descriptor_system = get_test_descriptor_system()
# construct source descriptor and module:
source_location = Location(["i4x", "edX", "conditional_test", "problem", "SampleProblem"])
if source_is_error_module:
# Make an error descriptor and module
source_descriptor = NonStaffErrorDescriptor.from_xml('some random xml data',
system,
org=source_location.org,
course=source_location.course,
error_msg='random error message')
source_module = source_descriptor.xmodule(system)
source_descriptor = NonStaffErrorDescriptor.from_xml(
'some random xml data',
system,
org=source_location.org,
course=source_location.course,
error_msg='random error message'
)
else:
source_descriptor = Mock()
source_descriptor.location = source_location
source_module = Mock()
source_descriptor.runtime = descriptor_system
# construct other descriptors:
child_descriptor = Mock()
cond_descriptor = Mock()
cond_descriptor.runtime = system
cond_descriptor.get_required_module_descriptors = lambda: [source_descriptor, ]
cond_descriptor.get_children = lambda: [child_descriptor, ]
cond_descriptor.xml_attributes = {"attempted": "true"}
child_descriptor._xmodule.student_view.return_value.content = u'<p>This is a secret</p>'
child_descriptor.displayable_items.return_value = [child_descriptor]
child_descriptor.runtime = descriptor_system
child_descriptor.xmodule_runtime = get_test_system()
# create child module:
child_module = Mock()
child_module.runtime = system
child_module.get_html.return_value = u'<p>This is a secret</p>'
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]
descriptor_system.load_item = {'child': child_descriptor, 'source': source_descriptor}.get
# construct conditional module:
cond_location = Location(["i4x", "edX", "conditional_test", "conditional", "SampleConditional"])
field_data = DictFieldData({'data': '<conditional/>', 'location': cond_location})
cond_module = ConditionalModule(
cond_descriptor,
system,
field_data = DictFieldData({
'data': '<conditional/>',
'xml_attributes': {'attempted': 'true'},
'children': ['child'],
})
cond_descriptor = ConditionalDescriptor(
descriptor_system,
field_data,
ScopeIds(None, None, cond_location, cond_location)
)
cond_descriptor.xmodule_runtime = system
system.get_module = lambda desc: desc
cond_descriptor.get_required_module_descriptors = Mock(return_value=[source_descriptor])
# return dict:
return {'cond_module': cond_module,
'source_module': source_module,
'child_module': child_module}
return {'cond_module': cond_descriptor,
'source_module': source_descriptor,
'child_module': child_descriptor}
class ConditionalModuleBasicTest(unittest.TestCase):
@@ -129,16 +125,20 @@ class ConditionalModuleBasicTest(unittest.TestCase):
modules = ConditionalFactory.create(self.test_system)
# because get_test_system returns the repr of the context dict passed to render_template,
# we reverse it here
html = modules['cond_module'].get_html()
html_dict = literal_eval(html)
self.assertEqual(html_dict['element_id'], 'i4x-edX-conditional_test-conditional-SampleConditional')
self.assertEqual(html_dict['id'], 'i4x://edX/conditional_test/conditional/SampleConditional')
self.assertEqual(html_dict['depends'], 'i4x-edX-conditional_test-problem-SampleProblem')
html = modules['cond_module'].runtime.render(modules['cond_module'], None, 'student_view').content
expected = modules['cond_module'].xmodule_runtime.render_template('conditional_ajax.html', {
'ajax_url': modules['cond_module'].xmodule_runtime.ajax_url,
'element_id': 'i4x-edX-conditional_test-conditional-SampleConditional',
'id': 'i4x://edX/conditional_test/conditional/SampleConditional',
'depends': 'i4x-edX-conditional_test-problem-SampleProblem',
})
self.assertEquals(expected, html)
def test_handle_ajax(self):
modules = ConditionalFactory.create(self.test_system)
modules['source_module'].is_attempted = "false"
ajax = json.loads(modules['cond_module'].handle_ajax('', ''))
modules['cond_module'].save()
print "ajax: ", ajax
html = ajax['html']
self.assertFalse(any(['This is a secret' in item for item in html]))
@@ -146,6 +146,7 @@ class ConditionalModuleBasicTest(unittest.TestCase):
# now change state of the capa problem to make it completed
modules['source_module'].is_attempted = "true"
ajax = json.loads(modules['cond_module'].handle_ajax('', ''))
modules['cond_module'].save()
print "post-attempt ajax: ", ajax
html = ajax['html']
self.assertTrue(any(['This is a secret' in item for item in html]))
@@ -157,6 +158,7 @@ class ConditionalModuleBasicTest(unittest.TestCase):
'''
modules = ConditionalFactory.create(self.test_system, source_is_error_module=True)
ajax = json.loads(modules['cond_module'].handle_ajax('', ''))
modules['cond_module'].save()
html = ajax['html']
self.assertFalse(any(['This is a secret' in item for item in html]))
@@ -196,7 +198,9 @@ class ConditionalModuleXmlTest(unittest.TestCase):
if isinstance(descriptor, Location):
location = descriptor
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
return descriptor.xmodule(self.test_system)
descriptor.xmodule_runtime = get_test_system()
descriptor.xmodule_runtime.get_module = inner_get_module
return descriptor
# edx - HarvardX
# cond_test - ER22x
@@ -209,20 +213,28 @@ class ConditionalModuleXmlTest(unittest.TestCase):
module = inner_get_module(location)
print "module: ", module
print "module.conditions_map: ", module.conditions_map
print "module children: ", module.get_children()
print "module display items (children): ", module.get_display_items()
html = module.get_html()
html = module.runtime.render(module, None, 'student_view').content
print "html type: ", type(html)
print "html: ", html
html_expect = "{'ajax_url': 'courses/course_id/modx/a_location', 'element_id': 'i4x-HarvardX-ER22x-conditional-condone', 'id': 'i4x://HarvardX/ER22x/conditional/condone', 'depends': 'i4x-HarvardX-ER22x-problem-choiceprob'}"
html_expect = module.xmodule_runtime.render_template(
'conditional_ajax.html',
{
'ajax_url': 'courses/course_id/modx/a_location',
'element_id': 'i4x-HarvardX-ER22x-conditional-condone',
'id': 'i4x://HarvardX/ER22x/conditional/condone',
'depends': 'i4x-HarvardX-ER22x-problem-choiceprob'
}
)
self.assertEqual(html, html_expect)
gdi = module.get_display_items()
print "gdi=", gdi
ajax = json.loads(module.handle_ajax('', ''))
module.save()
print "ajax: ", ajax
html = ajax['html']
self.assertFalse(any(['This is a secret' in item for item in html]))
@@ -234,6 +246,7 @@ class ConditionalModuleXmlTest(unittest.TestCase):
inner_module.save()
ajax = json.loads(module.handle_ajax('', ''))
module.save()
print "post-attempt ajax: ", ajax
html = ajax['html']
self.assertTrue(any(['This is a secret' in item for item in html]))

View File

@@ -10,6 +10,7 @@ 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 xblock.core import XBlock
from . import get_test_system
@@ -62,7 +63,8 @@ class CHModuleFactory(object):
"""
A factory method for making CHM's
"""
field_data = {'data': CHModuleFactory.sample_problem_xml}
# Should have a single child, but it doesn't matter what that child is
field_data = {'data': CHModuleFactory.sample_problem_xml, 'children': [None]}
if hints is not None:
field_data['hints'] = hints
@@ -106,7 +108,8 @@ class CHModuleFactory(object):
# Make the descriptor have a capa problem child.
capa_descriptor = MagicMock()
capa_descriptor.name = 'capa'
descriptor.get_children = lambda: [capa_descriptor]
capa_descriptor.displayable_items.return_value = [capa_descriptor]
descriptor.get_children.return_value = [capa_descriptor]
# Make a fake capa module.
capa_module = MagicMock()
@@ -128,7 +131,7 @@ class CHModuleFactory(object):
responder.compare_answer = compare_answer
capa_module.lcp.responders = {'responder0': responder}
capa_module.displayable_items = lambda: [capa_module]
capa_module.displayable_items.return_value = [capa_module]
system = get_test_system()
# Make the system have a marginally-functional get_module
@@ -137,8 +140,7 @@ class CHModuleFactory(object):
"""
A fake module-maker.
"""
if descriptor.name == 'capa':
return capa_module
return capa_module
system.get_module = fake_get_module
module = CrowdsourceHinterModule(descriptor, system, DictFieldData(field_data), Mock())
@@ -205,15 +207,15 @@ class VerticalWithModulesFactory(object):
return module
class FakeChild(object):
class FakeChild(XBlock):
"""
A fake Xmodule.
"""
def __init__(self):
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()
self.id = 'i4x://this/is/a/fake/id'
def get_html(self):
"""
@@ -243,7 +245,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module.get_display_items = fake_get_display_items
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)
self.assertTrue('i4x://this/is/a/fake/id' in out_html)
def test_gethtml_nochild(self):
"""

View File

@@ -30,8 +30,8 @@ class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = error_module.ErrorDescriptor.from_xml(
self.valid_xml, self.system, self.org, self.course, self.error_msg)
self.assertIsInstance(descriptor, error_module.ErrorDescriptor)
module = descriptor.xmodule(self.system)
context_repr = module.get_html()
descriptor.xmodule_runtime = self.system
context_repr = self.system.render(descriptor, None, 'student_view').content
self.assertIn(self.error_msg, context_repr)
self.assertIn(repr(self.valid_xml), context_repr)
@@ -44,8 +44,8 @@ class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
error_descriptor = error_module.ErrorDescriptor.from_descriptor(
descriptor, self.error_msg)
self.assertIsInstance(error_descriptor, error_module.ErrorDescriptor)
module = error_descriptor.xmodule(self.system)
context_repr = module.get_html()
error_descriptor.xmodule_runtime = self.system
context_repr = self.system.render(error_descriptor, None, 'student_view').content
self.assertIn(self.error_msg, context_repr)
self.assertIn(repr(descriptor), context_repr)
@@ -65,8 +65,8 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
def test_from_xml_render(self):
descriptor = error_module.NonStaffErrorDescriptor.from_xml(
self.valid_xml, self.system, self.org, self.course)
module = descriptor.xmodule(self.system)
context_repr = module.get_html()
descriptor.xmodule_runtime = self.system
context_repr = self.system.render(descriptor, None, 'student_view').content
self.assertNotIn(self.error_msg, context_repr)
self.assertNotIn(repr(self.valid_xml), context_repr)
@@ -79,7 +79,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
error_descriptor = error_module.NonStaffErrorDescriptor.from_descriptor(
descriptor, self.error_msg)
self.assertIsInstance(error_descriptor, error_module.ErrorDescriptor)
module = error_descriptor.xmodule(self.system)
context_repr = module.get_html()
error_descriptor.xmodule_runtime = self.system
context_repr = self.system.render(error_descriptor, None, 'student_view').content
self.assertNotIn(self.error_msg, context_repr)
self.assertNotIn(str(descriptor), context_repr)

View File

@@ -199,6 +199,7 @@ class PeerGradingModuleScoredTest(unittest.TestCase, DummyModulestore):
html = peer_grading.peer_grading()
self.assertIn("Peer-Graded", html)
class PeerGradingModuleLinkedTest(unittest.TestCase, DummyModulestore):
"""
Test peer grading that is linked to an open ended module.

View File

@@ -137,6 +137,6 @@ class ModuleProgressTest(unittest.TestCase):
'''
def test_xmodule_default(self):
'''Make sure default get_progress exists, returns None'''
xm = x_module.XModule(None, get_test_system(), DictFieldData({'location': 'a://b/c/d/e'}), Mock())
xm = x_module.XModule(Mock(), get_test_system(), DictFieldData({'location': 'a://b/c/d/e'}), Mock())
p = xm.get_progress()
self.assertEqual(p, None)

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,8 @@
Tests for the wrapping layer that provides the XBlock API using XModule/Descriptor
functionality
"""
# For tests, ignore access to protected members
# pylint: disable=protected-access
from nose.tools import assert_equal # pylint: disable=E0611
from unittest.case import SkipTest
@@ -17,6 +19,7 @@ from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
from xmodule.combined_open_ended_module import CombinedOpenEndedDescriptor
from xmodule.discussion_module import DiscussionDescriptor
from xmodule.error_module import ErrorDescriptor
from xmodule.gst_module import GraphicalSliderToolDescriptor
from xmodule.html_module import HtmlDescriptor
from xmodule.peer_grading_module import PeerGradingDescriptor
@@ -29,7 +32,7 @@ from xmodule.conditional_module import ConditionalDescriptor
from xmodule.randomize_module import RandomizeDescriptor
from xmodule.vertical_module import VerticalDescriptor
from xmodule.wrapper_module import WrapperDescriptor
from xmodule.tests import get_test_descriptor_system
from xmodule.tests import get_test_descriptor_system, mock_render_template
LEAF_XMODULES = (
AnnotatableDescriptor,
@@ -40,21 +43,20 @@ LEAF_XMODULES = (
HtmlDescriptor,
PeerGradingDescriptor,
PollDescriptor,
WordCloudDescriptor,
# This is being excluded because it has dependencies on django
#VideoDescriptor,
WordCloudDescriptor,
)
CONTAINER_XMODULES = (
CrowdsourceHinterDescriptor,
CourseDescriptor,
SequenceDescriptor,
ConditionalDescriptor,
CourseDescriptor,
CrowdsourceHinterDescriptor,
RandomizeDescriptor,
SequenceDescriptor,
VerticalDescriptor,
WrapperDescriptor,
CourseDescriptor,
)
# These modules are editable in studio yet
@@ -66,26 +68,24 @@ NOT_STUDIO_EDITABLE = (
class TestXBlockWrapper(object):
@property
def leaf_module_runtime(self):
runtime = ModuleSystem(
render_template=lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs),
render_template=mock_render_template,
anonymous_student_id='dummy_anonymous_student_id',
open_ended_grading_interface={},
static_url='/static',
ajax_url='dummy_ajax_url',
xmodule_field_data=lambda d: d._field_data,
get_module=Mock(),
replace_urls=Mock(),
track_function=Mock(),
error_descriptor_class=ErrorDescriptor,
)
return runtime
def leaf_descriptor(self, descriptor_cls):
location = 'i4x://org/course/category/name'
runtime = get_test_descriptor_system()
runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
return runtime.construct_xblock_from_class(
descriptor_cls,
ScopeIds(None, descriptor_cls.__name__, location, location),
@@ -93,7 +93,10 @@ class TestXBlockWrapper(object):
)
def leaf_module(self, descriptor_cls):
return self.leaf_descriptor(descriptor_cls).xmodule(self.leaf_module_runtime)
"""Returns a descriptor that is ready to proxy as an xmodule"""
descriptor = self.leaf_descriptor(descriptor_cls)
descriptor.xmodule_runtime = self.leaf_module_runtime
return descriptor
def container_module_runtime(self, depth):
runtime = self.leaf_module_runtime
@@ -104,10 +107,16 @@ class TestXBlockWrapper(object):
runtime.position = 2
return runtime
def container_descriptor(self, descriptor_cls):
def container_descriptor(self, descriptor_cls, depth):
"""Return an instance of `descriptor_cls` with `depth` levels of children"""
location = 'i4x://org/course/category/name'
runtime = get_test_descriptor_system()
runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
if depth == 0:
runtime.load_item.side_effect = lambda x: self.leaf_module(HtmlDescriptor)
else:
runtime.load_item.side_effect = lambda x: self.container_module(VerticalDescriptor, depth - 1)
return runtime.construct_xblock_from_class(
descriptor_cls,
ScopeIds(None, descriptor_cls.__name__, location, location),
@@ -117,7 +126,10 @@ class TestXBlockWrapper(object):
)
def container_module(self, descriptor_cls, depth):
return self.container_descriptor(descriptor_cls).xmodule(self.container_module_runtime(depth))
"""Returns a descriptor that is ready to proxy as an xmodule"""
descriptor = self.container_descriptor(descriptor_cls, depth)
descriptor.xmodule_runtime = self.container_module_runtime(depth)
return descriptor
class TestStudentView(TestXBlockWrapper):
@@ -131,9 +143,11 @@ class TestStudentView(TestXBlockWrapper):
# Check that when an xmodule is instantiated from descriptor_cls
# 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.runtime.render(xmodule, None, 'student_view').content)
descriptor = self.leaf_module(descriptor_cls)
assert_equal(
descriptor._xmodule.get_html(),
descriptor.runtime.render(descriptor, None, 'student_view').content
)
# Test that for all container XModule Descriptors,
# their corresponding XModule renders the same thing using student_view
@@ -147,13 +161,15 @@ class TestStudentView(TestXBlockWrapper):
yield self.check_student_view_container_node_mixed, descriptor_cls
yield self.check_student_view_container_node_xblocks_only, descriptor_cls
# Check that when an xmodule is generated from descriptor_cls
# with only xmodule children, it generates the same html from student_view
# 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.runtime.render(xmodule, None, 'student_view').content)
descriptor = self.container_module(descriptor_cls, 2)
assert_equal(
descriptor._xmodule.get_html(),
descriptor.runtime.render(descriptor, 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
@@ -206,7 +222,7 @@ class TestStudioView(TestXBlockWrapper):
if descriptor_cls in NOT_STUDIO_EDITABLE:
raise SkipTest(descriptor_cls.__name__ + "is not editable in studio")
descriptor = self.container_descriptor(descriptor_cls)
descriptor = self.container_descriptor(descriptor_cls, 2)
assert_equal(descriptor.get_html(), descriptor.runtime.render(descriptor, None, 'studio_view').content)
# Check that when a descriptor is generated from descriptor_cls

View File

@@ -157,10 +157,6 @@ class VideoModule(VideoFields, XModule):
log.debug(u"DISPATCH {0}".format(dispatch))
raise Http404()
def get_instance_state(self):
"""Return information about state (position)."""
return json.dumps({'position': self.position})
def get_html(self):
caption_asset_path = "/static/subs/"

View File

@@ -1,7 +1,9 @@
import logging
import yaml
import os
import sys
from functools import partial
from lxml import etree
from collections import namedtuple
from pkg_resources import resource_listdir, resource_string, resource_isdir
@@ -13,6 +15,7 @@ from xblock.core import XBlock
from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String
from xblock.fragment import Fragment
from xblock.runtime import Runtime
from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore.locator import BlockUsageLocator
log = logging.getLogger(__name__)
@@ -214,78 +217,6 @@ class XModuleMixin(XBlockMixin):
return child
return None
class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-method
""" Implements a generic learning module.
Subclasses must at a minimum provide a definition for get_html in order
to be displayed to users.
See the HTML module for a simple example.
"""
# The default implementation of get_icon_class returns the icon_class
# attribute of the class
#
# This attribute can be overridden by subclasses, and
# the function can also be overridden if the icon class depends on the data
# in the module
icon_class = 'other'
def __init__(self, descriptor, *args, **kwargs):
"""
Construct a new xmodule
runtime: An XBlock runtime allowing access to external resources
descriptor: the XModuleDescriptor that this module is an instance of.
field_data: A dictionary-like object that maps field names to values
for those fields.
"""
super(XModule, self).__init__(*args, **kwargs)
self.system = self.runtime
self.descriptor = descriptor
self._loaded_children = None
def get_children(self):
"""
Return module instances for all the children of this module.
"""
if self._loaded_children is None:
child_descriptors = self.get_child_descriptors()
# This deliberately uses system.get_module, rather than runtime.get_block,
# because we're looking at XModule children, rather than XModuleDescriptor children.
# That means it can use the deprecated XModule apis, rather than future XBlock apis
# TODO: Once we're in a system where this returns a mix of XModuleDescriptors
# and XBlocks, we're likely to have to change this more
children = [self.system.get_module(descriptor) for descriptor in child_descriptors]
# get_module returns None if the current user doesn't have access
# to the location.
self._loaded_children = [c for c in children if c is not None]
return self._loaded_children
def __unicode__(self):
return '<x_module(id={0})>'.format(self.id)
def get_child_descriptors(self):
"""
Returns the descriptors of the child modules
Overriding this changes the behavior of get_children and
anything that uses get_children, such as get_display_items.
This method will not instantiate the modules of the children
unless absolutely necessary, so it is cheaper to call than get_children
These children will be the same children returned by the
descriptor unless descriptor.has_dynamic_children() is true.
"""
return self.descriptor.get_children()
def get_icon_class(self):
"""
Return a css class identifying this module in the context of an icon
@@ -333,11 +264,156 @@ class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-me
"""
return None
def bind_for_student(self, xmodule_runtime, field_data):
"""
Set up this XBlock to act as an XModule instead of an XModuleDescriptor.
:param xmodule_runtime: the runtime to use when accessing student facing methods
:type xmodule_runtime: :class:`ModuleSystem`
:param field_data: The :class:`FieldData` to use for all subsequent data access
:type field_data: :class:`FieldData`
"""
# pylint: disable=attribute-defined-outside-init
self.xmodule_runtime = xmodule_runtime
self._field_data = field_data
class ProxyAttribute(object):
"""
A (python) descriptor that proxies attribute access.
For example:
class Foo(object):
def __init__(self, value):
self.foo_attr = value
class Bar(object):
foo = Foo('x')
foo_attr = ProxyAttribute('foo', 'foo_attr')
bar = Bar()
assert bar.foo_attr == 'x'
bar.foo_attr = 'y'
assert bar.foo.foo_attr == 'y'
del bar.foo_attr
assert not hasattr(bar.foo, 'foo_attr')
"""
def __init__(self, source, name):
"""
:param source: The name of the attribute to proxy to
:param name: The name of the attribute to proxy
"""
self._source = source
self._name = name
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(getattr(instance, self._source), self._name)
def __set__(self, instance, value):
setattr(getattr(instance, self._source), self._name, value)
def __delete__(self, instance):
delattr(getattr(instance, self._source), self._name)
module_attr = partial(ProxyAttribute, '_xmodule') # pylint: disable=invalid-name
descriptor_attr = partial(ProxyAttribute, 'descriptor') # pylint: disable=invalid-name
module_runtime_attr = partial(ProxyAttribute, 'xmodule_runtime') # pylint: disable=invalid-name
class XModule(XModuleMixin, HTMLSnippet, XBlock): # pylint: disable=abstract-method
""" Implements a generic learning module.
Subclasses must at a minimum provide a definition for get_html in order
to be displayed to users.
See the HTML module for a simple example.
"""
# The default implementation of get_icon_class returns the icon_class
# attribute of the class
#
# This attribute can be overridden by subclasses, and
# the function can also be overridden if the icon class depends on the data
# in the module
icon_class = 'other'
has_score = descriptor_attr('has_score')
_field_data_cache = descriptor_attr('_field_data_cache')
_field_data = descriptor_attr('_field_data')
_dirty_fields = descriptor_attr('_dirty_fields')
def __init__(self, descriptor, *args, **kwargs):
"""
Construct a new xmodule
runtime: An XBlock runtime allowing access to external resources
descriptor: the XModuleDescriptor that this module is an instance of.
field_data: A dictionary-like object that maps field names to values
for those fields.
"""
# Set the descriptor first so that we can proxy to it
self.descriptor = descriptor
super(XModule, self).__init__(*args, **kwargs)
self._loaded_children = None
self.system = self.runtime
def __unicode__(self):
return u'<x_module(id={0})>'.format(self.id)
def handle_ajax(self, _dispatch, _data):
""" dispatch is last part of the URL.
data is a dictionary-like object with the content of the request"""
return ""
return u""
def get_children(self):
"""
Return module instances for all the children of this module.
"""
if self._loaded_children is None:
child_descriptors = self.get_child_descriptors()
# This deliberately uses system.get_module, rather than runtime.get_block,
# because we're looking at XModule children, rather than XModuleDescriptor children.
# That means it can use the deprecated XModule apis, rather than future XBlock apis
# TODO: Once we're in a system where this returns a mix of XModuleDescriptors
# and XBlocks, we're likely to have to change this more
children = [self.system.get_module(descriptor) for descriptor in child_descriptors]
# get_module returns None if the current user doesn't have access
# to the location.
self._loaded_children = [c for c in children if c is not None]
return self._loaded_children
def get_child_descriptors(self):
"""
Returns the descriptors of the child modules
Overriding this changes the behavior of get_children and
anything that uses get_children, such as get_display_items.
This method will not instantiate the modules of the children
unless absolutely necessary, so it is cheaper to call than get_children
These children will be the same children returned by the
descriptor unless descriptor.has_dynamic_children() is true.
"""
return self.descriptor.get_children()
def displayable_items(self):
"""
Returns list of displayable modules contained by this module. If this
module is visible, should return [self].
"""
return [self.descriptor]
# ~~~~~~~~~~~~~~~ XBlock API Wrappers ~~~~~~~~~~~~~~~~
def student_view(self, context):
@@ -489,24 +565,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
# by following previous until None
# definition_locator is only used by mongostores which separate definitions from blocks
self.edited_by = self.edited_on = self.previous_version = self.update_version = self.definition_locator = None
self._child_instances = None
def xmodule(self, system):
"""
Returns an XModule.
system: Module system
"""
# save any field changes
module = system.construct_xblock_from_class(
self.module_class,
descriptor=self,
scope_ids=self.scope_ids,
field_data=system.xmodule_field_data(self),
)
module.save()
return module
self.xmodule_runtime = None
def has_dynamic_children(self):
"""
@@ -644,6 +703,42 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
return metadata_fields
# ~~~~~~~~~~~~~~~ XModule Indirection ~~~~~~~~~~~~~~~~
@property
def _xmodule(self):
"""
Returns the XModule corresponding to this descriptor. Expects that the system
already supports all of the attributes needed by xmodules
"""
assert self.xmodule_runtime is not None
assert self.xmodule_runtime.error_descriptor_class is not None
if self.xmodule_runtime.xmodule_instance is None:
try:
self.xmodule_runtime.xmodule_instance = self.xmodule_runtime.construct_xblock_from_class(
self.module_class,
descriptor=self,
scope_ids=self.scope_ids,
field_data=self._field_data,
)
self.xmodule_runtime.xmodule_instance.save()
except Exception: # pylint: disable=broad-except
log.exception('Error creating xmodule')
descriptor = self.xmodule_runtime.error_descriptor_class.from_descriptor(
self,
error_msg=exc_info_to_str(sys.exc_info())
)
self.xmodule_runtime.xmodule_instance = descriptor._xmodule # pylint: disable=protected-access
return self.xmodule_runtime.xmodule_instance
displayable_items = module_attr('displayable_items')
get_display_items = module_attr('get_display_items')
get_icon_class = module_attr('get_icon_class')
get_progress = module_attr('get_progress')
get_score = module_attr('get_score')
handle_ajax = module_attr('handle_ajax')
max_score = module_attr('max_score')
student_view = module_attr('student_view')
# ~~~~~~~~~~~~~~~ XBlock API Wrappers ~~~~~~~~~~~~~~~~
def studio_view(self, _context):
"""
@@ -766,6 +861,13 @@ class DescriptorSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable
result['default_value'] = field.to_json(field.default)
return result
def render(self, block, context, view_name):
if isinstance(block, (XModule, XModuleDescriptor)) and view_name == 'student_view':
assert block.xmodule_runtime is not None
return block.xmodule_runtime.render(block._xmodule, context, view_name)
else:
return super(DescriptorSystem, self).render(block, context, view_name)
class XMLParsingSystem(DescriptorSystem):
def __init__(self, process_xml, policy, **kwargs):
@@ -795,12 +897,12 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
"""
def __init__(
self, static_url, ajax_url, track_function, get_module, render_template,
replace_urls, xmodule_field_data, user=None, filestore=None,
replace_urls, user=None, filestore=None,
debug=False, hostname="", xqueue=None, publish=None, node_path="",
anonymous_student_id='', course_id=None,
open_ended_grading_interface=None, s3_interface=None,
cache=None, can_execute_unsafe_code=None, replace_course_urls=None,
replace_jump_to_id_urls=None, **kwargs):
replace_jump_to_id_urls=None, error_descriptor_class=None, **kwargs):
"""
Create a closure around the system environment.
@@ -842,9 +944,6 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
publish(event) - A function that allows XModules to publish events (such as grade changes)
xmodule_field_data - A function that constructs a field_data for an xblock from its
corresponding descriptor
cache - A cache object with two methods:
.get(key) returns an object from the cache or None.
.set(key, value, timeout_secs=None) stores a value in the cache with a timeout.
@@ -852,6 +951,8 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
can_execute_unsafe_code - A function returning a boolean, whether or
not to allow the execution of unsafe, unsandboxed code.
error_descriptor_class - The class to use to render XModules with errors
"""
# Right now, usage_store is unused, and field_data is always supplanted
@@ -873,7 +974,6 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
self.anonymous_student_id = anonymous_student_id
self.course_id = course_id
self.user_is_staff = user is not None and user.is_staff
self.xmodule_field_data = xmodule_field_data
if publish is None:
publish = lambda e: None
@@ -887,6 +987,8 @@ class ModuleSystem(ConfigurableFragmentWrapper, Runtime): # pylint: disable=abs
self.can_execute_unsafe_code = can_execute_unsafe_code or (lambda: False)
self.replace_course_urls = replace_course_urls
self.replace_jump_to_id_urls = replace_jump_to_id_urls
self.error_descriptor_class = error_descriptor_class
self.xmodule_instance = None
def get(self, attr):
""" provide uniform access to attributes (like etree)."""

View File

@@ -1,7 +1,7 @@
<sequential>
<video display_name="default" youtube_id_0_75="JMD_ifUUfsU" youtube_id_1_0="OEoXaMPEzfM" youtube_id_1_25="AKqURZnYqpk" youtube_id_1_5="DYpADpL7jAY" name="sample_video"/>
<video url_name="separate_file_video"/>
<poll_question name="T1_changemind_poll_foo_2" display_name="Change your answer" reset="false">
<video display_name="default" youtube_id_0_75="JMD_ifUUfsU" youtube_id_1_0="OEoXaMPEzfM" youtube_id_1_25="AKqURZnYqpk" youtube_id_1_5="DYpADpL7jAY" name="sample_video"/>
<video url_name="separate_file_video"/>
<poll_question name="T1_changemind_poll_foo_2" display_name="Change your answer" reset="false">
<p>Have you changed your mind?</p>
<answer id="yes">Yes</answer>
<answer id="no">No</answer>

View File

@@ -305,9 +305,9 @@ def progress_summary(student, request, course, field_data_cache):
graded = section_module.graded
scores = []
module_creator = section_module.system.get_module
module_creator = section_module.xmodule_runtime.get_module
for module_descriptor in yield_dynamic_descriptor_descendents(section_module.descriptor, module_creator):
for module_descriptor in yield_dynamic_descriptor_descendents(section_module, module_creator):
course_id = course.id
(correct, total) = get_score(course_id, student, module_descriptor, module_creator, field_data_cache)

View File

@@ -289,7 +289,7 @@ class DjangoKeyValueStore(KeyValueStore):
def get(self, key):
if key.scope not in self._allowed_scopes:
raise InvalidScopeError(key.scope)
raise InvalidScopeError(key)
field_object = self._field_data_cache.find(key)
if field_object is None:
@@ -320,7 +320,7 @@ class DjangoKeyValueStore(KeyValueStore):
for field in kv_dict:
# Check field for validity
if field.scope not in self._allowed_scopes:
raise InvalidScopeError(field.scope)
raise InvalidScopeError(field)
# If the field is valid and isn't already in the dictionary, add it.
field_object = self._field_data_cache.find_or_create(field)
@@ -352,7 +352,7 @@ class DjangoKeyValueStore(KeyValueStore):
def delete(self, key):
if key.scope not in self._allowed_scopes:
raise InvalidScopeError(key.scope)
raise InvalidScopeError(key)
field_object = self._field_data_cache.find(key)
if field_object is None:
@@ -368,7 +368,7 @@ class DjangoKeyValueStore(KeyValueStore):
def has(self, key):
if key.scope not in self._allowed_scopes:
raise InvalidScopeError(key.scope)
raise InvalidScopeError(key)
field_object = self._field_data_cache.find(key)
if field_object is None:

View File

@@ -219,6 +219,9 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
if not has_access(user, descriptor, 'load', course_id):
return None
student_data = DbModel(DjangoKeyValueStore(field_data_cache))
descriptor._field_data = lms_field_data(descriptor._field_data, student_data)
# Setup system context for module instance
ajax_url = reverse(
'modx_dispatch',
@@ -294,10 +297,6 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
position, wrap_xmodule_display, grade_bucket_type,
static_asset_path)
def xmodule_field_data(descriptor):
student_data = DbModel(DjangoKeyValueStore(field_data_cache))
return lms_field_data(descriptor._field_data, student_data)
def publish(event):
"""A function that allows XModules to publish events. This only supports grade changes right now."""
if event.get('event_name') != 'grade':
@@ -405,7 +404,6 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
jump_to_id_base_url=reverse('jump_to_id', kwargs={'course_id': course_id, 'module_id': ''})
),
node_path=settings.NODE_PATH,
xmodule_field_data=xmodule_field_data,
publish=publish,
anonymous_student_id=unique_id_for_user(user),
course_id=course_id,
@@ -426,27 +424,17 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
make_psychometrics_data_update_handler(course_id, user, descriptor.location.url())
)
try:
module = descriptor.xmodule(system)
except:
log.exception("Error creating module from descriptor {0}".format(descriptor))
# make an ErrorDescriptor -- assuming that the descriptor's system is ok
if has_access(user, descriptor.location, 'staff', course_id):
err_descriptor_class = ErrorDescriptor
else:
err_descriptor_class = NonStaffErrorDescriptor
err_descriptor = err_descriptor_class.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
)
# Make an error module
return err_descriptor.xmodule(system)
system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id))
return module
# make an ErrorDescriptor -- assuming that the descriptor's system is ok
if has_access(user, descriptor.location, 'staff', course_id):
system.error_descriptor_class = ErrorDescriptor
else:
system.error_descriptor_class = NonStaffErrorDescriptor
descriptor.xmodule_runtime = system
descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id)
return descriptor
def find_target_student_module(request, user_id, course_id, mod_id):

View File

@@ -16,7 +16,7 @@ from student.tests.factories import UserFactory, CourseEnrollmentFactory
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xblock.field_data import DictFieldData
from xblock.fields import Scope
from xmodule.tests import get_test_system
from xmodule.tests import get_test_system, get_test_descriptor_system
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@@ -49,11 +49,26 @@ class BaseTestXmodule(ModuleStoreTestCase):
DATA = ''
MODEL_DATA = {'data': '<some_module></some_module>'}
def xmodule_field_data(self, descriptor):
field_data = {}
field_data.update(self.MODEL_DATA)
student_data = DictFieldData(field_data)
return lms_field_data(descriptor._field_data, student_data)
def new_module_runtime(self):
"""
Generate a new ModuleSystem that is minimally set up for testing
"""
runtime = get_test_system(course_id=self.course.id)
# When asked for a module out of a descriptor, just create a new xmodule runtime,
# and inject it into the descriptor
def get_module(descr):
descr.xmodule_runtime = self.new_module_runtime()
return descr
runtime.get_module = get_module
return runtime
def new_descriptor_runtime(self):
runtime = get_test_descriptor_system()
runtime.get_block = modulestore().get_item
return runtime
def setUp(self):
@@ -87,16 +102,15 @@ class BaseTestXmodule(ModuleStoreTestCase):
data=self.DATA
)
self.runtime = get_test_system(course_id=self.course.id)
# Allow us to assert that the template was called in the same way from
# different code paths while maintaining the type returned by render_template
self.runtime.render_template = lambda template, context: u'{!r}, {!r}'.format(template, sorted(context.items()))
self.runtime = self.new_descriptor_runtime()
self.runtime.xmodule_field_data = self.xmodule_field_data
field_data = {}
field_data.update(self.MODEL_DATA)
student_data = DictFieldData(field_data)
self.item_descriptor._field_data = lms_field_data(self.item_descriptor._field_data, student_data)
self.runtime.get_module = lambda descr: descr.xmodule(self.runtime)
self.item_module = self.item_descriptor.xmodule(self.runtime)
self.item_descriptor.xmodule_runtime = self.new_module_runtime()
self.item_module = self.item_descriptor
self.item_url = Location(self.item_module.location).url()
@@ -119,7 +133,11 @@ class BaseTestXmodule(ModuleStoreTestCase):
class XModuleRenderingTestBase(BaseTestXmodule):
def setUp(self):
super(XModuleRenderingTestBase, self).setUp()
self.runtime.render_template = render_to_string
def new_module_runtime(self):
"""
Create a runtime that actually does html rendering
"""
runtime = super(XModuleRenderingTestBase, self).new_module_runtime()
runtime.render_template = render_to_string
return runtime

View File

@@ -39,7 +39,7 @@ class TestLTI(BaseTestXmodule):
u'oauth_consumer_key': u'',
u'oauth_signature_method': u'HMAC-SHA1',
u'oauth_version': u'1.0',
u'user_id': self.runtime.anonymous_student_id,
u'user_id': self.item_descriptor.xmodule_runtime.anonymous_student_id,
u'role': u'student',
u'oauth_signature': mocked_decoded_signature
}
@@ -69,12 +69,14 @@ class TestLTI(BaseTestXmodule):
"""
Makes sure that all parameters extracted.
"""
self.runtime.render_template = lambda template, context: context
generated_context = self.item_module.get_html()
generated_context = self.item_module.runtime.render(self.item_module, None, 'student_view').content
expected_context = {
'input_fields': self.correct_headers,
'element_class': self.item_module.location.category,
'element_id': self.item_module.location.html_id(),
'launch_url': 'http://www.example.com', # default value
}
self.assertDictEqual(generated_context, expected_context)
self.assertEqual(
generated_context,
self.runtime.render_template('lti.html', expected_context),
)

View File

@@ -74,7 +74,7 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
)
# get the rendered HTML output which should have the rewritten link
html = module.system.render(module, None, 'student_view').content
html = module.runtime.render(module, None, 'student_view').content
# See if the url got rewritten to the target link
# note if the URL mapping changes then this assertion will break

View File

@@ -17,10 +17,13 @@ class TestTimeLimitModuleRendering(XModuleRenderingTestBase):
"""
def test_with_children(self):
block = ItemFactory.create(category='timelimit')
block.xmodule_runtime = self.new_module_runtime()
ItemFactory.create(category='html', data='<html>This is just text</html>', parent=block)
assert_student_view(block, self.runtime.render(block.xmodule(self.runtime), None, 'student_view'))
assert_student_view(block, self.runtime.render(block, None, 'student_view'))
def test_without_children(self):
block = ItemFactory.create(category='timelimit')
assert_student_view(block, self.runtime.render(block.xmodule(self.runtime), None, 'student_view'))
block.xmodule_runtime = self.new_module_runtime()
assert_student_view(block, self.runtime.render(block, None, 'student_view'))

View File

@@ -13,14 +13,6 @@ class TestVideo(BaseTestXmodule):
CATEGORY = "video"
DATA = SOURCE_XML
def setUp(self):
# Since the VideoDescriptor changes `self._field_data`,
# we need to instantiate `self.item_module` through
# `self.item_descriptor` rather than directly constructing it
super(TestVideo, self).setUp()
self.item_module = self.item_descriptor.xmodule(self.runtime)
self.item_module.runtime.render_template = lambda template, context: context
def test_handle_ajax_dispatch(self):
responses = {
user.username: self.clients[user.username].post(
@@ -40,7 +32,7 @@ class TestVideo(BaseTestXmodule):
def test_video_constructor(self):
"""Make sure that all parameters extracted correclty from xml"""
context = self.item_module.get_html()
context = self.item_module.runtime.render(self.item_module, None, 'student_view').content
sources = {
'main': u'example.mp4',
@@ -53,7 +45,7 @@ class TestVideo(BaseTestXmodule):
'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/static/subs/',
'show_captions': 'true',
'display_name': 'A Name',
'display_name': u'A Name',
'end': 3610.0,
'id': self.item_module.location.html_id(),
'sources': sources,
@@ -66,8 +58,10 @@ class TestVideo(BaseTestXmodule):
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
}
self.maxDiff = None
self.assertEqual(context, expected_context)
self.assertEqual(
context,
self.item_module.xmodule_runtime.render_template('video.html', expected_context)
)
class TestVideoNonYouTube(TestVideo):
@@ -93,24 +87,24 @@ class TestVideoNonYouTube(TestVideo):
the template generates an empty string for the YouTube streams.
"""
sources = {
u'main': u'example.mp4',
'main': u'example.mp4',
u'mp4': u'example.mp4',
u'webm': u'example.webm',
u'ogv': u'example.ogv'
}
context = self.item_module.get_html()
context = self.item_module.runtime.render(self.item_module, None, 'student_view').content
expected_context = {
'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/static/subs/',
'show_captions': 'true',
'display_name': 'A Name',
'display_name': u'A Name',
'end': 3610.0,
'id': self.item_module.location.html_id(),
'sources': sources,
'start': 3603.0,
'sub': 'a_sub_file.srt.sjson',
'sub': u'a_sub_file.srt.sjson',
'track': '',
'youtube_streams': '1.00:OEoXaMPEzfM',
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
@@ -118,4 +112,7 @@ class TestVideoNonYouTube(TestVideo):
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
}
self.assertEqual(context, expected_context)
self.assertEqual(
context,
self.item_module.xmodule_runtime.render_template('video.html', expected_context)
)

View File

@@ -22,7 +22,7 @@ from django.conf import settings
from xmodule.video_module import VideoDescriptor, _create_youtube_string
from xmodule.modulestore import Location
from xmodule.tests import get_test_system, LogicTest
from xmodule.tests import get_test_system, LogicTest, get_test_descriptor_system
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
@@ -57,23 +57,18 @@ class VideoFactory(object):
field_data = {'data': VideoFactory.sample_problem_xml_youtube,
'location': location}
system = get_test_system()
system.render_template = lambda template, context: context
system = get_test_descriptor_system()
descriptor = VideoDescriptor(system, DictFieldData(field_data), ScopeIds(None, None, None, None))
module = descriptor.xmodule(system)
return module
descriptor.xmodule_runtime = get_test_system()
return descriptor
class VideoModuleUnitTest(unittest.TestCase):
"""Unit tests for Video Xmodule."""
def test_video_get_html(self):
"""Make sure that all parameters extracted correclty from xml"""
module = VideoFactory.create()
module.runtime.render_template = lambda template, context: context
sources = {
'main': 'example.mp4',
@@ -99,14 +94,10 @@ class VideoModuleUnitTest(unittest.TestCase):
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
}
self.assertEqual(module.get_html(), expected_context)
def test_video_instance_state(self):
module = VideoFactory.create()
self.assertDictEqual(
json.loads(module.get_instance_state()),
{'position': 0})
self.assertEqual(
module.runtime.render(module, None, 'student_view').content,
module.runtime.render_template('video.html', expected_context)
)
class VideoModuleLogicTest(LogicTest):

View File

@@ -245,7 +245,7 @@ class TestWordCloud(BaseTestXmodule):
fragment = self.runtime.render(self.item_module, None, 'student_view')
expected_context = {
'ajax_url': self.item_module.system.ajax_url,
'ajax_url': self.item_module.xmodule_runtime.ajax_url,
'element_class': self.item_module.location.category,
'element_id': self.item_module.location.html_id(),
'num_inputs': 5, # default value

View File

@@ -142,7 +142,7 @@ def redirect_to_course_position(course_module):
the first child.
"""
urlargs = {'course_id': course_module.descriptor.id}
urlargs = {'course_id': course_module.id}
chapter = get_current_child(course_module)
if chapter is None:
# oops. Something bad has happened.

View File

@@ -310,6 +310,7 @@ def rescore_problem_module_state(module_descriptor, student_module, xmodule_inst
raise UpdateProblemModuleStateError(msg)
result = instance.rescore_problem()
instance.save()
if 'success' not in result:
# don't consider these fatal, but false means that the individual call didn't complete:
TASK_LOG.warning(u"error processing rescore call for course {course}, problem {loc} and student {student}: "

View File

@@ -13,8 +13,6 @@ from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
import datetime
from xblock.field_data import DictFieldData
log = logging.getLogger(__name__)
NOTIFICATION_CACHE_TIME = 300
@@ -70,7 +68,6 @@ def peer_grading_notifications(course, user):
get_module = None,
render_template=render_to_string,
replace_urls=None,
xmodule_field_data=DictFieldData({}),
)
peer_gs = peer_grading_service.PeerGradingService(settings.OPEN_ENDED_GRADING_INTERFACE, system)
pending_grading = False
@@ -132,7 +129,6 @@ def combined_notifications(course, user):
get_module = None,
render_template=render_to_string,
replace_urls=None,
xmodule_field_data=DictFieldData({})
)
#Initialize controller query service using our mock system
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, system)

View File

@@ -9,8 +9,6 @@ from xmodule.open_ended_grading_classes.grading_service_module import GradingSer
from django.conf import settings
from django.http import HttpResponse, Http404
from xblock.field_data import DictFieldData
from courseware.access import has_access
from util.json_request import expect_json
from xmodule.course_module import CourseDescriptor
@@ -76,7 +74,6 @@ class StaffGradingService(GradingService):
get_module = None,
render_template=render_to_string,
replace_urls=None,
xmodule_field_data=DictFieldData({})
)
super(StaffGradingService, self).__init__(config)
self.url = config['url'] + config['staff_grading']

View File

@@ -16,6 +16,7 @@ from xmodule.open_ended_grading_classes import peer_grading_service, controller_
from xmodule import peer_grading_module
from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xblock.fields import ScopeIds
from open_ended_grading import staff_grading_service, views, utils
@@ -251,13 +252,14 @@ class TestPeerGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
get_module=None,
render_template=render_to_string,
replace_urls=None,
xmodule_field_data=lambda d: d._field_data,
s3_interface=test_util_open_ended.S3_INTERFACE,
open_ended_grading_interface=test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
mixins=settings.XBLOCK_MIXINS,
error_descriptor_class=ErrorDescriptor,
)
self.descriptor = peer_grading_module.PeerGradingDescriptor(self.system, field_data, ScopeIds(None, None, None, None))
self.peer_module = self.descriptor.xmodule(self.system)
self.descriptor.xmodule_runtime = self.system
self.peer_module = self.descriptor
self.peer_module.peer_gs = self.mock_service
self.logout()

View File

@@ -36,7 +36,6 @@ system = ModuleSystem(
get_module=None,
render_template=render_to_string,
replace_urls=None,
xmodule_field_data=DictFieldData({}),
)