Merge pull request #841 from MITx/victor/capa_cleanup
Victor/capa cleanup Looks good. Yes, the code exec part will need more thought, particularly with respect to security.
This commit is contained in:
@@ -48,7 +48,7 @@ general_whitespace = re.compile('[^\w]+')
|
||||
|
||||
|
||||
def check_variables(string, variables):
|
||||
''' Confirm the only variables in string are defined.
|
||||
'''Confirm the only variables in string are defined.
|
||||
|
||||
Pyparsing uses a left-to-right parser, which makes the more
|
||||
elegant approach pretty hopeless.
|
||||
@@ -56,7 +56,8 @@ def check_variables(string, variables):
|
||||
achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character
|
||||
undefined_variable = achar + Word(alphanums)
|
||||
undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself())
|
||||
varnames = varnames | undefined_variable'''
|
||||
varnames = varnames | undefined_variable
|
||||
'''
|
||||
possible_variables = re.split(general_whitespace, string) # List of all alnums in string
|
||||
bad_variables = list()
|
||||
for v in possible_variables:
|
||||
@@ -71,7 +72,8 @@ def check_variables(string, variables):
|
||||
|
||||
|
||||
def evaluator(variables, functions, string, cs=False):
|
||||
''' Evaluate an expression. Variables are passed as a dictionary
|
||||
'''
|
||||
Evaluate an expression. Variables are passed as a dictionary
|
||||
from string to value. Unary functions are passed as a dictionary
|
||||
from string to function. Variables must be floats.
|
||||
cs: Case sensitive
|
||||
@@ -108,6 +110,7 @@ def evaluator(variables, functions, string, cs=False):
|
||||
|
||||
if string.strip() == "":
|
||||
return float('nan')
|
||||
|
||||
ops = {"^": operator.pow,
|
||||
"*": operator.mul,
|
||||
"/": operator.truediv,
|
||||
@@ -169,14 +172,19 @@ def evaluator(variables, functions, string, cs=False):
|
||||
def func_parse_action(x):
|
||||
return [all_functions[x[0]](x[1])]
|
||||
|
||||
number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) # SI suffixes and percent
|
||||
# SI suffixes and percent
|
||||
number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch())
|
||||
(dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^")
|
||||
|
||||
number_part = Word(nums)
|
||||
inner_number = (number_part + Optional("." + number_part)) | ("." + number_part) # 0.33 or 7 or .34
|
||||
number = Optional(minus | plus) + inner_number + \
|
||||
Optional(CaselessLiteral("E") + Optional("-") + number_part) + \
|
||||
Optional(number_suffix) # 0.33k or -17
|
||||
|
||||
# 0.33 or 7 or .34
|
||||
inner_number = (number_part + Optional("." + number_part)) | ("." + number_part)
|
||||
|
||||
# 0.33k or -17
|
||||
number = (Optional(minus | plus) + inner_number
|
||||
+ Optional(CaselessLiteral("E") + Optional("-") + number_part)
|
||||
+ Optional(number_suffix))
|
||||
number = number.setParseAction(number_parse_action) # Convert to number
|
||||
|
||||
# Predefine recursive variables
|
||||
@@ -201,9 +209,11 @@ def evaluator(variables, functions, string, cs=False):
|
||||
varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x))
|
||||
else:
|
||||
varnames = NoMatch()
|
||||
|
||||
# Same thing for functions.
|
||||
if len(all_functions) > 0:
|
||||
funcnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_functions.keys()))
|
||||
funcnames = sreduce(lambda x, y: x | y,
|
||||
map(lambda x: CasedLiteral(x), all_functions.keys()))
|
||||
function = funcnames + lpar.suppress() + expr + rpar.suppress()
|
||||
function.setParseAction(func_parse_action)
|
||||
else:
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
#
|
||||
# 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.
|
||||
# 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).
|
||||
@@ -42,9 +43,23 @@ 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__])
|
||||
|
||||
entry_types = ['textline', 'schematic', 'textbox', 'imageinput', 'optioninput', 'choicegroup', 'radiogroup', 'checkboxgroup', 'filesubmission', 'javascriptinput']
|
||||
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
|
||||
response_properties = ["codeparam", "responseparam", "answer"] # these get captured as student responses
|
||||
# Different ways students can input code
|
||||
entry_types = ['textline',
|
||||
'schematic',
|
||||
'textbox',
|
||||
'imageinput',
|
||||
'optioninput',
|
||||
'choicegroup',
|
||||
'radiogroup',
|
||||
'checkboxgroup',
|
||||
'filesubmission',
|
||||
'javascriptinput',]
|
||||
|
||||
# extra things displayed after "show answers" is pressed
|
||||
solution_types = ['solution']
|
||||
|
||||
# these get captured as student responses
|
||||
response_properties = ["codeparam", "responseparam", "answer"]
|
||||
|
||||
# special problem tags which should be turned into innocuous HTML
|
||||
html_transforms = {'problem': {'tag': 'div'},
|
||||
@@ -83,7 +98,8 @@ class LoncapaProblem(object):
|
||||
- id (string): identifier for this problem; often a filename (no spaces)
|
||||
- state (dict): student state
|
||||
- seed (int): random number generator seed (int)
|
||||
- system (ModuleSystem): ModuleSystem instance which provides OS, rendering, and user context
|
||||
- system (ModuleSystem): ModuleSystem instance which provides OS,
|
||||
rendering, and user context
|
||||
|
||||
'''
|
||||
|
||||
@@ -107,19 +123,24 @@ class LoncapaProblem(object):
|
||||
if not self.seed:
|
||||
self.seed = struct.unpack('i', os.urandom(4))[0]
|
||||
|
||||
problem_text = re.sub("startouttext\s*/", "text", problem_text) # Convert startouttext and endouttext to proper <text></text>
|
||||
# Convert startouttext and endouttext to proper <text></text>
|
||||
problem_text = re.sub("startouttext\s*/", "text", problem_text)
|
||||
problem_text = re.sub("endouttext\s*/", "/text", problem_text)
|
||||
self.problem_text = problem_text
|
||||
|
||||
self.tree = etree.XML(problem_text) # parse problem XML file into an element tree
|
||||
self._process_includes() # handle any <include file="foo"> tags
|
||||
# parse problem XML file into an element tree
|
||||
self.tree = etree.XML(problem_text)
|
||||
|
||||
# handle any <include file="foo"> tags
|
||||
self._process_includes()
|
||||
|
||||
# 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
|
||||
# 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)
|
||||
|
||||
if not self.student_answers: # True when student_answers is an empty dict
|
||||
@@ -134,6 +155,9 @@ class LoncapaProblem(object):
|
||||
self.done = False
|
||||
|
||||
def set_initial_display(self):
|
||||
"""
|
||||
Set the student's answers to the responders' initial displays, if specified.
|
||||
"""
|
||||
initial_answers = dict()
|
||||
for responder in self.responders.values():
|
||||
if hasattr(responder, 'get_initial_display'):
|
||||
@@ -145,9 +169,11 @@ class LoncapaProblem(object):
|
||||
return u"LoncapaProblem ({0})".format(self.problem_id)
|
||||
|
||||
def get_state(self):
|
||||
''' Stored per-user session data neeeded to:
|
||||
'''
|
||||
Stored per-user session data neeeded to:
|
||||
1) Recreate the problem
|
||||
2) Populate any student answers. '''
|
||||
2) Populate any student answers.
|
||||
'''
|
||||
|
||||
return {'seed': self.seed,
|
||||
'student_answers': self.student_answers,
|
||||
@@ -156,7 +182,7 @@ class LoncapaProblem(object):
|
||||
|
||||
def get_max_score(self):
|
||||
'''
|
||||
Return maximum score for this problem.
|
||||
Return the maximum score for this problem.
|
||||
'''
|
||||
maxscore = 0
|
||||
for response, responder in self.responders.iteritems():
|
||||
@@ -164,11 +190,11 @@ class LoncapaProblem(object):
|
||||
return maxscore
|
||||
|
||||
def get_score(self):
|
||||
'''
|
||||
"""
|
||||
Compute score for this problem. The score is the number of points awarded.
|
||||
Returns a dictionary {'score': integer, from 0 to get_max_score(),
|
||||
'total': get_max_score()}.
|
||||
'''
|
||||
"""
|
||||
correct = 0
|
||||
for key in self.correct_map:
|
||||
try:
|
||||
@@ -204,22 +230,25 @@ class LoncapaProblem(object):
|
||||
def is_queued(self):
|
||||
'''
|
||||
Returns True if any part of the problem has been submitted to an external queue
|
||||
(e.g. for grading.)
|
||||
'''
|
||||
return any(self.correct_map.is_queued(answer_id) for answer_id in self.correct_map)
|
||||
|
||||
|
||||
def get_recentmost_queuetime(self):
|
||||
'''
|
||||
Returns a DateTime object that represents the timestamp of the most recent queueing request, or None if not queued
|
||||
Returns a DateTime object that represents the timestamp of the most recent
|
||||
queueing request, or None if not queued
|
||||
'''
|
||||
if not self.is_queued():
|
||||
return None
|
||||
|
||||
# Get a list of timestamps of all queueing requests, then convert it to a DateTime object
|
||||
queuetime_strs = [self.correct_map.get_queuetime_str(answer_id)
|
||||
for answer_id in self.correct_map
|
||||
for answer_id in self.correct_map
|
||||
if self.correct_map.is_queued(answer_id)]
|
||||
queuetimes = [datetime.strptime(qt_str, xqueue_interface.dateformat) for qt_str in queuetime_strs]
|
||||
queuetimes = [datetime.strptime(qt_str, xqueue_interface.dateformat)
|
||||
for qt_str in queuetime_strs]
|
||||
|
||||
return max(queuetimes)
|
||||
|
||||
@@ -235,14 +264,20 @@ class LoncapaProblem(object):
|
||||
Calls the Response for each question in this problem, to do the actual grading.
|
||||
'''
|
||||
|
||||
# if answers include File objects, convert them to filenames.
|
||||
self.student_answers = convert_files_to_filenames(answers)
|
||||
|
||||
oldcmap = self.correct_map # old CorrectMap
|
||||
newcmap = CorrectMap() # start new with empty CorrectMap
|
||||
# old CorrectMap
|
||||
oldcmap = self.correct_map
|
||||
|
||||
# start new with empty CorrectMap
|
||||
newcmap = CorrectMap()
|
||||
# log.debug('Responders: %s' % self.responders)
|
||||
for responder in self.responders.values(): # Call each responsetype instance to do actual grading
|
||||
if 'filesubmission' in responder.allowed_inputfields: # File objects are passed only if responsetype
|
||||
# explicitly allows for file submissions
|
||||
# Call each responsetype instance to do actual grading
|
||||
for responder in self.responders.values():
|
||||
# File objects are passed only if responsetype explicitly allows for file
|
||||
# submissions
|
||||
if 'filesubmission' in responder.allowed_inputfields:
|
||||
results = responder.evaluate_answers(answers, oldcmap)
|
||||
else:
|
||||
results = responder.evaluate_answers(convert_files_to_filenames(answers), oldcmap)
|
||||
@@ -252,28 +287,33 @@ class LoncapaProblem(object):
|
||||
return newcmap
|
||||
|
||||
def get_question_answers(self):
|
||||
"""Returns a dict of answer_ids to answer values. If we cannot 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)
|
||||
"""
|
||||
# dict of (id, correct_answer)
|
||||
answer_map = dict()
|
||||
for response in self.responders.keys():
|
||||
results = self.responder_answers[response]
|
||||
answer_map.update(results) # dict of (id,correct_answer)
|
||||
answer_map.update(results)
|
||||
|
||||
# include solutions from <solution>...</solution> stanzas
|
||||
for entry in self.tree.xpath("//" + "|//".join(solution_types)):
|
||||
answer = etree.tostring(entry)
|
||||
if answer: answer_map[entry.get('id')] = contextualize_text(answer, self.context)
|
||||
if answer:
|
||||
answer_map[entry.get('id')] = contextualize_text(answer, self.context)
|
||||
|
||||
log.debug('answer_map = %s' % answer_map)
|
||||
return answer_map
|
||||
|
||||
def get_answer_ids(self):
|
||||
"""Return the IDs of all the responses -- these are the keys used for
|
||||
"""
|
||||
Return the IDs of all the responses -- these are the keys used for
|
||||
the dicts returned by grade_answers and get_question_answers. (Though
|
||||
get_question_answers may only return a subset of these."""
|
||||
get_question_answers may only return a subset of these.
|
||||
"""
|
||||
answer_ids = []
|
||||
for response in self.responders.keys():
|
||||
results = self.responder_answers[response]
|
||||
@@ -298,7 +338,8 @@ class LoncapaProblem(object):
|
||||
file = inc.get('file')
|
||||
if file is not None:
|
||||
try:
|
||||
ifp = self.system.filestore.open(file) # open using ModuleSystem OSFS filestore
|
||||
# open using ModuleSystem OSFS filestore
|
||||
ifp = self.system.filestore.open(file)
|
||||
except Exception as err:
|
||||
log.warning('Error %s in problem xml include: %s' % (
|
||||
err, etree.tostring(inc, pretty_print=True)))
|
||||
@@ -311,7 +352,8 @@ class LoncapaProblem(object):
|
||||
else:
|
||||
continue
|
||||
try:
|
||||
incxml = etree.XML(ifp.read()) # read in and convert to XML
|
||||
# read in and convert to XML
|
||||
incxml = etree.XML(ifp.read())
|
||||
except Exception as err:
|
||||
log.warning('Error %s in problem xml include: %s' % (
|
||||
err, etree.tostring(inc, pretty_print=True)))
|
||||
@@ -322,6 +364,7 @@ class LoncapaProblem(object):
|
||||
raise
|
||||
else:
|
||||
continue
|
||||
|
||||
# insert new XML into tree in place of inlcude
|
||||
parent = inc.getparent()
|
||||
parent.insert(parent.index(inc), incxml)
|
||||
@@ -329,11 +372,13 @@ class LoncapaProblem(object):
|
||||
log.debug('Included %s into %s' % (file, self.problem_id))
|
||||
|
||||
def _extract_system_path(self, script):
|
||||
'''
|
||||
"""
|
||||
Extracts and normalizes additional paths for code execution.
|
||||
For now, there's a default path of data/course/code; this may be removed
|
||||
at some point.
|
||||
'''
|
||||
|
||||
script : ?? (TODO)
|
||||
"""
|
||||
|
||||
DEFAULT_PATH = ['code']
|
||||
|
||||
@@ -351,7 +396,6 @@ class LoncapaProblem(object):
|
||||
# path is an absolute path or a path relative to the data dir
|
||||
dir = os.path.join(self.system.filestore.root_path, dir)
|
||||
abs_dir = os.path.normpath(dir)
|
||||
#log.debug("appending to path: %s" % abs_dir)
|
||||
path.append(abs_dir)
|
||||
|
||||
return path
|
||||
@@ -362,13 +406,20 @@ class LoncapaProblem(object):
|
||||
context of this problem. Provides ability to randomize problems, and also set
|
||||
variables for problem answer checking.
|
||||
|
||||
Problem XML goes to Python execution context. Runs everything in script tags
|
||||
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
|
||||
# save global context in here also
|
||||
context = {'global_context': global_context}
|
||||
|
||||
# initialize context to have stuff in global_context
|
||||
context.update(global_context)
|
||||
|
||||
# put globals there also
|
||||
context['__builtins__'] = globals()['__builtins__']
|
||||
|
||||
# pass instance of LoncapaProblem in
|
||||
context['the_lcp'] = self
|
||||
context['script_code'] = ''
|
||||
|
||||
self._execute_scripts(tree.findall('.//script'), context)
|
||||
@@ -395,12 +446,14 @@ class LoncapaProblem(object):
|
||||
code = script.text
|
||||
XMLESC = {"'": "'", """: '"'}
|
||||
code = unescape(code, XMLESC)
|
||||
context['script_code'] += code # store code source in context
|
||||
# store code source in context
|
||||
context['script_code'] += code
|
||||
try:
|
||||
exec code in context, context # use "context" for global context; thus defs in code are global within code
|
||||
# use "context" for global context; thus defs in code are global within code
|
||||
exec code in context, context
|
||||
except Exception as err:
|
||||
log.exception("Error while execing script code: " + code)
|
||||
msg = "Error while executing script code: %s" % str(err).replace('<','<')
|
||||
msg = "Error while executing script code: %s" % str(err).replace('<','<')
|
||||
raise responsetypes.LoncapaProblemError(msg)
|
||||
finally:
|
||||
sys.path = original_path
|
||||
@@ -415,7 +468,8 @@ class LoncapaProblem(object):
|
||||
|
||||
Used by get_html.
|
||||
'''
|
||||
if problemtree.tag == 'script' and problemtree.get('type') and 'javascript' in problemtree.get('type'):
|
||||
if (problemtree.tag == 'script' and problemtree.get('type')
|
||||
and 'javascript' in problemtree.get('type')):
|
||||
# leave javascript intact.
|
||||
return problemtree
|
||||
|
||||
@@ -453,21 +507,26 @@ class LoncapaProblem(object):
|
||||
}
|
||||
},
|
||||
use='capa_input')
|
||||
return render_object.get_html() # function(problemtree, value, status, msg) # render the special response (textline, schematic,...)
|
||||
# function(problemtree, value, status, msg)
|
||||
# render the special response (textline, schematic,...)
|
||||
return render_object.get_html()
|
||||
|
||||
if problemtree in self.responders: # let each Response render itself
|
||||
# let each Response render itself
|
||||
if problemtree in self.responders:
|
||||
return self.responders[problemtree].render_html(self._extract_html)
|
||||
|
||||
tree = etree.Element(problemtree.tag)
|
||||
for item in problemtree:
|
||||
item_xhtml = self._extract_html(item) # nothing special: recurse
|
||||
# render child recursively
|
||||
item_xhtml = self._extract_html(item)
|
||||
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
|
||||
# copy attributes over if not innocufying
|
||||
for (key, value) in problemtree.items():
|
||||
tree.set(key, value)
|
||||
|
||||
tree.text = problemtree.text
|
||||
@@ -490,31 +549,41 @@ class LoncapaProblem(object):
|
||||
self.responders = {}
|
||||
for response in tree.xpath('//' + "|//".join(response_tag_dict)):
|
||||
response_id_str = self.problem_id + "_" + str(response_id)
|
||||
response.set('id', response_id_str) # create and save ID for this response
|
||||
# create and save ID for this response
|
||||
response.set('id', response_id_str)
|
||||
response_id += 1
|
||||
|
||||
answer_id = 1
|
||||
inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]),
|
||||
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
|
||||
|
||||
# assign one answer_id for each entry_type or solution_type
|
||||
for entry in inputfields:
|
||||
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
|
||||
# instantiate capa Response
|
||||
responder = response_tag_dict[response.tag](response, inputfields,
|
||||
self.context, self.system)
|
||||
# save in list in self
|
||||
self.responders[response] = responder
|
||||
|
||||
# get responder answers (do this only once, since there may be a performance cost, eg with externalresponse)
|
||||
# get responder answers (do this only once, since there may be a performance cost,
|
||||
# eg with externalresponse)
|
||||
self.responder_answers = {}
|
||||
for response in self.responders.keys():
|
||||
try:
|
||||
self.responder_answers[response] = self.responders[response].get_answers()
|
||||
except:
|
||||
log.debug('responder %s failed to properly return get_answers()' % self.responders[response]) # FIXME
|
||||
log.debug('responder %s failed to properly return get_answers()',
|
||||
self.responders[response]) # FIXME
|
||||
raise
|
||||
|
||||
# <solution>...</solution> may not be associated with any specific response; give IDs for those separately
|
||||
# <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'):
|
||||
|
||||
@@ -5,23 +5,26 @@
|
||||
|
||||
|
||||
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)
|
||||
- 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
|
||||
- queuestate : Dict {key:'', time:''} where key is a secret string, and time is a string dump
|
||||
of a DateTime object in the format '%Y%m%d%H%M%S'. Is None when not queued
|
||||
|
||||
Behaves as a dict.
|
||||
'''
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cmap = dict() # start with empty dict
|
||||
# start with empty dict
|
||||
self.cmap = dict()
|
||||
self.items = self.cmap.items
|
||||
self.keys = self.cmap.keys
|
||||
self.set(*args, **kwargs)
|
||||
@@ -33,7 +36,15 @@ class CorrectMap(object):
|
||||
return self.cmap.__iter__()
|
||||
|
||||
# See the documentation for 'set_dict' for the use of kwargs
|
||||
def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None, queuestate=None, **kwargs):
|
||||
def set(self,
|
||||
answer_id=None,
|
||||
correctness=None,
|
||||
npoints=None,
|
||||
msg='',
|
||||
hint='',
|
||||
hintmode=None,
|
||||
queuestate=None, **kwargs):
|
||||
|
||||
if answer_id is not None:
|
||||
self.cmap[answer_id] = {'correctness': correctness,
|
||||
'npoints': npoints,
|
||||
@@ -56,12 +67,13 @@ class CorrectMap(object):
|
||||
'''
|
||||
Set internal dict of CorrectMap to provided correct_map dict
|
||||
|
||||
correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This means that
|
||||
when the definition of CorrectMap (e.g. its properties) are altered, existing correct_map dict
|
||||
not coincide with the newest CorrectMap format as defined by self.set.
|
||||
correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This
|
||||
means that when the definition of CorrectMap (e.g. its properties) are altered,
|
||||
an existing correct_map dict not coincide with the newest CorrectMap format as
|
||||
defined by self.set.
|
||||
|
||||
For graceful migration, feed the contents of each correct map to self.set, rather than
|
||||
making a direct copy of the given correct_map dict. This way, the common keys between
|
||||
making a direct copy of the given correct_map dict. This way, the common keys between
|
||||
the incoming correct_map dict and the new CorrectMap instance will be written, while
|
||||
mismatched keys will be gracefully ignored.
|
||||
|
||||
@@ -69,14 +81,20 @@ class CorrectMap(object):
|
||||
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
|
||||
# empty current dict
|
||||
self.__init__()
|
||||
|
||||
# create new dict entries
|
||||
for k in correct_map:
|
||||
self.set(k, correct_map[k])
|
||||
else:
|
||||
self.__init__()
|
||||
for k in correct_map: self.set(k, **correct_map[k])
|
||||
for k in correct_map:
|
||||
self.set(k, **correct_map[k])
|
||||
|
||||
def is_correct(self, answer_id):
|
||||
if answer_id in self.cmap: return self.cmap[answer_id]['correctness'] == 'correct'
|
||||
if answer_id in self.cmap:
|
||||
return self.cmap[answer_id]['correctness'] == 'correct'
|
||||
return None
|
||||
|
||||
def is_queued(self, answer_id):
|
||||
@@ -94,14 +112,18 @@ class CorrectMap(object):
|
||||
return npoints
|
||||
elif self.is_correct(answer_id):
|
||||
return 1
|
||||
return 0 # if not correct and no points have been assigned, return 0
|
||||
# if not correct and no points have been assigned, return 0
|
||||
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}
|
||||
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)
|
||||
if answer_id in self.cmap:
|
||||
return self.cmap[answer_id].get(property, default)
|
||||
return default
|
||||
|
||||
def get_correctness(self, answer_id):
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
""" Standard resistor codes.
|
||||
"""
|
||||
Standard resistor codes.
|
||||
http://en.wikipedia.org/wiki/Electronic_color_code
|
||||
"""
|
||||
E6 = [10, 15, 22, 33, 47, 68]
|
||||
|
||||
E12 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82]
|
||||
|
||||
E24 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82, 11, 13, 16, 20, 24, 30, 36, 43, 51, 62, 75, 91]
|
||||
|
||||
E48 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953]
|
||||
|
||||
E96 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976]
|
||||
|
||||
E192 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 101, 123, 149, 180, 218, 264, 320, 388, 470, 569, 690, 835, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 104, 126, 152, 184, 223, 271, 328, 397, 481, 583, 706, 856, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 106, 129, 156, 189, 229, 277, 336, 407, 493, 597, 723, 876, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 109, 132, 160, 193, 234, 284, 344, 417, 505, 612, 741, 898, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 111, 135, 164, 198, 240, 291, 352, 427, 517, 626, 759, 920, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 114, 138, 167, 203, 246, 298, 361, 437, 530, 642, 777, 942, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 117, 142, 172, 208, 252, 305, 370, 448, 542, 657, 796, 965, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976, 120, 145, 176, 213, 258, 312, 379, 459, 556, 673, 816, 988]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# File: courseware/capa/inputtypes.py
|
||||
#
|
||||
|
||||
'''
|
||||
"""
|
||||
Module containing the problem elements which render into input objects
|
||||
|
||||
- textline
|
||||
@@ -16,15 +16,16 @@ Module containing the problem elements which render into input objects
|
||||
- optioninput (for option list)
|
||||
- filesubmission (upload a file)
|
||||
|
||||
These are matched by *.html files templates/*.html which are mako templates with the actual html.
|
||||
These are matched by *.html files templates/*.html which are mako templates with the
|
||||
actual html.
|
||||
|
||||
Each input type takes the xml tree as 'element', the previous answer as 'value', and the graded status as 'status'
|
||||
Each input type takes the xml tree as 'element', the previous answer as 'value', and the
|
||||
graded status as'status'
|
||||
"""
|
||||
|
||||
'''
|
||||
|
||||
# TODO: rename "state" to "status" for all below
|
||||
# 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.
|
||||
# TODO: rename "state" to "status" for all below. 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.
|
||||
|
||||
import logging
|
||||
import re
|
||||
@@ -47,7 +48,8 @@ class SimpleInput():# XModule
|
||||
Type for simple inputs -- plain HTML with a form element
|
||||
'''
|
||||
|
||||
xml_tags = {} # # Maps tags to functions
|
||||
# Maps tags to functions
|
||||
xml_tags = {}
|
||||
|
||||
def __init__(self, system, xml, item_id=None, track_url=None, state=None, use='capa_input'):
|
||||
'''
|
||||
@@ -69,19 +71,23 @@ class SimpleInput():# XModule
|
||||
self.xml = xml
|
||||
self.tag = xml.tag
|
||||
self.system = system
|
||||
if not state: state = {}
|
||||
if not state:
|
||||
state = {}
|
||||
|
||||
## ID should only come from one place.
|
||||
## NOTE: 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']
|
||||
if item_id:
|
||||
self.id = item_id
|
||||
|
||||
self.value = ''
|
||||
if 'value' in state:
|
||||
self.value = state['value']
|
||||
if xml.get('id'):
|
||||
self.id = xml.get('id')
|
||||
|
||||
if 'id' in state:
|
||||
self.id = state['id']
|
||||
|
||||
self.value = state.get('value', '')
|
||||
|
||||
self.msg = ''
|
||||
feedback = state.get('feedback')
|
||||
@@ -92,6 +98,7 @@ class SimpleInput():# XModule
|
||||
|
||||
# put hint above msg if to be displayed
|
||||
if self.hintmode == 'always':
|
||||
# TODO: is the '.' in <br/.> below a bug?
|
||||
self.msg = self.hint + ('<br/.>' if self.msg else '') + self.msg
|
||||
|
||||
self.status = 'unanswered'
|
||||
@@ -107,7 +114,8 @@ class SimpleInput():# XModule
|
||||
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)
|
||||
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):
|
||||
@@ -125,24 +133,26 @@ def register_render_function(fn, names=None, cls=SimpleInput):
|
||||
|
||||
@register_render_function
|
||||
def optioninput(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
"""
|
||||
Select option input type.
|
||||
|
||||
Example:
|
||||
|
||||
<optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
|
||||
'''
|
||||
"""
|
||||
eid = element.get('id')
|
||||
options = element.get('options')
|
||||
if not options:
|
||||
raise Exception("[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element))
|
||||
raise Exception(
|
||||
"[courseware.capa.inputtypes.optioninput] Missing options specification in "
|
||||
+ etree.tostring(element))
|
||||
oset = shlex.shlex(options[1:-1])
|
||||
oset.quotes = "'"
|
||||
oset.whitespace = ","
|
||||
oset = [x[1:-1] for x in list(oset)]
|
||||
|
||||
# osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ]) # make dict with IDs
|
||||
osetdict = [(oset[x], oset[x]) for x in range(len(oset))] # make ordered list with (key,value) same
|
||||
# make ordered list with (key,value) same
|
||||
osetdict = [(oset[x], oset[x]) for x in range(len(oset))]
|
||||
# TODO: allow ordering to be randomized
|
||||
|
||||
context = {'id': eid,
|
||||
@@ -166,26 +176,35 @@ def choicegroup(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
Radio button inputs: multiple choice or true/false
|
||||
|
||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute,
|
||||
ie random, top, bottom.
|
||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use
|
||||
"location" attribute, ie random, top, bottom.
|
||||
'''
|
||||
eid = element.get('id')
|
||||
if element.get('type') == "MultipleChoice":
|
||||
type = "radio"
|
||||
element_type = "radio"
|
||||
elif element.get('type') == "TrueFalse":
|
||||
type = "checkbox"
|
||||
element_type = "checkbox"
|
||||
else:
|
||||
type = "radio"
|
||||
element_type = "radio"
|
||||
choices = []
|
||||
for choice in element:
|
||||
if not choice.tag == 'choice':
|
||||
raise Exception("[courseware.capa.inputtypes.choicegroup] Error only <choice> tags should be immediate children of a <choicegroup>, found %s instead" % choice.tag)
|
||||
raise Exception("[courseware.capa.inputtypes.choicegroup] "
|
||||
"Error: only <choice> tags should be immediate children "
|
||||
"of a <choicegroup>, found %s instead" % choice.tag)
|
||||
ctext = ""
|
||||
ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it?
|
||||
# TODO: what if choice[0] has math tags in it?
|
||||
ctext += ''.join([etree.tostring(x) for x in choice])
|
||||
if choice.text is not None:
|
||||
ctext += choice.text # TODO: fix order?
|
||||
# TODO: fix order?
|
||||
ctext += choice.text
|
||||
choices.append((choice.get("name"), ctext))
|
||||
context = {'id': eid, 'value': value, 'state': status, 'input_type': type, 'choices': choices, 'name_array_suffix': ''}
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'input_type': element_type,
|
||||
'choices': choices,
|
||||
'name_array_suffix': ''}
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
@@ -196,8 +215,8 @@ def extract_choices(element):
|
||||
Extracts choices for a few input types, such as radiogroup and
|
||||
checkboxgroup.
|
||||
|
||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute,
|
||||
ie random, top, bottom.
|
||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use
|
||||
"location" attribute, ie random, top, bottom.
|
||||
'''
|
||||
|
||||
choices = []
|
||||
@@ -226,7 +245,12 @@ def radiogroup(element, value, status, render_template, msg=''):
|
||||
|
||||
choices = extract_choices(element)
|
||||
|
||||
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'radio', 'choices': choices, 'name_array_suffix': '[]'}
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'input_type': 'radio',
|
||||
'choices': choices,
|
||||
'name_array_suffix': '[]'}
|
||||
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
@@ -244,7 +268,12 @@ def checkboxgroup(element, value, status, render_template, msg=''):
|
||||
|
||||
choices = extract_choices(element)
|
||||
|
||||
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'checkbox', 'choices': choices, 'name_array_suffix': '[]'}
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'input_type': 'checkbox',
|
||||
'choices': choices,
|
||||
'name_array_suffix': '[]'}
|
||||
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
@@ -260,49 +289,67 @@ def javascriptinput(element, value, status, render_template, msg='null'):
|
||||
problem_state = element.get('problem_state')
|
||||
display_class = element.get('display_class')
|
||||
display_file = element.get('display_file')
|
||||
|
||||
|
||||
# Need to provide a value that JSON can parse if there is no
|
||||
# student-supplied value yet.
|
||||
if value == "":
|
||||
value = 'null'
|
||||
|
||||
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(value, escapedict)
|
||||
msg = saxutils.escape(msg, escapedict)
|
||||
context = {'id': eid, 'params': params, 'display_file': display_file,
|
||||
'display_class': display_class, 'problem_state': problem_state,
|
||||
'value': value, 'evaluation': msg,
|
||||
context = {'id': eid,
|
||||
'params': params,
|
||||
'display_file': display_file,
|
||||
'display_class': display_class,
|
||||
'problem_state': problem_state,
|
||||
'value': value,
|
||||
'evaluation': msg,
|
||||
}
|
||||
html = render_template("javascriptinput.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
|
||||
|
||||
@register_render_function
|
||||
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, status, render_template, msg)
|
||||
# TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x
|
||||
if element.get('math') or element.get('dojs'):
|
||||
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')
|
||||
hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
# if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
hidden = element.get('hidden', '')
|
||||
|
||||
# Escape answers with quotes, so they don't crash the system!
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(value, escapedict) # otherwise, answers with quotes in them crashes the system!
|
||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, 'hidden': hidden,
|
||||
value = saxutils.escape(value, escapedict)
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'count': count,
|
||||
'size': size,
|
||||
'msg': msg,
|
||||
'hidden': hidden,
|
||||
'inline': element.get('inline',''),
|
||||
}
|
||||
|
||||
html = render_template("textinput.html", context)
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
if True: # TODO needs to be self.system.DEBUG - but can't access system
|
||||
# TODO: needs to be self.system.DEBUG - but can't access system
|
||||
if True:
|
||||
log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html)
|
||||
raise
|
||||
return xhtml
|
||||
@@ -313,7 +360,8 @@ def textline(element, value, status, render_template, msg=""):
|
||||
@register_render_function
|
||||
def textline_dynamath(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
Text line input with dynamic math display (equation rendered on client in real time during input).
|
||||
Text line input with dynamic math display (equation rendered on client in real time
|
||||
during input).
|
||||
'''
|
||||
# TODO: Make a wrapper for <formulainput>
|
||||
# TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
|
||||
@@ -325,7 +373,8 @@ def textline_dynamath(element, value, status, render_template, msg=''):
|
||||
eid = element.get('id')
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
# if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
hidden = element.get('hidden', '')
|
||||
|
||||
# Preprocessor to insert between raw input and Mathjax
|
||||
preprocessor = {'class_name': element.get('preprocessorClassName',''),
|
||||
@@ -337,10 +386,14 @@ def textline_dynamath(element, value, status, render_template, msg=''):
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(value, escapedict)
|
||||
|
||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size,
|
||||
'msg': msg, 'hidden': hidden,
|
||||
'preprocessor': preprocessor,
|
||||
}
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'count': count,
|
||||
'size': size,
|
||||
'msg': msg,
|
||||
'hidden': hidden,
|
||||
'preprocessor': preprocessor,}
|
||||
html = render_template("textinput_dynamath.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
@@ -360,15 +413,19 @@ def filesubmission(element, value, status, render_template, msg=''):
|
||||
|
||||
# Check if problem has been queued
|
||||
queue_len = 0
|
||||
if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue
|
||||
# Flag indicating that the problem has been queued, 'msg' is length of queue
|
||||
if status == 'incomplete':
|
||||
status = 'queued'
|
||||
queue_len = msg
|
||||
msg = 'Submitted to grader.'
|
||||
|
||||
context = { 'id': eid, 'state': status, 'msg': msg, 'value': value,
|
||||
'queue_len': queue_len, 'allowed_files': allowed_files,
|
||||
'required_files': required_files
|
||||
}
|
||||
context = { 'id': eid,
|
||||
'state': status,
|
||||
'msg': msg,
|
||||
'value': value,
|
||||
'queue_len': queue_len,
|
||||
'allowed_files': allowed_files,
|
||||
'required_files': required_files,}
|
||||
html = render_template("filesubmission.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
@@ -387,13 +444,17 @@ def textbox(element, value, status, render_template, msg=''):
|
||||
size = element.get('size')
|
||||
rows = element.get('rows') or '30'
|
||||
cols = element.get('cols') or '80'
|
||||
hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
# if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
hidden = element.get('hidden', '')
|
||||
|
||||
if not value: value = element.text # if no student input yet, then use the default input given by the problem
|
||||
# if no student input yet, then use the default input given by the problem
|
||||
if not value:
|
||||
value = element.text
|
||||
|
||||
# Check if problem has been queued
|
||||
queue_len = 0
|
||||
if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue
|
||||
# Flag indicating that the problem has been queued, 'msg' is length of queue
|
||||
if status == 'incomplete':
|
||||
status = 'queued'
|
||||
queue_len = msg
|
||||
msg = 'Submitted to grader.'
|
||||
@@ -404,10 +465,18 @@ def textbox(element, value, status, render_template, msg=''):
|
||||
tabsize = element.get('tabsize','4')
|
||||
tabsize = int(tabsize)
|
||||
|
||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg,
|
||||
'mode': mode, 'linenumbers': linenumbers,
|
||||
'rows': rows, 'cols': cols,
|
||||
'hidden': hidden, 'tabsize': tabsize,
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'count': count,
|
||||
'size': size,
|
||||
'msg': msg,
|
||||
'mode': mode,
|
||||
'linenumbers': linenumbers,
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
'hidden': hidden,
|
||||
'tabsize': tabsize,
|
||||
'queue_len': queue_len,
|
||||
}
|
||||
html = render_template("textbox.html", context)
|
||||
@@ -475,7 +544,8 @@ def math(element, value, status, render_template, msg=''):
|
||||
# mathstr = mathstr.replace('\\displaystyle','')
|
||||
#else:
|
||||
# isinline = True
|
||||
# html = render_template("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
|
||||
# html = render_template("mathstring.html", {'mathstr':mathstr,
|
||||
# 'isinline':isinline,'tail':element.tail})
|
||||
|
||||
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr, saxutils.escape(element.tail))
|
||||
try:
|
||||
@@ -483,13 +553,14 @@ def math(element, value, status, render_template, msg=''):
|
||||
except Exception as err:
|
||||
if False: # TODO needs to be self.system.DEBUG - but can't access system
|
||||
msg = '<html><div class="inline-error"><p>Error %s</p>' % str(err).replace('<', '<')
|
||||
msg += '<p>Failed to construct math expression from <pre>%s</pre></p>' % html.replace('<', '<')
|
||||
msg += ('<p>Failed to construct math expression from <pre>%s</pre></p>' %
|
||||
html.replace('<', '<'))
|
||||
msg += "</div></html>"
|
||||
log.error(msg)
|
||||
return etree.XML(msg)
|
||||
else:
|
||||
raise
|
||||
# xhtml.tail = element.tail # don't forget to include the tail!
|
||||
# xhtml.tail = element.tail # don't forget to include the tail!
|
||||
return xhtml
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
@@ -520,11 +591,13 @@ def solution(element, value, status, render_template, msg=''):
|
||||
@register_render_function
|
||||
def imageinput(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
Clickable image as an input field. Element should specify the image source, height, and width, eg
|
||||
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="388" height="560" />
|
||||
Clickable image as an input field. Element should specify the image source, height,
|
||||
and width, e.g.
|
||||
|
||||
TODO: showanswer for imageimput does not work yet - need javascript to put rectangle over acceptable area of image.
|
||||
<imageinput src="/static/Figures/Skier-conservation-of-energy.jpg" width="388" height="560" />
|
||||
|
||||
TODO: showanswer for imageimput does not work yet - need javascript to put rectangle
|
||||
over acceptable area of image.
|
||||
'''
|
||||
eid = element.get('id')
|
||||
src = element.get('src')
|
||||
|
||||
@@ -63,7 +63,7 @@ class StudentInputError(Exception):
|
||||
|
||||
|
||||
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:
|
||||
@@ -77,19 +77,29 @@ class LoncapaResponse(object):
|
||||
|
||||
In addition, these methods are optional:
|
||||
|
||||
- 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)
|
||||
- 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
|
||||
- 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
|
||||
@@ -121,26 +131,32 @@ class LoncapaResponse(object):
|
||||
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 = "%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 = "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
|
||||
# ordered list of answer_id values for this response
|
||||
self.answer_ids = [x.get('id') for x in self.inputfields]
|
||||
if self.max_inputfields == 1:
|
||||
self.answer_id = self.answer_ids[0] # for convenience
|
||||
# for convenience
|
||||
self.answer_id = self.answer_ids[0]
|
||||
|
||||
self.maxpoints = dict()
|
||||
for inputfield in self.inputfields:
|
||||
maxpoints = inputfield.get('points','1') # By default, each answerfield is worth 1 point
|
||||
# By default, each answerfield is worth 1 point
|
||||
maxpoints = inputfield.get('points', '1')
|
||||
self.maxpoints.update({inputfield.get('id'): int(maxpoints)})
|
||||
|
||||
self.default_answer_map = {} # dict for default answer map (provided in input elements)
|
||||
# dict for default answer map (provided in input elements)
|
||||
self.default_answer_map = {}
|
||||
for entry in self.inputfields:
|
||||
answer = entry.get('correct_answer')
|
||||
if answer:
|
||||
@@ -163,10 +179,13 @@ class LoncapaResponse(object):
|
||||
|
||||
- renderer : procedure which produces HTML given an ElementTree
|
||||
'''
|
||||
tree = etree.Element('span') # render ourself as a <span> + our content
|
||||
# render ourself as a <span> + our content
|
||||
tree = etree.Element('span')
|
||||
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)
|
||||
# call provided procedure to do the rendering
|
||||
item_xhtml = renderer(item)
|
||||
if item_xhtml is not None:
|
||||
tree.append(item_xhtml)
|
||||
tree.tail = self.xml.tail
|
||||
return tree
|
||||
|
||||
@@ -192,21 +211,21 @@ class LoncapaResponse(object):
|
||||
Modifies new_cmap, by adding hints to answer_id entries as appropriate.
|
||||
'''
|
||||
hintgroup = self.xml.find('hintgroup')
|
||||
if hintgroup is None: return
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -237,14 +256,20 @@ class LoncapaResponse(object):
|
||||
# </hintgroup>
|
||||
# </formularesponse>
|
||||
|
||||
if self.hint_tag is not None and hintgroup.find(self.hint_tag) is not None and hasattr(self, 'check_hint_condition'):
|
||||
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)
|
||||
|
||||
# can be 'on_request' or 'always' (default)
|
||||
hintmode = hintgroup.get('mode', 'always')
|
||||
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
|
||||
# make the hint appear after the last answer box in this response
|
||||
aid = self.answer_ids[-1]
|
||||
new_cmap.set_hint_and_mode(aid, hint_text, hintmode)
|
||||
log.debug('after hint: new_cmap = %s' % new_cmap)
|
||||
|
||||
@@ -255,10 +280,10 @@ class LoncapaResponse(object):
|
||||
(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
|
||||
|
||||
- old_cmap : previous CorrectMap (may be empty); useful for analyzing or
|
||||
recording history of responses
|
||||
'''
|
||||
pass
|
||||
|
||||
@@ -273,10 +298,13 @@ class LoncapaResponse(object):
|
||||
'''
|
||||
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
|
||||
- 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.
|
||||
Returns a list of names of hint conditions which were satisfied. Those are used
|
||||
to determine which hints are displayed.
|
||||
'''
|
||||
pass
|
||||
|
||||
@@ -290,10 +318,10 @@ class LoncapaResponse(object):
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class JavascriptResponse(LoncapaResponse):
|
||||
'''
|
||||
"""
|
||||
This response type is used when the student's answer is graded via
|
||||
Javascript using Node.js.
|
||||
'''
|
||||
"""
|
||||
|
||||
response_tag = 'javascriptresponse'
|
||||
max_inputfields = 1
|
||||
@@ -312,11 +340,11 @@ class JavascriptResponse(LoncapaResponse):
|
||||
self.problem_state = self.generate_problem_state()
|
||||
else:
|
||||
self.problem_state = None
|
||||
|
||||
|
||||
self.solution = None
|
||||
|
||||
self.prepare_inputfield()
|
||||
|
||||
|
||||
def compile_display_javascript(self):
|
||||
|
||||
# TODO FIXME
|
||||
@@ -355,10 +383,10 @@ class JavascriptResponse(LoncapaResponse):
|
||||
self.generator_xml = self.xml.xpath('//*[@id=$id]//generator',
|
||||
id=self.xml.get('id'))[0]
|
||||
|
||||
self.grader_xml = self.xml.xpath('//*[@id=$id]//grader',
|
||||
self.grader_xml = self.xml.xpath('//*[@id=$id]//grader',
|
||||
id=self.xml.get('id'))[0]
|
||||
|
||||
self.display_xml = self.xml.xpath('//*[@id=$id]//display',
|
||||
self.display_xml = self.xml.xpath('//*[@id=$id]//display',
|
||||
id=self.xml.get('id'))[0]
|
||||
|
||||
self.xml.remove(self.generator_xml)
|
||||
@@ -385,7 +413,7 @@ class JavascriptResponse(LoncapaResponse):
|
||||
self.display_dependencies = []
|
||||
|
||||
self.display_class = self.display_xml.get("class")
|
||||
|
||||
|
||||
def get_node_env(self):
|
||||
|
||||
js_dir = os.path.join(self.system.filestore.root_path, 'js')
|
||||
@@ -393,7 +421,7 @@ class JavascriptResponse(LoncapaResponse):
|
||||
node_path = self.system.node_path + ":" + os.path.normpath(js_dir)
|
||||
tmp_env["NODE_PATH"] = node_path
|
||||
return tmp_env
|
||||
|
||||
|
||||
def call_node(self, args):
|
||||
|
||||
subprocess_args = ["node"]
|
||||
@@ -406,7 +434,7 @@ class JavascriptResponse(LoncapaResponse):
|
||||
|
||||
generator_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_generator.js'
|
||||
output = self.call_node([generator_file,
|
||||
self.generator,
|
||||
self.generator,
|
||||
json.dumps(self.generator_dependencies),
|
||||
json.dumps(str(self.context['the_lcp'].seed)),
|
||||
json.dumps(self.params)]).strip()
|
||||
@@ -416,18 +444,18 @@ class JavascriptResponse(LoncapaResponse):
|
||||
def extract_params(self):
|
||||
|
||||
params = {}
|
||||
|
||||
for param in self.xml.xpath('//*[@id=$id]//responseparam',
|
||||
|
||||
for param in self.xml.xpath('//*[@id=$id]//responseparam',
|
||||
id=self.xml.get('id')):
|
||||
|
||||
raw_param = param.get("value")
|
||||
params[param.get("name")] = json.loads(contextualize_text(raw_param, self.context))
|
||||
|
||||
|
||||
return params
|
||||
|
||||
def prepare_inputfield(self):
|
||||
|
||||
for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput',
|
||||
for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput',
|
||||
id=self.xml.get('id')):
|
||||
|
||||
escapedict = {'"': '"'}
|
||||
@@ -454,36 +482,36 @@ class JavascriptResponse(LoncapaResponse):
|
||||
else:
|
||||
points = 0
|
||||
return CorrectMap(self.answer_id, correctness, npoints=points, msg=evaluation)
|
||||
|
||||
|
||||
def run_grader(self, submission):
|
||||
if submission is None or submission == '':
|
||||
submission = json.dumps(None)
|
||||
|
||||
grader_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_grader.js'
|
||||
outputs = self.call_node([grader_file,
|
||||
self.grader,
|
||||
outputs = self.call_node([grader_file,
|
||||
self.grader,
|
||||
json.dumps(self.grader_dependencies),
|
||||
submission,
|
||||
json.dumps(self.problem_state),
|
||||
submission,
|
||||
json.dumps(self.problem_state),
|
||||
json.dumps(self.params)]).split('\n')
|
||||
|
||||
all_correct = json.loads(outputs[0].strip())
|
||||
evaluation = outputs[1].strip()
|
||||
solution = outputs[2].strip()
|
||||
return (all_correct, evaluation, solution)
|
||||
|
||||
|
||||
def get_answers(self):
|
||||
if self.solution is None:
|
||||
(_, _, self.solution) = self.run_grader(None)
|
||||
|
||||
return {self.answer_id: self.solution}
|
||||
|
||||
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class ChoiceResponse(LoncapaResponse):
|
||||
'''
|
||||
"""
|
||||
This response type is used when the student chooses from a discrete set of
|
||||
choices. Currently, to be marked correct, all "correct" choices must be
|
||||
supplied by the student, and no extraneous choices may be included.
|
||||
@@ -528,7 +556,7 @@ class ChoiceResponse(LoncapaResponse):
|
||||
choices must be given names. This behavior seems like a leaky abstraction,
|
||||
and it'd be nice to change this at some point.
|
||||
|
||||
'''
|
||||
"""
|
||||
|
||||
response_tag = 'choiceresponse'
|
||||
max_inputfields = 1
|
||||
@@ -594,7 +622,8 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
allowed_inputfields = ['choicegroup']
|
||||
|
||||
def setup_response(self):
|
||||
self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes
|
||||
# call secondary setup for MultipleChoice questions, to set name attributes
|
||||
self.mc_setup_response()
|
||||
|
||||
# define correct choices (after calling secondary setup)
|
||||
xml = self.xml
|
||||
@@ -609,7 +638,8 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
for response in self.xml.xpath("choicegroup"):
|
||||
rtype = response.get('type')
|
||||
if rtype not in ["MultipleChoice"]:
|
||||
response.set("type", "MultipleChoice") # force choicegroup to be MultipleChoice if not valid
|
||||
# force choicegroup to be MultipleChoice if not valid
|
||||
response.set("type", "MultipleChoice")
|
||||
for choice in list(response):
|
||||
if choice.get("name") is None:
|
||||
choice.set("name", "choice_" + str(i))
|
||||
@@ -621,8 +651,10 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
'''
|
||||
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:
|
||||
# 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')
|
||||
@@ -662,10 +694,14 @@ class OptionResponse(LoncapaResponse):
|
||||
'''
|
||||
TODO: handle direction and randomize
|
||||
'''
|
||||
snippets = [{'snippet': '''<optionresponse direction="vertical" randomize="yes">
|
||||
<optioninput options="('Up','Down')" correct="Up"><text>The location of the sky</text></optioninput>
|
||||
<optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput>
|
||||
</optionresponse>'''}]
|
||||
snippets = [{'snippet': """<optionresponse direction="vertical" randomize="yes">
|
||||
<optioninput options="('Up','Down')" correct="Up">
|
||||
<text>The location of the sky</text>
|
||||
</optioninput>
|
||||
<optioninput options="('Up','Down')" correct="Down">
|
||||
<text>The location of the earth</text>
|
||||
</optioninput>
|
||||
</optionresponse>"""}]
|
||||
|
||||
response_tag = 'optionresponse'
|
||||
hint_tag = 'optionhint'
|
||||
@@ -721,12 +757,13 @@ class NumericalResponse(LoncapaResponse):
|
||||
'''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)
|
||||
correct = compare_with_tolerance(evaluator(dict(), dict(), student_answer),
|
||||
complex(self.correct_answer), self.tolerance)
|
||||
# We should catch this explicitly.
|
||||
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
|
||||
# But we'd need to confirm
|
||||
except:
|
||||
raise StudentInputError("Invalid input: could not interpret '%s' as a number" %\
|
||||
raise StudentInputError("Invalid input: could not interpret '%s' as a number" %
|
||||
cgi.escape(student_answer))
|
||||
|
||||
if correct:
|
||||
@@ -734,7 +771,7 @@ class NumericalResponse(LoncapaResponse):
|
||||
else:
|
||||
return CorrectMap(self.answer_id, 'incorrect')
|
||||
|
||||
# TODO: add check_hint_condition(self,hxml_set,student_answers)
|
||||
# TODO: add check_hint_condition(self, hxml_set, student_answers)
|
||||
|
||||
def get_answers(self):
|
||||
return {self.answer_id: self.correct_answer}
|
||||
@@ -784,7 +821,7 @@ class CustomResponse(LoncapaResponse):
|
||||
Custom response. The python code to be run should be in <answer>...</answer>
|
||||
or in a <script>...</script>
|
||||
'''
|
||||
snippets = [{'snippet': '''<customresponse>
|
||||
snippets = [{'snippet': """<customresponse>
|
||||
<text>
|
||||
<br/>
|
||||
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\)
|
||||
@@ -802,8 +839,8 @@ class CustomResponse(LoncapaResponse):
|
||||
if not(r=="IS*u(t-t0)"):
|
||||
correct[0] ='incorrect'
|
||||
</answer>
|
||||
</customresponse>'''},
|
||||
{'snippet': '''<script type="loncapa/python"><![CDATA[
|
||||
</customresponse>"""},
|
||||
{'snippet': """<script type="loncapa/python"><![CDATA[
|
||||
|
||||
def sympy_check2():
|
||||
messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','<'))
|
||||
@@ -816,7 +853,7 @@ def sympy_check2():
|
||||
<customresponse cfn="sympy_check2" type="cs" expect="2.27E-39" dojs="math" size="30" answer="2.27E-39">
|
||||
<textline size="40" dojs="math" />
|
||||
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
|
||||
</customresponse>'''}]
|
||||
</customresponse>"""}]
|
||||
|
||||
response_tag = 'customresponse'
|
||||
allowed_inputfields = ['textline', 'textbox']
|
||||
@@ -830,7 +867,8 @@ def sympy_check2():
|
||||
|
||||
log.debug('answer_ids=%s' % self.answer_ids)
|
||||
|
||||
# the <answer>...</answer> stanza should be local to the current <customresponse>. So try looking there first.
|
||||
# the <answer>...</answer> stanza should be local to the current <customresponse>.
|
||||
# So try looking there first.
|
||||
self.code = None
|
||||
answer = None
|
||||
try:
|
||||
@@ -838,8 +876,9 @@ def sympy_check2():
|
||||
except IndexError:
|
||||
# print "xml = ",etree.tostring(xml,pretty_print=True)
|
||||
|
||||
# if we have a "cfn" attribute then look for the function specified by cfn, in the problem context
|
||||
# ie the comparison function is defined in the <script>...</script> stanza instead
|
||||
# if we have a "cfn" attribute then look for the function specified by cfn, in
|
||||
# the problem context ie the comparison function is defined in the
|
||||
# <script>...</script> stanza instead
|
||||
cfn = xml.get('cfn')
|
||||
if cfn:
|
||||
log.debug("cfn = %s" % cfn)
|
||||
@@ -847,13 +886,14 @@ def sympy_check2():
|
||||
self.code = self.context[cfn]
|
||||
else:
|
||||
msg = "%s: can't find cfn %s in context" % (unicode(self), cfn)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
|
||||
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
|
||||
log.error("[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')
|
||||
@@ -870,43 +910,70 @@ def sympy_check2():
|
||||
|
||||
log.debug('%s: student_answers=%s' % (unicode(self), student_answers))
|
||||
|
||||
idset = sorted(self.answer_ids) # ordered list of answer id's
|
||||
# ordered list of answer id's
|
||||
idset = sorted(self.answer_ids)
|
||||
try:
|
||||
submission = [student_answers[k] for k in idset] # ordered list of answers
|
||||
# ordered list of answers
|
||||
submission = [student_answers[k] for k in idset]
|
||||
except Exception as err:
|
||||
msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers
|
||||
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)
|
||||
|
||||
# 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
|
||||
# ordered list of dynamath responses
|
||||
dynamath = [student_answers.get(k + '_dynamath', None) for k in idset]
|
||||
|
||||
# if there is only one box, and it's empty, then don't evaluate
|
||||
if len(idset) == 1 and not submission[0]:
|
||||
# default to no error message on empty answer (to be consistent with other responsetypes)
|
||||
# but allow author to still have the old behavior by setting empty_answer_err attribute
|
||||
msg = '<span class="inline-error">No answer entered!</span>' if self.xml.get('empty_answer_err') else ''
|
||||
# default to no error message on empty answer (to be consistent with other
|
||||
# responsetypes) but allow author to still have the old behavior by setting
|
||||
# empty_answer_err attribute
|
||||
msg = ('<span class="inline-error">No answer entered!</span>'
|
||||
if self.xml.get('empty_answer_err') else '')
|
||||
return CorrectMap(idset[0], 'incorrect', msg=msg)
|
||||
|
||||
# NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are not expecting 'unknown's
|
||||
# NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are
|
||||
# not expecting 'unknown's
|
||||
correct = ['unknown'] * len(idset)
|
||||
messages = [''] * len(idset)
|
||||
|
||||
# put these in the context of the check function evaluator
|
||||
# note that this doesn't help the "cfn" version - only the exec version
|
||||
self.context.update({'xml': self.xml, # our subtree
|
||||
'response_id': self.myid, # my ID
|
||||
'expect': self.expect, # expected answer (if given as attribute)
|
||||
'submission': submission, # ordered list of student answers from entry boxes in our subtree
|
||||
'idset': idset, # ordered list of ID's of all entry boxes in our subtree
|
||||
'dynamath': dynamath, # ordered list of all javascript inputs in our subtree
|
||||
'answers': student_answers, # dict of student's responses, with keys being entry box IDs
|
||||
'correct': correct, # the list to be filled in by the check function
|
||||
'messages': messages, # the list of messages to be filled in by the check function
|
||||
'options': self.xml.get('options'), # any options to be passed to the cfn
|
||||
'testdat': 'hello world',
|
||||
})
|
||||
self.context.update({
|
||||
# our subtree
|
||||
'xml': self.xml,
|
||||
|
||||
# my ID
|
||||
'response_id': self.myid,
|
||||
|
||||
# expected answer (if given as attribute)
|
||||
'expect': self.expect,
|
||||
|
||||
# ordered list of student answers from entry boxes in our subtree
|
||||
'submission': submission,
|
||||
|
||||
# ordered list of ID's of all entry boxes in our subtree
|
||||
'idset': idset,
|
||||
|
||||
# ordered list of all javascript inputs in our subtree
|
||||
'dynamath': dynamath,
|
||||
|
||||
# dict of student's responses, with keys being entry box IDs
|
||||
'answers': student_answers,
|
||||
|
||||
# the list to be filled in by the check function
|
||||
'correct': correct,
|
||||
|
||||
# the list of messages to be filled in by the check function
|
||||
'messages': messages,
|
||||
|
||||
# any options to be passed to the cfn
|
||||
'options': self.xml.get('options'),
|
||||
'testdat': 'hello world',
|
||||
})
|
||||
|
||||
# pass self.system.debug to cfn
|
||||
self.context['debug'] = self.system.DEBUG
|
||||
@@ -921,8 +988,10 @@ def sympy_check2():
|
||||
print "oops in customresponse (code) error %s" % err
|
||||
print "context = ", self.context
|
||||
print traceback.format_exc()
|
||||
raise StudentInputError("Error: Problem could not be evaluated with your input") # Notify student
|
||||
else: # self.code is not a string; assume its a function
|
||||
# Notify student
|
||||
raise StudentInputError("Error: Problem could not be evaluated with your input")
|
||||
else:
|
||||
# self.code is not a string; assume its a function
|
||||
|
||||
# this is an interface to the Tutor2 check functions
|
||||
fn = self.code
|
||||
@@ -958,7 +1027,8 @@ def sympy_check2():
|
||||
msg = '<html>' + msg + '</html>'
|
||||
msg = msg.replace('<', '<')
|
||||
#msg = msg.replace('<','<')
|
||||
msg = etree.tostring(fromstring_bs(msg, convertEntities=None), pretty_print=True)
|
||||
msg = etree.tostring(fromstring_bs(msg, convertEntities=None),
|
||||
pretty_print=True)
|
||||
#msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
|
||||
msg = msg.replace(' ', '')
|
||||
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
|
||||
@@ -1022,18 +1092,19 @@ class SymbolicResponse(CustomResponse):
|
||||
|
||||
|
||||
class CodeResponse(LoncapaResponse):
|
||||
'''
|
||||
"""
|
||||
Grade student code using an external queueing server, called 'xqueue'
|
||||
|
||||
Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse:
|
||||
system.xqueue = { 'interface': XqueueInterface object,
|
||||
'callback_url': Per-StudentModule callback URL where results are posted (string),
|
||||
'callback_url': Per-StudentModule callback URL
|
||||
where results are posted (string),
|
||||
'default_queuename': Default queuename to submit request (string)
|
||||
}
|
||||
|
||||
External requests are only submitted for student submission grading
|
||||
External requests are only submitted for student submission grading
|
||||
(i.e. and not for getting reference answers)
|
||||
'''
|
||||
"""
|
||||
|
||||
response_tag = 'coderesponse'
|
||||
allowed_inputfields = ['textbox', 'filesubmission']
|
||||
@@ -1046,7 +1117,8 @@ class CodeResponse(LoncapaResponse):
|
||||
TODO: Determines whether in synchronous or asynchronous (queued) mode
|
||||
'''
|
||||
xml = self.xml
|
||||
self.url = xml.get('url', None) # TODO: XML can override external resource (grader/queue) URL
|
||||
# TODO: XML can override external resource (grader/queue) URL
|
||||
self.url = xml.get('url', None)
|
||||
self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename'])
|
||||
|
||||
# VS[compat]:
|
||||
@@ -1107,7 +1179,8 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
# Extract 'answer' and 'initial_display' from XML. Note that the code to be exec'ed here is:
|
||||
# (1) Internal edX code, i.e. NOT student submissions, and
|
||||
# (2) The code should only define the strings 'initial_display', 'answer', 'preamble', 'test_program'
|
||||
# (2) The code should only define the strings 'initial_display', 'answer',
|
||||
# 'preamble', 'test_program'
|
||||
# following the ExternalResponse XML format
|
||||
penv = {}
|
||||
penv['__builtins__'] = globals()['__builtins__']
|
||||
@@ -1120,10 +1193,12 @@ class CodeResponse(LoncapaResponse):
|
||||
self.answer = penv['answer']
|
||||
self.initial_display = penv['initial_display']
|
||||
except Exception as err:
|
||||
log.error("Error in CodeResponse %s: Problem reference code does not define 'answer' and/or 'initial_display' in <answer>...</answer>" % err)
|
||||
log.error("Error in CodeResponse %s: Problem reference code does not define"
|
||||
" 'answer' and/or 'initial_display' in <answer>...</answer>" % err)
|
||||
raise Exception(err)
|
||||
|
||||
# Finally, make the ExternalResponse input XML format conform to the generic exteral grader interface
|
||||
# Finally, make the ExternalResponse input XML format conform to the generic
|
||||
# exteral grader interface
|
||||
# The XML tagging of grader_payload is pyxserver-specific
|
||||
grader_payload = '<pyxserver>'
|
||||
grader_payload += '<tests>' + tests + '</tests>\n'
|
||||
@@ -1133,14 +1208,16 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
def get_score(self, student_answers):
|
||||
try:
|
||||
submission = student_answers[self.answer_id] # Note that submission can be a file
|
||||
# Note that submission can be a file
|
||||
submission = student_answers[self.answer_id]
|
||||
except Exception as err:
|
||||
log.error('Error in CodeResponse %s: cannot get student answer for %s; student_answers=%s' %
|
||||
log.error('Error in CodeResponse %s: cannot get student answer for %s;'
|
||||
' student_answers=%s' %
|
||||
(err, self.answer_id, convert_files_to_filenames(student_answers)))
|
||||
raise Exception(err)
|
||||
|
||||
# Prepare xqueue request
|
||||
#------------------------------------------------------------
|
||||
#------------------------------------------------------------
|
||||
|
||||
qinterface = self.system.xqueue['interface']
|
||||
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
||||
@@ -1149,19 +1226,20 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
# Generate header
|
||||
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
|
||||
anonymous_student_id +
|
||||
anonymous_student_id +
|
||||
self.answer_id)
|
||||
xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'],
|
||||
lms_key=queuekey,
|
||||
queue_name=self.queue_name)
|
||||
|
||||
|
||||
# Generate body
|
||||
if is_list_of_files(submission):
|
||||
self.context.update({'submission': ''}) # TODO: Get S3 pointer from the Queue
|
||||
# TODO: Get S3 pointer from the Queue
|
||||
self.context.update({'submission': ''})
|
||||
else:
|
||||
self.context.update({'submission': submission})
|
||||
|
||||
contents = self.payload.copy()
|
||||
contents = self.payload.copy()
|
||||
|
||||
# Metadata related to the student submission revealed to the external grader
|
||||
student_info = {'anonymous_student_id': anonymous_student_id,
|
||||
@@ -1171,7 +1249,8 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
# Submit request. When successful, 'msg' is the prior length of the queue
|
||||
if is_list_of_files(submission):
|
||||
contents.update({'student_response': ''}) # TODO: Is there any information we want to send here?
|
||||
# TODO: Is there any information we want to send here?
|
||||
contents.update({'student_response': ''})
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body=json.dumps(contents),
|
||||
files_to_upload=submission)
|
||||
@@ -1182,44 +1261,51 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
# State associated with the queueing request
|
||||
queuestate = {'key': queuekey,
|
||||
'time': qtime,
|
||||
}
|
||||
'time': qtime,}
|
||||
|
||||
cmap = CorrectMap()
|
||||
cmap = CorrectMap()
|
||||
if error:
|
||||
cmap.set(self.answer_id, queuestate=None,
|
||||
msg='Unable to deliver your submission to grader. (Reason: %s.) Please try again later.' % msg)
|
||||
msg='Unable to deliver your submission to grader. (Reason: %s.)'
|
||||
' Please try again later.' % msg)
|
||||
else:
|
||||
# Queueing mechanism flags:
|
||||
# 1) Backend: Non-null CorrectMap['queuestate'] indicates that the problem has been queued
|
||||
# 2) Frontend: correctness='incomplete' eventually trickles down through inputtypes.textbox
|
||||
# and .filesubmission to inform the browser to poll the LMS
|
||||
# 1) Backend: Non-null CorrectMap['queuestate'] indicates that
|
||||
# the problem has been queued
|
||||
# 2) Frontend: correctness='incomplete' eventually trickles down
|
||||
# through inputtypes.textbox and .filesubmission to inform the
|
||||
# browser to poll the LMS
|
||||
cmap.set(self.answer_id, queuestate=queuestate, correctness='incomplete', msg=msg)
|
||||
|
||||
return cmap
|
||||
|
||||
def update_score(self, score_msg, oldcmap, queuekey):
|
||||
|
||||
(valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg)
|
||||
(valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg)
|
||||
if not valid_score_msg:
|
||||
oldcmap.set(self.answer_id, msg='Invalid grader reply. Please contact the course staff.')
|
||||
oldcmap.set(self.answer_id,
|
||||
msg='Invalid grader reply. Please contact the course staff.')
|
||||
return oldcmap
|
||||
|
||||
|
||||
correctness = 'correct' if correct else 'incorrect'
|
||||
|
||||
self.context['correct'] = correctness # TODO: Find out how this is used elsewhere, if any
|
||||
# TODO: Find out how this is used elsewhere, if any
|
||||
self.context['correct'] = correctness
|
||||
|
||||
# Replace 'oldcmap' with new grading results if queuekey matches.
|
||||
# If queuekey does not match, we keep waiting for the score_msg whose key actually matches
|
||||
# Replace 'oldcmap' with new grading results if queuekey matches. If queuekey
|
||||
# does not match, we keep waiting for the score_msg whose key actually matches
|
||||
if oldcmap.is_right_queuekey(self.answer_id, queuekey):
|
||||
# Sanity check on returned points
|
||||
# Sanity check on returned points
|
||||
if points < 0:
|
||||
points = 0
|
||||
elif points > self.maxpoints[self.answer_id]:
|
||||
points = self.maxpoints[self.answer_id]
|
||||
oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace(' ', ' '), queuestate=None) # Queuestate is consumed
|
||||
# Queuestate is consumed
|
||||
oldcmap.set(self.answer_id, npoints=points, correctness=correctness,
|
||||
msg=msg.replace(' ', ' '), queuestate=None)
|
||||
else:
|
||||
log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id))
|
||||
log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' %
|
||||
(queuekey, self.answer_id))
|
||||
|
||||
return oldcmap
|
||||
|
||||
@@ -1231,7 +1317,7 @@ class CodeResponse(LoncapaResponse):
|
||||
return {self.answer_id: self.initial_display}
|
||||
|
||||
def _parse_score_msg(self, score_msg):
|
||||
'''
|
||||
"""
|
||||
Grader reply is a JSON-dump of the following dict
|
||||
{ 'correct': True/False,
|
||||
'score': Numeric value (floating point is okay) to assign to answer
|
||||
@@ -1242,22 +1328,25 @@ class CodeResponse(LoncapaResponse):
|
||||
correct: Correctness of submission (Boolean)
|
||||
score: Points to be assigned (numeric, can be float)
|
||||
msg: Message from grader to display to student (string)
|
||||
'''
|
||||
"""
|
||||
fail = (False, False, 0, '')
|
||||
try:
|
||||
score_result = json.loads(score_msg)
|
||||
except (TypeError, ValueError):
|
||||
log.error("External grader message should be a JSON-serialized dict. Received score_msg = %s" % score_msg)
|
||||
log.error("External grader message should be a JSON-serialized dict."
|
||||
" Received score_msg = %s" % score_msg)
|
||||
return fail
|
||||
if not isinstance(score_result, dict):
|
||||
log.error("External grader message should be a JSON-serialized dict. Received score_result = %s" % score_result)
|
||||
log.error("External grader message should be a JSON-serialized dict."
|
||||
" Received score_result = %s" % score_result)
|
||||
return fail
|
||||
for tag in ['correct', 'score', 'msg']:
|
||||
if tag not in score_result:
|
||||
log.error("External grader message is missing one or more required tags: 'correct', 'score', 'msg'")
|
||||
log.error("External grader message is missing one or more required"
|
||||
" tags: 'correct', 'score', 'msg'")
|
||||
return fail
|
||||
|
||||
# Next, we need to check that the contents of the external grader message
|
||||
# Next, we need to check that the contents of the external grader message
|
||||
# is safe for the LMS.
|
||||
# 1) Make sure that the message is valid XML (proper opening/closing tags)
|
||||
# 2) TODO: Is the message actually HTML?
|
||||
@@ -1265,11 +1354,12 @@ class CodeResponse(LoncapaResponse):
|
||||
try:
|
||||
etree.fromstring(msg)
|
||||
except etree.XMLSyntaxError as err:
|
||||
log.error("Unable to parse external grader message as valid XML: score_msg['msg']=%s" % msg)
|
||||
log.error("Unable to parse external grader message as valid"
|
||||
" XML: score_msg['msg']=%s" % msg)
|
||||
return fail
|
||||
|
||||
|
||||
return (True, score_result['correct'], score_result['score'], msg)
|
||||
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
@@ -1325,9 +1415,9 @@ main()
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
|
||||
# FIXME - hardcoded URL
|
||||
self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa"
|
||||
|
||||
# answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors
|
||||
answer = xml.find('answer')
|
||||
if answer is not None:
|
||||
answer_src = answer.get('src')
|
||||
@@ -1335,7 +1425,8 @@ main()
|
||||
self.code = self.system.filesystem.open('src/' + answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
else: # no <answer> stanza; get code from <script>
|
||||
else:
|
||||
# no <answer> stanza; get code from <script>
|
||||
self.code = self.context['script_code']
|
||||
if not self.code:
|
||||
msg = '%s: Missing answer script code for externalresponse' % unicode(self)
|
||||
@@ -1362,19 +1453,22 @@ main()
|
||||
payload.update(extra_payload)
|
||||
|
||||
try:
|
||||
r = requests.post(self.url, data=payload) # call external server
|
||||
# call external server. TODO: synchronous call, can block for a long time
|
||||
r = requests.post(self.url, data=payload)
|
||||
except Exception as err:
|
||||
msg = 'Error %s - cannot connect to external server url=%s' % (err, self.url)
|
||||
log.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
if self.system.DEBUG: log.info('response = %s' % r.text)
|
||||
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)
|
||||
|
||||
try:
|
||||
rxml = etree.fromstring(r.text) # response is XML; prase it
|
||||
# response is XML; parse it
|
||||
rxml = etree.fromstring(r.text)
|
||||
except Exception as err:
|
||||
msg = 'Error %s - cannot parse response from external server r.text=%s' % (err, r.text)
|
||||
log.error(msg)
|
||||
@@ -1388,7 +1482,8 @@ main()
|
||||
try:
|
||||
submission = [student_answers[k] for k in idset]
|
||||
except Exception as err:
|
||||
log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err, self.answer_ids, student_answers))
|
||||
log.error('Error %s: cannot get student answer for %s; student_answers=%s' %
|
||||
(err, self.answer_ids, student_answers))
|
||||
raise Exception(err)
|
||||
|
||||
self.context.update({'submission': submission})
|
||||
@@ -1401,7 +1496,9 @@ main()
|
||||
log.error('Error %s' % err)
|
||||
if self.system.DEBUG:
|
||||
cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset))))
|
||||
cmap.set_property(self.answer_ids[0], 'msg', '<span class="inline-error">%s</span>' % str(err).replace('<', '<'))
|
||||
cmap.set_property(
|
||||
self.answer_ids[0], 'msg',
|
||||
'<span class="inline-error">%s</span>' % str(err).replace('<', '<'))
|
||||
return cmap
|
||||
|
||||
ad = rxml.find('awarddetail').text
|
||||
@@ -1435,7 +1532,8 @@ main()
|
||||
exans[0] = msg
|
||||
|
||||
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)))
|
||||
log.error('Expected %d answers from external server, only got %d!' %
|
||||
(len(self.answer_ids), len(exans)))
|
||||
raise Exception('Short response from external server')
|
||||
return dict(zip(self.answer_ids, exans))
|
||||
|
||||
@@ -1487,11 +1585,14 @@ class FormulaResponse(LoncapaResponse):
|
||||
typeslist = []
|
||||
else:
|
||||
typeslist = ts.split(',')
|
||||
if 'ci' in typeslist: # Case insensitive
|
||||
if 'ci' in typeslist:
|
||||
# Case insensitive
|
||||
self.case_sensitive = False
|
||||
elif 'cs' in typeslist: # Case sensitive
|
||||
elif 'cs' in typeslist:
|
||||
# Case sensitive
|
||||
self.case_sensitive = True
|
||||
else: # Default
|
||||
else:
|
||||
# Default
|
||||
self.case_sensitive = False
|
||||
|
||||
def get_score(self, student_answers):
|
||||
@@ -1509,12 +1610,14 @@ class FormulaResponse(LoncapaResponse):
|
||||
for i in range(numsamples):
|
||||
instructor_variables = self.strip_dict(dict(self.context))
|
||||
student_variables = dict()
|
||||
for var in ranges: # ranges give numerical ranges for testing
|
||||
# ranges give numerical ranges for testing
|
||||
for var in ranges:
|
||||
value = random.uniform(*ranges[var])
|
||||
instructor_variables[str(var)] = value
|
||||
student_variables[str(var)] = value
|
||||
#log.debug('formula: instructor_vars=%s, expected=%s' % (instructor_variables,expected))
|
||||
instructor_result = evaluator(instructor_variables, dict(), expected, cs=self.case_sensitive)
|
||||
instructor_result = evaluator(instructor_variables, dict(),
|
||||
expected, cs=self.case_sensitive)
|
||||
try:
|
||||
#log.debug('formula: student_vars=%s, given=%s' % (student_variables,given))
|
||||
student_result = evaluator(student_variables,
|
||||
@@ -1540,9 +1643,9 @@ class FormulaResponse(LoncapaResponse):
|
||||
keys and all non-numeric values stripped out. All values also
|
||||
converted to float. Used so we can safely use Python contexts.
|
||||
'''
|
||||
d = dict([(k, numpy.complex(d[k])) for k in d if type(k) == str and \
|
||||
k.isalnum() and \
|
||||
isinstance(d[k], numbers.Number)])
|
||||
d = dict([(k, numpy.complex(d[k])) for k in d if type(k) == str and
|
||||
k.isalnum() and
|
||||
isinstance(d[k], numbers.Number)])
|
||||
return d
|
||||
|
||||
def check_hint_condition(self, hxml_set, student_answers):
|
||||
@@ -1577,7 +1680,8 @@ class SchematicResponse(LoncapaResponse):
|
||||
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
|
||||
# Untested; never used
|
||||
self.code = self.system.filestore.open('src/' + answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
|
||||
@@ -1633,17 +1737,19 @@ class ImageResponse(LoncapaResponse):
|
||||
|
||||
# parse expected answer
|
||||
# TODO: Compile regexp on file load
|
||||
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', expectedset[aid].strip().replace(' ', ''))
|
||||
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
|
||||
expectedset[aid].strip().replace(' ', ''))
|
||||
if not m:
|
||||
msg = 'Error in problem specification! cannot parse rectangle in %s' % (etree.tostring(self.ielements[aid],
|
||||
pretty_print=True))
|
||||
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)
|
||||
(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
|
||||
@@ -1660,4 +1766,17 @@ class ImageResponse(LoncapaResponse):
|
||||
# TEMPORARY: List of all response subclasses
|
||||
# FIXME: To be replaced by auto-registration
|
||||
|
||||
__all__ = [CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse, JavascriptResponse]
|
||||
__all__ = [CodeResponse,
|
||||
NumericalResponse,
|
||||
FormulaResponse,
|
||||
CustomResponse,
|
||||
SchematicResponse,
|
||||
ExternalResponse,
|
||||
ImageResponse,
|
||||
OptionResponse,
|
||||
SymbolicResponse,
|
||||
StringResponse,
|
||||
ChoiceResponse,
|
||||
MultipleChoiceResponse,
|
||||
TrueFalseResponse,
|
||||
JavascriptResponse]
|
||||
|
||||
@@ -53,8 +53,4 @@ def is_file(file_to_test):
|
||||
'''
|
||||
Duck typing to check if 'file_to_test' is a File object
|
||||
'''
|
||||
is_file = True
|
||||
for method in ['read', 'name']:
|
||||
if not hasattr(file_to_test, method):
|
||||
is_file = False
|
||||
return is_file
|
||||
return all(hasattr(file_to_test, method) for method in ['read', 'name'])
|
||||
|
||||
@@ -12,7 +12,7 @@ dateformat = '%Y%m%d%H%M%S'
|
||||
|
||||
def make_hashkey(seed):
|
||||
'''
|
||||
Generate a string key by hashing
|
||||
Generate a string key by hashing
|
||||
'''
|
||||
h = hashlib.md5()
|
||||
h.update(str(seed))
|
||||
@@ -20,27 +20,27 @@ def make_hashkey(seed):
|
||||
|
||||
|
||||
def make_xheader(lms_callback_url, lms_key, queue_name):
|
||||
'''
|
||||
"""
|
||||
Generate header for delivery and reply of queue request.
|
||||
|
||||
Xqueue header is a JSON-serialized dict:
|
||||
{ 'lms_callback_url': url to which xqueue will return the request (string),
|
||||
'lms_key': secret key used by LMS to protect its state (string),
|
||||
'lms_key': secret key used by LMS to protect its state (string),
|
||||
'queue_name': designate a specific queue within xqueue server, e.g. 'MITx-6.00x' (string)
|
||||
}
|
||||
'''
|
||||
"""
|
||||
return json.dumps({ 'lms_callback_url': lms_callback_url,
|
||||
'lms_key': lms_key,
|
||||
'queue_name': queue_name })
|
||||
|
||||
|
||||
def parse_xreply(xreply):
|
||||
'''
|
||||
"""
|
||||
Parse the reply from xqueue. Messages are JSON-serialized dict:
|
||||
{ 'return_code': 0 (success), 1 (fail)
|
||||
'content': Message from xqueue (string)
|
||||
}
|
||||
'''
|
||||
"""
|
||||
try:
|
||||
xreply = json.loads(xreply)
|
||||
except ValueError, err:
|
||||
@@ -61,11 +61,11 @@ class XQueueInterface(object):
|
||||
self.url = url
|
||||
self.auth = django_auth
|
||||
self.session = requests.session(auth=requests_auth)
|
||||
|
||||
|
||||
def send_to_queue(self, header, body, files_to_upload=None):
|
||||
'''
|
||||
"""
|
||||
Submit a request to xqueue.
|
||||
|
||||
|
||||
header: JSON-serialized dict in the format described in 'xqueue_interface.make_xheader'
|
||||
|
||||
body: Serialized data for the receipient behind the queueing service. The operation of
|
||||
@@ -74,14 +74,16 @@ class XQueueInterface(object):
|
||||
files_to_upload: List of file objects to be uploaded to xqueue along with queue request
|
||||
|
||||
Returns (error_code, msg) where error_code != 0 indicates an error
|
||||
'''
|
||||
"""
|
||||
# Attempt to send to queue
|
||||
(error, msg) = self._send_to_queue(header, body, files_to_upload)
|
||||
|
||||
if error and (msg == 'login_required'): # Log in, then try again
|
||||
# Log in, then try again
|
||||
if error and (msg == 'login_required'):
|
||||
self._login()
|
||||
if files_to_upload is not None:
|
||||
for f in files_to_upload: # Need to rewind file pointers
|
||||
# Need to rewind file pointers
|
||||
for f in files_to_upload:
|
||||
f.seek(0)
|
||||
(error, msg) = self._send_to_queue(header, body, files_to_upload)
|
||||
|
||||
@@ -91,18 +93,18 @@ class XQueueInterface(object):
|
||||
def _login(self):
|
||||
payload = { 'username': self.auth['username'],
|
||||
'password': self.auth['password'] }
|
||||
return self._http_post(self.url+'/xqueue/login/', payload)
|
||||
return self._http_post(self.url + '/xqueue/login/', payload)
|
||||
|
||||
|
||||
def _send_to_queue(self, header, body, files_to_upload):
|
||||
payload = {'xqueue_header': header,
|
||||
'xqueue_body' : body}
|
||||
files = {}
|
||||
files = {}
|
||||
if files_to_upload is not None:
|
||||
for f in files_to_upload:
|
||||
files.update({ f.name: f })
|
||||
|
||||
return self._http_post(self.url+'/xqueue/submit/', payload, files=files)
|
||||
return self._http_post(self.url + '/xqueue/submit/', payload, files=files)
|
||||
|
||||
|
||||
def _http_post(self, url, data, files=None):
|
||||
@@ -111,7 +113,7 @@ class XQueueInterface(object):
|
||||
except requests.exceptions.ConnectionError, err:
|
||||
log.error(err)
|
||||
return (1, 'cannot connect to server')
|
||||
|
||||
|
||||
if r.status_code not in [200]:
|
||||
return (1, 'unexpected HTTP status code [%d]' % r.status_code)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user