Commit 5ac13e03 by ichuang

fourth pass in capa cleanup:

  - Added hints + hintmethod
  - hintgroup compatible with loncapa spec
  - also does hintfn for custom hints (can do answer history)
  - GenericResponse -> LoncapaResponse
  - moved response type tags into responsetype classes
  - capa_problem should use __future__ division
  - hints stored in CorrectMap, copied to 'feedback' in SimpleInput for display
parent c724affe
...@@ -12,6 +12,8 @@ Main module which shows problems (of "capa" type). ...@@ -12,6 +12,8 @@ Main module which shows problems (of "capa" type).
This is used by capa_module. This is used by capa_module.
''' '''
from __future__ import division
import copy import copy
import logging import logging
import math import math
...@@ -32,20 +34,10 @@ import inputtypes ...@@ -32,20 +34,10 @@ import inputtypes
from util import contextualize_text from util import contextualize_text
# to be replaced with auto-registering # to be replaced with auto-registering
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse import responsetypes
# 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_tag_dict = dict([(x.response_tag,x) for x in responsetypes.__all__])
'formularesponse': FormulaResponse,
'customresponse': CustomResponse,
'schematicresponse': SchematicResponse,
'externalresponse': ExternalResponse,
'multiplechoiceresponse': MultipleChoiceResponse,
'truefalseresponse': TrueFalseResponse,
'imageresponse': ImageResponse,
'optionresponse': OptionResponse,
'symbolicresponse': SymbolicResponse,
}
entry_types = ['textline', 'schematic', 'choicegroup', 'textbox', 'imageinput', 'optioninput'] entry_types = ['textline', 'schematic', 'choicegroup', 'textbox', 'imageinput', 'optioninput']
solution_types = ['solution'] # extra things displayed after "show answers" is pressed solution_types = ['solution'] # extra things displayed after "show answers" is pressed
...@@ -65,7 +57,7 @@ global_context = {'random': random, ...@@ -65,7 +57,7 @@ global_context = {'random': random,
'eia': eia} 'eia': eia}
# 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","hintgroup"]
#log = logging.getLogger(__name__) #log = logging.getLogger(__name__)
log = logging.getLogger('mitx.common.lib.capa.capa_problem') log = logging.getLogger('mitx.common.lib.capa.capa_problem')
...@@ -209,7 +201,7 @@ class LoncapaProblem(object): ...@@ -209,7 +201,7 @@ class LoncapaProblem(object):
oldcmap = self.correct_map # old CorrectMap oldcmap = self.correct_map # old CorrectMap
newcmap = CorrectMap() # start new with empty CorrectMap newcmap = CorrectMap() # start new with empty CorrectMap
for responder in self.responders.values(): for responder in self.responders.values():
results = responder.get_score(answers,oldcmap) # call the responsetype instance to do the actual grading results = responder.evaluate_answers(answers,oldcmap) # call the responsetype instance to do the actual grading
newcmap.update(results) newcmap.update(results)
self.correct_map = newcmap self.correct_map = newcmap
log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap)) log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap))
...@@ -248,7 +240,8 @@ class LoncapaProblem(object): ...@@ -248,7 +240,8 @@ class LoncapaProblem(object):
''' '''
return contextualize_text(etree.tostring(self.extract_html(self.tree)), self.context) return contextualize_text(etree.tostring(self.extract_html(self.tree)), self.context)
# ======= Private ======== # ======= Private Methods Below ========
def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
''' '''
Extract content of <script>...</script> from the problem.xml file, and exec it in the Extract content of <script>...</script> from the problem.xml file, and exec it in the
...@@ -296,15 +289,17 @@ class LoncapaProblem(object): ...@@ -296,15 +289,17 @@ class LoncapaProblem(object):
problemid = problemtree.get('id') # my ID problemid = problemtree.get('id') # my ID
if problemtree.tag in inputtypes.get_input_xml_tags(): if problemtree.tag in inputtypes.get_input_xml_tags():
# status is currently the answer for the problem ID for the input element,
# 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" status = "unsubmitted"
msg = '' msg = ''
hint = ''
hintmode = None
if problemid in self.correct_map: if problemid in self.correct_map:
pid = problemtree.get('id') pid = problemtree.get('id')
status = self.correct_map.get_correctness(pid) status = self.correct_map.get_correctness(pid)
msg = self.correct_map.get_msg(pid) msg = self.correct_map.get_msg(pid)
hint = self.correct_map.get_hint(pid)
hintmode = self.correct_map.get_hintmode(pid)
value = "" value = ""
if self.student_answers and problemid in self.student_answers: if self.student_answers and problemid in self.student_answers:
...@@ -316,7 +311,10 @@ class LoncapaProblem(object): ...@@ -316,7 +311,10 @@ class LoncapaProblem(object):
state={'value': value, state={'value': value,
'status': status, 'status': status,
'id': problemtree.get('id'), 'id': problemtree.get('id'),
'feedback': {'message': msg} 'feedback': {'message': msg,
'hint' : hint,
'hintmode' : hintmode,
}
}, },
use='capa_input') use='capa_input')
return render_object.get_html() # function(problemtree, value, status, msg) # render the special response (textline, schematic,...) return render_object.get_html() # function(problemtree, value, status, msg) # render the special response (textline, schematic,...)
...@@ -352,7 +350,7 @@ class LoncapaProblem(object): ...@@ -352,7 +350,7 @@ class LoncapaProblem(object):
''' '''
response_id = 1 response_id = 1
self.responders = {} self.responders = {}
for response in tree.xpath('//' + "|//".join(response_types)): for response in tree.xpath('//' + "|//".join(response_tag_dict)):
response_id_str = self.problem_id + "_" + str(response_id) response_id_str = self.problem_id + "_" + str(response_id)
response.set('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 response_id += 1
...@@ -366,7 +364,7 @@ class LoncapaProblem(object): ...@@ -366,7 +364,7 @@ class LoncapaProblem(object):
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id) entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
answer_id = answer_id + 1 answer_id = answer_id + 1
responder = response_types[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
self.responders[response] = responder # save in list in self self.responders[response] = responder # save in list in self
# <solution>...</solution> may not be associated with any specific response; give IDs for those separately # <solution>...</solution> may not be associated with any specific response; give IDs for those separately
......
...@@ -5,7 +5,16 @@ ...@@ -5,7 +5,16 @@
class CorrectMap(object): class CorrectMap(object):
''' '''
Stores (correctness, npoints, msg) for each answer_id. Stores map between answer_id and response evaluation result for each question
in a capa problem. The response evaluation result for each answer_id includes
(correctness, npoints, msg, hint, hintmode).
- correctness : either 'correct' or 'incorrect'
- npoints : None, or integer specifying number of points awarded for this answer_id
- msg : string (may have HTML) giving extra message response (displayed below textline or textbox)
- hint : string (may have HTML) giving optional hint (displayed below textline or textbox, above msg)
- hintmode : one of (None,'on_request','always') criteria for displaying hint
Behaves as a dict. Behaves as a dict.
''' '''
cmap = {} cmap = {}
...@@ -13,11 +22,14 @@ class CorrectMap(object): ...@@ -13,11 +22,14 @@ class CorrectMap(object):
def __init__(self,*args,**kwargs): def __init__(self,*args,**kwargs):
self.set(*args,**kwargs) self.set(*args,**kwargs)
def set(self,answer_id=None,correctness=None,npoints=None,msg=''): def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None):
if answer_id is not None: if answer_id is not None:
self.cmap[answer_id] = {'correctness': correctness, self.cmap[answer_id] = {'correctness': correctness,
'npoints': npoints, 'npoints': npoints,
'msg': msg } 'msg': msg,
'hint' : hint,
'hintmode' : hintmode,
}
def __repr__(self): def __repr__(self):
return repr(self.cmap) return repr(self.cmap)
...@@ -64,6 +76,20 @@ class CorrectMap(object): ...@@ -64,6 +76,20 @@ class CorrectMap(object):
def get_msg(self,answer_id): def get_msg(self,answer_id):
return self.get_property(answer_id,'msg','') return self.get_property(answer_id,'msg','')
def get_hint(self,answer_id):
return self.get_property(answer_id,'hint','')
def get_hintmode(self,answer_id):
return self.get_property(answer_id,'hintmode',None)
def set_hint_and_mode(self,answer_id,hint,hintmode):
'''
- hint : (string) HTML text for hint
- hintmode : (string) mode for hint display ('always' or 'on_request')
'''
self.set_property(answer_id,'hint',hint)
self.set_property(answer_id,'hintmode',hintmode)
def update(self,other_cmap): def update(self,other_cmap):
''' '''
Update this CorrectMap with the contents of another CorrectMap Update this CorrectMap with the contents of another CorrectMap
......
...@@ -32,44 +32,57 @@ def get_input_xml_tags(): ...@@ -32,44 +32,57 @@ def get_input_xml_tags():
return SimpleInput.get_xml_tags() return SimpleInput.get_xml_tags()
class SimpleInput():# XModule class SimpleInput():# XModule
''' Type for simple inputs -- plain HTML with a form element '''
Type for simple inputs -- plain HTML with a form element
State is a dictionary with optional keys:
* Value
* ID
* Status (answered, unanswered, unsubmitted)
* Feedback (dictionary containing keys for hints, errors, or other
feedback from previous attempt)
''' '''
xml_tags = {} ## Maps tags to functions xml_tags = {} ## Maps tags to functions
def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'): def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'):
'''
Instantiate a SimpleInput class. Arguments:
- system : I4xSystem instance which provides OS, rendering, and user context
- xml : Element tree of this Input element
- item_id : id for this input element (assigned by capa_problem.LoncapProblem) - string
- track_url : URL used for tracking - string
- state : a dictionary with optional keys:
* Value
* ID
* Status (answered, unanswered, unsubmitted)
* Feedback (dictionary containing keys for hints, errors, or other
feedback from previous attempt)
- use :
'''
self.xml = xml self.xml = xml
self.tag = xml.tag self.tag = xml.tag
if not state: self.system = system
state = {} if not state: state = {}
## ID should only come from one place. ## ID should only come from one place.
## If it comes from multiple, we use state first, XML second, and parameter ## If it comes from multiple, we use state first, XML second, and parameter
## third. Since we don't make this guarantee, we can swap this around in ## third. Since we don't make this guarantee, we can swap this around in
## the future if there's a more logical order. ## the future if there's a more logical order.
if item_id: if item_id: self.id = item_id
self.id = item_id if xml.get('id'): self.id = xml.get('id')
if xml.get('id'): if 'id' in state: self.id = state['id']
self.id = xml.get('id')
if 'id' in state:
self.id = state['id']
self.system = system
self.value = '' self.value = ''
if 'value' in state: if 'value' in state:
self.value = state['value'] self.value = state['value']
self.msg = '' self.msg = ''
if 'feedback' in state and 'message' in state['feedback']: feedback = state.get('feedback')
self.msg = state['feedback']['message'] if feedback is not None:
self.msg = feedback.get('message','')
self.hint = feedback.get('hint','')
self.hintmode = feedback.get('hintmode',None)
# put hint above msg if to be displayed
if self.hintmode == 'always':
self.msg = self.hint + ('<br/.>' if self.msg else '') + self.msg
self.status = 'unanswered' self.status = 'unanswered'
if 'status' in state: if 'status' in state:
self.status = state['status'] self.status = state['status']
......
...@@ -7,6 +7,11 @@ from calc import evaluator, UndefinedVariable ...@@ -7,6 +7,11 @@ from calc import evaluator, UndefinedVariable
def compare_with_tolerance(v1, v2, tol): def compare_with_tolerance(v1, v2, tol):
''' Compare v1 to v2 with maximum tolerance tol ''' Compare v1 to v2 with maximum tolerance tol
tol is relative if it ends in %; otherwise, it is absolute tol is relative if it ends in %; otherwise, it is absolute
- v1 : student result (number)
- v2 : instructor result (number)
- tol : tolerance (string or number)
''' '''
relative = "%" in tol relative = "%" in tol
if relative: if relative:
......
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