Commit 2337eeba by Arjun Singh

Adding javascriptresponse. This responsetype is a framework for problems that…

Adding javascriptresponse. This responsetype is a framework for problems that are entirely controlled by javascript and graded by Node.js on the server.
parent 869e638e
......@@ -39,7 +39,7 @@ import responsetypes
# dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
entry_types = ['textline', 'schematic', 'textbox', 'imageinput', 'optioninput', 'choicegroup', 'radiogroup', 'checkboxgroup', 'filesubmission']
entry_types = ['textline', 'schematic', 'textbox', 'imageinput', 'optioninput', 'choicegroup', 'radiogroup', 'checkboxgroup', 'filesubmission', 'javascriptinput']
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
response_properties = ["responseparam", "answer"] # these get captured as student responses
......
......@@ -11,6 +11,7 @@ Module containing the problem elements which render into input objects
- choicegroup
- radiogroup
- checkboxgroup
- javascriptinput
- imageinput (for clickable image)
- optioninput (for option list)
- filesubmission (upload a file)
......@@ -246,6 +247,34 @@ def checkboxgroup(element, value, status, render_template, msg=''):
html = render_template("choicegroup.html", context)
return etree.XML(html)
@register_render_function
def javascriptinput(element, value, status, render_template, msg='null'):
'''
Hidden field for javascript to communicate via; also loads the required
scripts for rendering the problem and passes data to the problem.
'''
eid = element.get('id')
params = element.get('params')
problem_state = element.get('problem_state')
display_class = element.get('display_class')
display_file = element.get('display_file')
# Need to provide a value that JSON can parse if there is no
# student-supplied value yet.
if value == "":
value = 'null'
escapedict = {'"': '"'}
value = saxutils.escape(value, escapedict)
msg = saxutils.escape(msg, escapedict)
context = {'id': eid, 'params': params, 'display_file': display_file,
'display_class': display_class, 'problem_state': problem_state,
'value': value, 'evaluation': msg,
}
html = render_template("javascriptinput.html", context)
return etree.XML(html)
@register_render_function
def textline(element, value, status, render_template, msg=""):
......
var importAll = function (modulePath) {
module = require(modulePath);
for(key in module){
global[key] = module[key];
}
}
importAll("mersenne-twister-min");
importAll("xproblem");
generatorModulePath = process.argv[2];
seed = process.argv[3];
params = JSON.parse(process.argv[4]);
if(seed==null){
seed = 4;
}else{
seed = parseInt(seed);
}
generatorModule = require(generatorModulePath);
generatorClass = generatorModule.generatorClass;
generator = new generatorClass(seed, params);
console.log(JSON.stringify(generator.generate()));
var importAll = function (modulePath) {
module = require(modulePath);
for(key in module){
global[key] = module[key];
}
}
importAll("xproblem");
importAll("minimax.js");
graderModulePath = process.argv[2];
submission = JSON.parse(process.argv[3]);
problemState = JSON.parse(process.argv[4]);
params = JSON.parse(process.argv[5]);
graderModule = require(graderModulePath);
graderClass = graderModule.graderClass;
grader = new graderClass(submission, problemState, params);
console.log(JSON.stringify(grader.grade()));
console.log(JSON.stringify(grader.evaluation));
console.log(JSON.stringify(grader.solution));
......@@ -18,6 +18,8 @@ import re
import requests
import traceback
import abc
import os
import xml.sax.saxutils as saxutils
# specific library imports
from calc import evaluator, UndefinedVariable
......@@ -273,9 +275,172 @@ class LoncapaResponse(object):
#-----------------------------------------------------------------------------
class JavascriptResponse(LoncapaResponse):
'''
This response type is used when the student's answer is graded via
Javascript using Node.js.
'''
response_tag = '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()
print self.problem_state
else:
self.problem_state = None
self.solution = None
self.prepare_inputfield()
def compile_display_javascript(self):
latestTimestamp = 0
basepath = self.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()
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()
if self.grader_xml.get("dependencies"):
self.grader_dependencies = self.grader_xml.get("dependencies").split()
if self.display_xml.get("dependencies"):
self.display_dependencies = self.display_xml.get("dependencies").split()
self.display_class = self.display_xml.get("class")
def generate_problem_state(self):
js_dir = os.path.join(self.system.filestore.root_path, 'js')
node_path = os.path.normpath(js_dir)
generator_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_generator.js'
command = "NODE_PATH=%s node %s %s '%s' '%s'" % (node_path, generator_file, self.generator, json.dumps(self.system.seed), json.dumps(self.params))
node_process = os.popen(command)
output = node_process.readline().strip()
node_process.close()
return json.loads(output)
def extract_params(self):
params = {}
for param in self.xml.xpath('//*[@id=$id]//responseparam',
id=self.xml.get('id')):
params[param.get("name")] = json.loads(param.get("value"))
return params
def prepare_inputfield(self):
for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput',
id=self.xml.get('id')):
escapedict = {'"': '&quot;'}
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'
return CorrectMap(self.answer_id, correctness, msg=evaluation)
def run_grader(self, submission):
if submission is None or submission == '':
submission = json.dumps(None)
js_dir = os.path.join(self.system.filestore.root_path, 'js')
node_path = os.path.normpath(js_dir)
grader_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_grader.js'
command = "NODE_PATH=%s node %s %s '%s' '%s' '%s'" % (node_path,
grader_file,
self.grader,
submission,
json.dumps(self.problem_state),
json.dumps(self.params))
node_process = os.popen(command)
all_correct = json.loads(node_process.readline().strip())
evaluation = node_process.readline().strip()
solution = node_process.readline().strip()
node_process.close()
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}
#-----------------------------------------------------------------------------
class ChoiceResponse(LoncapaResponse):
'''
This Response type is used when the student chooses from a discrete set of
This response type is used when the student chooses from a discrete set of
choices. Currently, to be marked correct, all "correct" choices must be
supplied by the student, and no extraneous choices may be included.
......@@ -314,6 +479,11 @@ class ChoiceResponse(LoncapaResponse):
In the above example, radiogroup can be replaced with checkboxgroup to allow
the student to select more than one choice.
TODO: In order for the inputtypes to render properly, this response type
must run setup_response prior to the input type rendering. Specifically, the
choices must be given names. This behavior seems like a leaky abstraction,
and it'd be nice to change this at some point.
'''
response_tag = 'choiceresponse'
......@@ -1314,4 +1484,4 @@ class ImageResponse(LoncapaResponse):
# TEMPORARY: List of all response subclasses
# FIXME: To be replaced by auto-registration
__all__ = [CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse]
__all__ = [CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse, JavascriptResponse]
<form class="javascriptinput capa_inputtype">
<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="${evaluation}">
</div>
<div class="script_placeholder" data-src="/static/js/${display_file}"></div>
<div class="javascriptinput_container"></div>
</form>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment