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.
This commit is contained in:
@@ -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 <text></text>
|
||||
# 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 <text></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)
|
||||
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: <textline size="5" correct_answer="saturated" />
|
||||
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 <solution>...</solution> 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
|
||||
|
||||
# <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).
|
||||
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
|
||||
|
||||
@@ -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','<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
|
||||
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):
|
||||
</choicegroup>
|
||||
</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
|
||||
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 <choice> stanzas in the <choicegroup> 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):
|
||||
<optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput>
|
||||
</optionresponse>'''}]
|
||||
|
||||
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','<unavailable>')
|
||||
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():
|
||||
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
|
||||
</customresponse>'''}]
|
||||
|
||||
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 <customresponse> 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','<unavailable>')
|
||||
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 <tt>[[1,2],[3,4]]</tt>.
|
||||
</text>
|
||||
</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 *"
|
||||
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()
|
||||
</answer>
|
||||
</externalresponse>'''}]
|
||||
|
||||
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):
|
||||
|
||||
</problem>'''}]
|
||||
|
||||
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):
|
||||
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" />
|
||||
</imageresponse>'''}]
|
||||
|
||||
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):
|
||||
|
||||
@@ -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 '''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user