Commit bcdc11c3 by Ned Batchelder

Hint functions are now run in the sandbox.

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