Commit bcdc11c3 by Ned Batchelder

Hint functions are now run in the sandbox.

parent ed13f0a0
...@@ -22,6 +22,7 @@ import random ...@@ -22,6 +22,7 @@ import random
import re import re
import requests import requests
import subprocess import subprocess
import textwrap
import traceback import traceback
import xml.sax.saxutils as saxutils import xml.sax.saxutils as saxutils
...@@ -30,7 +31,7 @@ from shapely.geometry import Point, MultiPoint ...@@ -30,7 +31,7 @@ from shapely.geometry import Point, MultiPoint
# specific library imports # specific library imports
from calc import evaluator, UndefinedVariable from calc import evaluator, UndefinedVariable
from .correctmap import CorrectMap from . import correctmap
from datetime import datetime from datetime import datetime
from .util import * from .util import *
from lxml import etree from lxml import etree
...@@ -42,6 +43,10 @@ import safe_exec ...@@ -42,6 +43,10 @@ import safe_exec
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
CorrectMap = correctmap.CorrectMap
CORRECTMAP_PY = None
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Exceptions # Exceptions
...@@ -253,20 +258,41 @@ class LoncapaResponse(object): ...@@ -253,20 +258,41 @@ class LoncapaResponse(object):
# We may extend this in the future to add another argument which provides a # We may extend this in the future to add another argument which provides a
# callback procedure to a social hint generation system. # callback procedure to a social hint generation system.
if not hintfn in self.context:
msg = 'missing specified hint function %s in script context' % hintfn global CORRECTMAP_PY
msg += "\nSee XML source line %s" % getattr( if CORRECTMAP_PY is None:
self.xml, 'sourceline', '<unavailable>') # We need the CorrectMap code for hint functions. No, this is not great.
raise LoncapaProblemError(msg) CORRECTMAP_PY = inspect.getsource(correctmap)
code = (
CORRECTMAP_PY + "\n" +
self.context['script_code'] + "\n" +
textwrap.dedent("""
new_cmap = CorrectMap()
new_cmap.set_dict(new_cmap_dict)
old_cmap = CorrectMap()
old_cmap.set_dict(old_cmap_dict)
{hintfn}(answer_ids, student_answers, new_cmap, old_cmap)
new_cmap_dict.update(new_cmap.get_dict())
old_cmap_dict.update(old_cmap.get_dict())
""").format(hintfn=hintfn)
)
globals_dict = {
'answer_ids': self.answer_ids,
'student_answers': student_answers,
'new_cmap_dict': new_cmap.get_dict(),
'old_cmap_dict': old_cmap.get_dict(),
}
try: try:
self.context[hintfn]( safe_exec.safe_exec(code, globals_dict)
self.answer_ids, student_answers, new_cmap, old_cmap)
except Exception as err: except Exception as err:
msg = 'Error %s in evaluating hint function %s' % (err, hintfn) msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
msg += "\nSee XML source line %s" % getattr( msg += "\nSee XML source line %s" % getattr(
self.xml, 'sourceline', '<unavailable>') self.xml, 'sourceline', '<unavailable>')
raise ResponseError(msg) raise ResponseError(msg)
new_cmap.set_dict(globals_dict['new_cmap_dict'])
return return
# hint specified by conditions and text dependent on conditions (a-la Loncapa design) # hint specified by conditions and text dependent on conditions (a-la Loncapa design)
......
...@@ -667,12 +667,16 @@ class StringResponseXMLFactory(ResponseXMLFactory): ...@@ -667,12 +667,16 @@ class StringResponseXMLFactory(ResponseXMLFactory):
Where *hint_prompt* is the string for which we show the hint, Where *hint_prompt* is the string for which we show the hint,
*hint_name* is an internal identifier for the hint, *hint_name* is an internal identifier for the hint,
and *hint_text* is the text we show for the hint. and *hint_text* is the text we show for the hint.
*hintfn*: The name of a function in the script to use for hints.
""" """
# Retrieve the **kwargs # Retrieve the **kwargs
answer = kwargs.get("answer", None) answer = kwargs.get("answer", None)
case_sensitive = kwargs.get("case_sensitive", True) case_sensitive = kwargs.get("case_sensitive", True)
hint_list = kwargs.get('hints', None) hint_list = kwargs.get('hints', None)
assert(answer) hint_fn = kwargs.get('hintfn', None)
assert answer
# Create the <stringresponse> element # Create the <stringresponse> element
response_element = etree.Element("stringresponse") response_element = etree.Element("stringresponse")
...@@ -684,18 +688,24 @@ class StringResponseXMLFactory(ResponseXMLFactory): ...@@ -684,18 +688,24 @@ class StringResponseXMLFactory(ResponseXMLFactory):
response_element.set("type", "cs" if case_sensitive else "ci") response_element.set("type", "cs" if case_sensitive else "ci")
# Add the hints if specified # Add the hints if specified
if hint_list: if hint_list or hint_fn:
hintgroup_element = etree.SubElement(response_element, "hintgroup") hintgroup_element = etree.SubElement(response_element, "hintgroup")
for (hint_prompt, hint_name, hint_text) in hint_list: if hint_list:
stringhint_element = etree.SubElement(hintgroup_element, "stringhint") assert not hint_fn
stringhint_element.set("answer", str(hint_prompt)) for (hint_prompt, hint_name, hint_text) in hint_list:
stringhint_element.set("name", str(hint_name)) stringhint_element = etree.SubElement(hintgroup_element, "stringhint")
stringhint_element.set("answer", str(hint_prompt))
hintpart_element = etree.SubElement(hintgroup_element, "hintpart") stringhint_element.set("name", str(hint_name))
hintpart_element.set("on", str(hint_name))
hintpart_element = etree.SubElement(hintgroup_element, "hintpart")
hint_text_element = etree.SubElement(hintpart_element, "text") hintpart_element.set("on", str(hint_name))
hint_text_element.text = str(hint_text)
hint_text_element = etree.SubElement(hintpart_element, "text")
hint_text_element.text = str(hint_text)
if hint_fn:
assert not hint_list
hintgroup_element.set("hintfn", hint_fn)
return response_element return response_element
......
...@@ -552,6 +552,22 @@ class StringResponseTest(ResponseTest): ...@@ -552,6 +552,22 @@ class StringResponseTest(ResponseTest):
correct_map = problem.grade_answers(input_dict) correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'), "") self.assertEquals(correct_map.get_hint('1_2_1'), "")
def test_computed_hints(self):
problem = self.build_problem(
answer="Michigan",
hintfn="gimme_a_hint",
script = textwrap.dedent("""
def gimme_a_hint(answer_ids, student_answers, new_cmap, old_cmap):
aid = answer_ids[0]
answer = student_answers[aid]
new_cmap.set_hint_and_mode(aid, answer+"??", "always")
""")
)
input_dict = {'1_2_1': 'Hello'}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'), "Hello??")
class CodeResponseTest(ResponseTest): class CodeResponseTest(ResponseTest):
from response_xml_factory import CodeResponseXMLFactory from response_xml_factory import CodeResponseXMLFactory
......
"""Safe execution of untrusted Python code.""" """Safe execution of untrusted Python code."""
import json import json
import logging
import os.path import os.path
import shutil import shutil
import sys import sys
...@@ -9,6 +10,8 @@ import textwrap ...@@ -9,6 +10,8 @@ import textwrap
from codejail import jailpy from codejail import jailpy
from codejail.util import temp_directory, change_directory from codejail.util import temp_directory, change_directory
log = logging.getLogger(__name__)
def safe_exec(code, globals_dict, files=None, python_path=None): def safe_exec(code, globals_dict, files=None, python_path=None):
"""Execute code as "exec" does, but safely. """Execute code as "exec" does, but safely.
...@@ -78,10 +81,9 @@ def safe_exec(code, globals_dict, files=None, python_path=None): ...@@ -78,10 +81,9 @@ def safe_exec(code, globals_dict, files=None, python_path=None):
# Turn this on to see what's being executed. # Turn this on to see what's being executed.
if 0: if 0:
print "--{:-<40}".format(" jailed ") log.debug("Jailed code: %s", jailed_code)
print jailed_code log.debug("Exec: %s", code)
print "--{:-<40}".format(" exec ") log.debug("Stdin: %s", stdin)
print code
res = jailpy.jailpy(jailed_code, stdin=stdin, files=files) res = jailpy.jailpy(jailed_code, stdin=stdin, files=files)
if res.status != 0: if res.status != 0:
......
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