diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 696b12377f..6580114bcc 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -32,6 +32,8 @@ from copy import deepcopy import chem import chem.miller +import chem.chemcalc +import chem.chemtools import verifiers import verifiers.draganddrop @@ -67,6 +69,9 @@ global_context = {'random': random, 'scipy': scipy, 'calc': calc, 'eia': eia, + 'chemcalc': chem.chemcalc, + 'chemtools': chem.chemtools, + 'miller': chem.miller, 'draganddrop': verifiers.draganddrop} # These should be removed from HTML output, including all subelements diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 5b1b46d858..ec1d5ffe1b 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -2,7 +2,8 @@ # File: courseware/capa/responsetypes.py # ''' -Problem response evaluation. Handles checking of student responses, of a variety of types. +Problem response evaluation. Handles checking of +student responses, of a variety of types. Used by capa_problem.py ''' @@ -10,7 +11,6 @@ Used by capa_problem.py # standard library imports import abc import cgi -import hashlib import inspect import json import logging @@ -34,7 +34,10 @@ from .correctmap import CorrectMap from datetime import datetime from .util import * from lxml import etree -from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? + +# uses Beautiful Soup!!! FIXME? +from lxml.html.soupparser import fromstring as fromstring_bs + import xqueue_interface log = logging.getLogger(__name__) @@ -243,13 +246,17 @@ class LoncapaResponse(object): # hint specified by function? hintfn = hintgroup.get('hintfn') if hintfn: - # Hint is determined by a function defined in the stanza instead cfn = xml.get('cfn') @@ -973,8 +986,8 @@ def sympy_check2(): else: msg = "%s: can't find cfn %s in context" % ( unicode(self), cfn) - msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', - '') + msg += ("\nSee XML source line %s" % + getattr(self.xml, 'sourceline', '')) raise LoncapaProblemError(msg) if not self.code: @@ -1010,14 +1023,17 @@ def sympy_check2(): log.error(msg) raise Exception(msg) - # global variable in context which holds the Presentation MathML from dynamic math input + # global variable in context which holds the + # Presentation MathML from dynamic math input # ordered list of dynamath responses dynamath = [student_answers.get(k + '_dynamath', None) for k in idset] # if there is only one box, and it's empty, then don't evaluate if len(idset) == 1 and not submission[0]: - # default to no error message on empty answer (to be consistent with other - # responsetypes) but allow author to still have the old behavior by setting + # default to no error message on empty answer + # (to be consistent with other + # responsetypes) but allow author to still + # have the old behavior by setting # empty_answer_err attribute msg = ('No answer entered!' if self.xml.get('empty_answer_err') else '') @@ -1092,7 +1108,8 @@ def sympy_check2(): try: answer_given = submission[0] if ( len(idset) == 1) else submission - # handle variable number of arguments in check function, for backwards compatibility + # 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]] @@ -1124,7 +1141,8 @@ def sympy_check2(): msg = ret.get('msg', None) msg = self.clean_message_html(msg) - # If there is only one input, apply the message to that input + # If there is only one input, apply the message to + # that input # Otherwise, apply the message to the whole problem if len(idset) > 1: overall_message = msg @@ -1137,7 +1155,8 @@ def sympy_check2(): # 'input_list': [{ 'ok': BOOLEAN, 'msg': STRING }, ...] } # # This allows the function to return an 'overall message' - # that applies to the entire problem, as well as correct/incorrect + # that applies to the entire problem, as well as + # correct/incorrect # status and messages for individual inputs elif 'input_list' in ret: overall_message = ret.get('overall_message', '') @@ -1370,7 +1389,8 @@ class CodeResponse(LoncapaResponse): tests = self.xml.get('tests') - # Extract 'answer' and 'initial_display' from XML. Note that the code to be exec'ed here is: + # Extract 'answer' and 'initial_display' from XML. + # Note that the code to be exec'ed here is: # (1) Internal edX code, i.e. NOT student submissions, and # (2) The code should only define the strings 'initial_display', 'answer', # 'preamble', 'test_program' @@ -1391,7 +1411,8 @@ class CodeResponse(LoncapaResponse): " 'answer' and/or 'initial_display' in ..." % err) raise Exception(err) - # Finally, make the ExternalResponse input XML format conform to the generic + # Finally, make the ExternalResponse input XML format + # conform to the generic # exteral grader interface # The XML tagging of grader_payload is pyxserver-specific grader_payload = '' @@ -1500,7 +1521,8 @@ class CodeResponse(LoncapaResponse): # TODO: Find out how this is used elsewhere, if any self.context['correct'] = correctness - # Replace 'oldcmap' with new grading results if queuekey matches. If queuekey + # Replace 'oldcmap' with new grading results if queuekey matches. + # If queuekey # does not match, we keep waiting for the score_msg whose key actually # matches if oldcmap.is_right_queuekey(self.answer_id, queuekey): diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 9ade578622..bf64d3cc69 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -905,6 +905,66 @@ class CustomResponseTest(ResponseTest): problem.grade_answers({'1_2_1': '42'}) + def test_module_imports_inline(self): + ''' + Check that the correct modules are available to custom + response scripts + ''' + + for module_name in ['random', 'numpy', 'math', 'scipy', + 'calc', 'eia', 'chemcalc', 'chemtools', + 'miller', 'draganddrop']: + + # Create a script that checks that the name is defined + # If the name is not defined, then the script + # will raise an exception + script = textwrap.dedent(''' + correct[0] = 'correct' + assert('%s' in globals())''' % module_name) + + # Create the problem + problem = self.build_problem(answer=script) + + # Expect that we can grade an answer without + # getting an exception + try: + problem.grade_answers({'1_2_1': '42'}) + + except ResponseError: + self.fail("Could not use name '%s' in custom response" + % module_name) + + def test_module_imports_function(self): + ''' + Check that the correct modules are available to custom + response scripts + ''' + + for module_name in ['random', 'numpy', 'math', 'scipy', + 'calc', 'eia', 'chemcalc', 'chemtools', + 'miller', 'draganddrop']: + + # Create a script that checks that the name is defined + # If the name is not defined, then the script + # will raise an exception + script = textwrap.dedent(''' + def check_func(expect, answer_given): + assert('%s' in globals()) + return True''' % module_name) + + # Create the problem + problem = self.build_problem(script=script, cfn="check_func") + + # Expect that we can grade an answer without + # getting an exception + try: + problem.grade_answers({'1_2_1': '42'}) + + except ResponseError: + self.fail("Could not use name '%s' in custom response" + % module_name) + + class SchematicResponseTest(ResponseTest): from response_xml_factory import SchematicResponseXMLFactory xml_factory_class = SchematicResponseXMLFactory