diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py
index f790190215..c63c13d420 100644
--- a/common/lib/capa/capa_problem.py
+++ b/common/lib/capa/capa_problem.py
@@ -23,7 +23,6 @@ import scipy
import struct
from lxml import etree
-from lxml.etree import Element
from xml.sax.saxutils import unescape
from util import contextualize_text
@@ -36,6 +35,7 @@ import eia
log = logging.getLogger(__name__)
+# dict of tagname, Response Class -- this should come from auto-registering
response_types = {'numericalresponse': NumericalResponse,
'formularesponse': FormulaResponse,
'customresponse': CustomResponse,
@@ -47,20 +47,13 @@ response_types = {'numericalresponse': NumericalResponse,
'optionresponse': OptionResponse,
'symbolicresponse': SymbolicResponse,
}
-entry_types = ['textline', 'schematic', 'choicegroup', 'textbox', 'imageinput', 'optioninput']
-solution_types = ['solution'] # extra things displayed after "show answers" is pressed
-response_properties = ["responseparam", "answer"] # these get captured as student responses
-# How to convert from original XML to HTML
-# We should do this with xlst later
+entry_types = ['textline', 'schematic', 'choicegroup', 'textbox', 'imageinput', 'optioninput']
+solution_types = ['solution'] # extra things displayed after "show answers" is pressed
+response_properties = ["responseparam", "answer"] # these get captured as student responses
+
+# special problem tags which should be turned into innocuous HTML
html_transforms = {'problem': {'tag': 'div'},
- "numericalresponse": {'tag': 'span'},
- "customresponse": {'tag': 'span'},
- "externalresponse": {'tag': 'span'},
- "schematicresponse": {'tag': 'span'},
- "formularesponse": {'tag': 'span'},
- "symbolicresponse": {'tag': 'span'},
- "multiplechoiceresponse": {'tag': 'span'},
"text": {'tag': 'span'},
"math": {'tag': 'span'},
}
@@ -74,18 +67,6 @@ global_context = {'random': random,
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["responseparam", "answer", "script"]
-# These should be removed from HTML output, but keeping subelements
-html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text", "externalresponse", 'symbolicresponse']
-
-# removed in MC
-## These should be transformed
-#html_special_response = {"textline":inputtypes.textline.render,
-# "schematic":inputtypes.schematic.render,
-# "textbox":inputtypes.textbox.render,
-# "formulainput":inputtypes.jstextline.render,
-# "solution":inputtypes.solution.render,
-# }
-
class LoncapaProblem(object):
'''
@@ -142,7 +123,8 @@ class LoncapaProblem(object):
self.context = self.extract_context(self.tree, seed=self.seed)
# pre-parse the XML tree: modifies it to add ID's and perform some in-place transformations
- # this also creates the list (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
self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map=self.student_answers)
def __unicode__(self):
@@ -166,7 +148,7 @@ class LoncapaProblem(object):
used to give complex problems (eg programming questions) multiple points.
'''
maxscore = 0
- for responder in self.responders:
+ for responder in self.responders.values():
if hasattr(responder,'get_max_score'):
try:
maxscore += responder.get_max_score()
@@ -182,6 +164,10 @@ class LoncapaProblem(object):
return maxscore
def get_score(self):
+ '''
+ Compute score for this problem. The score is the number of points awarded.
+ Returns an integer, from 0 to get_max_score().
+ '''
correct = 0
for key in self.correct_map:
if self.correct_map[key] == u'correct':
@@ -206,7 +192,7 @@ class LoncapaProblem(object):
self.student_answers = answers
self.correct_map = dict()
log.info('%s: in grade_answers, answers=%s' % (self,answers))
- for responder in self.responders:
+ 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
@@ -218,24 +204,14 @@ class LoncapaProblem(object):
(see capa_module)
"""
answer_map = dict()
- for responder in self.responders:
+ for responder in self.responders.values():
results = responder.get_answers()
answer_map.update(results) # dict of (id,correct_answer)
- # This should be handled in each responsetype, not here.
- # example for the following:
- for responder in self.responders:
- for entry in responder.inputfields:
- answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline
- if answer:
- answer_map[entry.get('id')] = contextualize_text(answer, self.context)
-
# include solutions from ... stanzas
- # Tentative merge; we should figure out how we want to handle hints and solutions
for entry in self.tree.xpath("//" + "|//".join(solution_types)):
answer = etree.tostring(entry)
- if answer:
- answer_map[entry.get('id')] = answer
+ if answer: answer_map[entry.get('id')] = answer
return answer_map
@@ -244,7 +220,7 @@ class LoncapaProblem(object):
the dicts returned by grade_answers and get_question_answers. (Though
get_question_answers may only return a subset of these."""
answer_ids = []
- for responder in self.responders:
+ for responder in self.responders.values():
answer_ids.append(responder.get_answers().keys())
return answer_ids
@@ -252,7 +228,7 @@ class LoncapaProblem(object):
'''
Main method called externally to get the HTML to be rendered for this capa Problem.
'''
- return contextualize_text(etree.tostring(self.extract_html(self.tree)[0]), self.context)
+ return contextualize_text(etree.tostring(self.extract_html(self.tree)), self.context)
# ======= Private ========
def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
@@ -264,12 +240,11 @@ class LoncapaProblem(object):
Problem XML goes to Python execution context. Runs everything in script tags
'''
random.seed(self.seed)
- context = {'global_context': global_context} # save global context in here also
- context.update(global_context) # initialize context to have stuff in global_context
- context['__builtins__'] = globals()['__builtins__'] # put globals there also
- context['the_lcp'] = self # pass instance of LoncapaProblem in
+ context = {'global_context': global_context} # save global context in here also
+ context.update(global_context) # initialize context to have stuff in global_context
+ context['__builtins__'] = globals()['__builtins__'] # put globals there also
+ context['the_lcp'] = self # pass instance of LoncapaProblem in
- #for script in tree.xpath('/problem/script'):
for script in tree.findall('.//script'):
stype = script.get('type')
if stype:
@@ -288,16 +263,20 @@ class LoncapaProblem(object):
return context
def extract_html(self, problemtree): # private
- ''' Helper function for get_html. Recursively converts XML tree to HTML
+ '''
+ Main (private) function which converts Problem XML tree to HTML.
+ Calls itself recursively.
+
+ Returns Element tree of XHTML representation of problemtree.
+ Calls render_html of Response instances to render responses into XHTML.
+
+ Used by get_html.
'''
if problemtree.tag in html_problem_semantics:
return
problemid = problemtree.get('id') # my ID
- # used to be
- # if problemtree.tag in html_special_response:
-
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
@@ -334,31 +313,25 @@ class LoncapaProblem(object):
use='capa_input')
return render_object.get_html() # function(problemtree, value, status, msg) # render the special response (textline, schematic,...)
- tree = Element(problemtree.tag)
+ if problemtree in self.responders: # let each Response render itself
+ return self.responders[problemtree].render_html(self.extract_html)
+
+ tree = etree.Element(problemtree.tag)
for item in problemtree:
- subitems = self.extract_html(item)
- if subitems is not None:
- for subitem in subitems:
- tree.append(subitem)
- for (key, value) in problemtree.items():
- tree.set(key, value)
+ item_xhtml = self.extract_html(item) # nothing special: recurse
+ if item_xhtml is not None:
+ tree.append(item_xhtml)
+
+ if tree.tag in html_transforms:
+ tree.tag = html_transforms[problemtree.tag]['tag']
+ else:
+ for (key, value) in problemtree.items(): # copy attributes over if not innocufying
+ tree.set(key, value)
tree.text = problemtree.text
tree.tail = problemtree.tail
- if problemtree.tag in html_transforms:
- tree.tag = html_transforms[problemtree.tag]['tag']
- # Reset attributes. Otherwise, we get metadata in HTML
- # (e.g. answers)
- # TODO: We should remove and not zero them.
- # I'm not sure how to do that quickly with lxml
- for k in tree.keys():
- tree.set(k, "")
-
- # TODO: Fix. This loses Element().tail
- #if problemtree.tag in html_skip:
- # return tree
- return [tree]
+ return tree
def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private
'''
@@ -370,7 +343,7 @@ class LoncapaProblem(object):
Also create capa Response instances for each responsetype and save as self.responders
'''
response_id = 1
- self.responders = []
+ 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
@@ -389,7 +362,7 @@ class LoncapaProblem(object):
answer_id = answer_id + 1
responder = response_types[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
- self.responders.append(responder) # save in list in self
+ self.responders[response] = responder # save in list in self
# ... may not be associated with any specific response; give IDs for those separately
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
diff --git a/common/lib/capa/inputtypes.py b/common/lib/capa/inputtypes.py
index 3b25be3db7..10fbdb7f98 100644
--- a/common/lib/capa/inputtypes.py
+++ b/common/lib/capa/inputtypes.py
@@ -33,26 +33,17 @@ def get_input_xml_tags():
class SimpleInput():# XModule
''' 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
-
- @classmethod
- def get_xml_tags(c):
- return c.xml_tags.keys()
-
- @classmethod
- def get_uses(c):
- return ['capa_input', 'capa_transform']
-
- def get_html(self):
- return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg)
def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'):
self.xml = xml
@@ -83,49 +74,16 @@ class SimpleInput():# XModule
if 'status' in state:
self.status = state['status']
-## TODO
-# class SimpleTransform():
-# ''' Type for simple XML to HTML transforms. Examples:
-# * Math tags, which go from LON-CAPA-style m-tags to MathJAX
-# '''
-# xml_tags = {} ## Maps tags to functions
-
-# @classmethod
-# def get_xml_tags(c):
-# return c.xml_tags.keys()
+ @classmethod
+ def get_xml_tags(c):
+ return c.xml_tags.keys()
-# @classmethod
-# def get_uses(c):
-# return ['capa_transform']
-
-# def get_html(self):
-# return self.xml_tags[self.tag](self.xml, self.value, self.status, self.msg)
-
-# def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'):
-# self.xml = xml
-# self.tag = xml.tag
-# if not state:
-# state = {}
-# if item_id:
-# self.id = item_id
-# if xml.get('id'):
-# self.id = xml.get('id')
-# if 'id' in state:
-# self.id = state['id']
-# self.system = system
-
-# self.value = ''
-# if 'value' in state:
-# self.value = state['value']
-
-# self.msg = ''
-# if 'feedback' in state and 'message' in state['feedback']:
-# self.msg = state['feedback']['message']
-
-# self.status = 'unanswered'
-# if 'status' in state:
-# self.status = state['status']
+ @classmethod
+ def get_uses(c):
+ return ['capa_input', 'capa_transform']
+ def get_html(self):
+ return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg)
def register_render_function(fn, names=None, cls=SimpleInput):
if names is None:
@@ -136,9 +94,6 @@ def register_render_function(fn, names=None, cls=SimpleInput):
return fn
return wrapped
-
-
-
#-----------------------------------------------------------------------------
@register_render_function
@@ -201,16 +156,16 @@ def choicegroup(element, value, status, render_template, msg=''):
return etree.XML(html)
@register_render_function
-def textline(element, value, state, render_template, msg=""):
+def textline(element, value, status, render_template, msg=""):
'''
Simple text line input, with optional size specification.
'''
if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x
- return SimpleInput.xml_tags['textline_dynamath'](element,value,state,render_template,msg)
+ return SimpleInput.xml_tags['textline_dynamath'](element,value,status,render_template,msg)
eid=element.get('id')
count = int(eid.split('_')[-2])-1 # HACK
size = element.get('size')
- context = {'id':eid, 'value':value, 'state':state, 'count':count, 'size': size, 'msg': msg}
+ context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg': msg}
html = render_template("textinput.html", context)
return etree.XML(html)
diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py
index 1c09493b03..bfd42814f7 100644
--- a/common/lib/capa/responsetypes.py
+++ b/common/lib/capa/responsetypes.py
@@ -63,7 +63,8 @@ class GenericResponse(object):
- 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__
- - __unicode__ : unicode representation of this Response
+ - render_html : render this Response as HTML (must return XHTML compliant string)
+ - __unicode__ : unicode representation of this Response
Each response type may also specify the following attributes:
@@ -114,9 +115,30 @@ class GenericResponse(object):
if self.max_inputfields==1:
self.answer_id = self.answer_ids[0] # for convenience
+ self.default_answer_map = {} # dict for default answer map (provided in input elements)
+ for entry in self.inputfields:
+ answer = entry.get('correct_answer')
+ if answer:
+ self.default_answer_map[entry.get('id')] = contextualize_text(answer, self.context)
+
if hasattr(self,'setup_response'):
self.setup_response()
+ def render_html(self,renderer):
+ '''
+ Return XHTML Element tree representation of this Response.
+
+ Arguments:
+
+ - renderer : procedure which produces HTML given an ElementTree
+ '''
+ tree = etree.Element('span') # render ourself as a + our content
+ for item in self.xml:
+ item_xhtml = renderer(item) # call provided procedure to do the rendering
+ if item_xhtml is not None: tree.append(item_xhtml)
+ tree.tail = self.xml.tail
+ return tree
+
@abc.abstractmethod
def get_score(self, student_answers):
'''
@@ -132,7 +154,6 @@ class GenericResponse(object):
'''
pass
- #not an abstract method because plenty of responses will not want to preprocess anything, and we should not require that they override this method.
def setup_response(self):
pass
@@ -485,17 +506,17 @@ def sympy_check2():
'''
Give correct answer expected for this response.
- capa_problem handles correct_answers from entry objects like textline, and that
- is what should be used when this response has multiple entry objects.
+ use default_answer_map from entry elements (eg textline),
+ when this response has multiple entry objects.
but for simplicity, if an "expect" attribute was given by the content author
- ie then return it now.
+ ie then that.
'''
if len(self.answer_ids)>1:
- return {}
+ return self.default_answer_map
if self.expect:
return {self.answer_ids[0] : self.expect}
- return {}
+ return self.default_answer_map
#-----------------------------------------------------------------------------
@@ -797,9 +818,8 @@ class SchematicResponse(GenericResponse):
return zip(sorted(self.answer_ids), self.context['correct'])
def get_answers(self):
- # Since this is explicitly specified in the problem, this will
- # be handled by capa_problem
- return {}
+ # use answers provided in input elements
+ return self.default_answer_map
#-----------------------------------------------------------------------------