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:
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}: "
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ system = ModuleSystem(
|
||||
get_module=None,
|
||||
render_template=render_to_string,
|
||||
replace_urls=None,
|
||||
xmodule_field_data=DictFieldData({}),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user