Commit f1a12a26 by ichuang Committed by Matthew Mongeau

first pass in capa cleanup:

   - responsetype used to be instantiated multiple times(!) in capa_problem
     now it is instantiated once, and stored in self.responders
   - responsetypes.GenericResponse restructured; each superclass
     show now provide setup_response (and not __init__), and may
     provide get_max_score(); general __init__ provided to
     clean up superclasses.
parent ec0b451e
# #
# File: capa/capa_problem.py # 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). Main module which shows problems (of "capa" type).
...@@ -83,17 +88,32 @@ html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formul ...@@ -83,17 +88,32 @@ html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formul
class LoncapaProblem(object): class LoncapaProblem(object):
'''
Main class for capa Problems.
'''
def __init__(self, fileobject, id, state=None, seed=None, system=None): 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 ## Initialize class variables from state
self.seed = None
self.student_answers = dict() self.student_answers = dict()
self.correct_map = dict() self.correct_map = dict()
self.done = False self.done = False
self.problem_id = id self.problem_id = id
self.system = system self.system = system
self.seed = seed
if seed is not None:
self.seed = seed
if state: if state:
if 'seed' in state: if 'seed' in state:
...@@ -109,22 +129,21 @@ class LoncapaProblem(object): ...@@ -109,22 +129,21 @@ class LoncapaProblem(object):
if not self.seed: if not self.seed:
self.seed = struct.unpack('i', os.urandom(4))[0] self.seed = struct.unpack('i', os.urandom(4))[0]
## Parse XML file self.fileobject = fileobject # save problem file object, so we can use for debugging information later
if getattr(system, 'DEBUG', False): 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) log.info("[courseware.capa.capa_problem.lcp.init] fileobject = %s" % fileobject)
file_text = fileobject.read() file_text = fileobject.read()
self.fileobject = fileobject # save it, so we can use for debugging information later file_text = re.sub("startouttext\s*/", "text", file_text) # Convert startouttext and endouttext to proper <text></text>
# Convert startouttext and endouttext to proper <text></text>
# TODO: Do with XML operations
file_text = re.sub("startouttext\s*/", "text", file_text)
file_text = re.sub("endouttext\s*/", "/text", file_text) 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) 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) # pre-parse the XML tree: modifies it to add ID's and perform some in-place transformations
responder.preprocess_response() # 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): def __unicode__(self):
return u"LoncapaProblem ({0})".format(self.fileobject) return u"LoncapaProblem ({0})".format(self.fileobject)
...@@ -140,12 +159,27 @@ class LoncapaProblem(object): ...@@ -140,12 +159,27 @@ class LoncapaProblem(object):
def get_max_score(self): 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 maxscore = 0
for et in entry_types: for responder in self.responders:
sum = sum + self.tree.xpath('count(//' + et + ')') if hasattr(responder,'get_max_score'):
return int(sum) 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): def get_score(self):
correct = 0 correct = 0
...@@ -166,34 +200,35 @@ class LoncapaProblem(object): ...@@ -166,34 +200,35 @@ class LoncapaProblem(object):
of each key removed (the string before the first "_"). of each key removed (the string before the first "_").
Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123 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.student_answers = answers
self.correct_map = dict() self.correct_map = dict()
problems_simple = self.extract_problems(self.tree) log.info('%s: in grade_answers, answers=%s' % (self,answers))
for response in problems_simple: for responder in self.responders:
grader = response_types[response.tag](response, self.context, self.system) results = responder.get_score(answers) # call the responsetype instance to do the actual grading
results = grader.get_score(answers) # call the responsetype instance to do the actual grading
self.correct_map.update(results) self.correct_map.update(results)
return self.correct_map return self.correct_map
def get_question_answers(self): 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 an answer (this sometimes happens in customresponses), that answer_id is
not included. Called by "show answers" button JSON request not included. Called by "show answers" button JSON request
(see capa_module) (see capa_module)
""" """
answer_map = dict() answer_map = dict()
problems_simple = self.extract_problems(self.tree) # purified (flat) XML tree of just response queries for responder in self.responders:
for response in problems_simple:
responder = response_types[response.tag](response, self.context, self.system) # instance of numericalresponse, customresponse,...
results = responder.get_answers() results = responder.get_answers()
answer_map.update(results) # dict of (id,correct_answer) answer_map.update(results) # dict of (id,correct_answer)
# This should be handled in each responsetype, not here.
# example for the following: <textline size="5" correct_answer="saturated" /> # example for the following: <textline size="5" correct_answer="saturated" />
for entry in problems_simple.xpath("//" + "|//".join(response_properties + entry_types)): for responder in self.responders:
answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline for entry in responder.inputfields:
if answer: answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline
answer_map[entry.get('id')] = contextualize_text(answer, self.context) if answer:
answer_map[entry.get('id')] = contextualize_text(answer, self.context)
# include solutions from <solution>...</solution> stanzas # include solutions from <solution>...</solution> stanzas
# Tentative merge; we should figure out how we want to handle hints and solutions # Tentative merge; we should figure out how we want to handle hints and solutions
...@@ -209,17 +244,16 @@ class LoncapaProblem(object): ...@@ -209,17 +244,16 @@ class LoncapaProblem(object):
the dicts returned by grade_answers and get_question_answers. (Though the dicts returned by grade_answers and get_question_answers. (Though
get_question_answers may only return a subset of these.""" get_question_answers may only return a subset of these."""
answer_ids = [] answer_ids = []
problems_simple = self.extract_problems(self.tree) for responder in self.responders:
for response in problems_simple: answer_ids.append(responder.get_answers().keys())
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)
return answer_ids 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 ======== # ======= Private ========
def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
''' '''
...@@ -253,9 +287,6 @@ class LoncapaProblem(object): ...@@ -253,9 +287,6 @@ class LoncapaProblem(object):
log.exception("Error while execing code: " + code) log.exception("Error while execing code: " + code)
return context 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 def extract_html(self, problemtree): # private
''' Helper function for get_html. Recursively converts XML tree to HTML ''' Helper function for get_html. Recursively converts XML tree to HTML
''' '''
...@@ -335,76 +366,34 @@ class LoncapaProblem(object): ...@@ -335,76 +366,34 @@ class LoncapaProblem(object):
Assign sub-IDs to all entries (textline, schematic, etc.) Assign sub-IDs to all entries (textline, schematic, etc.)
Annoted correctness and value Annoted correctness and value
In-place transformation In-place transformation
Also create capa Response instances for each responsetype and save as self.responders
''' '''
response_id = 1 response_id = 1
self.responders = []
for response in tree.xpath('//' + "|//".join(response_types)): for response in tree.xpath('//' + "|//".join(response_types)):
response_id_str = self.problem_id + "_" + str(response_id) response_id_str = self.problem_id + "_" + str(response_id)
response.attrib['id'] = response_id_str response.attrib['id'] = response_id_str # create and save ID for this response
if response_id not in correct_map:
correct = 'unsubmitted' # if response_id not in correct_map: correct = 'unsubmitted' # unused - to be removed
response.attrib['state'] = correct # response.attrib['state'] = correct
response_id = response_id + 1 response_id += response_id
answer_id = 1 answer_id = 1
for entry in tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]), inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]),
id=response_id_str): id=response_id_str)
# assign one answer_id for each entry_type or solution_type for entry in inputfields: # assign one answer_id for each entry_type or solution_type
entry.attrib['response_id'] = str(response_id) entry.attrib['response_id'] = str(response_id)
entry.attrib['answer_id'] = str(answer_id) entry.attrib['answer_id'] = str(answer_id)
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id) entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
answer_id = answer_id + 1 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
# <solution>...</solution> may not be associated with any specific response; give IDs for those separately # <solution>...</solution> 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). # TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
solution_id = 1 solution_id = 1
for solution in tree.findall('.//solution'): for solution in tree.findall('.//solution'):
solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id) solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id)
solution_id += 1 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
...@@ -21,40 +21,123 @@ import abc ...@@ -21,40 +21,123 @@ import abc
# specific library imports # specific library imports
from calc import evaluator, UndefinedVariable from calc import evaluator, UndefinedVariable
from util import contextualize_text from util import *
from lxml import etree from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? 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 # Exceptions
tol is relative if it ends in %; otherwise, it is absolute
class LoncapaProblemError(Exception):
'''
Error in specification of a problem
'''
pass
class ResponseError(Exception):
''' '''
relative = "%" in tol Error for failure in processing a response
if relative: '''
tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01 pass
tolerance = tolerance_rel * max(abs(v1), abs(v2))
else: class StudentInputError(Exception):
tolerance = evaluator(dict(),dict(),tol) pass
return abs(v1-v2) <= tolerance
#-----------------------------------------------------------------------------
#
# Main base class for CAPA responsetypes
class GenericResponse(object): 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 __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','<unavailable>')
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','<unavailable>')
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','<unavailable>')
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 @abc.abstractmethod
def get_score(self, student_answers): 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 pass
@abc.abstractmethod @abc.abstractmethod
def get_answers(self): def get_answers(self):
'''
Return a dict of (answer_id,answer_text) for each answer for this question.
'''
pass 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. #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 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): ...@@ -69,30 +152,19 @@ class MultipleChoiceResponse(GenericResponse):
</choicegroup> </choicegroup>
</multiplechoiceresponse> </multiplechoiceresponse>
'''}] '''}]
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 max_inputfields = 1
self.answer_id = xml.xpath('//*[@id=$id]//choicegroup/@id', allowed_inputfields = ['choicegroup']
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]
def get_score(self, student_answers): def setup_response(self):
if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices: self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes
return {self.answer_id:'correct'}
else:
return {self.answer_id:'incorrect'}
def get_answers(self): # define correct choices (after calling secondary setup)
return {self.answer_id:self.correct_choices} 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 <choice> stanzas in the <choicegroup> in this response. Initialize name attributes in <choice> stanzas in the <choicegroup> in this response.
''' '''
...@@ -107,9 +179,22 @@ class MultipleChoiceResponse(GenericResponse): ...@@ -107,9 +179,22 @@ class MultipleChoiceResponse(GenericResponse):
i+=1 i+=1
else: else:
choice.set("name", "choice_"+choice.get("name")) 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): class TrueFalseResponse(MultipleChoiceResponse):
def preprocess_response(self): def mc_setup_response(self):
i=0 i=0
for response in self.xml.xpath("choicegroup"): for response in self.xml.xpath("choicegroup"):
response.set("type", "TrueFalse") response.set("type", "TrueFalse")
...@@ -140,12 +225,13 @@ class OptionResponse(GenericResponse): ...@@ -140,12 +225,13 @@ class OptionResponse(GenericResponse):
<optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput> <optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput>
</optionresponse>'''}] </optionresponse>'''}]
def __init__(self, xml, context, system=None): allowed_inputfields = ['optioninput']
self.xml = xml
self.answer_fields = xml.findall('optioninput') def setup_response(self):
self.context = context self.answer_fields = self.inputfields
def get_score(self, student_answers): def get_score(self, student_answers):
# log.debug('%s: student_answers=%s' % (unicode(self),student_answers))
cmap = {} cmap = {}
amap = self.get_answers() amap = self.get_answers()
for aid in amap: for aid in amap:
...@@ -157,17 +243,20 @@ class OptionResponse(GenericResponse): ...@@ -157,17 +243,20 @@ class OptionResponse(GenericResponse):
def get_answers(self): def get_answers(self):
amap = dict([(af.get('id'),af.get('correct')) for af in self.answer_fields]) 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 return amap
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class NumericalResponse(GenericResponse): class NumericalResponse(GenericResponse):
def __init__(self, xml, context, system=None):
self.xml = xml allowed_inputfields = ['textline']
if not xml.get('answer'): required_attributes = ['answer']
msg = "Error in problem specification: numericalresponse missing required answer attribute\n" max_inputfields = 1
msg += "See XML source line %s" % getattr(xml,'sourceline','<unavailable>')
raise Exception,msg def setup_response(self):
xml = self.xml
context = self.context
self.correct_answer = contextualize_text(xml.get('answer'), context) self.correct_answer = contextualize_text(xml.get('answer'), context)
try: try:
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
...@@ -182,7 +271,7 @@ class NumericalResponse(GenericResponse): ...@@ -182,7 +271,7 @@ class NumericalResponse(GenericResponse):
self.answer_id = None self.answer_id = None
def get_score(self, student_answers): def get_score(self, student_answers):
''' Display HTML for a numeric response ''' '''Grade a numeric response '''
student_answer = student_answers[self.answer_id] student_answer = student_answers[self.answer_id]
try: try:
correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), complex(self.correct_answer), self.tolerance) correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), complex(self.correct_answer), self.tolerance)
...@@ -241,16 +330,11 @@ def sympy_check2(): ...@@ -241,16 +330,11 @@ def sympy_check2():
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/> <responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
</customresponse>'''}] </customresponse>'''}]
def __init__(self, xml, context, system=None): allowed_inputfields = ['textline','textbox']
self.xml = xml
self.system = system def setup_response(self):
## CRITICAL TODO: Should cover all entrytypes xml = self.xml
## NOTE: xpath will look at root of XML tree, not just context = self.context
## 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
# if <customresponse> has an "expect" (or "answer") attribute then save that # if <customresponse> has an "expect" (or "answer") attribute then save that
self.expect = xml.get('expect') or xml.get('answer') self.expect = xml.get('expect') or xml.get('answer')
...@@ -271,15 +355,17 @@ def sympy_check2(): ...@@ -271,15 +355,17 @@ def sympy_check2():
cfn = xml.get('cfn') cfn = xml.get('cfn')
if cfn: if cfn:
log.debug("cfn = %s" % cfn) log.debug("cfn = %s" % cfn)
if cfn in context: if cfn in self.context:
self.code = context[cfn] self.code = self.context[cfn]
else: 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','<unavailable>')
raise LoncapaProblemError(msg)
if not self.code: if not self.code:
if answer is None: if answer is None:
# raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid # 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 = '' self.code = ''
else: else:
answer_src = answer.get('src') answer_src = answer.get('src')
...@@ -294,6 +380,8 @@ def sympy_check2(): ...@@ -294,6 +380,8 @@ def sympy_check2():
of each key removed (the string before the first "_"). 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 idset = sorted(self.answer_ids) # ordered list of answer id's
try: try:
submission = [student_answers[k] for k in idset] # ordered list of answers submission = [student_answers[k] for k in idset] # ordered list of answers
...@@ -425,12 +513,12 @@ class SymbolicResponse(CustomResponse): ...@@ -425,12 +513,12 @@ class SymbolicResponse(CustomResponse):
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>. Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>.
</text> </text>
</problem>'''}] </problem>'''}]
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 *" code = "from symmath import *"
exec code in context,context exec code in self.context,self.context
CustomResponse.__init__(self,xml,context,system) CustomResponse.setup_response(self)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -480,15 +568,13 @@ main() ...@@ -480,15 +568,13 @@ main()
</answer> </answer>
</externalresponse>'''}] </externalresponse>'''}]
def __init__(self, xml, context, system=None): allowed_inputfields = ['textline','textbox']
self.xml = xml
def setup_response(self):
xml = self.xml
self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL 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]
answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors
answer_src = answer.get('src') answer_src = answer.get('src')
if answer_src is not None: if answer_src is not None:
self.code = self.system.filesystem.open('src/'+answer_src).read() self.code = self.system.filesystem.open('src/'+answer_src).read()
...@@ -590,8 +676,6 @@ main() ...@@ -590,8 +676,6 @@ main()
raise Exception,'Short response from external server' raise Exception,'Short response from external server'
return dict(zip(self.answer_ids,exans)) return dict(zip(self.answer_ids,exans))
class StudentInputError(Exception):
pass
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -617,8 +701,13 @@ class FormulaResponse(GenericResponse): ...@@ -617,8 +701,13 @@ class FormulaResponse(GenericResponse):
</problem>'''}] </problem>'''}]
def __init__(self, xml, context, system=None): allowed_inputfields = ['textline']
self.xml = xml 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.correct_answer = contextualize_text(xml.get('answer'), context)
self.samples = contextualize_text(xml.get('samples'), context) self.samples = contextualize_text(xml.get('samples'), context)
try: try:
...@@ -628,14 +717,6 @@ class FormulaResponse(GenericResponse): ...@@ -628,14 +717,6 @@ class FormulaResponse(GenericResponse):
except Exception: except Exception:
self.tolerance = 0 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') ts = xml.get('type')
if ts is None: if ts is None:
typeslist = [] typeslist = []
...@@ -648,7 +729,6 @@ class FormulaResponse(GenericResponse): ...@@ -648,7 +729,6 @@ class FormulaResponse(GenericResponse):
else: # Default else: # Default
self.case_sensitive = False self.case_sensitive = False
def get_score(self, student_answers): def get_score(self, student_answers):
variables=self.samples.split('@')[0].split(',') variables=self.samples.split('@')[0].split(',')
numsamples=int(self.samples.split('@')[1].split('#')[1]) numsamples=int(self.samples.split('@')[1].split('#')[1])
...@@ -697,13 +777,12 @@ class FormulaResponse(GenericResponse): ...@@ -697,13 +777,12 @@ class FormulaResponse(GenericResponse):
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class SchematicResponse(GenericResponse): class SchematicResponse(GenericResponse):
def __init__(self, xml, context, system=None):
self.xml = xml allowed_inputfields = ['schematic']
self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id',
id=xml.get('id')) def setup_response(self):
self.context = context xml = self.xml
answer = xml.xpath('//*[@id=$id]//answer', answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0]
id=xml.get('id'))[0]
answer_src = answer.get('src') answer_src = answer.get('src')
if answer_src is not None: if answer_src is not None:
self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used
...@@ -740,10 +819,10 @@ class ImageResponse(GenericResponse): ...@@ -740,10 +819,10 @@ class ImageResponse(GenericResponse):
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" /> <imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" />
</imageresponse>'''}] </imageresponse>'''}]
def __init__(self, xml, context, system=None): allowed_inputfields = ['imageinput']
self.xml = xml
self.context = context def setup_response(self):
self.ielements = xml.findall('imageinput') self.ielements = self.inputfields
self.answer_ids = [ie.get('id') for ie in self.ielements] self.answer_ids = [ie.get('id') for ie in self.ielements]
def get_score(self, student_answers): def get_score(self, student_answers):
......
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 def contextualize_text(text, context): # private
''' Takes a string with variables. E.g. $a+$b. ''' Takes a string with variables. E.g. $a+$b.
Does a substitution of those variables from the context ''' Does a substitution of those variables from the context '''
......
...@@ -169,7 +169,7 @@ def render_x_module(user, request, xml_module, module_object_preload, position=N ...@@ -169,7 +169,7 @@ def render_x_module(user, request, xml_module, module_object_preload, position=N
content = instance.get_html() content = instance.get_html()
# special extra information about each problem, only for users who are staff # 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') module_id = xml_module.get('id')
histogram = grade_histogram(module_id) histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0 render_histogram = len(histogram) > 0
......
...@@ -48,7 +48,7 @@ class @Problem ...@@ -48,7 +48,7 @@ class @Problem
@$("label[for='input_#{key}_#{choice}']").attr @$("label[for='input_#{key}_#{choice}']").attr
correct_answer: 'true' correct_answer: 'true'
else else
@$("#answer_#{key}").text(value) @$("#answer_#{key}").html(value) // needs to be html, not text, for complex solutions (eg coding)
@$('.show').val 'Hide Answer' @$('.show').val 'Hide Answer'
@element.addClass 'showed' @element.addClass 'showed'
else else
......
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