Commit 46b45969 by ichuang

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 ae44d86e
#
# 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):
'''
Error in specification of a problem
'''
pass
class ResponseError(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 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
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
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')
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):
......
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
......
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