Commit 7b3c7969 by ichuang

second pass in capa cleanup:

  - each response can now render its own xhtml
  - cleaned up LoncapaProblem.extract_html
parent 7b3ad553
......@@ -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
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
# 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_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_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 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
#-----------------------------------------------------------------------------
......
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