Commit 57c1aa7b by ichuang Committed by Matthew Mongeau

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 a3d24d41
...@@ -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
...@@ -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,16 +430,9 @@ class Module(XModule): ...@@ -430,16 +430,9 @@ 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)
......
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