Commit 32924d8b by Calen Pennington

Merge pull request #398 from MITx/kimth/partialgrading

Partial scoring
parents 95b79574 1ff8e271
...@@ -154,21 +154,10 @@ class LoncapaProblem(object): ...@@ -154,21 +154,10 @@ class LoncapaProblem(object):
def get_max_score(self): def get_max_score(self):
''' '''
Return maximum score for this problem. 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.
''' '''
maxscore = 0 maxscore = 0
for response, responder in self.responders.iteritems(): for response, responder in self.responders.iteritems():
if hasattr(responder, 'get_max_score'): maxscore += responder.get_max_score()
try:
maxscore += responder.get_max_score()
except Exception:
log.debug('responder %s failed to properly return from get_max_score()' % responder) # FIXME
raise
else:
maxscore += len(self.responder_answers[response])
return maxscore return maxscore
def get_score(self): def get_score(self):
......
...@@ -75,7 +75,6 @@ class LoncapaResponse(object): ...@@ -75,7 +75,6 @@ class LoncapaResponse(object):
In addition, these methods are optional: 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__ - setup_response : find and note the answer input field IDs for the response; called by __init__
- check_hint_condition : check to see if the student's answers satisfy a particular condition for a hint to be displayed - check_hint_condition : check to see if the student's answers satisfy a particular condition for a hint to be displayed
- render_html : render this Response as HTML (must return XHTML compliant string) - render_html : render this Response as HTML (must return XHTML compliant string)
...@@ -134,6 +133,11 @@ class LoncapaResponse(object): ...@@ -134,6 +133,11 @@ class LoncapaResponse(object):
if self.max_inputfields == 1: if self.max_inputfields == 1:
self.answer_id = self.answer_ids[0] # for convenience self.answer_id = self.answer_ids[0] # for convenience
self.maxpoints = dict()
for inputfield in self.inputfields:
maxpoints = inputfield.get('points','1') # By default, each answerfield is worth 1 point
self.maxpoints.update({inputfield.get('id'): int(maxpoints)})
self.default_answer_map = {} # dict for default answer map (provided in input elements) self.default_answer_map = {} # dict for default answer map (provided in input elements)
for entry in self.inputfields: for entry in self.inputfields:
answer = entry.get('correct_answer') answer = entry.get('correct_answer')
...@@ -143,6 +147,12 @@ class LoncapaResponse(object): ...@@ -143,6 +147,12 @@ class LoncapaResponse(object):
if hasattr(self, 'setup_response'): if hasattr(self, 'setup_response'):
self.setup_response() self.setup_response()
def get_max_score(self):
'''
Return the total maximum points of all answer fields under this Response
'''
return sum(self.maxpoints.values())
def render_html(self, renderer): def render_html(self, renderer):
''' '''
Return XHTML Element tree representation of this Response. Return XHTML Element tree representation of this Response.
...@@ -1067,7 +1077,10 @@ class CodeResponse(LoncapaResponse): ...@@ -1067,7 +1077,10 @@ class CodeResponse(LoncapaResponse):
(err, self.answer_id, convert_files_to_filenames(student_answers))) (err, self.answer_id, convert_files_to_filenames(student_answers)))
raise Exception(err) raise Exception(err)
self.context.update({'submission': unicode(submission)}) if is_file(submission):
self.context.update({'submission': submission.name})
else:
self.context.update({'submission': submission})
# Prepare xqueue request # Prepare xqueue request
#------------------------------------------------------------ #------------------------------------------------------------
...@@ -1114,21 +1127,24 @@ class CodeResponse(LoncapaResponse): ...@@ -1114,21 +1127,24 @@ class CodeResponse(LoncapaResponse):
def update_score(self, score_msg, oldcmap, queuekey): def update_score(self, score_msg, oldcmap, queuekey):
(valid_score_msg, correct, score, msg) = self._parse_score_msg(score_msg) (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg)
if not valid_score_msg: if not valid_score_msg:
oldcmap.set(self.answer_id, msg='Error: Invalid grader reply.') oldcmap.set(self.answer_id, msg='Error: Invalid grader reply.')
return oldcmap return oldcmap
correctness = 'incorrect' correctness = 'correct' if correct else 'incorrect'
if correct:
correctness = 'correct'
self.context['correct'] = correctness # TODO: Find out how this is used elsewhere, if any self.context['correct'] = correctness # TODO: Find out how this is used elsewhere, if any
# Replace 'oldcmap' with new grading results if queuekey matches. # Replace 'oldcmap' with new grading results if queuekey matches.
# If queuekey does not match, we keep waiting for the score_msg whose key actually matches # If queuekey does not match, we keep waiting for the score_msg whose key actually matches
if oldcmap.is_right_queuekey(self.answer_id, queuekey): if oldcmap.is_right_queuekey(self.answer_id, queuekey):
oldcmap.set(self.answer_id, correctness=correctness, msg=msg.replace(' ', ' '), queuekey=None) # Queuekey is consumed # Sanity check on returned points
if points < 0:
points = 0
elif points > self.maxpoints[self.answer_id]:
points = self.maxpoints[self.answer_id]
oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace('&nbsp;', '&#160;'), queuekey=None) # Queuekey is consumed
else: else:
log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id)) log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id))
......
...@@ -464,7 +464,7 @@ class CapaModule(XModule): ...@@ -464,7 +464,7 @@ class CapaModule(XModule):
return {'success': msg} return {'success': msg}
log.exception("Error in capa_module problem checking") log.exception("Error in capa_module problem checking")
raise Exception("error in capa_module") raise Exception("error in capa_module")
self.attempts = self.attempts + 1 self.attempts = self.attempts + 1
self.lcp.done = True self.lcp.done = True
......
...@@ -325,7 +325,8 @@ class CodeResponseTest(unittest.TestCase): ...@@ -325,7 +325,8 @@ class CodeResponseTest(unittest.TestCase):
new_cmap = CorrectMap() new_cmap = CorrectMap()
new_cmap.update(old_cmap) new_cmap.update(old_cmap)
new_cmap.set(answer_id=answer_ids[i], correctness=correctness, msg='MESSAGE', queuekey=None) npoints = 1 if correctness=='correct' else 0
new_cmap.set(answer_id=answer_ids[i], npoints=npoints, correctness=correctness, msg='MESSAGE', queuekey=None)
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i) test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i)
self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict()) self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict())
......
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