From 833e7777db3c4f1ede42576a63c912bbd48aed04 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Mon, 6 Aug 2012 05:27:36 -0400 Subject: [PATCH 01/14] Watching xmodule js --- run_watch_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_watch_data.py b/run_watch_data.py index f5605a5c6a..c6cdd4f0df 100755 --- a/run_watch_data.py +++ b/run_watch_data.py @@ -17,7 +17,7 @@ from watchdog.events import LoggingEventHandler, FileSystemEventHandler # watch fewer or more extensions, you can change EXTENSIONS. To watch all # extensions, add "*" to EXTENSIONS. -WATCH_DIRS = ["../data"] +WATCH_DIRS = ["../data", "common/lib/xmodule/xmodule/js"] EXTENSIONS = ["*", "xml", "js", "css", "coffee", "scss", "html"] WATCH_DIRS = [os.path.abspath(os.path.normpath(dir)) for dir in WATCH_DIRS] From 869e638e07b37e57cd9dd65e455352de7a352297 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Mon, 6 Aug 2012 05:28:19 -0400 Subject: [PATCH 02/14] Removing compiled js files in data directories before recompiling them; this causes compilation errors to fail loudly. --- lms/envs/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/envs/common.py b/lms/envs/common.py index 83a4bd4181..8b41470f92 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -457,6 +457,7 @@ if os.path.isdir(DATA_DIR): js_timestamp = os.stat(js_dir / new_filename).st_mtime if coffee_timestamp <= js_timestamp: continue + os.system("rm %s" % (js_dir / new_filename)) os.system("coffee -c %s" % (js_dir / filename)) PIPELINE_COMPILERS = [ From 2337eeba1e0c74c45af2a184cd1ddd569caf6865 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Mon, 6 Aug 2012 18:02:18 -0400 Subject: [PATCH 03/14] Adding javascriptresponse. This responsetype is a framework for problems that are entirely controlled by javascript and graded by Node.js on the server. --- common/lib/capa/capa/capa_problem.py | 2 +- common/lib/capa/capa/inputtypes.py | 29 +++ .../capa/capa/javascript_problem_generator.js | 24 +++ .../capa/capa/javascript_problem_grader.js | 21 +++ common/lib/capa/capa/responsetypes.py | 174 +++++++++++++++++- .../capa/capa/templates/javascriptinput.html | 10 + 6 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 common/lib/capa/capa/javascript_problem_generator.js create mode 100644 common/lib/capa/capa/javascript_problem_grader.js create mode 100644 common/lib/capa/capa/templates/javascriptinput.html diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index ba99ee681e..2dc059e7d7 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -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 diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 583d29b82e..2d1da53625 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -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=""): diff --git a/common/lib/capa/capa/javascript_problem_generator.js b/common/lib/capa/capa/javascript_problem_generator.js new file mode 100644 index 0000000000..670f6b838e --- /dev/null +++ b/common/lib/capa/capa/javascript_problem_generator.js @@ -0,0 +1,24 @@ +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())); diff --git a/common/lib/capa/capa/javascript_problem_grader.js b/common/lib/capa/capa/javascript_problem_grader.js new file mode 100644 index 0000000000..9e796f0e32 --- /dev/null +++ b/common/lib/capa/capa/javascript_problem_grader.js @@ -0,0 +1,21 @@ +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)); diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 12f619a881..903c697459 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -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 = {'"': '"'} + + 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] diff --git a/common/lib/capa/capa/templates/javascriptinput.html b/common/lib/capa/capa/templates/javascriptinput.html new file mode 100644 index 0000000000..bafcb23cc7 --- /dev/null +++ b/common/lib/capa/capa/templates/javascriptinput.html @@ -0,0 +1,10 @@ +
+ +
+
+
+
+
+ From 99e7d0076e4ee1c67e6e3e124db10c9e6107b833 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Mon, 6 Aug 2012 18:03:32 -0400 Subject: [PATCH 04/14] Adding a method of executing scripts fetched via AJAX in IE8+. Also adds inputtype-specific handlers so that javascript can be executed when an inputtype is encountered. --- .../xmodule/js/src/capa/display.coffee | 86 +++++++++++++++++-- 1 file changed, 77 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 0e760e98ff..92900aa851 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -1,4 +1,5 @@ class @Problem + constructor: (element) -> @el = $(element).find('.problems-wrapper') @id = @el.data('problem-id') @@ -28,22 +29,69 @@ class @Problem render: (content) -> if content @el.html(content) - @bind() + @executeProblemScripts () => + @setupInputTypes() + @bind() else $.postWithPrefix "#{@url}/problem_get", (response) => @el.html(response.html) - @executeProblemScripts() - @bind() + @executeProblemScripts () => + @setupInputTypes() + @bind() - executeProblemScripts: -> - @el.find(".script_placeholder").each (index, placeholder) -> - s = $("