second pass in capa cleanup:
- each response can now render its own xhtml - cleaned up LoncapaProblem.extract_html
This commit is contained in:
@@ -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: <textline size="5" correct_answer="saturated" />
|
||||
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 <solution>...</solution> 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
|
||||
|
||||
# <solution>...</solution> 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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 <span> + 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 <customresponse expect="foo" ...> then return it now.
|
||||
ie <customresponse expect="foo" ...> 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
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user