Commit eeaad1ee by Calen Pennington

Merge pull request #94 from MITx/capa-cleanup

Capa cleanup
parents d51d9d08 3cf2f593
#
# File: capa/capa_problem.py
#
# Nomenclature:
#
# A capa Problem is a collection of text and capa Response questions. Each Response may have one or more
# Input entry fields. The capa Problem may include a solution.
#
'''
Main module which shows problems (of "capa" type).
This is used by capa_module.
'''
import copy
from __future__ import division
import logging
import math
import numpy
......@@ -18,44 +24,26 @@ import scipy
import struct
from lxml import etree
from lxml.etree import Element
from xml.sax.saxutils import unescape
from util import contextualize_text
import calc
from correctmap import CorrectMap
import eia
import inputtypes
from util import contextualize_text
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse
# to be replaced with auto-registering
import responsetypes
import calc
import eia
# dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag,x) for x in responsetypes.__all__])
log = logging.getLogger(__name__)
response_types = {'numericalresponse': NumericalResponse,
'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']
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'},
}
......@@ -68,31 +56,37 @@ global_context = {'random': random,
'eia': eia}
# 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']
html_problem_semantics = ["responseparam", "answer", "script","hintgroup"]
# 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,
# }
log = logging.getLogger('mitx.' + __name__)
#-----------------------------------------------------------------------------
# main class for this module
class LoncapaProblem(object):
'''
Main class for capa Problems.
'''
def __init__(self, fileobject, id, state=None, seed=None, system=None):
'''
Initializes capa Problem. The problem itself is defined by the XML file
pointed to by fileobject.
Arguments:
- filesobject : an OSFS instance: see fs.osfs
- id : string used as the identifier for this problem; often a filename (no spaces)
- state : student state (represented as a dict)
- seed : random number generator seed (int)
- system : I4xSystem instance which provides OS, rendering, and user context
'''
## Initialize class variables from state
self.seed = None
self.student_answers = dict()
self.correct_map = dict()
self.done = False
self.do_reset()
self.problem_id = id
self.system = system
if seed is not None:
self.seed = seed
if state:
......@@ -101,7 +95,7 @@ class LoncapaProblem(object):
if 'student_answers' in state:
self.student_answers = state['student_answers']
if 'correct_map' in state:
self.correct_map = state['correct_map']
self.correct_map.set_dict(state['correct_map'])
if 'done' in state:
self.done = state['done']
......@@ -109,22 +103,30 @@ class LoncapaProblem(object):
if not self.seed:
self.seed = struct.unpack('i', os.urandom(4))[0]
## Parse XML file
if getattr(system, 'DEBUG', False):
self.fileobject = fileobject # save problem file object, so we can use for debugging information later
if getattr(system, 'DEBUG', False): # get the problem XML string from the problem file
log.info("[courseware.capa.capa_problem.lcp.init] fileobject = %s" % fileobject)
file_text = fileobject.read()
self.fileobject = fileobject # save it, so we can use for debugging information later
# Convert startouttext and endouttext to proper <text></text>
# TODO: Do with XML operations
file_text = re.sub("startouttext\s*/", "text", file_text)
file_text = re.sub("startouttext\s*/", "text", file_text) # Convert startouttext and endouttext to proper <text></text>
file_text = re.sub("endouttext\s*/", "/text", file_text)
self.tree = etree.XML(file_text)
self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map=self.student_answers)
self.context = self.extract_context(self.tree, seed=self.seed)
for response in self.tree.xpath('//' + "|//".join(response_types)):
responder = response_types[response.tag](response, self.context, self.system)
responder.preprocess_response()
self.tree = etree.XML(file_text) # parse problem XML file into an element tree
# construct script processor context (eg for customresponse problems)
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 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)
def do_reset(self):
'''
Reset internal state to unfinished, with no answers
'''
self.student_answers = dict()
self.correct_map = CorrectMap()
self.done = False
def __unicode__(self):
return u"LoncapaProblem ({0})".format(self.fileobject)
......@@ -133,25 +135,49 @@ class LoncapaProblem(object):
''' Stored per-user session data neeeded to:
1) Recreate the problem
2) Populate any student answers. '''
return {'seed': self.seed,
'student_answers': self.student_answers,
'correct_map': self.correct_map,
'correct_map': self.correct_map.get_dict(),
'done': self.done}
def get_max_score(self):
'''
TODO: multiple points for programming problems.
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.
'''
sum = 0
for et in entry_types:
sum = sum + self.tree.xpath('count(//' + et + ')')
return int(sum)
maxscore = 0
for responder in self.responders.values():
if hasattr(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:
try:
maxscore += len(responder.get_answers())
except:
log.debug('responder %s failed to properly return get_answers()' % responder) # FIXME
raise
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':
correct += 1
try:
correct += self.correct_map.get_npoints(key)
except Exception:
log.error('key=%s, correct_map = %s' % (key,self.correct_map))
raise
if (not self.student_answers) or len(self.student_answers) == 0:
return {'score': 0,
'total': self.get_max_score()}
......@@ -166,42 +192,37 @@ class LoncapaProblem(object):
of each key removed (the string before the first "_").
Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123
Calles the Response for each question in this problem, to do the actual grading.
'''
self.student_answers = answers
self.correct_map = dict()
problems_simple = self.extract_problems(self.tree)
for response in problems_simple:
grader = response_types[response.tag](response, self.context, self.system)
results = grader.get_score(answers) # call the responsetype instance to do the actual grading
self.correct_map.update(results)
return self.correct_map
oldcmap = self.correct_map # old CorrectMap
newcmap = CorrectMap() # start new with empty CorrectMap
# log.debug('Responders: %s' % self.responders)
for responder in self.responders.values():
results = responder.evaluate_answers(answers,oldcmap) # call the responsetype instance to do the actual grading
newcmap.update(results)
self.correct_map = newcmap
# log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap))
return newcmap
def get_question_answers(self):
"""Returns a dict of answer_ids to answer values. If we can't generate
"""Returns a dict of answer_ids to answer values. If we cannot generate
an answer (this sometimes happens in customresponses), that answer_id is
not included. Called by "show answers" button JSON request
(see capa_module)
"""
answer_map = dict()
problems_simple = self.extract_problems(self.tree) # purified (flat) XML tree of just response queries
for response in problems_simple:
responder = response_types[response.tag](response, self.context, self.system) # instance of numericalresponse, customresponse,...
for responder in self.responders.values():
results = responder.get_answers()
answer_map.update(results) # dict of (id,correct_answer)
# example for the following: <textline size="5" correct_answer="saturated" />
for entry in problems_simple.xpath("//" + "|//".join(response_properties + entry_types)):
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
log.debug('answer_map = %s' % answer_map)
return answer_map
def get_answer_ids(self):
......@@ -209,19 +230,19 @@ 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 = []
problems_simple = self.extract_problems(self.tree)
for response in problems_simple:
responder = response_types[response.tag](response, self.context)
if hasattr(responder, "answer_id"):
answer_ids.append(responder.answer_id)
# customresponse types can have multiple answer_ids
elif hasattr(responder, "answer_ids"):
answer_ids.extend(responder.answer_ids)
for responder in self.responders.values():
answer_ids.append(responder.get_answers().keys())
return answer_ids
# ======= Private ========
def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
def get_html(self):
'''
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)), self.context)
# ======= Private Methods Below ========
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
context of this problem. Provides ability to randomize problems, and also set
......@@ -235,7 +256,6 @@ class LoncapaProblem(object):
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:
......@@ -253,158 +273,103 @@ class LoncapaProblem(object):
log.exception("Error while execing code: " + code)
return context
def get_html(self):
return contextualize_text(etree.tostring(self.extract_html(self.tree)[0]), self.context)
def _extract_html(self, problemtree): # private
'''
Main (private) function which converts Problem XML tree to HTML.
Calls itself recursively.
def extract_html(self, problemtree): # private
''' Helper function for get_html. Recursively converts XML tree to HTML
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
# for the problem ID for the input element.
status = "unsubmitted"
msg = ''
hint = ''
hintmode = None
if problemid in self.correct_map:
status = self.correct_map[problemtree.get('id')]
pid = problemtree.get('id')
status = self.correct_map.get_correctness(pid)
msg = self.correct_map.get_msg(pid)
hint = self.correct_map.get_hint(pid)
hintmode = self.correct_map.get_hintmode(pid)
value = ""
if self.student_answers and problemid in self.student_answers:
value = self.student_answers[problemid]
#### This code is a hack. It was merged to help bring two branches
#### in sync, but should be replaced. msg should be passed in a
#### response_type
# prepare the response message, if it exists in correct_map
if 'msg' in self.correct_map:
msg = self.correct_map['msg']
elif ('msg_%s' % problemid) in self.correct_map:
msg = self.correct_map['msg_%s' % problemid]
else:
msg = ''
# do the rendering
# This should be broken out into a helper function
# that handles all input objects
render_object = inputtypes.SimpleInput(system=self.system,
xml=problemtree,
state={'value': value,
'status': status,
'id': problemtree.get('id'),
'feedback': {'message': msg}
'feedback': {'message': msg,
'hint' : hint,
'hintmode' : hintmode,
}
},
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():
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]
def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private
return tree
def _preprocess_problem(self, tree): # private
'''
Assign IDs to all the responses
Assign sub-IDs to all entries (textline, schematic, etc.)
Annoted correctness and value
In-place transformation
Also create capa Response instances for each responsetype and save as self.responders
'''
response_id = 1
for response in tree.xpath('//' + "|//".join(response_types)):
self.responders = {}
for response in tree.xpath('//' + "|//".join(response_tag_dict)):
response_id_str = self.problem_id + "_" + str(response_id)
response.attrib['id'] = response_id_str
if response_id not in correct_map:
correct = 'unsubmitted'
response.attrib['state'] = correct
response_id = response_id + 1
response.set('id',response_id_str) # create and save ID for this response
response_id += 1
answer_id = 1
for entry in tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]),
id=response_id_str):
# assign one answer_id for each entry_type or solution_type
inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]),
id=response_id_str)
for entry in inputfields: # assign one answer_id for each entry_type or solution_type
entry.attrib['response_id'] = str(response_id)
entry.attrib['answer_id'] = str(answer_id)
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
answer_id = answer_id + 1
responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
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).
solution_id = 1
for solution in tree.findall('.//solution'):
solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id)
solution_id += 1
def extract_problems(self, problem_tree):
''' Remove layout from the problem, and give a purified XML tree of just the problems '''
problem_tree = copy.deepcopy(problem_tree)
tree = Element('problem')
for response in problem_tree.xpath("//" + "|//".join(response_types)):
newresponse = copy.copy(response)
for e in newresponse:
newresponse.remove(e)
# copy.copy is needed to make xpath work right. Otherwise, it starts at the root
# of the tree. We should figure out if there's some work-around
for e in copy.copy(response).xpath("//" + "|//".join(response_properties + entry_types)):
newresponse.append(e)
tree.append(newresponse)
return tree
if __name__ == '__main__':
problem_id = 'simpleFormula'
filename = 'simpleFormula.xml'
problem_id = 'resistor'
filename = 'resistor.xml'
lcp = LoncapaProblem(filename, problem_id)
context = lcp.extract_context(lcp.tree)
problem = lcp.extract_problems(lcp.tree)
print lcp.grade_problems({'resistor_2_1': '1.0', 'resistor_3_1': '2.0'})
#print lcp.grade_problems({'simpleFormula_2_1':'3*x^3'})
#numericalresponse(problem, context)
#print etree.tostring((lcp.tree))
print '============'
print
#print etree.tostring(lcp.extract_problems(lcp.tree))
print lcp.get_html()
#print extract_context(tree)
# def handle_fr(self, element):
# problem={"answer":self.contextualize_text(answer),
# "type":"formularesponse",
# "tolerance":evaluator({},{},self.contextualize_text(tolerance)),
# "sample_range":dict(zip(variables, sranges)),
# "samples_count": numsamples,
# "id":id,
# self.questions[self.lid]=problem
#-----------------------------------------------------------------------------
# class used to store graded responses to CAPA questions
#
# Used by responsetypes and capa_problem
class CorrectMap(object):
'''
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.
'''
def __init__(self,*args,**kwargs):
self.cmap = dict() # start with empty dict
self.items = self.cmap.items
self.keys = self.cmap.keys
self.set(*args,**kwargs)
def __getitem__(self, *args, **kwargs):
return self.cmap.__getitem__(*args, **kwargs)
def __iter__(self):
return self.cmap.__iter__()
def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None):
if answer_id is not None:
self.cmap[answer_id] = {'correctness': correctness,
'npoints': npoints,
'msg': msg,
'hint' : hint,
'hintmode' : hintmode,
}
def __repr__(self):
return repr(self.cmap)
def get_dict(self):
'''
return dict version of self
'''
return self.cmap
def set_dict(self,correct_map):
'''
set internal dict to provided correct_map dict
for graceful migration, if correct_map is a one-level dict, then convert it to the new
dict of dicts format.
'''
if correct_map and not (type(correct_map[correct_map.keys()[0]])==dict):
self.__init__() # empty current dict
for k in correct_map: self.set(k,correct_map[k]) # create new dict entries
else:
self.cmap = correct_map
def is_correct(self,answer_id):
if answer_id in self.cmap: return self.cmap[answer_id]['correctness'] == 'correct'
return None
def get_npoints(self,answer_id):
if self.is_correct(answer_id):
npoints = self.cmap[answer_id].get('npoints',1) # default to 1 point if correct
return npoints or 1
return 0 # if not correct, return 0
def set_property(self,answer_id,property,value):
if answer_id in self.cmap: self.cmap[answer_id][property] = value
else: self.cmap[answer_id] = {property:value}
def get_property(self,answer_id,property,default=None):
if answer_id in self.cmap: return self.cmap[answer_id].get(property,default)
return default
def get_correctness(self,answer_id):
return self.get_property(answer_id,'correctness')
def get_msg(self,answer_id):
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):
'''
Update this CorrectMap with the contents of another CorrectMap
'''
if not isinstance(other_cmap,CorrectMap):
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
self.cmap.update(other_cmap.get_dict())
......@@ -32,100 +32,71 @@ def get_input_xml_tags():
return SimpleInput.get_xml_tags()
class SimpleInput():# XModule
''' Type for simple inputs -- plain HTML with a form element
State is a dictionary with optional keys:
'''
Type for simple inputs -- plain HTML with a form element
'''
xml_tags = {} ## Maps tags to functions
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 :
'''
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
self.tag = xml.tag
if not state:
state = {}
self.system = system
if not state: state = {}
## ID should only come from one place.
## 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
## the future if there's a more logical order.
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
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.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']
feedback = state.get('feedback')
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'
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 +107,6 @@ def register_render_function(fn, names=None, cls=SimpleInput):
return fn
return wrapped
#-----------------------------------------------------------------------------
@register_render_function
......@@ -201,16 +169,20 @@ 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')
if eid is None:
msg = 'textline has no id: it probably appears outside of a known response type'
msg += "\nSee problem XML source line %s" % getattr(element,'sourceline','<unavailable>')
raise Exception(msg)
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)
......
......@@ -21,44 +21,252 @@ import abc
# specific library imports
from calc import evaluator, UndefinedVariable
from util import contextualize_text
from correctmap import CorrectMap
from util import *
from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
log = logging.getLogger(__name__)
log = logging.getLogger('mitx.' + __name__)
def compare_with_tolerance(v1, v2, tol):
''' Compare v1 to v2 with maximum tolerance tol
tol is relative if it ends in %; otherwise, it is absolute
#-----------------------------------------------------------------------------
# Exceptions
class LoncapaProblemError(Exception):
'''
relative = "%" in tol
if relative:
tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01
tolerance = tolerance_rel * max(abs(v1), abs(v2))
else:
tolerance = evaluator(dict(),dict(),tol)
return abs(v1-v2) <= tolerance
Error in specification of a problem
'''
pass
class ResponseError(Exception):
'''
Error for failure in processing a response
'''
pass
class StudentInputError(Exception):
pass
#-----------------------------------------------------------------------------
#
# Main base class for CAPA responsetypes
class LoncapaResponse(object):
'''
Base class for CAPA responsetypes. Each response type (ie a capa question,
which is part of a capa problem) is represented as a subclass,
which should provide the following methods:
- get_score : evaluate the given student answers, and return a CorrectMap
- get_answers : provide a dict of the expected answers for this problem
Each subclass must also define the following attributes:
- response_tag : xhtml tag identifying this response (used in auto-registering)
class GenericResponse(object):
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__
- 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)
- __unicode__ : unicode representation of this Response
Each response type may also specify the following attributes:
- max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None)
- allowed_inputfields : list of allowed input fields (each a string) for this Response
- required_attributes : list of required attributes (each a string) on the main response XML stanza
- hint_tag : xhtml tag identifying hint associated with this response inside hintgroup
'''
__metaclass__=abc.ABCMeta # abc = Abstract Base Class
response_tag = None
hint_tag = None
max_inputfields = None
allowed_inputfields = []
required_attributes = []
def __init__(self, xml, inputfields, context, system=None):
'''
Init is passed the following arguments:
- xml : ElementTree of this Response
- inputfields : ordered list of ElementTrees for each input entry field in this Response
- context : script processor context
- system : I4xSystem instance which provides OS, rendering, and user context
'''
self.xml = xml
self.inputfields = inputfields
self.context = context
self.system = system
for abox in inputfields:
if abox.tag not in self.allowed_inputfields:
msg = "%s: cannot have input field %s" % (unicode(self),abox.tag)
msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>')
raise LoncapaProblemError(msg)
if self.max_inputfields and len(inputfields)>self.max_inputfields:
msg = "%s: cannot have more than %s input fields" % (unicode(self),self.max_inputfields)
msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>')
raise LoncapaProblemError(msg)
for prop in self.required_attributes:
if not xml.get(prop):
msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self),prop)
msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>')
raise LoncapaProblemError(msg)
self.answer_ids = [x.get('id') for x in self.inputfields] # ordered list of answer_id values for this response
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
def evaluate_answers(self,student_answers,old_cmap):
'''
Called by capa_problem.LoncapaProblem to evaluate student answers, and to
generate hints (if any).
Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id.
'''
new_cmap = self.get_score(student_answers)
self.get_hints(student_answers, new_cmap, old_cmap)
# log.debug('new_cmap = %s' % new_cmap)
return new_cmap
def get_hints(self, student_answers, new_cmap, old_cmap):
'''
Generate adaptive hints for this problem based on student answers, the old CorrectMap,
and the new CorrectMap produced by get_score.
Does not return anything.
Modifies new_cmap, by adding hints to answer_id entries as appropriate.
'''
hintgroup = self.xml.find('hintgroup')
if hintgroup is None: return
# hint specified by function?
hintfn = hintgroup.get('hintfn')
if hintfn:
'''
Hint is determined by a function defined in the <script> context; evaluate that function to obtain
list of hint, hintmode for each answer_id.
The function should take arguments (answer_ids, student_answers, new_cmap, old_cmap)
and it should modify new_cmap as appropriate.
We may extend this in the future to add another argument which provides a callback procedure
to a social hint generation system.
'''
if not hintfn in self.context:
msg = 'missing specified hint function %s in script context' % hintfn
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
raise LoncapaProblemError(msg)
try:
self.context[hintfn](self.answer_ids, student_answers, new_cmap, old_cmap)
except Exception, err:
msg = 'Error %s in evaluating hint function %s' % (err,hintfn)
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
raise ResponseError(msg)
return
# hint specified by conditions and text dependent on conditions (a-la Loncapa design)
# see http://help.loncapa.org/cgi-bin/fom?file=291
#
# Example:
#
# <formularesponse samples="x@-5:5#11" id="11" answer="$answer">
# <textline size="25" />
# <hintgroup>
# <formulahint samples="x@-5:5#11" answer="$wrongans" name="inversegrad"></formulahint>
# <hintpart on="inversegrad">
# <text>You have inverted the slope in the question. The slope is
# (y2-y1)/(x2 - x1) you have the slope as (x2-x1)/(y2-y1).</text>
# </hintpart>
# </hintgroup>
# </formularesponse>
if self.hint_tag is not None and hintgroup.find(self.hint_tag) is not None and hasattr(self,'check_hint_condition'):
rephints = hintgroup.findall(self.hint_tag)
hints_to_show = self.check_hint_condition(rephints,student_answers)
hintmode = hintgroup.get('mode','always') # can be 'on_request' or 'always' (default)
for hintpart in hintgroup.findall('hintpart'):
if hintpart.get('on') in hints_to_show:
hint_text = hintpart.find('text').text
aid = self.answer_ids[-1] # make the hint appear after the last answer box in this response
new_cmap.set_hint_and_mode(aid,hint_text,hintmode)
log.debug('after hint: new_cmap = %s' % new_cmap)
@abc.abstractmethod
def get_score(self, student_answers):
'''
Return a CorrectMap for the answers expected vs given. This includes
(correctness, npoints, msg) for each answer_id.
Arguments:
- student_answers : dict of (answer_id,answer) where answer = student input (string)
- old_cmap : previous CorrectMap (may be empty); useful for analyzing or recording history of responses
'''
pass
@abc.abstractmethod
def get_answers(self):
'''
Return a dict of (answer_id,answer_text) for each answer for this question.
'''
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 preprocess_response(self):
def check_hint_condition(self,hxml_set,student_answers):
'''
Return a list of hints to show.
- hxml_set : list of Element trees, each specifying a condition to be satisfied for a named hint condition
- student_answers : dict of student answers
Returns a list of names of hint conditions which were satisfied. Those are used to determine which hints are displayed.
'''
pass
#Every response type needs methods "get_score" and "get_answers"
def setup_response(self):
pass
def __unicode__(self):
return u'LoncapaProblem Response %s' % self.xml.tag
#-----------------------------------------------------------------------------
class MultipleChoiceResponse(GenericResponse):
class MultipleChoiceResponse(LoncapaResponse):
# TODO: handle direction and randomize
snippets = [{'snippet': '''<multiplechoiceresponse direction="vertical" randomize="yes">
<choicegroup type="MultipleChoice">
......@@ -69,30 +277,20 @@ class MultipleChoiceResponse(GenericResponse):
</choicegroup>
</multiplechoiceresponse>
'''}]
def __init__(self, xml, context, system=None):
self.xml = xml
self.correct_choices = xml.xpath('//*[@id=$id]//choice[@correct="true"]',
id=xml.get('id'))
self.correct_choices = [choice.get('name') for choice in self.correct_choices]
self.context = context
self.answer_field = xml.find('choicegroup') # assumes only ONE choicegroup within this response
self.answer_id = xml.xpath('//*[@id=$id]//choicegroup/@id',
id=xml.get('id'))
if not len(self.answer_id) == 1:
raise Exception("should have exactly one choice group per multiplechoicceresponse")
self.answer_id=self.answer_id[0]
response_tag = 'multiplechoiceresponse'
max_inputfields = 1
allowed_inputfields = ['choicegroup']
def get_score(self, student_answers):
if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices:
return {self.answer_id:'correct'}
else:
return {self.answer_id:'incorrect'}
def setup_response(self):
self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes
def get_answers(self):
return {self.answer_id:self.correct_choices}
# define correct choices (after calling secondary setup)
xml = self.xml
cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]',id=xml.get('id'))
self.correct_choices = [choice.get('name') for choice in cxml]
def preprocess_response(self):
def mc_setup_response(self):
'''
Initialize name attributes in <choice> stanzas in the <choicegroup> in this response.
'''
......@@ -108,8 +306,24 @@ class MultipleChoiceResponse(GenericResponse):
else:
choice.set("name", "choice_"+choice.get("name"))
def get_score(self, student_answers):
'''
grade student response.
'''
# log.debug('%s: student_answers=%s, correct_choices=%s' % (unicode(self),student_answers,self.correct_choices))
if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices:
return CorrectMap(self.answer_id,'correct')
else:
return CorrectMap(self.answer_id,'incorrect')
def get_answers(self):
return {self.answer_id:self.correct_choices}
class TrueFalseResponse(MultipleChoiceResponse):
def preprocess_response(self):
response_tag = 'truefalseresponse'
def mc_setup_response(self):
i=0
for response in self.xml.xpath("choicegroup"):
response.set("type", "TrueFalse")
......@@ -125,13 +339,13 @@ class TrueFalseResponse(MultipleChoiceResponse):
answers = set(student_answers.get(self.answer_id, []))
if correct == answers:
return { self.answer_id : 'correct'}
return CorrectMap( self.answer_id , 'correct')
return {self.answer_id : 'incorrect'}
return CorrectMap(self.answer_id ,'incorrect')
#-----------------------------------------------------------------------------
class OptionResponse(GenericResponse):
class OptionResponse(LoncapaResponse):
'''
TODO: handle direction and randomize
'''
......@@ -140,34 +354,42 @@ class OptionResponse(GenericResponse):
<optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput>
</optionresponse>'''}]
def __init__(self, xml, context, system=None):
self.xml = xml
self.answer_fields = xml.findall('optioninput')
self.context = context
response_tag = 'optionresponse'
hint_tag = 'optionhint'
allowed_inputfields = ['optioninput']
def setup_response(self):
self.answer_fields = self.inputfields
def get_score(self, student_answers):
cmap = {}
# log.debug('%s: student_answers=%s' % (unicode(self),student_answers))
cmap = CorrectMap()
amap = self.get_answers()
for aid in amap:
if aid in student_answers and student_answers[aid]==amap[aid]:
cmap[aid] = 'correct'
cmap.set(aid,'correct')
else:
cmap[aid] = 'incorrect'
cmap.set(aid,'incorrect')
return cmap
def get_answers(self):
amap = dict([(af.get('id'),af.get('correct')) for af in self.answer_fields])
# log.debug('%s: expected answers=%s' % (unicode(self),amap))
return amap
#-----------------------------------------------------------------------------
class NumericalResponse(GenericResponse):
def __init__(self, xml, context, system=None):
self.xml = xml
if not xml.get('answer'):
msg = "Error in problem specification: numericalresponse missing required answer attribute\n"
msg += "See XML source line %s" % getattr(xml,'sourceline','<unavailable>')
raise Exception,msg
class NumericalResponse(LoncapaResponse):
response_tag = 'numericalresponse'
hint_tag = 'numericalhint'
allowed_inputfields = ['textline']
required_attributes = ['answer']
max_inputfields = 1
def setup_response(self):
xml = self.xml
context = self.context
self.correct_answer = contextualize_text(xml.get('answer'), context)
try:
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
......@@ -182,7 +404,7 @@ class NumericalResponse(GenericResponse):
self.answer_id = None
def get_score(self, student_answers):
''' Display HTML for a numeric response '''
'''Grade a numeric response '''
student_answer = student_answers[self.answer_id]
try:
correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), complex(self.correct_answer), self.tolerance)
......@@ -193,16 +415,54 @@ class NumericalResponse(GenericResponse):
raise StudentInputError('Invalid input -- please use a number only')
if correct:
return {self.answer_id:'correct'}
return CorrectMap(self.answer_id,'correct')
else:
return {self.answer_id:'incorrect'}
return CorrectMap(self.answer_id,'incorrect')
# TODO: add check_hint_condition(self,hxml_set,student_answers)
def get_answers(self):
return {self.answer_id:self.correct_answer}
#-----------------------------------------------------------------------------
class CustomResponse(GenericResponse):
class StringResponse(LoncapaResponse):
response_tag = 'stringresponse'
hint_tag = 'stringhint'
allowed_inputfields = ['textline']
required_attributes = ['answer']
max_inputfields = 1
def setup_response(self):
self.correct_answer = contextualize_text(self.xml.get('answer'), self.context).strip()
def get_score(self, student_answers):
'''Grade a string response '''
student_answer = student_answers[self.answer_id].strip()
correct = self.check_string(self.correct_answer,student_answer)
return CorrectMap(self.answer_id,'correct' if correct else 'incorrect')
def check_string(self,expected,given):
if self.xml.get('type')=='ci': return given.lower() == expected.lower()
return given == expected
def check_hint_condition(self,hxml_set,student_answers):
given = student_answers[self.answer_id].strip()
hints_to_show = []
for hxml in hxml_set:
name = hxml.get('name')
correct_answer = contextualize_text(hxml.get('answer'),self.context).strip()
if self.check_string(correct_answer,given): hints_to_show.append(name)
log.debug('hints_to_show = %s' % hints_to_show)
return hints_to_show
def get_answers(self):
return {self.answer_id:self.correct_answer}
#-----------------------------------------------------------------------------
class CustomResponse(LoncapaResponse):
'''
Custom response. The python code to be run should be in <answer>...</answer>
or in a <script>...</script>
......@@ -241,16 +501,11 @@ def sympy_check2():
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
</customresponse>'''}]
def __init__(self, xml, context, system=None):
self.xml = xml
self.system = system
## CRITICAL TODO: Should cover all entrytypes
## NOTE: xpath will look at root of XML tree, not just
## what's in xml. @id=id keeps us in the right customresponse.
self.answer_ids = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))
self.answer_ids += [x.get('id') for x in xml.findall('textbox')] # also allow textbox inputs
self.context = context
response_tag = 'customresponse'
allowed_inputfields = ['textline','textbox']
def setup_response(self):
xml = self.xml
# if <customresponse> has an "expect" (or "answer") attribute then save that
self.expect = xml.get('expect') or xml.get('answer')
......@@ -271,15 +526,17 @@ def sympy_check2():
cfn = xml.get('cfn')
if cfn:
log.debug("cfn = %s" % cfn)
if cfn in context:
self.code = context[cfn]
if cfn in self.context:
self.code = self.context[cfn]
else:
print "can't find cfn in context = ",context
msg = "%s: can't find cfn %s in context" % (unicode(self),cfn)
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
raise LoncapaProblemError(msg)
if not self.code:
if answer is None:
# raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
print "[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
log.error("[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid)
self.code = ''
else:
answer_src = answer.get('src')
......@@ -294,6 +551,8 @@ def sympy_check2():
of each key removed (the string before the first "_").
'''
log.debug('%s: student_answers=%s' % (unicode(self),student_answers))
idset = sorted(self.answer_ids) # ordered list of answer id's
try:
submission = [student_answers[k] for k in idset] # ordered list of answers
......@@ -301,7 +560,7 @@ def sympy_check2():
msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers
msg += '\n idset = %s, error = %s' % (idset,err)
log.error(msg)
raise Exception,msg
raise Exception(msg)
# global variable in context which holds the Presentation MathML from dynamic math input
dynamath = [ student_answers.get(k+'_dynamath',None) for k in idset ] # ordered list of dynamath responses
......@@ -364,7 +623,7 @@ def sympy_check2():
log.error("oops in customresponse (cfn) error %s" % err)
# print "context = ",self.context
log.error(traceback.format_exc())
raise Exception,"oops in customresponse (cfn) error %s" % err
raise Exception("oops in customresponse (cfn) error %s" % err)
log.debug("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
if type(ret)==dict:
correct = ['correct']*len(idset) if ret['ok'] else ['incorrect']*len(idset)
......@@ -386,28 +645,26 @@ def sympy_check2():
correct = ['correct']*len(idset) if ret else ['incorrect']*len(idset)
# build map giving "correct"ness of the answer(s)
#correct_map = dict(zip(idset, self.context['correct']))
correct_map = {}
correct_map = CorrectMap()
for k in range(len(idset)):
correct_map[idset[k]] = correct[k]
correct_map['msg_%s' % idset[k]] = messages[k]
correct_map.set(idset[k], correct[k], msg=messages[k])
return correct_map
def get_answers(self):
'''
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
#-----------------------------------------------------------------------------
......@@ -425,16 +682,18 @@ class SymbolicResponse(CustomResponse):
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>.
</text>
</problem>'''}]
def __init__(self, xml, context, system=None):
xml.set('cfn','symmath_check')
code = "from symmath import *"
exec code in context,context
CustomResponse.__init__(self,xml,context,system)
response_tag = 'symbolicresponse'
def setup_response(self):
self.xml.set('cfn','symmath_check')
code = "from symmath import *"
exec code in self.context,self.context
CustomResponse.setup_response(self)
#-----------------------------------------------------------------------------
class ExternalResponse(GenericResponse):
class ExternalResponse(LoncapaResponse):
'''
Grade the students input using an external server.
......@@ -480,15 +739,14 @@ main()
</answer>
</externalresponse>'''}]
def __init__(self, xml, context, system=None):
self.xml = xml
response_tag = 'externalresponse'
allowed_inputfields = ['textline','textbox']
def setup_response(self):
xml = self.xml
self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id',
id=xml.get('id'))
self.context = context
answer = xml.xpath('//*[@id=$id]//answer',
id=xml.get('id'))[0]
answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors
answer_src = answer.get('src')
if answer_src is not None:
self.code = self.system.filesystem.open('src/'+answer_src).read()
......@@ -519,28 +777,30 @@ main()
except Exception,err:
msg = 'Error %s - cannot connect to external server url=%s' % (err,self.url)
log.error(msg)
raise Exception, msg
raise Exception(msg)
if self.system.DEBUG: log.info('response = %s' % r.text)
if (not r.text ) or (not r.text.strip()):
raise Exception,'Error: no response from external server url=%s' % self.url
raise Exception('Error: no response from external server url=%s' % self.url)
try:
rxml = etree.fromstring(r.text) # response is XML; prase it
except Exception,err:
msg = 'Error %s - cannot parse response from external server r.text=%s' % (err,r.text)
log.error(msg)
raise Exception, msg
raise Exception(msg)
return rxml
def get_score(self, student_answers):
idset = sorted(self.answer_ids)
cmap = CorrectMap()
try:
submission = [student_answers[k] for k in sorted(self.answer_ids)]
submission = [student_answers[k] for k in idset]
except Exception,err:
log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err,self.answer_ids,student_answers))
raise Exception,err
raise Exception(err)
self.context.update({'submission':submission})
......@@ -551,9 +811,9 @@ main()
except Exception, err:
log.error('Error %s' % err)
if self.system.DEBUG:
correct_map = dict(zip(sorted(self.answer_ids), ['incorrect'] * len(self.answer_ids) ))
correct_map['msg_%s' % self.answer_ids[0]] = '<font color="red" size="+2">%s</font>' % str(err).replace('<','&lt;')
return correct_map
cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset) )))
cmap.set_property(self.answer_ids[0],'msg','<font color="red" size="+2">%s</font>' % str(err).replace('<','&lt;'))
return cmap
ad = rxml.find('awarddetail').text
admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses
......@@ -563,13 +823,13 @@ main()
if ad in admap:
self.context['correct'][0] = admap[ad]
# self.context['correct'] = ['correct','correct']
correct_map = dict(zip(sorted(self.answer_ids), self.context['correct']))
# store message in correct_map
correct_map['msg_%s' % self.answer_ids[0]] = rxml.find('message').text.replace('&nbsp;','&#160;')
# create CorrectMap
for key in idset:
idx = idset.index(key)
msg = rxml.find('message').text.replace('&nbsp;','&#160;') if idx==0 else None
cmap.set(key, self.context['correct'][idx], msg=msg)
return correct_map
return cmap
def get_answers(self):
'''
......@@ -587,15 +847,13 @@ main()
if not (len(exans)==len(self.answer_ids)):
log.error('Expected %d answers from external server, only got %d!' % (len(self.answer_ids),len(exans)))
raise Exception,'Short response from external server'
raise Exception('Short response from external server')
return dict(zip(self.answer_ids,exans))
class StudentInputError(Exception):
pass
#-----------------------------------------------------------------------------
class FormulaResponse(GenericResponse):
class FormulaResponse(LoncapaResponse):
'''
Checking of symbolic math response using numerical sampling.
'''
......@@ -617,8 +875,15 @@ class FormulaResponse(GenericResponse):
</problem>'''}]
def __init__(self, xml, context, system=None):
self.xml = xml
response_tag = 'formularesponse'
hint_tag = 'formulahint'
allowed_inputfields = ['textline']
required_attributes = ['answer']
max_inputfields = 1
def setup_response(self):
xml = self.xml
context = self.context
self.correct_answer = contextualize_text(xml.get('answer'), context)
self.samples = contextualize_text(xml.get('samples'), context)
try:
......@@ -626,16 +891,8 @@ class FormulaResponse(GenericResponse):
id=xml.get('id'))[0]
self.tolerance = contextualize_text(self.tolerance_xml, context)
except Exception:
self.tolerance = 0
self.tolerance = '0.00001'
try:
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))[0]
except Exception:
self.answer_id = None
raise Exception, "[courseware.capa.responsetypes.FormulaResponse] Error: missing answer_id!!"
self.context = context
ts = xml.get('type')
if ts is None:
typeslist = []
......@@ -648,12 +905,16 @@ class FormulaResponse(GenericResponse):
else: # Default
self.case_sensitive = False
def get_score(self, student_answers):
variables=self.samples.split('@')[0].split(',')
numsamples=int(self.samples.split('@')[1].split('#')[1])
given = student_answers[self.answer_id]
correctness = self.check_formula(self.correct_answer, given, self.samples)
return CorrectMap(self.answer_id, correctness)
def check_formula(self,expected, given, samples):
variables=samples.split('@')[0].split(',')
numsamples=int(samples.split('@')[1].split('#')[1])
sranges=zip(*map(lambda x:map(float, x.split(",")),
self.samples.split('@')[1].split('#')[0].split(':')))
samples.split('@')[1].split('#')[0].split(':')))
ranges=dict(zip(variables, sranges))
for i in range(numsamples):
......@@ -663,23 +924,26 @@ class FormulaResponse(GenericResponse):
value = random.uniform(*ranges[var])
instructor_variables[str(var)] = value
student_variables[str(var)] = value
instructor_result = evaluator(instructor_variables,dict(),self.correct_answer, cs = self.case_sensitive)
#log.debug('formula: instructor_vars=%s, expected=%s' % (instructor_variables,expected))
instructor_result = evaluator(instructor_variables,dict(),expected, cs = self.case_sensitive)
try:
#print student_variables,dict(),student_answers[self.answer_id]
student_result = evaluator(student_variables,dict(),
student_answers[self.answer_id],
#log.debug('formula: student_vars=%s, given=%s' % (student_variables,given))
student_result = evaluator(student_variables,
dict(),
given,
cs = self.case_sensitive)
except UndefinedVariable as uv:
log.debug('formularesponse: undefined variable in given=%s' % given)
raise StudentInputError(uv.message+" not permitted in answer")
except:
except Exception, err:
#traceback.print_exc()
log.debug('formularesponse: error %s in formula' % err)
raise StudentInputError("Error in formula")
if numpy.isnan(student_result) or numpy.isinf(student_result):
return {self.answer_id:"incorrect"}
return "incorrect"
if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
return {self.answer_id:"incorrect"}
return {self.answer_id:"correct"}
return "incorrect"
return "correct"
def strip_dict(self, d):
''' Takes a dict. Returns an identical dict, with all non-word
......@@ -691,19 +955,35 @@ class FormulaResponse(GenericResponse):
isinstance(d[k], numbers.Number)])
return d
def check_hint_condition(self,hxml_set,student_answers):
given = student_answers[self.answer_id]
hints_to_show = []
for hxml in hxml_set:
samples = hxml.get('samples')
name = hxml.get('name')
correct_answer = contextualize_text(hxml.get('answer'),self.context)
try:
correctness = self.check_formula(correct_answer, given, samples)
except Exception:
correctness = 'incorrect'
if correctness=='correct':
hints_to_show.append(name)
log.debug('hints_to_show = %s' % hints_to_show)
return hints_to_show
def get_answers(self):
return {self.answer_id:self.correct_answer}
#-----------------------------------------------------------------------------
class SchematicResponse(GenericResponse):
def __init__(self, xml, context, system=None):
self.xml = xml
self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id',
id=xml.get('id'))
self.context = context
answer = xml.xpath('//*[@id=$id]//answer',
id=xml.get('id'))[0]
class SchematicResponse(LoncapaResponse):
response_tag = 'schematicresponse'
allowed_inputfields = ['schematic']
def setup_response(self):
xml = self.xml
answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0]
answer_src = answer.get('src')
if answer_src is not None:
self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used
......@@ -715,16 +995,17 @@ class SchematicResponse(GenericResponse):
submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)]
self.context.update({'submission':submission})
exec self.code in global_context, self.context
return zip(sorted(self.answer_ids), self.context['correct'])
cmap = CorrectMap()
cmap.set_dict(dict(zip(sorted(self.answer_ids), self.context['correct'])))
return cmap
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
#-----------------------------------------------------------------------------
class ImageResponse(GenericResponse):
class ImageResponse(LoncapaResponse):
"""
Handle student response for image input: the input is a click on an image,
which produces an [x,y] coordinate pair. The click is correct if it falls
......@@ -740,14 +1021,15 @@ class ImageResponse(GenericResponse):
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" />
</imageresponse>'''}]
def __init__(self, xml, context, system=None):
self.xml = xml
self.context = context
self.ielements = xml.findall('imageinput')
response_tag = 'imageresponse'
allowed_inputfields = ['imageinput']
def setup_response(self):
self.ielements = self.inputfields
self.answer_ids = [ie.get('id') for ie in self.ielements]
def get_score(self, student_answers):
correct_map = {}
correct_map = CorrectMap()
expectedset = self.get_answers()
for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza
......@@ -759,21 +1041,28 @@ class ImageResponse(GenericResponse):
if not m:
msg = 'Error in problem specification! cannot parse rectangle in %s' % (etree.tostring(self.ielements[aid],
pretty_print=True))
raise Exception,'[capamodule.capa.responsetypes.imageinput] '+msg
raise Exception('[capamodule.capa.responsetypes.imageinput] '+msg)
(llx,lly,urx,ury) = [int(x) for x in m.groups()]
# parse given answer
m = re.match('\[([0-9]+),([0-9]+)]',given.strip().replace(' ',''))
if not m:
raise Exception,'[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid,given)
raise Exception('[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid,given))
(gx,gy) = [int(x) for x in m.groups()]
# answer is correct if (x,y) is within the specified rectangle
if (llx <= gx <= urx) and (lly <= gy <= ury):
correct_map[aid] = 'correct'
correct_map.set(aid, 'correct')
else:
correct_map[aid] = 'incorrect'
correct_map.set(aid, 'incorrect')
return correct_map
def get_answers(self):
return dict([(ie.get('id'),ie.get('rectangle')) for ie in self.ielements])
#-----------------------------------------------------------------------------
# TEMPORARY: List of all response subclasses
# FIXME: To be replaced by auto-registration
__all__ = [ NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse ]
......@@ -30,4 +30,7 @@
</td>
</tr>
</table>
% if msg:
<span class="message">${msg|n}</span>
% endif
</section>
from calc import evaluator, UndefinedVariable
#-----------------------------------------------------------------------------
#
# Utility functions used in CAPA responsetypes
def compare_with_tolerance(v1, v2, tol):
''' Compare v1 to v2 with maximum tolerance tol
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 = tol.endswith('%')
if relative:
tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01
tolerance = tolerance_rel * max(abs(v1), abs(v2))
else:
tolerance = evaluator(dict(),dict(),tol)
return abs(v1-v2) <= tolerance
def contextualize_text(text, context): # private
''' Takes a string with variables. E.g. $a+$b.
Does a substitution of those variables from the context '''
......
......@@ -13,6 +13,7 @@ from lxml import etree
from x_module import XModule, XModuleDescriptor
from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError
log = logging.getLogger("mitx.courseware")
#-----------------------------------------------------------------------------
......@@ -365,18 +366,17 @@ class Module(XModule):
self.attempts = self.attempts + 1
self.lcp.done=True
success = 'correct'
for i in correct_map:
if correct_map[i]!='correct':
success = 'correct' # success = correct if ALL questions in this problem are correct
for answer_id in correct_map:
if not correct_map.is_correct(answer_id):
success = 'incorrect'
event_info['correct_map']=correct_map
event_info['correct_map']=correct_map.get_dict() # log this in the tracker
event_info['success']=success
self.tracker('save_problem_check', event_info)
try:
html = self.get_problem_html(encapsulate=False)
html = self.get_problem_html(encapsulate=False) # render problem into HTML
except Exception,err:
log.error('failed to generate html')
raise Exception,err
......@@ -430,16 +430,9 @@ class Module(XModule):
self.tracker('reset_problem_fail', event_info)
return "Refresh the page and make an attempt before resetting."
self.lcp.done=False
self.lcp.answers=dict()
self.lcp.correct_map=dict()
self.lcp.student_answers = dict()
self.lcp.do_reset() # call method in LoncapaProblem to reset itself
if self.rerandomize == "always":
self.lcp.context=dict()
self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid.
self.lcp.seed=None
self.lcp.seed=None # reset random number generator seed (note the self.lcp.get_state() in next line)
self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state(), system=self.system)
......
<problem>
<script type="loncapa/python">
# from loncapa import *
x1 = 4 # lc_random(2,4,1)
y1 = 5 # lc_random(3,7,1)
x2 = 10 # lc_random(x1+1,9,1)
y2 = 20 # lc_random(y1+1,15,1)
m = (y2-y1)/(x2-x1)
b = y1 - m*x1
answer = "%s*x+%s" % (m,b)
answer = answer.replace('+-','-')
inverted_m = (x2-x1)/(y2-y1)
inverted_b = b
wrongans = "%s*x+%s" % (inverted_m,inverted_b)
wrongans = wrongans.replace('+-','-')
</script>
<text>
<p>Hints can be provided to students, based on the last response given, as well as the history of responses given. Here is an example of a hint produced by a Formula Response problem.</p>
<p>
What is the equation of the line which passess through ($x1,$y1) and
($x2,$y2)?</p>
<p>The correct answer is <tt>$answer</tt>. A common error is to invert the equation for the slope. Enter <tt>
$wrongans</tt> to see a hint.</p>
</text>
<formularesponse samples="x@-5:5#11" id="11" answer="$answer">
<responseparam description="Numerical Tolerance" type="tolerance" default="0.001" name="tol" />
<text>y = <textline size="25" /></text>
<hintgroup>
<formulahint samples="x@-5:5#11" answer="$wrongans" name="inversegrad">
</formulahint>
<hintpart on="inversegrad">
<text>You have inverted the slope in the question.</text>
</hintpart>
</hintgroup>
</formularesponse>
</problem>
<problem >
<text><h2>Example: String Response Problem</h2>
<br/>
</text>
<text>Which US state has Lansing as its capital?</text>
<stringresponse answer="Michigan" type="ci">
<textline size="20" />
<hintgroup>
<stringhint answer="wisconsin" type="cs" name="wisc">
</stringhint>
<stringhint answer="minnesota" type="cs" name="minn">
</stringhint>
<hintpart on="wisc">
<text>The state capital of Wisconsin is Madison.</text>
</hintpart>
<hintpart on="minn">
<text>The state capital of Minnesota is St. Paul.</text>
</hintpart>
<hintpart on="default">
<text>The state you are looking for is also known as the 'Great Lakes State'</text>
</hintpart>
</hintgroup>
</stringresponse>
</problem>
#
# unittests for courseware
# unittests for xmodule (and capa)
#
# Note: run this using a like like this:
#
# django-admin.py test --settings=envs.test_ike --pythonpath=. courseware
# django-admin.py test --settings=lms.envs.test_ike --pythonpath=. common/lib/xmodule
import unittest
import os
......@@ -28,12 +28,13 @@ class I4xSystem(object):
self.track_function = lambda x: None
self.render_function = lambda x: {} # Probably incorrect
self.exception404 = Exception
self.DEBUG = True
def __repr__(self):
return repr(self.__dict__)
def __str__(self):
return str(self.__dict__)
i4xs = I4xSystem
i4xs = I4xSystem()
class ModelsTest(unittest.TestCase):
def setUp(self):
......@@ -96,31 +97,31 @@ class MultiChoiceTest(unittest.TestCase):
multichoice_file = os.path.dirname(__file__)+"/test_files/multichoice.xml"
test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs)
correct_answers = {'1_2_1':'choice_foil3'}
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
false_answers = {'1_2_1':'choice_foil2'}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
def test_MC_bare_grades(self):
multichoice_file = os.path.dirname(__file__)+"/test_files/multi_bare.xml"
test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs)
correct_answers = {'1_2_1':'choice_2'}
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
false_answers = {'1_2_1':'choice_1'}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
def test_TF_grade(self):
truefalse_file = os.path.dirname(__file__)+"/test_files/truefalse.xml"
test_lcp = lcp.LoncapaProblem(open(truefalse_file), '1', system=i4xs)
correct_answers = {'1_2_1':['choice_foil2', 'choice_foil1']}
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
false_answers = {'1_2_1':['choice_foil1']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
false_answers = {'1_2_1':['choice_foil1', 'choice_foil3']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
false_answers = {'1_2_1':['choice_foil3']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
false_answers = {'1_2_1':['choice_foil1', 'choice_foil2', 'choice_foil3']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
class ImageResponseTest(unittest.TestCase):
def test_ir_grade(self):
......@@ -131,8 +132,8 @@ class ImageResponseTest(unittest.TestCase):
test_answers = {'1_2_1':'[500,20]',
'1_2_2':'[250,300]',
}
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_2'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect')
class SymbolicResponseTest(unittest.TestCase):
def test_sr_grade(self):
......@@ -220,8 +221,8 @@ class SymbolicResponseTest(unittest.TestCase):
</mstyle>
</math>''',
}
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(wrong_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
self.assertEquals(test_lcp.grade_answers(wrong_answers).get_correctness('1_2_1'), 'incorrect')
class OptionResponseTest(unittest.TestCase):
'''
......@@ -237,8 +238,37 @@ class OptionResponseTest(unittest.TestCase):
test_answers = {'1_2_1':'True',
'1_2_2':'True',
}
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_2'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect')
class FormulaResponseWithHintTest(unittest.TestCase):
'''
Test Formula response problem with a hint
This problem also uses calc.
'''
def test_or_grade(self):
problem_file = os.path.dirname(__file__)+"/test_files/formularesponse_with_hint.xml"
test_lcp = lcp.LoncapaProblem(open(problem_file), '1', system=i4xs)
correct_answers = {'1_2_1':'2.5*x-5.0'}
test_answers = {'1_2_1':'0.4*x-5.0'}
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
cmap = test_lcp.grade_answers(test_answers)
self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect')
self.assertTrue('You have inverted' in cmap.get_hint('1_2_1'))
class StringResponseWithHintTest(unittest.TestCase):
'''
Test String response problem with a hint
'''
def test_or_grade(self):
problem_file = os.path.dirname(__file__)+"/test_files/stringresponse_with_hint.xml"
test_lcp = lcp.LoncapaProblem(open(problem_file), '1', system=i4xs)
correct_answers = {'1_2_1':'Michigan'}
test_answers = {'1_2_1':'Minnesota'}
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
cmap = test_lcp.grade_answers(test_answers)
self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect')
self.assertTrue('St. Paul' in cmap.get_hint('1_2_1'))
#-----------------------------------------------------------------------------
# Grading tests
......
......@@ -169,7 +169,7 @@ def render_x_module(user, request, xml_module, module_object_preload, position=N
content = instance.get_html()
# special extra information about each problem, only for users who are staff
if user.is_staff:
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff:
module_id = xml_module.get('id')
histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0
......
......@@ -37,6 +37,7 @@ PERFSTATS = False
MITX_FEATURES = {
'SAMPLE' : False,
'USE_DJANGO_PIPELINE' : True,
'DISPLAY_HISTOGRAMS_TO_STAFF' : True,
}
# Used for A/B testing
......
......@@ -27,7 +27,8 @@ DEBUG = True
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
QUICKEDIT = True
MITX_FEATURES['USE_DJANGO_PIPELINE'] = False
# MITX_FEATURES['USE_DJANGO_PIPELINE'] = False
MITX_FEATURES['DISPLAY_HISTOGRAMS_TO_STAFF'] = False
COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
'title' : 'Circuits and Electronics',
......
......@@ -44,9 +44,9 @@ def check_problem_code(ans,the_lcp,correct_answers,false_answers):
fp = the_lcp.system.filestore.open('problems/%s.xml' % pfn)
test_lcp = lcp.LoncapaProblem(fp, '1', system=the_lcp.system)
if not (test_lcp.grade_answers(correct_answers)['1_2_1']=='correct'):
if not (test_lcp.grade_answers(correct_answers).get_correctness('1_2_1')=='correct'):
is_ok = False
if (test_lcp.grade_answers(false_answers)['1_2_1']=='correct'):
if (test_lcp.grade_answers(false_answers).get_correctness('1_2_1')=='correct'):
is_ok = False
except Exception,err:
is_ok = False
......
......@@ -45,14 +45,14 @@ class @Problem
$.each response, (key, value) =>
if $.isArray(value)
for choice in value
@$("label[for='input_#{key}_#{choice}']").attr
correct_answer: 'true'
@$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true'
else
@$("#answer_#{key}").text(value)
@$("#answer_#{key}, #solution_#{key}").html(value)
MathJax.Hub.Queue ["Typeset", MathJax.Hub]
@$('.show').val 'Hide Answer'
@element.addClass 'showed'
else
@$('[id^=answer_]').text ''
@$('[id^=answer_], [id^=solution_]').text ''
@$('[correct_answer]').attr correct_answer: null
@element.removeClass 'showed'
@$('.show').val 'Show Answer'
......
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