Commit 57d86ea1 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 3a6d2ac5
......@@ -25,15 +25,14 @@ import struct
from lxml import etree
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
from correctmap import CorrectMap
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
response_types = {'numericalresponse': NumericalResponse,
......@@ -68,6 +67,12 @@ global_context = {'random': random,
# These should be removed from HTML output, including all subelements
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):
'''
Main class for capa Problems.
......@@ -89,9 +94,7 @@ class LoncapaProblem(object):
'''
## Initialize class variables from state
self.student_answers = dict()
self.correct_map = dict()
self.done = False
self.do_reset()
self.problem_id = id
self.system = system
self.seed = seed
......@@ -102,7 +105,7 @@ class LoncapaProblem(object):
if 'student_answers' in state:
self.student_answers = state['student_answers']
if 'correct_map' in state:
self.correct_map = state['correct_map']
self.correct_map.set_dict(state['correct_map'])
if 'done' in state:
self.done = state['done']
......@@ -125,7 +128,15 @@ class LoncapaProblem(object):
# 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.
# 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):
return u"LoncapaProblem ({0})".format(self.fileobject)
......@@ -134,9 +145,10 @@ class LoncapaProblem(object):
''' Stored per-user session data neeeded to:
1) Recreate the problem
2) Populate any student answers. '''
return {'seed': self.seed,
'student_answers': self.student_answers,
'correct_map': self.correct_map,
'correct_map': self.correct_map.get_dict(),
'done': self.done}
def get_max_score(self):
......@@ -170,8 +182,12 @@ class LoncapaProblem(object):
'''
correct = 0
for key in self.correct_map:
if self.correct_map[key] == u'correct':
correct += 1
try:
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:
return {'score': 0,
'total': self.get_max_score()}
......@@ -190,12 +206,14 @@ class LoncapaProblem(object):
Calles the Response for each question in this problem, to do the actual grading.
'''
self.student_answers = answers
self.correct_map = dict()
log.info('%s: in grade_answers, answers=%s' % (self,answers))
oldcmap = self.correct_map # old CorrectMap
newcmap = CorrectMap() # start new with empty CorrectMap
for responder in self.responders.values():
results = responder.get_score(answers) # call the responsetype instance to do the actual grading
self.correct_map.update(results)
return self.correct_map
results = responder.get_score(answers,oldcmap) # call the responsetype instance to do the actual grading
newcmap.update(results)
self.correct_map = newcmap
log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap))
return newcmap
def get_question_answers(self):
"""Returns a dict of answer_ids to answer values. If we cannot generate
......@@ -282,27 +300,17 @@ class LoncapaProblem(object):
# but it will turn into a dict containing both the answer and any associated message
# for the problem ID for the input element.
status = "unsubmitted"
msg = ''
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 = ""
if self.student_answers and problemid in self.student_answers:
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
# This should be broken out into a helper function
# that handles all input objects
render_object = inputtypes.SimpleInput(system=self.system,
xml=problemtree,
state={'value': value,
......@@ -333,7 +341,7 @@ class LoncapaProblem(object):
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 sub-IDs to all entries (textline, schematic, etc.)
......@@ -346,11 +354,8 @@ class LoncapaProblem(object):
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 # 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
response.set('id',response_id_str) # create and save ID for this response
response_id += 1
answer_id = 1
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
from x_module import XModule, XModuleDescriptor
from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError
log = logging.getLogger("mitx.courseware")
#-----------------------------------------------------------------------------
......@@ -365,18 +366,17 @@ class Module(XModule):
self.attempts = self.attempts + 1
self.lcp.done=True
success = 'correct'
for i in correct_map:
if correct_map[i]!='correct':
success = 'correct' # success = correct if ALL questions in this problem are correct
for answer_id in correct_map:
if not correct_map.is_correct(answer_id):
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
self.tracker('save_problem_check', event_info)
try:
html = self.get_problem_html(encapsulate=False)
html = self.get_problem_html(encapsulate=False) # render problem into HTML
except Exception,err:
log.error('failed to generate html')
raise Exception,err
......@@ -430,17 +430,10 @@ class Module(XModule):
self.tracker('reset_problem_fail', event_info)
return "Refresh the page and make an attempt before resetting."
self.lcp.done=False
self.lcp.answers=dict()
self.lcp.correct_map=dict()
self.lcp.student_answers = dict()
self.lcp.do_reset() # call method in LoncapaProblem to reset itself
if self.rerandomize == "always":
self.lcp.context=dict()
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.seed=None # reset random number generator seed (note the self.lcp.get_state() in next line)
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()
......
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