@@ -1,13 +1,19 @@
|
||||
#
|
||||
# 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 inputtypes
|
||||
|
||||
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse
|
||||
|
||||
import calc
|
||||
from correctmap import CorrectMap
|
||||
import eia
|
||||
import inputtypes
|
||||
from util import contextualize_text
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
# to be replaced with auto-registering
|
||||
import responsetypes
|
||||
|
||||
# dict of tagname, Response Class -- this should come from auto-registering
|
||||
response_tag_dict = dict([(x.response_tag,x) for x in responsetypes.__all__])
|
||||
|
||||
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
|
||||
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,32 +56,38 @@ 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
|
||||
self.seed = seed
|
||||
|
||||
if state:
|
||||
if 'seed' in 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
|
||||
@@ -230,12 +251,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:
|
||||
@@ -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():
|
||||
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, "")
|
||||
return tree
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
109
common/lib/capa/correctmap.py
Normal file
109
common/lib/capa/correctmap.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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,17 +32,61 @@ 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:
|
||||
* Value
|
||||
* ID
|
||||
* Status (answered, unanswered, unsubmitted)
|
||||
* Feedback (dictionary containing keys for hints, errors, or other
|
||||
feedback from previous attempt)
|
||||
'''
|
||||
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 :
|
||||
'''
|
||||
|
||||
self.xml = xml
|
||||
self.tag = xml.tag
|
||||
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.value = ''
|
||||
if 'value' in state:
|
||||
self.value = state['value']
|
||||
|
||||
self.msg = ''
|
||||
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']
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return c.xml_tags.keys()
|
||||
@@ -54,79 +98,6 @@ class SimpleInput():# XModule
|
||||
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 = {}
|
||||
## 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
|
||||
|
||||
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']
|
||||
|
||||
## 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']
|
||||
|
||||
|
||||
def register_render_function(fn, names=None, cls=SimpleInput):
|
||||
if names is None:
|
||||
SimpleInput.xml_tags[fn.__name__] = fn
|
||||
@@ -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 GenericResponse(object):
|
||||
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)
|
||||
|
||||
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.
|
||||
'''
|
||||
@@ -107,9 +305,25 @@ class MultipleChoiceResponse(GenericResponse):
|
||||
i+=1
|
||||
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]
|
||||
return correct_map
|
||||
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')
|
||||
|
||||
response_tag = 'symbolicresponse'
|
||||
|
||||
def setup_response(self):
|
||||
self.xml.set('cfn','symmath_check')
|
||||
code = "from symmath import *"
|
||||
exec code in context,context
|
||||
CustomResponse.__init__(self,xml,context,system)
|
||||
|
||||
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
|
||||
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]
|
||||
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
|
||||
|
||||
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,25 +811,25 @@ 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('<','<')
|
||||
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('<','<'))
|
||||
return cmap
|
||||
|
||||
ad = rxml.find('awarddetail').text
|
||||
admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses
|
||||
'WRONG_FORMAT': 'incorrect',
|
||||
'WRONG_FORMAT': 'incorrect',
|
||||
}
|
||||
self.context['correct'] = ['correct']
|
||||
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(' ',' ')
|
||||
# create CorrectMap
|
||||
for key in idset:
|
||||
idx = idset.index(key)
|
||||
msg = rxml.find('message').text.replace(' ',' ') 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>
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
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,17 +430,10 @@ 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)
|
||||
|
||||
event_info['new_state']=self.lcp.get_state()
|
||||
|
||||
45
common/lib/xmodule/test_files/formularesponse_with_hint.xml
Normal file
45
common/lib/xmodule/test_files/formularesponse_with_hint.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<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>
|
||||
|
||||
25
common/lib/xmodule/test_files/stringresponse_with_hint.xml
Normal file
25
common/lib/xmodule/test_files/stringresponse_with_hint.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<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>
|
||||
@@ -1,9 +1,9 @@
|
||||
#
|
||||
# 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'
|
||||
|
||||
Reference in New Issue
Block a user