Commit c724affe by ichuang

third pass in capa cleanup: correct_map -> CorrectMap

  - added correctmap.py with CorrectMap class
  - messages subsumed into CorrectMap
  - response get_score called with old CorrectMap so hints based on history are possible
parent 7b3c7969
...@@ -25,15 +25,14 @@ import struct ...@@ -25,15 +25,14 @@ import struct
from lxml import etree from lxml import etree
from xml.sax.saxutils import unescape from xml.sax.saxutils import unescape
from util import contextualize_text
import inputtypes
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse
import calc import calc
from correctmap import CorrectMap
import eia import eia
import inputtypes
from util import contextualize_text
log = logging.getLogger(__name__) # to be replaced with auto-registering
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse
# dict of tagname, Response Class -- this should come from auto-registering # dict of tagname, Response Class -- this should come from auto-registering
response_types = {'numericalresponse': NumericalResponse, response_types = {'numericalresponse': NumericalResponse,
...@@ -68,6 +67,12 @@ global_context = {'random': random, ...@@ -68,6 +67,12 @@ global_context = {'random': random,
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
html_problem_semantics = ["responseparam", "answer", "script"] html_problem_semantics = ["responseparam", "answer", "script"]
#log = logging.getLogger(__name__)
log = logging.getLogger('mitx.common.lib.capa.capa_problem')
#-----------------------------------------------------------------------------
# main class for this module
class LoncapaProblem(object): class LoncapaProblem(object):
''' '''
Main class for capa Problems. Main class for capa Problems.
...@@ -89,9 +94,7 @@ class LoncapaProblem(object): ...@@ -89,9 +94,7 @@ class LoncapaProblem(object):
''' '''
## Initialize class variables from state ## Initialize class variables from state
self.student_answers = dict() self.do_reset()
self.correct_map = dict()
self.done = False
self.problem_id = id self.problem_id = id
self.system = system self.system = system
self.seed = seed self.seed = seed
...@@ -102,7 +105,7 @@ class LoncapaProblem(object): ...@@ -102,7 +105,7 @@ class LoncapaProblem(object):
if 'student_answers' in state: if 'student_answers' in state:
self.student_answers = state['student_answers'] self.student_answers = state['student_answers']
if 'correct_map' in state: if 'correct_map' in state:
self.correct_map = state['correct_map'] self.correct_map.set_dict(state['correct_map'])
if 'done' in state: if 'done' in state:
self.done = state['done'] self.done = state['done']
...@@ -125,7 +128,15 @@ class LoncapaProblem(object): ...@@ -125,7 +128,15 @@ class LoncapaProblem(object):
# pre-parse the XML tree: modifies it to add ID's and perform some in-place transformations # pre-parse the XML tree: modifies it to add ID's and perform some in-place transformations
# this also creates the dict (self.responders) of Response instances for each question in the problem. # this also creates the dict (self.responders) of Response instances for each question in the problem.
# the dict has keys = xml subtree of Response, values = Response instance # the dict has keys = xml subtree of Response, values = Response instance
self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map=self.student_answers) self.preprocess_problem(self.tree, answer_map=self.student_answers)
def do_reset(self):
'''
Reset internal state to unfinished, with no answers
'''
self.student_answers = dict()
self.correct_map = CorrectMap()
self.done = False
def __unicode__(self): def __unicode__(self):
return u"LoncapaProblem ({0})".format(self.fileobject) return u"LoncapaProblem ({0})".format(self.fileobject)
...@@ -134,9 +145,10 @@ class LoncapaProblem(object): ...@@ -134,9 +145,10 @@ class LoncapaProblem(object):
''' Stored per-user session data neeeded to: ''' Stored per-user session data neeeded to:
1) Recreate the problem 1) Recreate the problem
2) Populate any student answers. ''' 2) Populate any student answers. '''
return {'seed': self.seed, return {'seed': self.seed,
'student_answers': self.student_answers, 'student_answers': self.student_answers,
'correct_map': self.correct_map, 'correct_map': self.correct_map.get_dict(),
'done': self.done} 'done': self.done}
def get_max_score(self): def get_max_score(self):
...@@ -170,8 +182,12 @@ class LoncapaProblem(object): ...@@ -170,8 +182,12 @@ class LoncapaProblem(object):
''' '''
correct = 0 correct = 0
for key in self.correct_map: for key in self.correct_map:
if self.correct_map[key] == u'correct': try:
correct += 1 correct += self.correct_map.get_npoints(key)
except Exception,err:
log.error('key=%s, correct_map = %s' % (key,self.correct_map))
raise
if (not self.student_answers) or len(self.student_answers) == 0: if (not self.student_answers) or len(self.student_answers) == 0:
return {'score': 0, return {'score': 0,
'total': self.get_max_score()} 'total': self.get_max_score()}
...@@ -190,12 +206,14 @@ class LoncapaProblem(object): ...@@ -190,12 +206,14 @@ class LoncapaProblem(object):
Calles the Response for each question in this problem, to do the actual grading. 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() oldcmap = self.correct_map # old CorrectMap
log.info('%s: in grade_answers, answers=%s' % (self,answers)) newcmap = CorrectMap() # start new with empty CorrectMap
for responder in self.responders.values(): for responder in self.responders.values():
results = responder.get_score(answers) # call the responsetype instance to do the actual grading results = responder.get_score(answers,oldcmap) # call the responsetype instance to do the actual grading
self.correct_map.update(results) newcmap.update(results)
return self.correct_map self.correct_map = newcmap
log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap))
return newcmap
def get_question_answers(self): def get_question_answers(self):
"""Returns a dict of answer_ids to answer values. If we cannot generate """Returns a dict of answer_ids to answer values. If we cannot generate
...@@ -282,27 +300,17 @@ class LoncapaProblem(object): ...@@ -282,27 +300,17 @@ class LoncapaProblem(object):
# but it will turn into a dict containing both the answer and any associated message # but it will turn into a dict containing both the answer and any associated message
# for the problem ID for the input element. # for the problem ID for the input element.
status = "unsubmitted" status = "unsubmitted"
msg = ''
if problemid in self.correct_map: if problemid in self.correct_map:
status = self.correct_map[problemtree.get('id')] pid = problemtree.get('id')
status = self.correct_map.get_correctness(pid)
msg = self.correct_map.get_msg(pid)
value = "" value = ""
if self.student_answers and problemid in self.student_answers: if self.student_answers and problemid in self.student_answers:
value = self.student_answers[problemid] value = self.student_answers[problemid]
#### This code is a hack. It was merged to help bring two branches
#### in sync, but should be replaced. msg should be passed in a
#### response_type
# prepare the response message, if it exists in correct_map
if 'msg' in self.correct_map:
msg = self.correct_map['msg']
elif ('msg_%s' % problemid) in self.correct_map:
msg = self.correct_map['msg_%s' % problemid]
else:
msg = ''
# do the rendering # do the rendering
# This should be broken out into a helper function
# that handles all input objects
render_object = inputtypes.SimpleInput(system=self.system, render_object = inputtypes.SimpleInput(system=self.system,
xml=problemtree, xml=problemtree,
state={'value': value, state={'value': value,
...@@ -333,7 +341,7 @@ class LoncapaProblem(object): ...@@ -333,7 +341,7 @@ class LoncapaProblem(object):
return tree return tree
def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private def preprocess_problem(self, tree, answer_map=dict()): # private
''' '''
Assign IDs to all the responses Assign IDs to all the responses
Assign sub-IDs to all entries (textline, schematic, etc.) Assign sub-IDs to all entries (textline, schematic, etc.)
...@@ -346,11 +354,8 @@ class LoncapaProblem(object): ...@@ -346,11 +354,8 @@ class LoncapaProblem(object):
self.responders = {} 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 # create and save ID for this response response.set('id',response_id_str) # create and save ID for this response
response_id += 1
# if response_id not in correct_map: correct = 'unsubmitted' # unused - to be removed
# response.attrib['state'] = correct
response_id += response_id
answer_id = 1 answer_id = 1
inputfields = 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)]),
......
#-----------------------------------------------------------------------------
# class used to store graded responses to CAPA questions
#
# Used by responsetypes and capa_problem
class CorrectMap(object):
'''
Stores (correctness, npoints, msg) for each answer_id.
Behaves as a dict.
'''
cmap = {}
def __init__(self,*args,**kwargs):
self.set(*args,**kwargs)
def set(self,answer_id=None,correctness=None,npoints=None,msg=''):
if answer_id is not None:
self.cmap[answer_id] = {'correctness': correctness,
'npoints': npoints,
'msg': msg }
def __repr__(self):
return repr(self.cmap)
def get_dict(self):
'''
return dict version of self
'''
return self.cmap
def set_dict(self,correct_map):
'''
set internal dict to provided correct_map dict
for graceful migration, if correct_map is a one-level dict, then convert it to the new
dict of dicts format.
'''
if correct_map and not (type(correct_map[correct_map.keys()[0]])==dict):
for k in self.cmap.keys(): self.cmap.pop(k) # empty current dict
for k in correct_map: self.set(k,correct_map[k]) # create new dict entries
else:
self.cmap = correct_map
def is_correct(self,answer_id):
if answer_id in self.cmap: return self.cmap[answer_id]['correctness'] == 'correct'
return None
def get_npoints(self,answer_id):
if self.is_correct(answer_id):
npoints = self.cmap[answer_id].get('npoints',1) # default to 1 point if correct
return npoints or 1
return 0 # if not correct, return 0
def set_property(self,answer_id,property,value):
if answer_id in self.cmap: self.cmap[answer_id][property] = value
else: self.cmap[answer_id] = {property:value}
def get_property(self,answer_id,property,default=None):
if answer_id in self.cmap: return self.cmap[answer_id].get(property,default)
return default
def get_correctness(self,answer_id):
return self.get_property(answer_id,'correctness')
def get_msg(self,answer_id):
return self.get_property(answer_id,'msg','')
def update(self,other_cmap):
'''
Update this CorrectMap with the contents of another CorrectMap
'''
if not isinstance(other_cmap,CorrectMap):
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
self.cmap.update(other_cmap.get_dict())
__getitem__ = cmap.__getitem__
__iter__ = cmap.__iter__
items = cmap.items
keys = cmap.keys
...@@ -21,6 +21,7 @@ import abc ...@@ -21,6 +21,7 @@ import abc
# specific library imports # specific library imports
from calc import evaluator, UndefinedVariable from calc import evaluator, UndefinedVariable
from correctmap import CorrectMap
from util import * 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?
...@@ -53,7 +54,7 @@ class StudentInputError(Exception): ...@@ -53,7 +54,7 @@ class StudentInputError(Exception):
class GenericResponse(object): class GenericResponse(object):
''' '''
Base class for CAPA responsetypes. Each response type (ie a capa question, 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 is part of a capa problem) is represented as a subclass,
which should provide the following methods: which should provide the following methods:
- get_score : evaluate the given student answers, and return a CorrectMap - get_score : evaluate the given student answers, and return a CorrectMap
...@@ -140,10 +141,16 @@ class GenericResponse(object): ...@@ -140,10 +141,16 @@ class GenericResponse(object):
return tree return tree
@abc.abstractmethod @abc.abstractmethod
def get_score(self, student_answers): def get_score(self, student_answers, old_cmap):
''' '''
Return a CorrectMap for the answers expected vs given. This includes Return a CorrectMap for the answers expected vs given. This includes
(correctness, npoints, msg) for each answer_id. (correctness, npoints, msg) for each answer_id.
Arguments:
- student_answers : dict of (answer_id,answer) where answer = student input (string)
- old_cmap : previous CorrectMap (may be empty); useful for analyzing or recording history of responses
''' '''
pass pass
...@@ -201,15 +208,15 @@ class MultipleChoiceResponse(GenericResponse): ...@@ -201,15 +208,15 @@ class MultipleChoiceResponse(GenericResponse):
else: else:
choice.set("name", "choice_"+choice.get("name")) choice.set("name", "choice_"+choice.get("name"))
def get_score(self, student_answers): def get_score(self, student_answers, old_cmap):
''' '''
grade student response. grade student response.
''' '''
# log.debug('%s: student_answers=%s, correct_choices=%s' % (unicode(self),student_answers,self.correct_choices)) # 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: if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices:
return {self.answer_id:'correct'} return CorrectMap(self.answer_id,'correct')
else: else:
return {self.answer_id:'incorrect'} return CorrectMap(self.answer_id,'incorrect')
def get_answers(self): def get_answers(self):
return {self.answer_id:self.correct_choices} return {self.answer_id:self.correct_choices}
...@@ -226,14 +233,14 @@ class TrueFalseResponse(MultipleChoiceResponse): ...@@ -226,14 +233,14 @@ class TrueFalseResponse(MultipleChoiceResponse):
else: else:
choice.set("name", "choice_"+choice.get("name")) choice.set("name", "choice_"+choice.get("name"))
def get_score(self, student_answers): def get_score(self, student_answers, old_cmap):
correct = set(self.correct_choices) correct = set(self.correct_choices)
answers = set(student_answers.get(self.answer_id, [])) answers = set(student_answers.get(self.answer_id, []))
if correct == answers: if correct == answers:
return { self.answer_id : 'correct'} return CorrectMap( self.answer_id , 'correct')
return {self.answer_id : 'incorrect'} return CorrectMap(self.answer_id ,'incorrect')
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -251,15 +258,15 @@ class OptionResponse(GenericResponse): ...@@ -251,15 +258,15 @@ class OptionResponse(GenericResponse):
def setup_response(self): def setup_response(self):
self.answer_fields = self.inputfields self.answer_fields = self.inputfields
def get_score(self, student_answers): def get_score(self, student_answers, old_cmap):
# log.debug('%s: student_answers=%s' % (unicode(self),student_answers)) # log.debug('%s: student_answers=%s' % (unicode(self),student_answers))
cmap = {} cmap = CorrectMap()
amap = self.get_answers() amap = self.get_answers()
for aid in amap: for aid in amap:
if aid in student_answers and student_answers[aid]==amap[aid]: if aid in student_answers and student_answers[aid]==amap[aid]:
cmap[aid] = 'correct' cmap.set(aid,'correct')
else: else:
cmap[aid] = 'incorrect' cmap.set(aid,'incorrect')
return cmap return cmap
def get_answers(self): def get_answers(self):
...@@ -291,7 +298,7 @@ class NumericalResponse(GenericResponse): ...@@ -291,7 +298,7 @@ class NumericalResponse(GenericResponse):
except Exception: except Exception:
self.answer_id = None self.answer_id = None
def get_score(self, student_answers): def get_score(self, student_answers, old_cmap):
'''Grade a numeric response ''' '''Grade a numeric response '''
student_answer = student_answers[self.answer_id] student_answer = student_answers[self.answer_id]
try: try:
...@@ -303,9 +310,9 @@ class NumericalResponse(GenericResponse): ...@@ -303,9 +310,9 @@ class NumericalResponse(GenericResponse):
raise StudentInputError('Invalid input -- please use a number only') raise StudentInputError('Invalid input -- please use a number only')
if correct: if correct:
return {self.answer_id:'correct'} return CorrectMap(self.answer_id,'correct')
else: else:
return {self.answer_id:'incorrect'} return CorrectMap(self.answer_id,'incorrect')
def get_answers(self): def get_answers(self):
return {self.answer_id:self.correct_answer} return {self.answer_id:self.correct_answer}
...@@ -395,7 +402,7 @@ def sympy_check2(): ...@@ -395,7 +402,7 @@ def sympy_check2():
else: else:
self.code = answer.text self.code = answer.text
def get_score(self, student_answers): def get_score(self, student_answers, old_cmap):
''' '''
student_answers is a dict with everything from request.POST, but with the first part student_answers is a dict with everything from request.POST, but with the first part
of each key removed (the string before the first "_"). of each key removed (the string before the first "_").
...@@ -495,12 +502,10 @@ def sympy_check2(): ...@@ -495,12 +502,10 @@ def sympy_check2():
correct = ['correct']*len(idset) if ret else ['incorrect']*len(idset) correct = ['correct']*len(idset) if ret else ['incorrect']*len(idset)
# build map giving "correct"ness of the answer(s) # build map giving "correct"ness of the answer(s)
#correct_map = dict(zip(idset, self.context['correct'])) correct_map = CorrectMap()
correct_map = {}
for k in range(len(idset)): for k in range(len(idset)):
correct_map[idset[k]] = correct[k] correct_map.set(idset[k], correct[k], msg=messages[k])
correct_map['msg_%s' % idset[k]] = messages[k] return correct_map
return correct_map
def get_answers(self): def get_answers(self):
''' '''
...@@ -642,9 +647,11 @@ main() ...@@ -642,9 +647,11 @@ main()
return rxml return rxml
def get_score(self, student_answers): def get_score(self, student_answers, old_cmap):
idset = sorted(self.answer_ids)
cmap = CorrectMap()
try: try:
submission = [student_answers[k] for k in sorted(self.answer_ids)] submission = [student_answers[k] for k in idset]
except Exception,err: except Exception,err:
log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err,self.answer_ids,student_answers)) log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err,self.answer_ids,student_answers))
raise Exception,err raise Exception,err
...@@ -658,9 +665,9 @@ main() ...@@ -658,9 +665,9 @@ main()
except Exception, err: except Exception, err:
log.error('Error %s' % err) log.error('Error %s' % err)
if self.system.DEBUG: if self.system.DEBUG:
correct_map = dict(zip(sorted(self.answer_ids), ['incorrect'] * len(self.answer_ids) )) cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset) )))
correct_map['msg_%s' % self.answer_ids[0]] = '<font color="red" size="+2">%s</font>' % str(err).replace('<','&lt;') cmap.set_property(self.answer_ids[0],'msg','<font color="red" size="+2">%s</font>' % str(err).replace('<','&lt;'))
return correct_map return cmap
ad = rxml.find('awarddetail').text ad = rxml.find('awarddetail').text
admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses
...@@ -670,13 +677,13 @@ main() ...@@ -670,13 +677,13 @@ main()
if ad in admap: if ad in admap:
self.context['correct'][0] = admap[ad] self.context['correct'][0] = admap[ad]
# self.context['correct'] = ['correct','correct'] # create CorrectMap
correct_map = dict(zip(sorted(self.answer_ids), self.context['correct'])) for key in idset:
idx = idset.index(key)
# store message in correct_map msg = rxml.find('message').text.replace('&nbsp;','&#160;') if idx==0 else None
correct_map['msg_%s' % self.answer_ids[0]] = rxml.find('message').text.replace('&nbsp;','&#160;') cmap.set(key, self.context['correct'][idx], msg=msg)
return correct_map return cmap
def get_answers(self): def get_answers(self):
''' '''
...@@ -750,7 +757,7 @@ class FormulaResponse(GenericResponse): ...@@ -750,7 +757,7 @@ 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, old_cmap):
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])
sranges=zip(*map(lambda x:map(float, x.split(",")), sranges=zip(*map(lambda x:map(float, x.split(",")),
...@@ -776,11 +783,11 @@ class FormulaResponse(GenericResponse): ...@@ -776,11 +783,11 @@ class FormulaResponse(GenericResponse):
#traceback.print_exc() #traceback.print_exc()
raise StudentInputError("Error in formula") raise StudentInputError("Error in formula")
if numpy.isnan(student_result) or numpy.isinf(student_result): if numpy.isnan(student_result) or numpy.isinf(student_result):
return {self.answer_id:"incorrect"} return CorrectMap(self.answer_id, "incorrect")
if not compare_with_tolerance(student_result, instructor_result, self.tolerance): if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
return {self.answer_id:"incorrect"} return CorrectMap(self.answer_id, "incorrect")
return {self.answer_id:"correct"} return CorrectMap(self.answer_id, "correct")
def strip_dict(self, d): def strip_dict(self, d):
''' Takes a dict. Returns an identical dict, with all non-word ''' Takes a dict. Returns an identical dict, with all non-word
...@@ -810,12 +817,13 @@ class SchematicResponse(GenericResponse): ...@@ -810,12 +817,13 @@ class SchematicResponse(GenericResponse):
else: else:
self.code = answer.text self.code = answer.text
def get_score(self, student_answers): def get_score(self, student_answers, old_cmap):
from capa_problem import global_context from capa_problem import global_context
submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)] submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)]
self.context.update({'submission':submission}) self.context.update({'submission':submission})
exec self.code in global_context, self.context exec self.code in global_context, self.context
return zip(sorted(self.answer_ids), self.context['correct']) cmap = CorrectMap()
return cmap.set_dict(zip(sorted(self.answer_ids), self.context['correct']))
def get_answers(self): def get_answers(self):
# use answers provided in input elements # use answers provided in input elements
...@@ -845,8 +853,8 @@ class ImageResponse(GenericResponse): ...@@ -845,8 +853,8 @@ class ImageResponse(GenericResponse):
self.ielements = self.inputfields 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, old_cmap):
correct_map = {} correct_map = CorrectMap()
expectedset = self.get_answers() expectedset = self.get_answers()
for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza
...@@ -869,9 +877,9 @@ class ImageResponse(GenericResponse): ...@@ -869,9 +877,9 @@ class ImageResponse(GenericResponse):
# answer is correct if (x,y) is within the specified rectangle # answer is correct if (x,y) is within the specified rectangle
if (llx <= gx <= urx) and (lly <= gy <= ury): if (llx <= gx <= urx) and (lly <= gy <= ury):
correct_map[aid] = 'correct' correct_map.set(aid, 'correct')
else: else:
correct_map[aid] = 'incorrect' correct_map.set(aid, 'incorrect')
return correct_map return correct_map
def get_answers(self): def get_answers(self):
......
...@@ -13,6 +13,7 @@ from lxml import etree ...@@ -13,6 +13,7 @@ from lxml import etree
from x_module import XModule, XModuleDescriptor from x_module import XModule, XModuleDescriptor
from capa.capa_problem import LoncapaProblem from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError from capa.responsetypes import StudentInputError
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -365,18 +366,17 @@ class Module(XModule): ...@@ -365,18 +366,17 @@ class Module(XModule):
self.attempts = self.attempts + 1 self.attempts = self.attempts + 1
self.lcp.done=True self.lcp.done=True
success = 'correct' success = 'correct' # success = correct if ALL questions in this problem are correct
for i in correct_map: for answer_id in correct_map:
if correct_map[i]!='correct': if not correct_map.is_correct(answer_id):
success = 'incorrect' success = 'incorrect'
event_info['correct_map']=correct_map event_info['correct_map']=correct_map.get_dict() # log this in the tracker
event_info['success']=success event_info['success']=success
self.tracker('save_problem_check', event_info) self.tracker('save_problem_check', event_info)
try: try:
html = self.get_problem_html(encapsulate=False) html = self.get_problem_html(encapsulate=False) # render problem into HTML
except Exception,err: except Exception,err:
log.error('failed to generate html') log.error('failed to generate html')
raise Exception,err raise Exception,err
...@@ -430,17 +430,10 @@ class Module(XModule): ...@@ -430,17 +430,10 @@ class Module(XModule):
self.tracker('reset_problem_fail', event_info) self.tracker('reset_problem_fail', event_info)
return "Refresh the page and make an attempt before resetting." return "Refresh the page and make an attempt before resetting."
self.lcp.done=False self.lcp.do_reset() # call method in LoncapaProblem to reset itself
self.lcp.answers=dict()
self.lcp.correct_map=dict()
self.lcp.student_answers = dict()
if self.rerandomize == "always": if self.rerandomize == "always":
self.lcp.context=dict() self.lcp.seed=None # reset random number generator seed (note the self.lcp.get_state() in next line)
self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid.
self.lcp.seed=None
self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state(), system=self.system) self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state(), system=self.system)
event_info['new_state']=self.lcp.get_state() event_info['new_state']=self.lcp.get_state()
......
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