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:
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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/"
|
||||
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user