diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 345716a236..de73dcda30 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -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', '') - 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', '') 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) diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py index ee9a7e6530..35c12800ae 100644 --- a/common/lib/capa/capa/tests/response_xml_factory.py +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -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 element response_element = etree.Element("stringresponse") @@ -684,18 +688,24 @@ 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") - for (hint_prompt, hint_name, hint_text) in hint_list: - stringhint_element = etree.SubElement(hintgroup_element, "stringhint") - stringhint_element.set("answer", str(hint_prompt)) - stringhint_element.set("name", str(hint_name)) + 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)) + stringhint_element.set("name", str(hint_name)) - hintpart_element = etree.SubElement(hintgroup_element, "hintpart") - hintpart_element.set("on", str(hint_name)) + hintpart_element = etree.SubElement(hintgroup_element, "hintpart") + hintpart_element.set("on", str(hint_name)) - hint_text_element = etree.SubElement(hintpart_element, "text") - 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 diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 3f88734884..8e41ff39de 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -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 diff --git a/common/lib/codejail/codejail/safe_exec.py b/common/lib/codejail/codejail/safe_exec.py index 5db9651438..682ae809af 100644 --- a/common/lib/codejail/codejail/safe_exec.py +++ b/common/lib/codejail/codejail/safe_exec.py @@ -1,6 +1,7 @@ """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: