diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py
index b655270a9a..f790190215 100644
--- a/common/lib/capa/capa_problem.py
+++ b/common/lib/capa/capa_problem.py
@@ -1,6 +1,11 @@
#
# File: capa/capa_problem.py
#
+# Nomenclature:
+#
+# A capa Problem is a collection of text and capa Response questions. Each Response may have one or more
+# Input entry fields. The capa Problem may include a solution.
+#
'''
Main module which shows problems (of "capa" type).
@@ -83,17 +88,32 @@ html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formul
class LoncapaProblem(object):
+ '''
+ Main class for capa Problems.
+ '''
+
def __init__(self, fileobject, id, state=None, seed=None, system=None):
+ '''
+ Initializes capa Problem. The problem itself is defined by the XML file
+ pointed to by fileobject.
+
+ Arguments:
+
+ - filesobject : an OSFS instance: see fs.osfs
+ - id : string used as the identifier for this problem; often a filename (no spaces)
+ - state : student state (represented as a dict)
+ - seed : random number generator seed (int)
+ - system : I4xSystem instance which provides OS, rendering, and user context
+
+ '''
+
## Initialize class variables from state
- self.seed = None
self.student_answers = dict()
self.correct_map = dict()
self.done = False
self.problem_id = id
self.system = system
-
- if seed is not None:
- self.seed = seed
+ self.seed = seed
if state:
if 'seed' in state:
@@ -109,22 +129,21 @@ class LoncapaProblem(object):
if not self.seed:
self.seed = struct.unpack('i', os.urandom(4))[0]
- ## Parse XML file
- if getattr(system, 'DEBUG', False):
+ self.fileobject = fileobject # save problem file object, so we can use for debugging information later
+ if getattr(system, 'DEBUG', False): # get the problem XML string from the problem file
log.info("[courseware.capa.capa_problem.lcp.init] fileobject = %s" % fileobject)
file_text = fileobject.read()
- self.fileobject = fileobject # save it, so we can use for debugging information later
- # Convert startouttext and endouttext to proper
- # TODO: Do with XML operations
- file_text = re.sub("startouttext\s*/", "text", file_text)
+ file_text = re.sub("startouttext\s*/", "text", file_text) # Convert startouttext and endouttext to proper
file_text = re.sub("endouttext\s*/", "/text", file_text)
- self.tree = etree.XML(file_text)
- self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map=self.student_answers)
+ self.tree = etree.XML(file_text) # parse problem XML file into an element tree
+
+ # construct script processor context (eg for customresponse problems)
self.context = self.extract_context(self.tree, seed=self.seed)
- for response in self.tree.xpath('//' + "|//".join(response_types)):
- responder = response_types[response.tag](response, self.context, self.system)
- responder.preprocess_response()
+
+ # pre-parse the XML tree: modifies it to add ID's and perform some in-place transformations
+ # this also creates the list (self.responders) of Response instances for each question in the problem
+ self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map=self.student_answers)
def __unicode__(self):
return u"LoncapaProblem ({0})".format(self.fileobject)
@@ -140,12 +159,27 @@ class LoncapaProblem(object):
def get_max_score(self):
'''
- TODO: multiple points for programming problems.
+ Return maximum score for this problem.
+ We do this by counting the number of answers available for each question
+ in the problem. If the Response for a question has a get_max_score() method
+ then we call that and add its return value to the count. That can be
+ used to give complex problems (eg programming questions) multiple points.
'''
- sum = 0
- for et in entry_types:
- sum = sum + self.tree.xpath('count(//' + et + ')')
- return int(sum)
+ maxscore = 0
+ for responder in self.responders:
+ if hasattr(responder,'get_max_score'):
+ try:
+ maxscore += responder.get_max_score()
+ except Exception, err:
+ log.error('responder %s failed to properly return from get_max_score()' % responder)
+ raise
+ else:
+ try:
+ maxscore += len(responder.get_answers())
+ except:
+ log.error('responder %s failed to properly return get_answers()' % responder)
+ raise
+ return maxscore
def get_score(self):
correct = 0
@@ -166,34 +200,35 @@ class LoncapaProblem(object):
of each key removed (the string before the first "_").
Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123
+
+ Calles the Response for each question in this problem, to do the actual grading.
'''
self.student_answers = answers
self.correct_map = dict()
- problems_simple = self.extract_problems(self.tree)
- for response in problems_simple:
- grader = response_types[response.tag](response, self.context, self.system)
- results = grader.get_score(answers) # call the responsetype instance to do the actual grading
+ log.info('%s: in grade_answers, answers=%s' % (self,answers))
+ for responder in self.responders:
+ results = responder.get_score(answers) # call the responsetype instance to do the actual grading
self.correct_map.update(results)
return self.correct_map
def get_question_answers(self):
- """Returns a dict of answer_ids to answer values. If we can't generate
+ """Returns a dict of answer_ids to answer values. If we cannot generate
an answer (this sometimes happens in customresponses), that answer_id is
not included. Called by "show answers" button JSON request
(see capa_module)
"""
answer_map = dict()
- problems_simple = self.extract_problems(self.tree) # purified (flat) XML tree of just response queries
- for response in problems_simple:
- responder = response_types[response.tag](response, self.context, self.system) # instance of numericalresponse, customresponse,...
+ for responder in self.responders:
results = responder.get_answers()
answer_map.update(results) # dict of (id,correct_answer)
+ # This should be handled in each responsetype, not here.
# example for the following:
- for entry in problems_simple.xpath("//" + "|//".join(response_properties + entry_types)):
- answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline
- if answer:
- answer_map[entry.get('id')] = contextualize_text(answer, self.context)
+ for responder in self.responders:
+ for entry in responder.inputfields:
+ answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline
+ if answer:
+ answer_map[entry.get('id')] = contextualize_text(answer, self.context)
# include solutions from ... stanzas
# Tentative merge; we should figure out how we want to handle hints and solutions
@@ -209,17 +244,16 @@ class LoncapaProblem(object):
the dicts returned by grade_answers and get_question_answers. (Though
get_question_answers may only return a subset of these."""
answer_ids = []
- problems_simple = self.extract_problems(self.tree)
- for response in problems_simple:
- responder = response_types[response.tag](response, self.context)
- if hasattr(responder, "answer_id"):
- answer_ids.append(responder.answer_id)
- # customresponse types can have multiple answer_ids
- elif hasattr(responder, "answer_ids"):
- answer_ids.extend(responder.answer_ids)
-
+ for responder in self.responders:
+ answer_ids.append(responder.get_answers().keys())
return answer_ids
+ def get_html(self):
+ '''
+ Main method called externally to get the HTML to be rendered for this capa Problem.
+ '''
+ return contextualize_text(etree.tostring(self.extract_html(self.tree)[0]), self.context)
+
# ======= Private ========
def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
'''
@@ -253,9 +287,6 @@ class LoncapaProblem(object):
log.exception("Error while execing code: " + code)
return context
- def get_html(self):
- return contextualize_text(etree.tostring(self.extract_html(self.tree)[0]), self.context)
-
def extract_html(self, problemtree): # private
''' Helper function for get_html. Recursively converts XML tree to HTML
'''
@@ -335,76 +366,34 @@ class LoncapaProblem(object):
Assign sub-IDs to all entries (textline, schematic, etc.)
Annoted correctness and value
In-place transformation
+
+ Also create capa Response instances for each responsetype and save as self.responders
'''
response_id = 1
+ self.responders = []
for response in tree.xpath('//' + "|//".join(response_types)):
response_id_str = self.problem_id + "_" + str(response_id)
- response.attrib['id'] = response_id_str
- if response_id not in correct_map:
- correct = 'unsubmitted'
- response.attrib['state'] = correct
- response_id = response_id + 1
+ response.attrib['id'] = response_id_str # create and save ID for this response
+
+ # if response_id not in correct_map: correct = 'unsubmitted' # unused - to be removed
+ # response.attrib['state'] = correct
+ response_id += response_id
+
answer_id = 1
- for entry in tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]),
- id=response_id_str):
- # assign one answer_id for each entry_type or solution_type
+ inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]),
+ id=response_id_str)
+ for entry in inputfields: # assign one answer_id for each entry_type or solution_type
entry.attrib['response_id'] = str(response_id)
entry.attrib['answer_id'] = str(answer_id)
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
answer_id = answer_id + 1
+ responder = response_types[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
+ self.responders.append(responder) # save in list in self
+
# ... may not be associated with any specific response; give IDs for those separately
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
solution_id = 1
for solution in tree.findall('.//solution'):
solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id)
solution_id += 1
-
- def extract_problems(self, problem_tree):
- ''' Remove layout from the problem, and give a purified XML tree of just the problems '''
- problem_tree = copy.deepcopy(problem_tree)
- tree = Element('problem')
- for response in problem_tree.xpath("//" + "|//".join(response_types)):
- newresponse = copy.copy(response)
- for e in newresponse:
- newresponse.remove(e)
- # copy.copy is needed to make xpath work right. Otherwise, it starts at the root
- # of the tree. We should figure out if there's some work-around
- for e in copy.copy(response).xpath("//" + "|//".join(response_properties + entry_types)):
- newresponse.append(e)
-
- tree.append(newresponse)
- return tree
-
-if __name__ == '__main__':
- problem_id = 'simpleFormula'
- filename = 'simpleFormula.xml'
-
- problem_id = 'resistor'
- filename = 'resistor.xml'
-
- lcp = LoncapaProblem(filename, problem_id)
-
- context = lcp.extract_context(lcp.tree)
- problem = lcp.extract_problems(lcp.tree)
- print lcp.grade_problems({'resistor_2_1': '1.0', 'resistor_3_1': '2.0'})
- #print lcp.grade_problems({'simpleFormula_2_1':'3*x^3'})
-#numericalresponse(problem, context)
-
-#print etree.tostring((lcp.tree))
- print '============'
- print
-#print etree.tostring(lcp.extract_problems(lcp.tree))
- print lcp.get_html()
-#print extract_context(tree)
-
-
-
- # def handle_fr(self, element):
- # problem={"answer":self.contextualize_text(answer),
- # "type":"formularesponse",
- # "tolerance":evaluator({},{},self.contextualize_text(tolerance)),
- # "sample_range":dict(zip(variables, sranges)),
- # "samples_count": numsamples,
- # "id":id,
- # self.questions[self.lid]=problem
diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py
index c5683bb0bf..c0ad98baa2 100644
--- a/common/lib/capa/responsetypes.py
+++ b/common/lib/capa/responsetypes.py
@@ -21,40 +21,123 @@ import abc
# specific library imports
from calc import evaluator, UndefinedVariable
-from util import contextualize_text
+from util import *
from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
-log = logging.getLogger(__name__)
+#log = logging.getLogger(__name__)
+log = logging.getLogger('mitx.common.lib.capa.responsetypes')
-def compare_with_tolerance(v1, v2, tol):
- ''' Compare v1 to v2 with maximum tolerance tol
- tol is relative if it ends in %; otherwise, it is absolute
+#-----------------------------------------------------------------------------
+# Exceptions
+
+class LoncapaProblemError(Exception):
'''
- relative = "%" in tol
- if relative:
- tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01
- tolerance = tolerance_rel * max(abs(v1), abs(v2))
- else:
- tolerance = evaluator(dict(),dict(),tol)
- return abs(v1-v2) <= tolerance
+ Error in specification of a problem
+ '''
+ pass
+
+class ResponseError(Exception):
+ '''
+ Error for failure in processing a response
+ '''
+ pass
+
+class StudentInputError(Exception):
+ pass
+
+#-----------------------------------------------------------------------------
+#
+# Main base class for CAPA responsetypes
class GenericResponse(object):
+ '''
+ Base class for CAPA responsetypes. Each response type (ie a capa question,
+ which is part of a capa problem) is represented as a superclass,
+ which should provide the following methods:
+
+ - get_score : evaluate the given student answers, and return a CorrectMap
+ - get_answers : provide a dict of the expected answers for this problem
+
+ In addition, these methods are optional:
+
+ - get_max_score : if defined, this is called to obtain the maximum score possible for this question
+ - setup_response : find and note the answer input field IDs for the response; called by __init__
+
+ Each response type may also specify the following attributes:
+
+ - max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None)
+ - allowed_inputfields : list of allowed input fields (each a string) for this Response
+ - required_attributes : list of required attributes (each a string) on the main response XML stanza
+
+ '''
__metaclass__=abc.ABCMeta # abc = Abstract Base Class
+ max_inputfields = None
+ allowed_inputfields = []
+ required_attributes = []
+
+ def __init__(self, xml, inputfields, context, system=None):
+ '''
+ Init is passed the following arguments:
+
+ - xml : ElementTree of this Response
+ - inputfields : list of ElementTrees for each input entry field in this Response
+ - context : script processor context
+ - system : I4xSystem instance which provides OS, rendering, and user context
+ - __unicode__ : unicode representation of this Response
+
+ '''
+ self.xml = xml
+ self.inputfields = inputfields
+ self.context = context
+ self.system = system
+
+ for abox in inputfields:
+ if not abox.tag in self.allowed_inputfields:
+ msg = "%s: cannot have input field %s" % (unicode(self),abox.tag)
+ msg += "\nSee XML source line %s" % getattr(xml,'sourceline','')
+ raise LoncapaProblemError(msg)
+
+ if self.max_inputfields and len(inputfields)>self.max_inputfields:
+ msg = "%s: cannot have more than %s input fields" % (unicode(self),self.max_inputfields)
+ msg += "\nSee XML source line %s" % getattr(xml,'sourceline','')
+ raise LoncapaProblemError(msg)
+
+ for prop in self.required_attributes:
+ if not xml.get(prop):
+ msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self),prop)
+ msg += "\nSee XML source line %s" % getattr(xml,'sourceline','')
+ raise LoncapaProblemError(msg)
+
+ self.answer_ids = [x.get('id') for x in self.inputfields]
+ if self.max_inputfields==1:
+ self.answer_id = self.answer_ids[0] # for convenience
+
+ if hasattr(self,'setup_response'):
+ self.setup_response()
+
@abc.abstractmethod
def get_score(self, student_answers):
+ '''
+ Return a CorrectMap for the answers expected vs given. This includes
+ (correctness, npoints, msg) for each answer_id.
+ '''
pass
@abc.abstractmethod
def get_answers(self):
+ '''
+ Return a dict of (answer_id,answer_text) for each answer for this question.
+ '''
pass
#not an abstract method because plenty of responses will not want to preprocess anything, and we should not require that they override this method.
- def preprocess_response(self):
+ def setup_response(self):
pass
-#Every response type needs methods "get_score" and "get_answers"
+ def __unicode__(self):
+ return 'LoncapaProblem Response %s' % self.xml.tag
#-----------------------------------------------------------------------------
@@ -69,30 +152,19 @@ class MultipleChoiceResponse(GenericResponse):
'''}]
- def __init__(self, xml, context, system=None):
- self.xml = xml
- self.correct_choices = xml.xpath('//*[@id=$id]//choice[@correct="true"]',
- id=xml.get('id'))
- self.correct_choices = [choice.get('name') for choice in self.correct_choices]
- self.context = context
- self.answer_field = xml.find('choicegroup') # assumes only ONE choicegroup within this response
- self.answer_id = xml.xpath('//*[@id=$id]//choicegroup/@id',
- id=xml.get('id'))
- if not len(self.answer_id) == 1:
- raise Exception("should have exactly one choice group per multiplechoicceresponse")
- self.answer_id=self.answer_id[0]
+ max_inputfields = 1
+ allowed_inputfields = ['choicegroup']
- def get_score(self, student_answers):
- if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices:
- return {self.answer_id:'correct'}
- else:
- return {self.answer_id:'incorrect'}
+ def setup_response(self):
+ self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes
- def get_answers(self):
- return {self.answer_id:self.correct_choices}
+ # define correct choices (after calling secondary setup)
+ xml = self.xml
+ cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]',id=xml.get('id'))
+ self.correct_choices = [choice.get('name') for choice in cxml]
- def preprocess_response(self):
+ def mc_setup_response(self):
'''
Initialize name attributes in stanzas in the in this response.
'''
@@ -107,9 +179,22 @@ class MultipleChoiceResponse(GenericResponse):
i+=1
else:
choice.set("name", "choice_"+choice.get("name"))
-
+
+ def get_score(self, student_answers):
+ '''
+ grade student response.
+ '''
+ # log.debug('%s: student_answers=%s, correct_choices=%s' % (unicode(self),student_answers,self.correct_choices))
+ if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices:
+ return {self.answer_id:'correct'}
+ else:
+ return {self.answer_id:'incorrect'}
+
+ def get_answers(self):
+ return {self.answer_id:self.correct_choices}
+
class TrueFalseResponse(MultipleChoiceResponse):
- def preprocess_response(self):
+ def mc_setup_response(self):
i=0
for response in self.xml.xpath("choicegroup"):
response.set("type", "TrueFalse")
@@ -140,12 +225,13 @@ class OptionResponse(GenericResponse):
The location of the earth
'''}]
- def __init__(self, xml, context, system=None):
- self.xml = xml
- self.answer_fields = xml.findall('optioninput')
- self.context = context
+ allowed_inputfields = ['optioninput']
+
+ def setup_response(self):
+ self.answer_fields = self.inputfields
def get_score(self, student_answers):
+ # log.debug('%s: student_answers=%s' % (unicode(self),student_answers))
cmap = {}
amap = self.get_answers()
for aid in amap:
@@ -157,17 +243,20 @@ class OptionResponse(GenericResponse):
def get_answers(self):
amap = dict([(af.get('id'),af.get('correct')) for af in self.answer_fields])
+ # log.debug('%s: expected answers=%s' % (unicode(self),amap))
return amap
#-----------------------------------------------------------------------------
class NumericalResponse(GenericResponse):
- def __init__(self, xml, context, system=None):
- self.xml = xml
- if not xml.get('answer'):
- msg = "Error in problem specification: numericalresponse missing required answer attribute\n"
- msg += "See XML source line %s" % getattr(xml,'sourceline','')
- raise Exception,msg
+
+ allowed_inputfields = ['textline']
+ required_attributes = ['answer']
+ max_inputfields = 1
+
+ def setup_response(self):
+ xml = self.xml
+ context = self.context
self.correct_answer = contextualize_text(xml.get('answer'), context)
try:
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
@@ -182,7 +271,7 @@ class NumericalResponse(GenericResponse):
self.answer_id = None
def get_score(self, student_answers):
- ''' Display HTML for a numeric response '''
+ '''Grade a numeric response '''
student_answer = student_answers[self.answer_id]
try:
correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), complex(self.correct_answer), self.tolerance)
@@ -241,16 +330,11 @@ def sympy_check2():
'''}]
- def __init__(self, xml, context, system=None):
- self.xml = xml
- self.system = system
- ## CRITICAL TODO: Should cover all entrytypes
- ## NOTE: xpath will look at root of XML tree, not just
- ## what's in xml. @id=id keeps us in the right customresponse.
- self.answer_ids = xml.xpath('//*[@id=$id]//textline/@id',
- id=xml.get('id'))
- self.answer_ids += [x.get('id') for x in xml.findall('textbox')] # also allow textbox inputs
- self.context = context
+ allowed_inputfields = ['textline','textbox']
+
+ def setup_response(self):
+ xml = self.xml
+ context = self.context
# if has an "expect" (or "answer") attribute then save that
self.expect = xml.get('expect') or xml.get('answer')
@@ -271,15 +355,17 @@ def sympy_check2():
cfn = xml.get('cfn')
if cfn:
log.debug("cfn = %s" % cfn)
- if cfn in context:
- self.code = context[cfn]
+ if cfn in self.context:
+ self.code = self.context[cfn]
else:
- print "can't find cfn in context = ",context
+ msg = "%s: can't find cfn in context = %s" % (unicode(self),self.context)
+ msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','')
+ raise LoncapaProblemError(msg)
if not self.code:
if answer is None:
# raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
- print "[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
+ log.error("[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid)
self.code = ''
else:
answer_src = answer.get('src')
@@ -294,6 +380,8 @@ def sympy_check2():
of each key removed (the string before the first "_").
'''
+ log.debug('%s: student_answers=%s' % (unicode(self),student_answers))
+
idset = sorted(self.answer_ids) # ordered list of answer id's
try:
submission = [student_answers[k] for k in idset] # ordered list of answers
@@ -425,12 +513,12 @@ class SymbolicResponse(CustomResponse):
Your input should be typed in as a list of lists, eg [[1,2],[3,4]].
'''}]
- def __init__(self, xml, context, system=None):
- xml.set('cfn','symmath_check')
+
+ def setup_response(self):
+ self.xml.set('cfn','symmath_check')
code = "from symmath import *"
- exec code in context,context
- CustomResponse.__init__(self,xml,context,system)
-
+ exec code in self.context,self.context
+ CustomResponse.setup_response(self)
#-----------------------------------------------------------------------------
@@ -480,15 +568,13 @@ main()
'''}]
- def __init__(self, xml, context, system=None):
- self.xml = xml
- self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
- self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id',
- id=xml.get('id'))
- self.context = context
- answer = xml.xpath('//*[@id=$id]//answer',
- id=xml.get('id'))[0]
+ allowed_inputfields = ['textline','textbox']
+ def setup_response(self):
+ xml = self.xml
+ self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
+
+ answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors
answer_src = answer.get('src')
if answer_src is not None:
self.code = self.system.filesystem.open('src/'+answer_src).read()
@@ -590,8 +676,6 @@ main()
raise Exception,'Short response from external server'
return dict(zip(self.answer_ids,exans))
-class StudentInputError(Exception):
- pass
#-----------------------------------------------------------------------------
@@ -617,8 +701,13 @@ class FormulaResponse(GenericResponse):
'''}]
- def __init__(self, xml, context, system=None):
- self.xml = xml
+ allowed_inputfields = ['textline']
+ required_attributes = ['answer']
+ max_inputfields = 1
+
+ def setup_response(self):
+ xml = self.xml
+ context = self.context
self.correct_answer = contextualize_text(xml.get('answer'), context)
self.samples = contextualize_text(xml.get('samples'), context)
try:
@@ -628,14 +717,6 @@ class FormulaResponse(GenericResponse):
except Exception:
self.tolerance = 0
- try:
- self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
- id=xml.get('id'))[0]
- except Exception:
- self.answer_id = None
- raise Exception, "[courseware.capa.responsetypes.FormulaResponse] Error: missing answer_id!!"
-
- self.context = context
ts = xml.get('type')
if ts is None:
typeslist = []
@@ -648,7 +729,6 @@ class FormulaResponse(GenericResponse):
else: # Default
self.case_sensitive = False
-
def get_score(self, student_answers):
variables=self.samples.split('@')[0].split(',')
numsamples=int(self.samples.split('@')[1].split('#')[1])
@@ -697,13 +777,12 @@ class FormulaResponse(GenericResponse):
#-----------------------------------------------------------------------------
class SchematicResponse(GenericResponse):
- def __init__(self, xml, context, system=None):
- self.xml = xml
- self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id',
- id=xml.get('id'))
- self.context = context
- answer = xml.xpath('//*[@id=$id]//answer',
- id=xml.get('id'))[0]
+
+ allowed_inputfields = ['schematic']
+
+ def setup_response(self):
+ xml = self.xml
+ answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0]
answer_src = answer.get('src')
if answer_src is not None:
self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used
@@ -740,10 +819,10 @@ class ImageResponse(GenericResponse):
'''}]
- def __init__(self, xml, context, system=None):
- self.xml = xml
- self.context = context
- self.ielements = xml.findall('imageinput')
+ allowed_inputfields = ['imageinput']
+
+ def setup_response(self):
+ self.ielements = self.inputfields
self.answer_ids = [ie.get('id') for ie in self.ielements]
def get_score(self, student_answers):
diff --git a/common/lib/capa/util.py b/common/lib/capa/util.py
index d042aa21d3..996f6c8dac 100644
--- a/common/lib/capa/util.py
+++ b/common/lib/capa/util.py
@@ -1,3 +1,21 @@
+from calc import evaluator, UndefinedVariable
+
+#-----------------------------------------------------------------------------
+#
+# Utility functions used in CAPA responsetypes
+
+def compare_with_tolerance(v1, v2, tol):
+ ''' Compare v1 to v2 with maximum tolerance tol
+ tol is relative if it ends in %; otherwise, it is absolute
+ '''
+ relative = "%" in tol
+ if relative:
+ tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01
+ tolerance = tolerance_rel * max(abs(v1), abs(v2))
+ else:
+ tolerance = evaluator(dict(),dict(),tol)
+ return abs(v1-v2) <= tolerance
+
def contextualize_text(text, context): # private
''' Takes a string with variables. E.g. $a+$b.
Does a substitution of those variables from the context '''
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 97fd1b948c..0f82d9ba94 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -169,7 +169,7 @@ def render_x_module(user, request, xml_module, module_object_preload, position=N
content = instance.get_html()
# special extra information about each problem, only for users who are staff
- if user.is_staff:
+ if False and user.is_staff:
module_id = xml_module.get('id')
histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0
diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee
index c1a801d2b2..e1e062e949 100644
--- a/lms/static/coffee/src/modules/problem.coffee
+++ b/lms/static/coffee/src/modules/problem.coffee
@@ -48,7 +48,7 @@ class @Problem
@$("label[for='input_#{key}_#{choice}']").attr
correct_answer: 'true'
else
- @$("#answer_#{key}").text(value)
+ @$("#answer_#{key}").html(value) // needs to be html, not text, for complex solutions (eg coding)
@$('.show').val 'Hide Answer'
@element.addClass 'showed'
else