Work in progress to sandbox the uses of eval in LMS.
This commit is contained in:
@@ -22,7 +22,6 @@ import numpy
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import scipy
|
||||
import struct
|
||||
import sys
|
||||
|
||||
@@ -30,6 +29,7 @@ from lxml import etree
|
||||
from xml.sax.saxutils import unescape
|
||||
from copy import deepcopy
|
||||
|
||||
<<<<<<< HEAD
|
||||
import chem
|
||||
import chem.miller
|
||||
import chem.chemcalc
|
||||
@@ -38,8 +38,9 @@ import verifiers
|
||||
import verifiers.draganddrop
|
||||
|
||||
import calc
|
||||
=======
|
||||
>>>>>>> Work in progress to sandbox the uses of eval in LMS.
|
||||
from .correctmap import CorrectMap
|
||||
import eia
|
||||
import inputtypes
|
||||
import customrender
|
||||
from .util import contextualize_text, convert_files_to_filenames
|
||||
@@ -48,6 +49,8 @@ import xqueue_interface
|
||||
# to be replaced with auto-registering
|
||||
import responsetypes
|
||||
|
||||
from codejail.safe_exec import safe_exec
|
||||
|
||||
# dict of tagname, Response Class -- this should come from auto-registering
|
||||
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
|
||||
|
||||
@@ -63,6 +66,7 @@ html_transforms = {'problem': {'tag': 'div'},
|
||||
"math": {'tag': 'span'},
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
global_context = {'random': random,
|
||||
'numpy': numpy,
|
||||
'math': math,
|
||||
@@ -73,6 +77,20 @@ global_context = {'random': random,
|
||||
'chemtools': chem.chemtools,
|
||||
'miller': chem.miller,
|
||||
'draganddrop': verifiers.draganddrop}
|
||||
=======
|
||||
safe_exec_assumed_imports = [
|
||||
"random",
|
||||
"numpy",
|
||||
"math",
|
||||
"scipy",
|
||||
"calc",
|
||||
"eia",
|
||||
("chemcalc", "chem.chemcalc"),
|
||||
("chemtools", "chem.chemtools"),
|
||||
("miller", "chem.miller"),
|
||||
("draganddrop", "verifiers.draganddrop"),
|
||||
]
|
||||
>>>>>>> Work in progress to sandbox the uses of eval in LMS.
|
||||
|
||||
# These should be removed from HTML output, including all subelements
|
||||
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"]
|
||||
@@ -144,7 +162,7 @@ class LoncapaProblem(object):
|
||||
self._process_includes()
|
||||
|
||||
# construct script processor context (eg for customresponse problems)
|
||||
self.context = self._extract_context(self.tree, seed=self.seed)
|
||||
self.context = self._extract_context(self.tree)
|
||||
|
||||
# Pre-parse the XML tree: modifies it to add ID's and perform some in-place
|
||||
# transformations. This also creates the dict (self.responders) of Response
|
||||
@@ -451,7 +469,7 @@ class LoncapaProblem(object):
|
||||
|
||||
return path
|
||||
|
||||
def _extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
|
||||
def _extract_context(self, tree):
|
||||
'''
|
||||
Extract content of <script>...</script> from the problem.xml file, and exec it in the
|
||||
context of this problem. Provides ability to randomize problems, and also set
|
||||
@@ -460,14 +478,18 @@ class LoncapaProblem(object):
|
||||
Problem XML goes to Python execution context. Runs everything in script tags.
|
||||
'''
|
||||
random.seed(self.seed)
|
||||
# save global context in here also
|
||||
context = {'global_context': global_context}
|
||||
|
||||
# initialize context to have stuff in global_context
|
||||
context.update(global_context)
|
||||
|
||||
# TODO: REMOVE THIS COMMENTED OUT CODE.
|
||||
## save global context in here also
|
||||
#context = {'global_context': global_context}
|
||||
#
|
||||
## initialize context to have stuff in global_context
|
||||
#context.update(global_context)
|
||||
#
|
||||
# put globals there also
|
||||
context['__builtins__'] = globals()['__builtins__']
|
||||
#context['__builtins__'] = globals()['__builtins__']
|
||||
|
||||
context = {}
|
||||
|
||||
# pass instance of LoncapaProblem in
|
||||
context['the_lcp'] = self
|
||||
@@ -501,7 +523,7 @@ class LoncapaProblem(object):
|
||||
context['script_code'] += code
|
||||
try:
|
||||
# use "context" for global context; thus defs in code are global within code
|
||||
exec code in context, context
|
||||
safe_exec(code, context, future_division=True, assumed_imports=safe_exec_assumed_imports)
|
||||
except Exception as err:
|
||||
log.exception("Error while execing script code: " + code)
|
||||
msg = "Error while executing script code: %s" % str(err).replace('<', '<')
|
||||
|
||||
@@ -37,6 +37,8 @@ from lxml import etree
|
||||
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
|
||||
import xqueue_interface
|
||||
|
||||
from codejail.safe_exec import safe_exec
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -968,14 +970,20 @@ def sympy_check2():
|
||||
cfn = xml.get('cfn')
|
||||
if cfn:
|
||||
log.debug("cfn = %s" % cfn)
|
||||
if cfn in self.context:
|
||||
self.code = self.context[cfn]
|
||||
else:
|
||||
msg = "%s: can't find cfn %s in context" % (
|
||||
unicode(self), cfn)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline',
|
||||
'<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
def make_check_function(script_code, cfn):
|
||||
def check_function(expect, ans):
|
||||
code = (script_code + "\n" +
|
||||
"cfn_return = %s(expect, ans)\n" % cfn)
|
||||
globals_dict = {
|
||||
'expect': expect,
|
||||
'ans': ans,
|
||||
}
|
||||
safe_exec(code, globals_dict)
|
||||
return globals_dict['cfn_return']
|
||||
return check_function
|
||||
|
||||
self.code = make_check_function(self.context['script_code'], cfn)
|
||||
|
||||
if not self.code:
|
||||
if answer is None:
|
||||
@@ -1074,6 +1082,7 @@ def sympy_check2():
|
||||
# exec the check function
|
||||
if isinstance(self.code, basestring):
|
||||
try:
|
||||
raise Exception("exec 1")
|
||||
exec self.code in self.context['global_context'], self.context
|
||||
correct = self.context['correct']
|
||||
messages = self.context['messages']
|
||||
@@ -1083,32 +1092,15 @@ def sympy_check2():
|
||||
self._handle_exec_exception(err)
|
||||
|
||||
else:
|
||||
# self.code is not a string; assume its a function
|
||||
# self.code is not a string; it's a function we created earlier.
|
||||
|
||||
# this is an interface to the Tutor2 check functions
|
||||
fn = self.code
|
||||
ret = None
|
||||
log.debug(" submission = %s" % submission)
|
||||
try:
|
||||
answer_given = submission[0] if (
|
||||
len(idset) == 1) else submission
|
||||
# handle variable number of arguments in check function, for backwards compatibility
|
||||
# with various Tutor2 check functions
|
||||
args = [self.expect, answer_given,
|
||||
student_answers, self.answer_ids[0]]
|
||||
argspec = inspect.getargspec(fn)
|
||||
nargs = len(argspec.args) - len(argspec.defaults or [])
|
||||
kwargs = {}
|
||||
for argname in argspec.args[nargs:]:
|
||||
kwargs[argname] = self.context[
|
||||
argname] if argname in self.context else None
|
||||
|
||||
log.debug('[customresponse] answer_given=%s' % answer_given)
|
||||
log.debug('nargs=%d, args=%s, kwargs=%s' % (
|
||||
nargs, args, kwargs))
|
||||
|
||||
ret = fn(*args[:nargs], **kwargs)
|
||||
|
||||
answer_given = submission[0] if (len(idset) == 1) else submission
|
||||
ret = fn(self.expect, answer_given)
|
||||
except Exception as err:
|
||||
self._handle_exec_exception(err)
|
||||
|
||||
@@ -1265,6 +1257,7 @@ class SymbolicResponse(CustomResponse):
|
||||
def setup_response(self):
|
||||
self.xml.set('cfn', 'symmath_check')
|
||||
code = "from symmath import *"
|
||||
raise Exception("exec 2")
|
||||
exec code in self.context, self.context
|
||||
CustomResponse.setup_response(self)
|
||||
|
||||
@@ -1378,6 +1371,7 @@ class CodeResponse(LoncapaResponse):
|
||||
penv = {}
|
||||
penv['__builtins__'] = globals()['__builtins__']
|
||||
try:
|
||||
raise Exception("exec 3")
|
||||
exec(code, penv, penv)
|
||||
except Exception as err:
|
||||
log.error(
|
||||
@@ -1925,18 +1919,12 @@ class SchematicResponse(LoncapaResponse):
|
||||
self.code = answer.text
|
||||
|
||||
def get_score(self, student_answers):
|
||||
from capa_problem import global_context
|
||||
submission = [json.loads(student_answers[
|
||||
k]) for k in sorted(self.answer_ids)]
|
||||
#from capa_problem import global_context
|
||||
submission = [
|
||||
json.loads(student_answers[k]) for k in sorted(self.answer_ids)
|
||||
]
|
||||
self.context.update({'submission': submission})
|
||||
|
||||
try:
|
||||
exec self.code in global_context, self.context
|
||||
|
||||
except Exception as err:
|
||||
_, _, traceback_obj = sys.exc_info()
|
||||
raise ResponseError, ResponseError(err.message), traceback_obj
|
||||
|
||||
safe_exec(self.code, {}, self.context)
|
||||
cmap = CorrectMap()
|
||||
cmap.set_dict(dict(zip(sorted(
|
||||
self.answer_ids), self.context['correct'])))
|
||||
|
||||
@@ -19,6 +19,11 @@ def jsonable_dict(d):
|
||||
return jd
|
||||
|
||||
def safe_exec(code, globals_dict, locals_dict=None, future_division=False, assumed_imports=None):
|
||||
"""Execute code safely.
|
||||
|
||||
Returns None. The code can modify globals in `global_dict`.
|
||||
|
||||
"""
|
||||
if future_division:
|
||||
code = "from __future__ import division\n" + code
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<course>
|
||||
<chapter url_name="GradedChapter">
|
||||
<chapter url_name="EmbeddedPythonChapter">
|
||||
|
||||
<vertical url_name="Homework1">
|
||||
<problem url_name="H1P1">
|
||||
<problem url_name="schematic_problem">
|
||||
<schematicresponse>
|
||||
<center>
|
||||
<schematic height="500" width="600" parts="g,n,s" analyses="dc,tran" submit_analyses="{"tran":[["Z",0.0000004,0.0000009,0.0000014,0.0000019,0.0000024,0.0000029,0.0000034,0.000039]]}" initial_value="[["w",[112,96,128,96]],["w",[256,96,240,96]],["w",[192,96,240,96]],["s",[240,96,0],{"color":"cyan","offset":"","plot offset":"0","_json_":3},["Z"]],["w",[32,224,192,224]],["w",[96,48,192,48]],["L",[256,96,3],{"label":"Z","_json_":6},["Z"]],["r",[192,48,0],{"name":"Rpullup","r":"10K","_json_":7},["1","Z"]],["w",[32,144,32,192]],["w",[32,224,32,192]],["w",[48,192,32,192]],["w",[32,96,32,144]],["w",[48,144,32,144]],["w",[32,48,32,96]],["w",[48,96,32,96]],["w",[32,48,48,48]],["g",[32,224,0],{"_json_":16},["0"]],["v",[96,192,1],{"name":"VC","value":"square(3,0,250K)","_json_":17},["C","0"]],["v",[96,144,1],{"name":"VB","value":"square(3,0,500K)","_json_":18},["B","0"]],["v",[96,96,1],{"name":"VA","value":"square(3,0,1000K)","_json_":19},["A","0"]],["v",[96,48,1],{"name":"Vpwr","value":"dc(3)","_json_":20},["1","0"]],["L",[96,96,2],{"label":"A","_json_":21},["A"]],["w",[96,96,104,96]],["L",[96,144,2],{"label":"B","_json_":23},["B"]],["w",[96,144,104,144]],["L",[96,192,2],{"label":"C","_json_":25},["C"]],["w",[96,192,104,192]],["w",[192,96,192,112]],["s",[112,96,0],{"color":"red","offset":"15","plot offset":"0","_json_":28},["A"]],["w",[104,96,112,96]],["s",[112,144,0],{"color":"green","offset":"10","plot offset":"0","_json_":30},["B"]],["w",[104,144,112,144]],["w",[128,144,112,144]],["s",[112,192,0],{"color":"blue","offset":"5","plot offset":"0","_json_":33},["C"]],["w",[104,192,112,192]],["w",[128,192,112,192]],["view",0,0,2,"5","10","10MEG",null,"100","4us"]]"/>
|
||||
<schematic height="500" width="600" parts="g,n,s" analyses="dc,tran"
|
||||
submit_analyses="{"tran":[["Z",0.0000004,0.0000009,0.0000014,0.0000019,0.0000024,0.0000029,0.0000034,0.000039]]}"
|
||||
initial_value="[["w",[112,96,128,96]],["w",[256,96,240,96]],["w",[192,96,240,96]],["s",[240,96,0],{"color":"cyan","offset":"","plot offset":"0","_json_":3},["Z"]],["w",[32,224,192,224]],["w",[96,48,192,48]],["L",[256,96,3],{"label":"Z","_json_":6},["Z"]],["r",[192,48,0],{"name":"Rpullup","r":"10K","_json_":7},["1","Z"]],["w",[32,144,32,192]],["w",[32,224,32,192]],["w",[48,192,32,192]],["w",[32,96,32,144]],["w",[48,144,32,144]],["w",[32,48,32,96]],["w",[48,96,32,96]],["w",[32,48,48,48]],["g",[32,224,0],{"_json_":16},["0"]],["v",[96,192,1],{"name":"VC","value":"square(3,0,250K)","_json_":17},["C","0"]],["v",[96,144,1],{"name":"VB","value":"square(3,0,500K)","_json_":18},["B","0"]],["v",[96,96,1],{"name":"VA","value":"square(3,0,1000K)","_json_":19},["A","0"]],["v",[96,48,1],{"name":"Vpwr","value":"dc(3)","_json_":20},["1","0"]],["L",[96,96,2],{"label":"A","_json_":21},["A"]],["w",[96,96,104,96]],["L",[96,144,2],{"label":"B","_json_":23},["B"]],["w",[96,144,104,144]],["L",[96,192,2],{"label":"C","_json_":25},["C"]],["w",[96,192,104,192]],["w",[192,96,192,112]],["s",[112,96,0],{"color":"red","offset":"15","plot offset":"0","_json_":28},["A"]],["w",[104,96,112,96]],["s",[112,144,0],{"color":"green","offset":"10","plot offset":"0","_json_":30},["B"]],["w",[104,144,112,144]],["w",[128,144,112,144]],["s",[112,192,0],{"color":"blue","offset":"5","plot offset":"0","_json_":33},["C"]],["w",[104,192,112,192]],["w",[128,192,112,192]],["view",0,0,2,"5","10","10MEG",null,"100","4us"]]"
|
||||
/>
|
||||
</center>
|
||||
<answer type="loncapa/python">
|
||||
# for a schematic response, submission[i] is the json representation
|
||||
@@ -44,6 +47,51 @@ correct = ['correct' if okay else 'incorrect']
|
||||
|
||||
</problem>
|
||||
|
||||
|
||||
|
||||
<problem url_name="cfn_problem">
|
||||
<text>
|
||||
<script type="text/python" system_path="python_lib">
|
||||
def test_csv(expect, ans):
|
||||
# Take out all spaces in expected answer
|
||||
expect = [i.strip(' ') for i in str(expect).split(',')]
|
||||
# Take out all spaces in student solution
|
||||
ans = [i.strip(' ') for i in str(ans).split(',')]
|
||||
|
||||
def strip_q(x):
|
||||
# Strip quotes around strings if students have entered them
|
||||
stripped_ans = []
|
||||
for item in x:
|
||||
if item[0] == "'" and item[-1]=="'":
|
||||
item = item.strip("'")
|
||||
elif item[0] == '"' and item[-1] == '"':
|
||||
item = item.strip('"')
|
||||
stripped_ans.append(item)
|
||||
return stripped_ans
|
||||
|
||||
return strip_q(expect) == strip_q(ans)
|
||||
</script>
|
||||
<ol class="enumerate">
|
||||
<li>
|
||||
<pre>
|
||||
num = 0
|
||||
while num <= 5:
|
||||
print(num)
|
||||
num += 1
|
||||
|
||||
print("Outside of loop")
|
||||
print(num)
|
||||
</pre>
|
||||
<p>
|
||||
<customresponse cfn="test_csv" expect="0, 1, 2, 3, 4, 5, 'Outside of loop', 6">
|
||||
<textline size="50" correct_answer="0, 1, 2, 3, 4, 5, 'Outside of loop', 6"/>
|
||||
</customresponse>
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</text>
|
||||
</problem>
|
||||
|
||||
</vertical>
|
||||
</chapter>
|
||||
</course>
|
||||
|
||||
@@ -986,7 +986,7 @@ class TestSchematicResponse(TestSubmittingProblems):
|
||||
return resp
|
||||
|
||||
def test_get_graded(self):
|
||||
resp = self.submit_question_answer('H1P1',
|
||||
resp = self.submit_question_answer('schematic_problem',
|
||||
[['transient', {'Z': [
|
||||
[0.0000004, 2.8],
|
||||
[0.0000009, 2.8],
|
||||
@@ -1001,8 +1001,8 @@ class TestSchematicResponse(TestSubmittingProblems):
|
||||
respdata = json.loads(resp.content)
|
||||
self.assertEqual(respdata['success'], 'correct')
|
||||
|
||||
self.reset_question_answer('H1P1')
|
||||
resp = self.submit_question_answer('H1P1',
|
||||
self.reset_question_answer('schematic_problem')
|
||||
resp = self.submit_question_answer('schematic_problem',
|
||||
[['transient', {'Z': [
|
||||
[0.0000004, 2.8],
|
||||
[0.0000009, 0.0], # wrong.
|
||||
@@ -1016,3 +1016,31 @@ class TestSchematicResponse(TestSubmittingProblems):
|
||||
)
|
||||
respdata = json.loads(resp.content)
|
||||
self.assertEqual(respdata['success'], 'incorrect')
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestCustomResponseCfnFunction(TestSubmittingProblems):
|
||||
"""Check that cfn functions work properly."""
|
||||
|
||||
course_slug = "embedded_python"
|
||||
course_when = "2013_Spring"
|
||||
|
||||
def submit_question_answer(self, problem_url_name, responses):
|
||||
"""Particular to the embedded_python/2013_Spring course."""
|
||||
problem_location = self.problem_location(problem_url_name)
|
||||
modx_url = self.modx_url(problem_location, 'problem_check')
|
||||
resp = self.client.post(modx_url, {
|
||||
'input_i4x-edX-embedded_python-problem-{0}_2_1'.format(problem_url_name): responses,
|
||||
})
|
||||
return resp
|
||||
|
||||
def test_get_graded(self):
|
||||
resp = self.submit_question_answer('cfn_problem', "0, 1, 2, 3, 4, 5, 'Outside of loop', 6")
|
||||
respdata = json.loads(resp.content)
|
||||
self.assertEqual(respdata['success'], 'correct')
|
||||
|
||||
self.reset_question_answer('cfn_problem')
|
||||
|
||||
resp = self.submit_question_answer('cfn_problem', "xyzzy!")
|
||||
respdata = json.loads(resp.content)
|
||||
self.assertEqual(respdata['success'], 'incorrect')
|
||||
|
||||
Reference in New Issue
Block a user