Delete javascriptinput and javascriptresponse.
TNL-6034
This commit is contained in:
@@ -9,7 +9,6 @@ Module containing the problem elements which render into input objects
|
||||
- textbox (aka codeinput)
|
||||
- schematic
|
||||
- choicegroup (aka radiogroup, checkboxgroup)
|
||||
- javascriptinput
|
||||
- imageinput (for clickable image)
|
||||
- optioninput (for option list)
|
||||
- filesubmission (upload a file)
|
||||
@@ -544,42 +543,6 @@ class ChoiceGroup(InputTypeBase):
|
||||
|
||||
return [self._choices_map[i] for i in internal_answer]
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@registry.register
|
||||
class JavascriptInput(InputTypeBase):
|
||||
"""
|
||||
Hidden field for javascript to communicate via; also loads the required
|
||||
scripts for rendering the problem and passes data to the problem.
|
||||
|
||||
TODO (arjun?): document this in detail. Initial notes:
|
||||
- display_class is a subclass of XProblemClassDisplay (see
|
||||
xmodule/xmodule/js/src/capa/display.js),
|
||||
- display_file is the js script to be in /static/js/ where display_class is defined.
|
||||
"""
|
||||
|
||||
template = "javascriptinput.html"
|
||||
tags = ['javascriptinput']
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
"""
|
||||
Register the attributes.
|
||||
"""
|
||||
return [Attribute('params', None),
|
||||
Attribute('problem_state', None),
|
||||
Attribute('display_class', None),
|
||||
Attribute('display_file', None), ]
|
||||
|
||||
def setup(self):
|
||||
# Need to provide a value that JSON can parse if there is no
|
||||
# student-supplied value yet.
|
||||
if self.value == "":
|
||||
self.value = 'null'
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -610,210 +610,6 @@ class LoncapaResponse(object):
|
||||
"""True if the response has an answer-pool transformation."""
|
||||
return hasattr(self, '_has_answerpool')
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@registry.register
|
||||
class JavascriptResponse(LoncapaResponse):
|
||||
"""
|
||||
This response type is used when the student's answer is graded via
|
||||
Javascript using Node.js.
|
||||
"""
|
||||
|
||||
human_name = _('JavaScript Input')
|
||||
tags = ['javascriptresponse']
|
||||
max_inputfields = 1
|
||||
allowed_inputfields = ['javascriptinput']
|
||||
|
||||
def setup_response(self):
|
||||
# Sets up generator, grader, display, and their dependencies.
|
||||
self.parse_xml()
|
||||
|
||||
self.compile_display_javascript()
|
||||
|
||||
self.params = self.extract_params()
|
||||
|
||||
if self.generator:
|
||||
self.problem_state = self.generate_problem_state()
|
||||
else:
|
||||
self.problem_state = None
|
||||
|
||||
self.solution = None
|
||||
|
||||
self.prepare_inputfield()
|
||||
|
||||
def compile_display_javascript(self):
|
||||
|
||||
# TODO FIXME
|
||||
# arjun: removing this behavior for now (and likely forever). Keeping
|
||||
# until we decide on exactly how to solve this issue. For now, files are
|
||||
# manually being compiled to DATA_DIR/js/compiled.
|
||||
|
||||
# latestTimestamp = 0
|
||||
# basepath = self.capa_system.filestore.root_path + '/js/'
|
||||
# for filename in (self.display_dependencies + [self.display]):
|
||||
# filepath = basepath + filename
|
||||
# timestamp = os.stat(filepath).st_mtime
|
||||
# if timestamp > latestTimestamp:
|
||||
# latestTimestamp = timestamp
|
||||
#
|
||||
# h = hashlib.md5()
|
||||
# h.update(self.answer_id + str(self.display_dependencies))
|
||||
# compiled_filename = 'compiled/' + h.hexdigest() + '.js'
|
||||
# compiled_filepath = basepath + compiled_filename
|
||||
|
||||
# if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp:
|
||||
# outfile = open(compiled_filepath, 'w')
|
||||
# for filename in (self.display_dependencies + [self.display]):
|
||||
# filepath = basepath + filename
|
||||
# infile = open(filepath, 'r')
|
||||
# outfile.write(infile.read())
|
||||
# outfile.write(';\n')
|
||||
# infile.close()
|
||||
# outfile.close()
|
||||
|
||||
# TODO this should also be fixed when the above is fixed.
|
||||
filename = self.capa_system.ajax_url.split('/')[-1] + '.js'
|
||||
self.display_filename = 'compiled/' + filename
|
||||
|
||||
def parse_xml(self):
|
||||
self.generator_xml = self.xml.xpath('//*[@id=$id]//generator',
|
||||
id=self.xml.get('id'))[0]
|
||||
|
||||
self.grader_xml = self.xml.xpath('//*[@id=$id]//grader',
|
||||
id=self.xml.get('id'))[0]
|
||||
|
||||
self.display_xml = self.xml.xpath('//*[@id=$id]//display',
|
||||
id=self.xml.get('id'))[0]
|
||||
|
||||
self.xml.remove(self.generator_xml)
|
||||
self.xml.remove(self.grader_xml)
|
||||
self.xml.remove(self.display_xml)
|
||||
|
||||
self.generator = self.generator_xml.get("src")
|
||||
self.grader = self.grader_xml.get("src")
|
||||
self.display = self.display_xml.get("src")
|
||||
|
||||
if self.generator_xml.get("dependencies"):
|
||||
self.generator_dependencies = self.generator_xml.get(
|
||||
"dependencies").split()
|
||||
else:
|
||||
self.generator_dependencies = []
|
||||
|
||||
if self.grader_xml.get("dependencies"):
|
||||
self.grader_dependencies = self.grader_xml.get(
|
||||
"dependencies").split()
|
||||
else:
|
||||
self.grader_dependencies = []
|
||||
|
||||
if self.display_xml.get("dependencies"):
|
||||
self.display_dependencies = self.display_xml.get(
|
||||
"dependencies").split()
|
||||
else:
|
||||
self.display_dependencies = []
|
||||
|
||||
self.display_class = self.display_xml.get("class")
|
||||
|
||||
def get_node_env(self):
|
||||
|
||||
js_dir = os.path.join(self.capa_system.filestore.root_path, 'js')
|
||||
tmp_env = os.environ.copy()
|
||||
node_path = self.capa_system.node_path + ":" + os.path.normpath(js_dir)
|
||||
tmp_env["NODE_PATH"] = node_path
|
||||
return tmp_env
|
||||
|
||||
def call_node(self, args):
|
||||
# Node.js code is un-sandboxed. If the LoncapaSystem says we aren't
|
||||
# allowed to run unsafe code, then stop now.
|
||||
if not self.capa_system.can_execute_unsafe_code():
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
msg = _("Execution of unsafe Javascript code is not allowed.")
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
subprocess_args = ["node"]
|
||||
subprocess_args.extend(args)
|
||||
|
||||
return subprocess.check_output(subprocess_args, env=self.get_node_env())
|
||||
|
||||
def generate_problem_state(self):
|
||||
|
||||
generator_file = os.path.dirname(os.path.normpath(
|
||||
__file__)) + '/javascript_problem_generator.js'
|
||||
output = self.call_node([generator_file,
|
||||
self.generator,
|
||||
json.dumps(self.generator_dependencies),
|
||||
json.dumps(str(self.context['seed'])),
|
||||
json.dumps(self.params)]).strip()
|
||||
|
||||
return json.loads(output)
|
||||
|
||||
def extract_params(self):
|
||||
|
||||
params = {}
|
||||
|
||||
for param in self.xml.xpath('//*[@id=$id]//responseparam',
|
||||
id=self.xml.get('id')):
|
||||
|
||||
raw_param = param.get("value")
|
||||
params[param.get("name")] = json.loads(
|
||||
contextualize_text(raw_param, self.context))
|
||||
|
||||
return params
|
||||
|
||||
def prepare_inputfield(self):
|
||||
|
||||
for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput',
|
||||
id=self.xml.get('id')):
|
||||
|
||||
escapedict = {'"': '"'}
|
||||
|
||||
encoded_params = json.dumps(self.params)
|
||||
encoded_params = saxutils.escape(encoded_params, escapedict)
|
||||
inputfield.set("params", encoded_params)
|
||||
|
||||
encoded_problem_state = json.dumps(self.problem_state)
|
||||
encoded_problem_state = saxutils.escape(encoded_problem_state,
|
||||
escapedict)
|
||||
inputfield.set("problem_state", encoded_problem_state)
|
||||
|
||||
inputfield.set("display_file", self.display_filename)
|
||||
inputfield.set("display_class", self.display_class)
|
||||
|
||||
def get_score(self, student_answers):
|
||||
json_submission = student_answers[self.answer_id]
|
||||
(all_correct, evaluation, solution) = self.run_grader(json_submission)
|
||||
self.solution = solution
|
||||
correctness = 'correct' if all_correct else 'incorrect'
|
||||
if all_correct:
|
||||
points = self.get_max_score()
|
||||
else:
|
||||
points = 0
|
||||
return CorrectMap(self.answer_id, correctness, npoints=points, msg=evaluation)
|
||||
|
||||
def run_grader(self, submission):
|
||||
if submission is None or submission == '':
|
||||
submission = json.dumps(None)
|
||||
|
||||
grader_file = os.path.dirname(os.path.normpath(
|
||||
__file__)) + '/javascript_problem_grader.js'
|
||||
outputs = self.call_node([grader_file,
|
||||
self.grader,
|
||||
json.dumps(self.grader_dependencies),
|
||||
submission,
|
||||
json.dumps(self.problem_state),
|
||||
json.dumps(self.params)]).split('\n')
|
||||
|
||||
all_correct = json.loads(outputs[0].strip())
|
||||
evaluation = outputs[1].strip()
|
||||
solution = outputs[2].strip()
|
||||
return (all_correct, evaluation, solution)
|
||||
|
||||
def get_answers(self):
|
||||
if self.solution is None:
|
||||
(_, _, self.solution) = self.run_grader(None)
|
||||
|
||||
return {self.answer_id: self.solution}
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
@registry.register
|
||||
@@ -4119,7 +3915,6 @@ __all__ = [
|
||||
ChoiceResponse,
|
||||
MultipleChoiceResponse,
|
||||
TrueFalseResponse,
|
||||
JavascriptResponse,
|
||||
AnnotationResponse,
|
||||
ChoiceTextResponse,
|
||||
]
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
<div class="javascriptinput capa_inputtype" id="inputtype_${id}">
|
||||
<input type="hidden" name="input_${id}" id="input_${id}" class="javascriptinput_input"/>
|
||||
<div class="javascriptinput_data" data-display_class="${display_class}"
|
||||
data-problem_state="${problem_state}" data-params="${params}"
|
||||
data-submission="${value}" data-evaluation="${msg}">
|
||||
</div>
|
||||
<div class="script_placeholder" data-src="/static/js/${display_file}"></div>
|
||||
<div class="javascriptinput_container"></div>
|
||||
</div>
|
||||
|
||||
@@ -608,60 +608,6 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
|
||||
return input_element
|
||||
|
||||
|
||||
class JavascriptResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <javascriptresponse> XML """
|
||||
|
||||
def create_response_element(self, **kwargs):
|
||||
""" Create the <javascriptresponse> element.
|
||||
|
||||
Uses **kwargs:
|
||||
|
||||
*generator_src*: Name of the JS file to generate the problem.
|
||||
*grader_src*: Name of the JS file to grade the problem.
|
||||
*display_class*: Name of the class used to display the problem
|
||||
*display_src*: Name of the JS file used to display the problem
|
||||
*param_dict*: Dictionary of parameters to pass to the JS
|
||||
"""
|
||||
# Get **kwargs
|
||||
generator_src = kwargs.get("generator_src", None)
|
||||
grader_src = kwargs.get("grader_src", None)
|
||||
display_class = kwargs.get("display_class", None)
|
||||
display_src = kwargs.get("display_src", None)
|
||||
param_dict = kwargs.get("param_dict", {})
|
||||
|
||||
# Both display_src and display_class given,
|
||||
# or neither given
|
||||
assert((display_src and display_class) or
|
||||
(not display_src and not display_class))
|
||||
|
||||
# Create the <javascriptresponse> element
|
||||
response_element = etree.Element("javascriptresponse")
|
||||
|
||||
if generator_src:
|
||||
generator_element = etree.SubElement(response_element, "generator")
|
||||
generator_element.set("src", str(generator_src))
|
||||
|
||||
if grader_src:
|
||||
grader_element = etree.SubElement(response_element, "grader")
|
||||
grader_element.set("src", str(grader_src))
|
||||
|
||||
if display_class and display_src:
|
||||
display_element = etree.SubElement(response_element, "display")
|
||||
display_element.set("class", str(display_class))
|
||||
display_element.set("src", str(display_src))
|
||||
|
||||
for (param_name, param_val) in param_dict.items():
|
||||
responseparam_element = etree.SubElement(response_element, "responseparam")
|
||||
responseparam_element.set("name", str(param_name))
|
||||
responseparam_element.set("value", str(param_val))
|
||||
|
||||
return response_element
|
||||
|
||||
def create_input_element(self, **kwargs):
|
||||
""" Create the <javascriptinput> element """
|
||||
return etree.Element("javascriptinput")
|
||||
|
||||
|
||||
class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <multiplechoiceresponse> XML """
|
||||
|
||||
|
||||
@@ -162,51 +162,6 @@ class ChoiceGroupTest(unittest.TestCase):
|
||||
self.check_group('checkboxgroup', 'checkbox', '[]')
|
||||
|
||||
|
||||
class JavascriptInputTest(unittest.TestCase):
|
||||
'''
|
||||
The javascript input is a pretty straightforward pass-thru, but test it anyway
|
||||
'''
|
||||
|
||||
def test_rendering(self):
|
||||
params = "(1,2,3)"
|
||||
|
||||
problem_state = "abc12',12&hi<there>"
|
||||
display_class = "a_class"
|
||||
display_file = "my_files/hi.js"
|
||||
|
||||
xml_str = """<javascriptinput id="prob_1_2" params="{params}" problem_state="{ps}"
|
||||
display_class="{dc}" display_file="{df}"/>""".format(
|
||||
params=params,
|
||||
ps=quote_attr(problem_state),
|
||||
dc=display_class, df=display_file)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {
|
||||
'value': '3',
|
||||
'response_data': RESPONSE_DATA
|
||||
}
|
||||
the_input = lookup_tag('javascriptinput')(test_capa_system(), element, state)
|
||||
|
||||
context = the_input._get_render_context() # pylint: disable=protected-access
|
||||
prob_id = 'prob_1_2'
|
||||
expected = {
|
||||
'STATIC_URL': '/dummy-static/',
|
||||
'id': prob_id,
|
||||
'status': inputtypes.Status('unanswered'),
|
||||
'msg': '',
|
||||
'value': '3',
|
||||
'params': params,
|
||||
'display_file': display_file,
|
||||
'display_class': display_class,
|
||||
'problem_state': problem_state,
|
||||
'response_data': RESPONSE_DATA,
|
||||
'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
class TextLineTest(unittest.TestCase):
|
||||
'''
|
||||
Check that textline inputs work, with and without math.
|
||||
|
||||
@@ -31,7 +31,6 @@ from capa.tests.response_xml_factory import (
|
||||
CustomResponseXMLFactory,
|
||||
FormulaResponseXMLFactory,
|
||||
ImageResponseXMLFactory,
|
||||
JavascriptResponseXMLFactory,
|
||||
MultipleChoiceResponseXMLFactory,
|
||||
NumericalResponseXMLFactory,
|
||||
OptionResponseXMLFactory,
|
||||
@@ -1342,46 +1341,6 @@ class ChoiceResponseTest(ResponseTest): # pylint: disable=missing-docstring
|
||||
self.assert_grade(problem, ['choice_1', 'choice_3'], 'incorrect')
|
||||
|
||||
|
||||
class JavascriptResponseTest(ResponseTest): # pylint: disable=missing-docstring
|
||||
xml_factory_class = JavascriptResponseXMLFactory
|
||||
|
||||
def test_grade(self):
|
||||
# Compile coffee files into javascript used by the response
|
||||
coffee_file_path = os.path.dirname(__file__) + "/test_files/js/*.coffee"
|
||||
os.system("node_modules/.bin/coffee -c %s" % (coffee_file_path))
|
||||
|
||||
capa_system = test_capa_system()
|
||||
capa_system.can_execute_unsafe_code = lambda: True
|
||||
problem = self.build_problem(
|
||||
capa_system=capa_system,
|
||||
generator_src="test_problem_generator.js",
|
||||
grader_src="test_problem_grader.js",
|
||||
display_class="TestProblemDisplay",
|
||||
display_src="test_problem_display.js",
|
||||
param_dict={'value': '4'},
|
||||
)
|
||||
|
||||
# Test that we get graded correctly
|
||||
self.assert_grade(problem, json.dumps({0: 4}), "correct")
|
||||
self.assert_grade(problem, json.dumps({0: 5}), "incorrect")
|
||||
|
||||
def test_cant_execute_javascript(self):
|
||||
# If the system says to disallow unsafe code execution, then making
|
||||
# this problem will raise an exception.
|
||||
capa_system = test_capa_system()
|
||||
capa_system.can_execute_unsafe_code = lambda: False
|
||||
|
||||
with self.assertRaises(LoncapaProblemError):
|
||||
self.build_problem(
|
||||
capa_system=capa_system,
|
||||
generator_src="test_problem_generator.js",
|
||||
grader_src="test_problem_grader.js",
|
||||
display_class="TestProblemDisplay",
|
||||
display_src="test_problem_display.js",
|
||||
param_dict={'value': '4'},
|
||||
)
|
||||
|
||||
|
||||
class NumericalResponseTest(ResponseTest): # pylint: disable=missing-docstring
|
||||
xml_factory_class = NumericalResponseXMLFactory
|
||||
|
||||
|
||||
@@ -997,24 +997,6 @@
|
||||
return preprocessor.fn;
|
||||
}
|
||||
},
|
||||
javascriptinput: function(element) {
|
||||
var container, data, display, displayClass, evaluation, params, problemState, submission,
|
||||
submissionField;
|
||||
data = $(element).find('.javascriptinput_data');
|
||||
params = data.data('params');
|
||||
submission = data.data('submission');
|
||||
evaluation = data.data('evaluation');
|
||||
problemState = data.data('problem_state');
|
||||
displayClass = window[data.data('display_class')];
|
||||
if (evaluation === '') {
|
||||
evaluation = null;
|
||||
}
|
||||
container = $(element).find('.javascriptinput_container');
|
||||
submissionField = $(element).find('.javascriptinput_input');
|
||||
display = new displayClass(problemState, submission, evaluation, container, submissionField, params);
|
||||
display.render();
|
||||
return display;
|
||||
},
|
||||
cminput: function(container) {
|
||||
var CodeMirrorEditor, CodeMirrorTextArea, element, id, linenumbers, mode, spaces, tabsize;
|
||||
element = $(container).find('textarea');
|
||||
@@ -1064,14 +1046,6 @@
|
||||
}
|
||||
return results;
|
||||
},
|
||||
javascriptinput: function(element, display, answers) {
|
||||
var answer, answerId;
|
||||
answerId = $(element).attr('id').split('_')
|
||||
.slice(1)
|
||||
.join('_');
|
||||
answer = JSON.parse(answers[answerId]);
|
||||
return display.showAnswer(answer);
|
||||
},
|
||||
choicetextgroup: function(element, display, answers) {
|
||||
var answer, choice, inputId, i, len, results, $element;
|
||||
$element = $(element);
|
||||
@@ -1172,20 +1146,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
Problem.prototype.inputtypeHideAnswerMethods = {
|
||||
choicegroup: function(element) {
|
||||
var $element = $(element);
|
||||
return $element.find('label').removeClass('choicegroup_correct');
|
||||
},
|
||||
javascriptinput: function(element, display) {
|
||||
return display.hideAnswer();
|
||||
},
|
||||
choicetextgroup: function(element) {
|
||||
var $element = $(element);
|
||||
return $element.find('section[id^="forinput"]').removeClass('choicetextgroup_show_correct');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to keep the buttons disabled while operationCallback is running.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user