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):
|
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
|
Pyparsing uses a left-to-right parser, which makes the more
|
||||||
elegant approach pretty hopeless.
|
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
|
achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character
|
||||||
undefined_variable = achar + Word(alphanums)
|
undefined_variable = achar + Word(alphanums)
|
||||||
undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself())
|
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
|
possible_variables = re.split(general_whitespace, string) # List of all alnums in string
|
||||||
bad_variables = list()
|
bad_variables = list()
|
||||||
for v in possible_variables:
|
for v in possible_variables:
|
||||||
@@ -71,7 +72,8 @@ def check_variables(string, variables):
|
|||||||
|
|
||||||
|
|
||||||
def evaluator(variables, functions, string, cs=False):
|
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 value. Unary functions are passed as a dictionary
|
||||||
from string to function. Variables must be floats.
|
from string to function. Variables must be floats.
|
||||||
cs: Case sensitive
|
cs: Case sensitive
|
||||||
@@ -108,6 +110,7 @@ def evaluator(variables, functions, string, cs=False):
|
|||||||
|
|
||||||
if string.strip() == "":
|
if string.strip() == "":
|
||||||
return float('nan')
|
return float('nan')
|
||||||
|
|
||||||
ops = {"^": operator.pow,
|
ops = {"^": operator.pow,
|
||||||
"*": operator.mul,
|
"*": operator.mul,
|
||||||
"/": operator.truediv,
|
"/": operator.truediv,
|
||||||
@@ -169,14 +172,19 @@ def evaluator(variables, functions, string, cs=False):
|
|||||||
def func_parse_action(x):
|
def func_parse_action(x):
|
||||||
return [all_functions[x[0]](x[1])]
|
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, ".-+*/()^")
|
(dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^")
|
||||||
|
|
||||||
number_part = Word(nums)
|
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 + \
|
# 0.33 or 7 or .34
|
||||||
Optional(CaselessLiteral("E") + Optional("-") + number_part) + \
|
inner_number = (number_part + Optional("." + number_part)) | ("." + number_part)
|
||||||
Optional(number_suffix) # 0.33k or -17
|
|
||||||
|
# 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
|
number = number.setParseAction(number_parse_action) # Convert to number
|
||||||
|
|
||||||
# Predefine recursive variables
|
# 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))
|
varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x))
|
||||||
else:
|
else:
|
||||||
varnames = NoMatch()
|
varnames = NoMatch()
|
||||||
|
|
||||||
# Same thing for functions.
|
# Same thing for functions.
|
||||||
if len(all_functions) > 0:
|
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 = funcnames + lpar.suppress() + expr + rpar.suppress()
|
||||||
function.setParseAction(func_parse_action)
|
function.setParseAction(func_parse_action)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
#
|
#
|
||||||
# Nomenclature:
|
# Nomenclature:
|
||||||
#
|
#
|
||||||
# A capa Problem is a collection of text and capa Response questions. Each Response may have one or more
|
# A capa Problem is a collection of text and capa Response questions.
|
||||||
# Input entry fields. The capa Problem may include a solution.
|
# 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).
|
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
|
# dict of tagname, Response Class -- this should come from auto-registering
|
||||||
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
|
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
|
||||||
|
|
||||||
entry_types = ['textline', 'schematic', 'textbox', 'imageinput', 'optioninput', 'choicegroup', 'radiogroup', 'checkboxgroup', 'filesubmission', 'javascriptinput']
|
# Different ways students can input code
|
||||||
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
|
entry_types = ['textline',
|
||||||
response_properties = ["codeparam", "responseparam", "answer"] # these get captured as student responses
|
'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
|
# special problem tags which should be turned into innocuous HTML
|
||||||
html_transforms = {'problem': {'tag': 'div'},
|
html_transforms = {'problem': {'tag': 'div'},
|
||||||
@@ -83,7 +98,8 @@ class LoncapaProblem(object):
|
|||||||
- id (string): identifier for this problem; often a filename (no spaces)
|
- id (string): identifier for this problem; often a filename (no spaces)
|
||||||
- state (dict): student state
|
- state (dict): student state
|
||||||
- seed (int): random number generator seed (int)
|
- 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:
|
if not self.seed:
|
||||||
self.seed = struct.unpack('i', os.urandom(4))[0]
|
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)
|
problem_text = re.sub("endouttext\s*/", "/text", problem_text)
|
||||||
self.problem_text = problem_text
|
self.problem_text = problem_text
|
||||||
|
|
||||||
self.tree = etree.XML(problem_text) # parse problem XML file into an element tree
|
# parse problem XML file into an element tree
|
||||||
self._process_includes() # handle any <include file="foo"> tags
|
self.tree = etree.XML(problem_text)
|
||||||
|
|
||||||
|
# handle any <include file="foo"> tags
|
||||||
|
self._process_includes()
|
||||||
|
|
||||||
# construct script processor context (eg for customresponse problems)
|
# construct script processor context (eg for customresponse problems)
|
||||||
self.context = self._extract_context(self.tree, seed=self.seed)
|
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
|
# Pre-parse the XML tree: modifies it to add ID's and perform some in-place
|
||||||
# this also creates the dict (self.responders) of Response instances for each question in the problem.
|
# transformations. This also creates the dict (self.responders) of Response
|
||||||
# the dict has keys = xml subtree of Response, values = Response instance
|
# instances for each question in the problem. The dict has keys = xml subtree of
|
||||||
|
# Response, values = Response instance
|
||||||
self._preprocess_problem(self.tree)
|
self._preprocess_problem(self.tree)
|
||||||
|
|
||||||
if not self.student_answers: # True when student_answers is an empty dict
|
if not self.student_answers: # True when student_answers is an empty dict
|
||||||
@@ -134,6 +155,9 @@ class LoncapaProblem(object):
|
|||||||
self.done = False
|
self.done = False
|
||||||
|
|
||||||
def set_initial_display(self):
|
def set_initial_display(self):
|
||||||
|
"""
|
||||||
|
Set the student's answers to the responders' initial displays, if specified.
|
||||||
|
"""
|
||||||
initial_answers = dict()
|
initial_answers = dict()
|
||||||
for responder in self.responders.values():
|
for responder in self.responders.values():
|
||||||
if hasattr(responder, 'get_initial_display'):
|
if hasattr(responder, 'get_initial_display'):
|
||||||
@@ -145,9 +169,11 @@ class LoncapaProblem(object):
|
|||||||
return u"LoncapaProblem ({0})".format(self.problem_id)
|
return u"LoncapaProblem ({0})".format(self.problem_id)
|
||||||
|
|
||||||
def get_state(self):
|
def get_state(self):
|
||||||
''' Stored per-user session data neeeded to:
|
'''
|
||||||
|
Stored per-user session data neeeded to:
|
||||||
1) Recreate the problem
|
1) Recreate the problem
|
||||||
2) Populate any student answers. '''
|
2) Populate any student answers.
|
||||||
|
'''
|
||||||
|
|
||||||
return {'seed': self.seed,
|
return {'seed': self.seed,
|
||||||
'student_answers': self.student_answers,
|
'student_answers': self.student_answers,
|
||||||
@@ -156,7 +182,7 @@ class LoncapaProblem(object):
|
|||||||
|
|
||||||
def get_max_score(self):
|
def get_max_score(self):
|
||||||
'''
|
'''
|
||||||
Return maximum score for this problem.
|
Return the maximum score for this problem.
|
||||||
'''
|
'''
|
||||||
maxscore = 0
|
maxscore = 0
|
||||||
for response, responder in self.responders.iteritems():
|
for response, responder in self.responders.iteritems():
|
||||||
@@ -164,11 +190,11 @@ class LoncapaProblem(object):
|
|||||||
return maxscore
|
return maxscore
|
||||||
|
|
||||||
def get_score(self):
|
def get_score(self):
|
||||||
'''
|
"""
|
||||||
Compute score for this problem. The score is the number of points awarded.
|
Compute score for this problem. The score is the number of points awarded.
|
||||||
Returns a dictionary {'score': integer, from 0 to get_max_score(),
|
Returns a dictionary {'score': integer, from 0 to get_max_score(),
|
||||||
'total': get_max_score()}.
|
'total': get_max_score()}.
|
||||||
'''
|
"""
|
||||||
correct = 0
|
correct = 0
|
||||||
for key in self.correct_map:
|
for key in self.correct_map:
|
||||||
try:
|
try:
|
||||||
@@ -204,22 +230,25 @@ class LoncapaProblem(object):
|
|||||||
def is_queued(self):
|
def is_queued(self):
|
||||||
'''
|
'''
|
||||||
Returns True if any part of the problem has been submitted to an external queue
|
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)
|
return any(self.correct_map.is_queued(answer_id) for answer_id in self.correct_map)
|
||||||
|
|
||||||
|
|
||||||
def get_recentmost_queuetime(self):
|
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():
|
if not self.is_queued():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get a list of timestamps of all queueing requests, then convert it to a DateTime object
|
# 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)
|
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)]
|
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)
|
return max(queuetimes)
|
||||||
|
|
||||||
@@ -235,14 +264,20 @@ class LoncapaProblem(object):
|
|||||||
Calls the Response for each question in this problem, to do the actual grading.
|
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)
|
self.student_answers = convert_files_to_filenames(answers)
|
||||||
|
|
||||||
oldcmap = self.correct_map # old CorrectMap
|
# old CorrectMap
|
||||||
newcmap = CorrectMap() # start new with empty CorrectMap
|
oldcmap = self.correct_map
|
||||||
|
|
||||||
|
# start new with empty CorrectMap
|
||||||
|
newcmap = CorrectMap()
|
||||||
# log.debug('Responders: %s' % self.responders)
|
# log.debug('Responders: %s' % self.responders)
|
||||||
for responder in self.responders.values(): # Call each responsetype instance to do actual grading
|
# Call each responsetype instance to do actual grading
|
||||||
if 'filesubmission' in responder.allowed_inputfields: # File objects are passed only if responsetype
|
for responder in self.responders.values():
|
||||||
# explicitly allows for file submissions
|
# File objects are passed only if responsetype explicitly allows for file
|
||||||
|
# submissions
|
||||||
|
if 'filesubmission' in responder.allowed_inputfields:
|
||||||
results = responder.evaluate_answers(answers, oldcmap)
|
results = responder.evaluate_answers(answers, oldcmap)
|
||||||
else:
|
else:
|
||||||
results = responder.evaluate_answers(convert_files_to_filenames(answers), oldcmap)
|
results = responder.evaluate_answers(convert_files_to_filenames(answers), oldcmap)
|
||||||
@@ -252,28 +287,33 @@ class LoncapaProblem(object):
|
|||||||
return newcmap
|
return newcmap
|
||||||
|
|
||||||
def get_question_answers(self):
|
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
|
an answer (this sometimes happens in customresponses), that answer_id is
|
||||||
not included. Called by "show answers" button JSON request
|
not included. Called by "show answers" button JSON request
|
||||||
(see capa_module)
|
(see capa_module)
|
||||||
"""
|
"""
|
||||||
|
# dict of (id, correct_answer)
|
||||||
answer_map = dict()
|
answer_map = dict()
|
||||||
for response in self.responders.keys():
|
for response in self.responders.keys():
|
||||||
results = self.responder_answers[response]
|
results = self.responder_answers[response]
|
||||||
answer_map.update(results) # dict of (id,correct_answer)
|
answer_map.update(results)
|
||||||
|
|
||||||
# include solutions from <solution>...</solution> stanzas
|
# include solutions from <solution>...</solution> stanzas
|
||||||
for entry in self.tree.xpath("//" + "|//".join(solution_types)):
|
for entry in self.tree.xpath("//" + "|//".join(solution_types)):
|
||||||
answer = etree.tostring(entry)
|
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)
|
log.debug('answer_map = %s' % answer_map)
|
||||||
return answer_map
|
return answer_map
|
||||||
|
|
||||||
def get_answer_ids(self):
|
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
|
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 = []
|
answer_ids = []
|
||||||
for response in self.responders.keys():
|
for response in self.responders.keys():
|
||||||
results = self.responder_answers[response]
|
results = self.responder_answers[response]
|
||||||
@@ -298,7 +338,8 @@ class LoncapaProblem(object):
|
|||||||
file = inc.get('file')
|
file = inc.get('file')
|
||||||
if file is not None:
|
if file is not None:
|
||||||
try:
|
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:
|
except Exception as err:
|
||||||
log.warning('Error %s in problem xml include: %s' % (
|
log.warning('Error %s in problem xml include: %s' % (
|
||||||
err, etree.tostring(inc, pretty_print=True)))
|
err, etree.tostring(inc, pretty_print=True)))
|
||||||
@@ -311,7 +352,8 @@ class LoncapaProblem(object):
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
try:
|
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:
|
except Exception as err:
|
||||||
log.warning('Error %s in problem xml include: %s' % (
|
log.warning('Error %s in problem xml include: %s' % (
|
||||||
err, etree.tostring(inc, pretty_print=True)))
|
err, etree.tostring(inc, pretty_print=True)))
|
||||||
@@ -322,6 +364,7 @@ class LoncapaProblem(object):
|
|||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# insert new XML into tree in place of inlcude
|
# insert new XML into tree in place of inlcude
|
||||||
parent = inc.getparent()
|
parent = inc.getparent()
|
||||||
parent.insert(parent.index(inc), incxml)
|
parent.insert(parent.index(inc), incxml)
|
||||||
@@ -329,11 +372,13 @@ class LoncapaProblem(object):
|
|||||||
log.debug('Included %s into %s' % (file, self.problem_id))
|
log.debug('Included %s into %s' % (file, self.problem_id))
|
||||||
|
|
||||||
def _extract_system_path(self, script):
|
def _extract_system_path(self, script):
|
||||||
'''
|
"""
|
||||||
Extracts and normalizes additional paths for code execution.
|
Extracts and normalizes additional paths for code execution.
|
||||||
For now, there's a default path of data/course/code; this may be removed
|
For now, there's a default path of data/course/code; this may be removed
|
||||||
at some point.
|
at some point.
|
||||||
'''
|
|
||||||
|
script : ?? (TODO)
|
||||||
|
"""
|
||||||
|
|
||||||
DEFAULT_PATH = ['code']
|
DEFAULT_PATH = ['code']
|
||||||
|
|
||||||
@@ -351,7 +396,6 @@ class LoncapaProblem(object):
|
|||||||
# path is an absolute path or a path relative to the data dir
|
# path is an absolute path or a path relative to the data dir
|
||||||
dir = os.path.join(self.system.filestore.root_path, dir)
|
dir = os.path.join(self.system.filestore.root_path, dir)
|
||||||
abs_dir = os.path.normpath(dir)
|
abs_dir = os.path.normpath(dir)
|
||||||
#log.debug("appending to path: %s" % abs_dir)
|
|
||||||
path.append(abs_dir)
|
path.append(abs_dir)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
@@ -362,13 +406,20 @@ class LoncapaProblem(object):
|
|||||||
context of this problem. Provides ability to randomize problems, and also set
|
context of this problem. Provides ability to randomize problems, and also set
|
||||||
variables for problem answer checking.
|
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)
|
random.seed(self.seed)
|
||||||
context = {'global_context': global_context} # save global context in here also
|
# save global context in here also
|
||||||
context.update(global_context) # initialize context to have stuff in global_context
|
context = {'global_context': global_context}
|
||||||
context['__builtins__'] = globals()['__builtins__'] # put globals there also
|
|
||||||
context['the_lcp'] = self # pass instance of LoncapaProblem in
|
# 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'] = ''
|
context['script_code'] = ''
|
||||||
|
|
||||||
self._execute_scripts(tree.findall('.//script'), context)
|
self._execute_scripts(tree.findall('.//script'), context)
|
||||||
@@ -395,12 +446,14 @@ class LoncapaProblem(object):
|
|||||||
code = script.text
|
code = script.text
|
||||||
XMLESC = {"'": "'", """: '"'}
|
XMLESC = {"'": "'", """: '"'}
|
||||||
code = unescape(code, XMLESC)
|
code = unescape(code, XMLESC)
|
||||||
context['script_code'] += code # store code source in context
|
# store code source in context
|
||||||
|
context['script_code'] += code
|
||||||
try:
|
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:
|
except Exception as err:
|
||||||
log.exception("Error while execing script code: " + code)
|
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)
|
raise responsetypes.LoncapaProblemError(msg)
|
||||||
finally:
|
finally:
|
||||||
sys.path = original_path
|
sys.path = original_path
|
||||||
@@ -415,7 +468,8 @@ class LoncapaProblem(object):
|
|||||||
|
|
||||||
Used by get_html.
|
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.
|
# leave javascript intact.
|
||||||
return problemtree
|
return problemtree
|
||||||
|
|
||||||
@@ -453,21 +507,26 @@ class LoncapaProblem(object):
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
use='capa_input')
|
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)
|
return self.responders[problemtree].render_html(self._extract_html)
|
||||||
|
|
||||||
tree = etree.Element(problemtree.tag)
|
tree = etree.Element(problemtree.tag)
|
||||||
for item in problemtree:
|
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:
|
if item_xhtml is not None:
|
||||||
tree.append(item_xhtml)
|
tree.append(item_xhtml)
|
||||||
|
|
||||||
if tree.tag in html_transforms:
|
if tree.tag in html_transforms:
|
||||||
tree.tag = html_transforms[problemtree.tag]['tag']
|
tree.tag = html_transforms[problemtree.tag]['tag']
|
||||||
else:
|
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.set(key, value)
|
||||||
|
|
||||||
tree.text = problemtree.text
|
tree.text = problemtree.text
|
||||||
@@ -490,31 +549,41 @@ class LoncapaProblem(object):
|
|||||||
self.responders = {}
|
self.responders = {}
|
||||||
for response in tree.xpath('//' + "|//".join(response_tag_dict)):
|
for response in tree.xpath('//' + "|//".join(response_tag_dict)):
|
||||||
response_id_str = self.problem_id + "_" + str(response_id)
|
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
|
response_id += 1
|
||||||
|
|
||||||
answer_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)
|
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['response_id'] = str(response_id)
|
||||||
entry.attrib['answer_id'] = str(answer_id)
|
entry.attrib['answer_id'] = str(answer_id)
|
||||||
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
|
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
|
||||||
answer_id = answer_id + 1
|
answer_id = answer_id + 1
|
||||||
|
|
||||||
responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
|
# instantiate capa Response
|
||||||
self.responders[response] = responder # save in list in self
|
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 = {}
|
self.responder_answers = {}
|
||||||
for response in self.responders.keys():
|
for response in self.responders.keys():
|
||||||
try:
|
try:
|
||||||
self.responder_answers[response] = self.responders[response].get_answers()
|
self.responder_answers[response] = self.responders[response].get_answers()
|
||||||
except:
|
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
|
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).
|
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
|
||||||
solution_id = 1
|
solution_id = 1
|
||||||
for solution in tree.findall('.//solution'):
|
for solution in tree.findall('.//solution'):
|
||||||
|
|||||||
@@ -5,23 +5,26 @@
|
|||||||
|
|
||||||
|
|
||||||
class CorrectMap(object):
|
class CorrectMap(object):
|
||||||
'''
|
"""
|
||||||
Stores map between answer_id and response evaluation result for each question
|
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
|
in a capa problem. The response evaluation result for each answer_id includes
|
||||||
(correctness, npoints, msg, hint, hintmode).
|
(correctness, npoints, msg, hint, hintmode).
|
||||||
|
|
||||||
- correctness : either 'correct' or 'incorrect'
|
- correctness : either 'correct' or 'incorrect'
|
||||||
- npoints : None, or integer specifying number of points awarded for this answer_id
|
- 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)
|
- msg : string (may have HTML) giving extra message response
|
||||||
- hint : string (may have HTML) giving optional hint (displayed below textline or textbox, above msg)
|
(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
|
- 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
|
- 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
|
of a DateTime object in the format '%Y%m%d%H%M%S'. Is None when not queued
|
||||||
|
|
||||||
Behaves as a dict.
|
Behaves as a dict.
|
||||||
'''
|
"""
|
||||||
def __init__(self, *args, **kwargs):
|
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.items = self.cmap.items
|
||||||
self.keys = self.cmap.keys
|
self.keys = self.cmap.keys
|
||||||
self.set(*args, **kwargs)
|
self.set(*args, **kwargs)
|
||||||
@@ -33,7 +36,15 @@ class CorrectMap(object):
|
|||||||
return self.cmap.__iter__()
|
return self.cmap.__iter__()
|
||||||
|
|
||||||
# See the documentation for 'set_dict' for the use of kwargs
|
# 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:
|
if answer_id is not None:
|
||||||
self.cmap[answer_id] = {'correctness': correctness,
|
self.cmap[answer_id] = {'correctness': correctness,
|
||||||
'npoints': npoints,
|
'npoints': npoints,
|
||||||
@@ -56,12 +67,13 @@ class CorrectMap(object):
|
|||||||
'''
|
'''
|
||||||
Set internal dict of CorrectMap to provided correct_map dict
|
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
|
correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This
|
||||||
when the definition of CorrectMap (e.g. its properties) are altered, existing correct_map dict
|
means that when the definition of CorrectMap (e.g. its properties) are altered,
|
||||||
not coincide with the newest CorrectMap format as defined by self.set.
|
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
|
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
|
the incoming correct_map dict and the new CorrectMap instance will be written, while
|
||||||
mismatched keys will be gracefully ignored.
|
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 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):
|
if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict):
|
||||||
self.__init__() # empty current dict
|
# empty current dict
|
||||||
for k in correct_map: self.set(k, correct_map[k]) # create new dict entries
|
self.__init__()
|
||||||
|
|
||||||
|
# create new dict entries
|
||||||
|
for k in correct_map:
|
||||||
|
self.set(k, correct_map[k])
|
||||||
else:
|
else:
|
||||||
self.__init__()
|
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):
|
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
|
return None
|
||||||
|
|
||||||
def is_queued(self, answer_id):
|
def is_queued(self, answer_id):
|
||||||
@@ -94,14 +112,18 @@ class CorrectMap(object):
|
|||||||
return npoints
|
return npoints
|
||||||
elif self.is_correct(answer_id):
|
elif self.is_correct(answer_id):
|
||||||
return 1
|
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):
|
def set_property(self, answer_id, property, value):
|
||||||
if answer_id in self.cmap: self.cmap[answer_id][property] = value
|
if answer_id in self.cmap:
|
||||||
else: self.cmap[answer_id] = {property: value}
|
self.cmap[answer_id][property] = value
|
||||||
|
else:
|
||||||
|
self.cmap[answer_id] = {property: value}
|
||||||
|
|
||||||
def get_property(self, answer_id, property, default=None):
|
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
|
return default
|
||||||
|
|
||||||
def get_correctness(self, answer_id):
|
def get_correctness(self, answer_id):
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
""" Standard resistor codes.
|
"""
|
||||||
|
Standard resistor codes.
|
||||||
http://en.wikipedia.org/wiki/Electronic_color_code
|
http://en.wikipedia.org/wiki/Electronic_color_code
|
||||||
"""
|
"""
|
||||||
E6 = [10, 15, 22, 33, 47, 68]
|
E6 = [10, 15, 22, 33, 47, 68]
|
||||||
|
|
||||||
E12 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82]
|
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]
|
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]
|
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]
|
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]
|
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
|
# File: courseware/capa/inputtypes.py
|
||||||
#
|
#
|
||||||
|
|
||||||
'''
|
"""
|
||||||
Module containing the problem elements which render into input objects
|
Module containing the problem elements which render into input objects
|
||||||
|
|
||||||
- textline
|
- textline
|
||||||
@@ -16,15 +16,16 @@ Module containing the problem elements which render into input objects
|
|||||||
- optioninput (for option list)
|
- optioninput (for option list)
|
||||||
- filesubmission (upload a file)
|
- 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
|
||||||
# TODO: rename "state" to "status" for all below
|
# answer and any associated message for the problem ID for the input element.
|
||||||
# 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 logging
|
||||||
import re
|
import re
|
||||||
@@ -47,7 +48,8 @@ class SimpleInput():# XModule
|
|||||||
Type for simple inputs -- plain HTML with a form element
|
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'):
|
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.xml = xml
|
||||||
self.tag = xml.tag
|
self.tag = xml.tag
|
||||||
self.system = system
|
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
|
## 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
|
## third. Since we don't make this guarantee, we can swap this around in
|
||||||
## the future if there's a more logical order.
|
## the future if there's a more logical order.
|
||||||
if item_id: self.id = item_id
|
if item_id:
|
||||||
if xml.get('id'): self.id = xml.get('id')
|
self.id = item_id
|
||||||
if 'id' in state: self.id = state['id']
|
|
||||||
|
|
||||||
self.value = ''
|
if xml.get('id'):
|
||||||
if 'value' in state:
|
self.id = xml.get('id')
|
||||||
self.value = state['value']
|
|
||||||
|
if 'id' in state:
|
||||||
|
self.id = state['id']
|
||||||
|
|
||||||
|
self.value = state.get('value', '')
|
||||||
|
|
||||||
self.msg = ''
|
self.msg = ''
|
||||||
feedback = state.get('feedback')
|
feedback = state.get('feedback')
|
||||||
@@ -92,6 +98,7 @@ class SimpleInput():# XModule
|
|||||||
|
|
||||||
# put hint above msg if to be displayed
|
# put hint above msg if to be displayed
|
||||||
if self.hintmode == 'always':
|
if self.hintmode == 'always':
|
||||||
|
# TODO: is the '.' in <br/.> below a bug?
|
||||||
self.msg = self.hint + ('<br/.>' if self.msg else '') + self.msg
|
self.msg = self.hint + ('<br/.>' if self.msg else '') + self.msg
|
||||||
|
|
||||||
self.status = 'unanswered'
|
self.status = 'unanswered'
|
||||||
@@ -107,7 +114,8 @@ class SimpleInput():# XModule
|
|||||||
return ['capa_input', 'capa_transform']
|
return ['capa_input', 'capa_transform']
|
||||||
|
|
||||||
def get_html(self):
|
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):
|
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
|
@register_render_function
|
||||||
def optioninput(element, value, status, render_template, msg=''):
|
def optioninput(element, value, status, render_template, msg=''):
|
||||||
'''
|
"""
|
||||||
Select option input type.
|
Select option input type.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
<optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
|
<optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
|
||||||
'''
|
"""
|
||||||
eid = element.get('id')
|
eid = element.get('id')
|
||||||
options = element.get('options')
|
options = element.get('options')
|
||||||
if not 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 = shlex.shlex(options[1:-1])
|
||||||
oset.quotes = "'"
|
oset.quotes = "'"
|
||||||
oset.whitespace = ","
|
oset.whitespace = ","
|
||||||
oset = [x[1:-1] for x in list(oset)]
|
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
|
# make ordered list with (key,value) same
|
||||||
osetdict = [(oset[x], oset[x]) for x in range(len(oset))] # make ordered list with (key,value) same
|
osetdict = [(oset[x], oset[x]) for x in range(len(oset))]
|
||||||
# TODO: allow ordering to be randomized
|
# TODO: allow ordering to be randomized
|
||||||
|
|
||||||
context = {'id': eid,
|
context = {'id': eid,
|
||||||
@@ -166,26 +176,35 @@ def choicegroup(element, value, status, render_template, msg=''):
|
|||||||
'''
|
'''
|
||||||
Radio button inputs: multiple choice or true/false
|
Radio button inputs: multiple choice or true/false
|
||||||
|
|
||||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute,
|
TODO: allow order of choices to be randomized, following lon-capa spec. Use
|
||||||
ie random, top, bottom.
|
"location" attribute, ie random, top, bottom.
|
||||||
'''
|
'''
|
||||||
eid = element.get('id')
|
eid = element.get('id')
|
||||||
if element.get('type') == "MultipleChoice":
|
if element.get('type') == "MultipleChoice":
|
||||||
type = "radio"
|
element_type = "radio"
|
||||||
elif element.get('type') == "TrueFalse":
|
elif element.get('type') == "TrueFalse":
|
||||||
type = "checkbox"
|
element_type = "checkbox"
|
||||||
else:
|
else:
|
||||||
type = "radio"
|
element_type = "radio"
|
||||||
choices = []
|
choices = []
|
||||||
for choice in element:
|
for choice in element:
|
||||||
if not choice.tag == 'choice':
|
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 = ""
|
||||||
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:
|
if choice.text is not None:
|
||||||
ctext += choice.text # TODO: fix order?
|
# TODO: fix order?
|
||||||
|
ctext += choice.text
|
||||||
choices.append((choice.get("name"), ctext))
|
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)
|
html = render_template("choicegroup.html", context)
|
||||||
return etree.XML(html)
|
return etree.XML(html)
|
||||||
|
|
||||||
@@ -196,8 +215,8 @@ def extract_choices(element):
|
|||||||
Extracts choices for a few input types, such as radiogroup and
|
Extracts choices for a few input types, such as radiogroup and
|
||||||
checkboxgroup.
|
checkboxgroup.
|
||||||
|
|
||||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute,
|
TODO: allow order of choices to be randomized, following lon-capa spec. Use
|
||||||
ie random, top, bottom.
|
"location" attribute, ie random, top, bottom.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
choices = []
|
choices = []
|
||||||
@@ -226,7 +245,12 @@ def radiogroup(element, value, status, render_template, msg=''):
|
|||||||
|
|
||||||
choices = extract_choices(element)
|
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)
|
html = render_template("choicegroup.html", context)
|
||||||
return etree.XML(html)
|
return etree.XML(html)
|
||||||
@@ -244,7 +268,12 @@ def checkboxgroup(element, value, status, render_template, msg=''):
|
|||||||
|
|
||||||
choices = extract_choices(element)
|
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)
|
html = render_template("choicegroup.html", context)
|
||||||
return etree.XML(html)
|
return etree.XML(html)
|
||||||
@@ -260,49 +289,67 @@ def javascriptinput(element, value, status, render_template, msg='null'):
|
|||||||
problem_state = element.get('problem_state')
|
problem_state = element.get('problem_state')
|
||||||
display_class = element.get('display_class')
|
display_class = element.get('display_class')
|
||||||
display_file = element.get('display_file')
|
display_file = element.get('display_file')
|
||||||
|
|
||||||
# Need to provide a value that JSON can parse if there is no
|
# Need to provide a value that JSON can parse if there is no
|
||||||
# student-supplied value yet.
|
# student-supplied value yet.
|
||||||
if value == "":
|
if value == "":
|
||||||
value = 'null'
|
value = 'null'
|
||||||
|
|
||||||
escapedict = {'"': '"'}
|
escapedict = {'"': '"'}
|
||||||
value = saxutils.escape(value, escapedict)
|
value = saxutils.escape(value, escapedict)
|
||||||
msg = saxutils.escape(msg, escapedict)
|
msg = saxutils.escape(msg, escapedict)
|
||||||
context = {'id': eid, 'params': params, 'display_file': display_file,
|
context = {'id': eid,
|
||||||
'display_class': display_class, 'problem_state': problem_state,
|
'params': params,
|
||||||
'value': value, 'evaluation': msg,
|
'display_file': display_file,
|
||||||
|
'display_class': display_class,
|
||||||
|
'problem_state': problem_state,
|
||||||
|
'value': value,
|
||||||
|
'evaluation': msg,
|
||||||
}
|
}
|
||||||
html = render_template("javascriptinput.html", context)
|
html = render_template("javascriptinput.html", context)
|
||||||
return etree.XML(html)
|
return etree.XML(html)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@register_render_function
|
@register_render_function
|
||||||
def textline(element, value, status, render_template, msg=""):
|
def textline(element, value, status, render_template, msg=""):
|
||||||
'''
|
'''
|
||||||
Simple text line input, with optional size specification.
|
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
|
# TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x
|
||||||
return SimpleInput.xml_tags['textline_dynamath'](element, value, status, render_template, msg)
|
if element.get('math') or element.get('dojs'):
|
||||||
|
return SimpleInput.xml_tags['textline_dynamath'](element, value, status,
|
||||||
|
render_template, msg)
|
||||||
eid = element.get('id')
|
eid = element.get('id')
|
||||||
if eid is None:
|
if eid is None:
|
||||||
msg = 'textline has no id: it probably appears outside of a known response type'
|
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>')
|
msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '<unavailable>')
|
||||||
raise Exception(msg)
|
raise Exception(msg)
|
||||||
|
|
||||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||||
size = element.get('size')
|
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 = {'"': '"'}
|
escapedict = {'"': '"'}
|
||||||
value = saxutils.escape(value, escapedict) # otherwise, answers with quotes in them crashes the system!
|
value = saxutils.escape(value, escapedict)
|
||||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, 'hidden': hidden,
|
|
||||||
|
context = {'id': eid,
|
||||||
|
'value': value,
|
||||||
|
'state': status,
|
||||||
|
'count': count,
|
||||||
|
'size': size,
|
||||||
|
'msg': msg,
|
||||||
|
'hidden': hidden,
|
||||||
'inline': element.get('inline',''),
|
'inline': element.get('inline',''),
|
||||||
}
|
}
|
||||||
|
|
||||||
html = render_template("textinput.html", context)
|
html = render_template("textinput.html", context)
|
||||||
try:
|
try:
|
||||||
xhtml = etree.XML(html)
|
xhtml = etree.XML(html)
|
||||||
except Exception as err:
|
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)
|
log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html)
|
||||||
raise
|
raise
|
||||||
return xhtml
|
return xhtml
|
||||||
@@ -313,7 +360,8 @@ def textline(element, value, status, render_template, msg=""):
|
|||||||
@register_render_function
|
@register_render_function
|
||||||
def textline_dynamath(element, value, status, render_template, msg=''):
|
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 a wrapper for <formulainput>
|
||||||
# TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
|
# 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')
|
eid = element.get('id')
|
||||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||||
size = element.get('size')
|
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 to insert between raw input and Mathjax
|
||||||
preprocessor = {'class_name': element.get('preprocessorClassName',''),
|
preprocessor = {'class_name': element.get('preprocessorClassName',''),
|
||||||
@@ -337,10 +386,14 @@ def textline_dynamath(element, value, status, render_template, msg=''):
|
|||||||
escapedict = {'"': '"'}
|
escapedict = {'"': '"'}
|
||||||
value = saxutils.escape(value, escapedict)
|
value = saxutils.escape(value, escapedict)
|
||||||
|
|
||||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size,
|
context = {'id': eid,
|
||||||
'msg': msg, 'hidden': hidden,
|
'value': value,
|
||||||
'preprocessor': preprocessor,
|
'state': status,
|
||||||
}
|
'count': count,
|
||||||
|
'size': size,
|
||||||
|
'msg': msg,
|
||||||
|
'hidden': hidden,
|
||||||
|
'preprocessor': preprocessor,}
|
||||||
html = render_template("textinput_dynamath.html", context)
|
html = render_template("textinput_dynamath.html", context)
|
||||||
return etree.XML(html)
|
return etree.XML(html)
|
||||||
|
|
||||||
@@ -360,15 +413,19 @@ def filesubmission(element, value, status, render_template, msg=''):
|
|||||||
|
|
||||||
# Check if problem has been queued
|
# Check if problem has been queued
|
||||||
queue_len = 0
|
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'
|
status = 'queued'
|
||||||
queue_len = msg
|
queue_len = msg
|
||||||
msg = 'Submitted to grader.'
|
msg = 'Submitted to grader.'
|
||||||
|
|
||||||
context = { 'id': eid, 'state': status, 'msg': msg, 'value': value,
|
context = { 'id': eid,
|
||||||
'queue_len': queue_len, 'allowed_files': allowed_files,
|
'state': status,
|
||||||
'required_files': required_files
|
'msg': msg,
|
||||||
}
|
'value': value,
|
||||||
|
'queue_len': queue_len,
|
||||||
|
'allowed_files': allowed_files,
|
||||||
|
'required_files': required_files,}
|
||||||
html = render_template("filesubmission.html", context)
|
html = render_template("filesubmission.html", context)
|
||||||
return etree.XML(html)
|
return etree.XML(html)
|
||||||
|
|
||||||
@@ -387,13 +444,17 @@ def textbox(element, value, status, render_template, msg=''):
|
|||||||
size = element.get('size')
|
size = element.get('size')
|
||||||
rows = element.get('rows') or '30'
|
rows = element.get('rows') or '30'
|
||||||
cols = element.get('cols') or '80'
|
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
|
# Check if problem has been queued
|
||||||
queue_len = 0
|
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'
|
status = 'queued'
|
||||||
queue_len = msg
|
queue_len = msg
|
||||||
msg = 'Submitted to grader.'
|
msg = 'Submitted to grader.'
|
||||||
@@ -404,10 +465,18 @@ def textbox(element, value, status, render_template, msg=''):
|
|||||||
tabsize = element.get('tabsize','4')
|
tabsize = element.get('tabsize','4')
|
||||||
tabsize = int(tabsize)
|
tabsize = int(tabsize)
|
||||||
|
|
||||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg,
|
context = {'id': eid,
|
||||||
'mode': mode, 'linenumbers': linenumbers,
|
'value': value,
|
||||||
'rows': rows, 'cols': cols,
|
'state': status,
|
||||||
'hidden': hidden, 'tabsize': tabsize,
|
'count': count,
|
||||||
|
'size': size,
|
||||||
|
'msg': msg,
|
||||||
|
'mode': mode,
|
||||||
|
'linenumbers': linenumbers,
|
||||||
|
'rows': rows,
|
||||||
|
'cols': cols,
|
||||||
|
'hidden': hidden,
|
||||||
|
'tabsize': tabsize,
|
||||||
'queue_len': queue_len,
|
'queue_len': queue_len,
|
||||||
}
|
}
|
||||||
html = render_template("textbox.html", context)
|
html = render_template("textbox.html", context)
|
||||||
@@ -475,7 +544,8 @@ def math(element, value, status, render_template, msg=''):
|
|||||||
# mathstr = mathstr.replace('\\displaystyle','')
|
# mathstr = mathstr.replace('\\displaystyle','')
|
||||||
#else:
|
#else:
|
||||||
# isinline = True
|
# 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))
|
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr, saxutils.escape(element.tail))
|
||||||
try:
|
try:
|
||||||
@@ -483,13 +553,14 @@ def math(element, value, status, render_template, msg=''):
|
|||||||
except Exception as err:
|
except Exception as err:
|
||||||
if False: # TODO needs to be self.system.DEBUG - but can't access system
|
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 = '<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>"
|
msg += "</div></html>"
|
||||||
log.error(msg)
|
log.error(msg)
|
||||||
return etree.XML(msg)
|
return etree.XML(msg)
|
||||||
else:
|
else:
|
||||||
raise
|
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
|
return xhtml
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
@@ -520,11 +591,13 @@ def solution(element, value, status, render_template, msg=''):
|
|||||||
@register_render_function
|
@register_render_function
|
||||||
def imageinput(element, value, status, render_template, msg=''):
|
def imageinput(element, value, status, render_template, msg=''):
|
||||||
'''
|
'''
|
||||||
Clickable image as an input field. Element should specify the image source, height, and width, eg
|
Clickable image as an input field. Element should specify the image source, height,
|
||||||
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="388" height="560" />
|
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')
|
eid = element.get('id')
|
||||||
src = element.get('src')
|
src = element.get('src')
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class StudentInputError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class LoncapaResponse(object):
|
class LoncapaResponse(object):
|
||||||
'''
|
"""
|
||||||
Base class for CAPA responsetypes. Each response type (ie a capa question,
|
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 is part of a capa problem) is represented as a subclass,
|
||||||
which should provide the following methods:
|
which should provide the following methods:
|
||||||
@@ -77,19 +77,29 @@ class LoncapaResponse(object):
|
|||||||
|
|
||||||
In addition, these methods are optional:
|
In addition, these methods are optional:
|
||||||
|
|
||||||
- setup_response : find and note the answer input field IDs for the response; called by __init__
|
- setup_response : find and note the answer input field IDs for the response; called
|
||||||
- check_hint_condition : check to see if the student's answers satisfy a particular condition for a hint to be displayed
|
by __init__
|
||||||
- render_html : render this Response as HTML (must return XHTML compliant string)
|
|
||||||
|
- 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
|
- __unicode__ : unicode representation of this Response
|
||||||
|
|
||||||
Each response type may also specify the following attributes:
|
Each response type may also specify the following attributes:
|
||||||
|
|
||||||
- max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None)
|
- max_inputfields : (int) maximum number of answer input fields (checked in __init__
|
||||||
- allowed_inputfields : list of allowed input fields (each a string) for this Response
|
if not None)
|
||||||
- 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
|
|
||||||
|
|
||||||
'''
|
- 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
|
__metaclass__ = abc.ABCMeta # abc = Abstract Base Class
|
||||||
|
|
||||||
response_tag = None
|
response_tag = None
|
||||||
@@ -121,26 +131,32 @@ class LoncapaResponse(object):
|
|||||||
raise LoncapaProblemError(msg)
|
raise LoncapaProblemError(msg)
|
||||||
|
|
||||||
if self.max_inputfields and len(inputfields) > self.max_inputfields:
|
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>')
|
msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>')
|
||||||
raise LoncapaProblemError(msg)
|
raise LoncapaProblemError(msg)
|
||||||
|
|
||||||
for prop in self.required_attributes:
|
for prop in self.required_attributes:
|
||||||
if not xml.get(prop):
|
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>')
|
msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>')
|
||||||
raise LoncapaProblemError(msg)
|
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:
|
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()
|
self.maxpoints = dict()
|
||||||
for inputfield in self.inputfields:
|
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.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:
|
for entry in self.inputfields:
|
||||||
answer = entry.get('correct_answer')
|
answer = entry.get('correct_answer')
|
||||||
if answer:
|
if answer:
|
||||||
@@ -163,10 +179,13 @@ class LoncapaResponse(object):
|
|||||||
|
|
||||||
- renderer : procedure which produces HTML given an ElementTree
|
- 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:
|
for item in self.xml:
|
||||||
item_xhtml = renderer(item) # call provided procedure to do the rendering
|
# call provided procedure to do the rendering
|
||||||
if item_xhtml is not None: tree.append(item_xhtml)
|
item_xhtml = renderer(item)
|
||||||
|
if item_xhtml is not None:
|
||||||
|
tree.append(item_xhtml)
|
||||||
tree.tail = self.xml.tail
|
tree.tail = self.xml.tail
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
@@ -192,21 +211,21 @@ class LoncapaResponse(object):
|
|||||||
Modifies new_cmap, by adding hints to answer_id entries as appropriate.
|
Modifies new_cmap, by adding hints to answer_id entries as appropriate.
|
||||||
'''
|
'''
|
||||||
hintgroup = self.xml.find('hintgroup')
|
hintgroup = self.xml.find('hintgroup')
|
||||||
if hintgroup is None: return
|
if hintgroup is None:
|
||||||
|
return
|
||||||
|
|
||||||
# hint specified by function?
|
# hint specified by function?
|
||||||
hintfn = hintgroup.get('hintfn')
|
hintfn = hintgroup.get('hintfn')
|
||||||
if hintfn:
|
if hintfn:
|
||||||
'''
|
'''
|
||||||
Hint is determined by a function defined in the <script> context; evaluate that function to obtain
|
Hint is determined by a function defined in the <script> context; evaluate
|
||||||
list of hint, hintmode for each answer_id.
|
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)
|
The function should take arguments (answer_ids, student_answers, new_cmap, old_cmap)
|
||||||
and it should modify new_cmap as appropriate.
|
and it should modify new_cmap as appropriate.
|
||||||
|
|
||||||
We may extend this in the future to add another argument which provides a callback procedure
|
We may extend this in the future to add another argument which provides a
|
||||||
to a social hint generation system.
|
callback procedure to a social hint generation system.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if not hintfn in self.context:
|
if not hintfn in self.context:
|
||||||
msg = 'missing specified hint function %s in script context' % hintfn
|
msg = 'missing specified hint function %s in script context' % hintfn
|
||||||
@@ -237,14 +256,20 @@ class LoncapaResponse(object):
|
|||||||
# </hintgroup>
|
# </hintgroup>
|
||||||
# </formularesponse>
|
# </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)
|
rephints = hintgroup.findall(self.hint_tag)
|
||||||
hints_to_show = self.check_hint_condition(rephints, student_answers)
|
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'):
|
for hintpart in hintgroup.findall('hintpart'):
|
||||||
if hintpart.get('on') in hints_to_show:
|
if hintpart.get('on') in hints_to_show:
|
||||||
hint_text = hintpart.find('text').text
|
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)
|
new_cmap.set_hint_and_mode(aid, hint_text, hintmode)
|
||||||
log.debug('after hint: new_cmap = %s' % new_cmap)
|
log.debug('after hint: new_cmap = %s' % new_cmap)
|
||||||
|
|
||||||
@@ -255,10 +280,10 @@ class LoncapaResponse(object):
|
|||||||
(correctness, npoints, msg) for each answer_id.
|
(correctness, npoints, msg) for each answer_id.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
|
|
||||||
- student_answers : dict of (answer_id,answer) where answer = student input (string)
|
- 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
|
pass
|
||||||
|
|
||||||
@@ -273,10 +298,13 @@ class LoncapaResponse(object):
|
|||||||
'''
|
'''
|
||||||
Return a list of hints to show.
|
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
|
- 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
|
pass
|
||||||
|
|
||||||
@@ -290,10 +318,10 @@ class LoncapaResponse(object):
|
|||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
class JavascriptResponse(LoncapaResponse):
|
class JavascriptResponse(LoncapaResponse):
|
||||||
'''
|
"""
|
||||||
This response type is used when the student's answer is graded via
|
This response type is used when the student's answer is graded via
|
||||||
Javascript using Node.js.
|
Javascript using Node.js.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
response_tag = 'javascriptresponse'
|
response_tag = 'javascriptresponse'
|
||||||
max_inputfields = 1
|
max_inputfields = 1
|
||||||
@@ -312,11 +340,11 @@ class JavascriptResponse(LoncapaResponse):
|
|||||||
self.problem_state = self.generate_problem_state()
|
self.problem_state = self.generate_problem_state()
|
||||||
else:
|
else:
|
||||||
self.problem_state = None
|
self.problem_state = None
|
||||||
|
|
||||||
self.solution = None
|
self.solution = None
|
||||||
|
|
||||||
self.prepare_inputfield()
|
self.prepare_inputfield()
|
||||||
|
|
||||||
def compile_display_javascript(self):
|
def compile_display_javascript(self):
|
||||||
|
|
||||||
# TODO FIXME
|
# TODO FIXME
|
||||||
@@ -355,10 +383,10 @@ class JavascriptResponse(LoncapaResponse):
|
|||||||
self.generator_xml = self.xml.xpath('//*[@id=$id]//generator',
|
self.generator_xml = self.xml.xpath('//*[@id=$id]//generator',
|
||||||
id=self.xml.get('id'))[0]
|
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]
|
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]
|
id=self.xml.get('id'))[0]
|
||||||
|
|
||||||
self.xml.remove(self.generator_xml)
|
self.xml.remove(self.generator_xml)
|
||||||
@@ -385,7 +413,7 @@ class JavascriptResponse(LoncapaResponse):
|
|||||||
self.display_dependencies = []
|
self.display_dependencies = []
|
||||||
|
|
||||||
self.display_class = self.display_xml.get("class")
|
self.display_class = self.display_xml.get("class")
|
||||||
|
|
||||||
def get_node_env(self):
|
def get_node_env(self):
|
||||||
|
|
||||||
js_dir = os.path.join(self.system.filestore.root_path, 'js')
|
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)
|
node_path = self.system.node_path + ":" + os.path.normpath(js_dir)
|
||||||
tmp_env["NODE_PATH"] = node_path
|
tmp_env["NODE_PATH"] = node_path
|
||||||
return tmp_env
|
return tmp_env
|
||||||
|
|
||||||
def call_node(self, args):
|
def call_node(self, args):
|
||||||
|
|
||||||
subprocess_args = ["node"]
|
subprocess_args = ["node"]
|
||||||
@@ -406,7 +434,7 @@ class JavascriptResponse(LoncapaResponse):
|
|||||||
|
|
||||||
generator_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_generator.js'
|
generator_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_generator.js'
|
||||||
output = self.call_node([generator_file,
|
output = self.call_node([generator_file,
|
||||||
self.generator,
|
self.generator,
|
||||||
json.dumps(self.generator_dependencies),
|
json.dumps(self.generator_dependencies),
|
||||||
json.dumps(str(self.context['the_lcp'].seed)),
|
json.dumps(str(self.context['the_lcp'].seed)),
|
||||||
json.dumps(self.params)]).strip()
|
json.dumps(self.params)]).strip()
|
||||||
@@ -416,18 +444,18 @@ class JavascriptResponse(LoncapaResponse):
|
|||||||
def extract_params(self):
|
def extract_params(self):
|
||||||
|
|
||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
for param in self.xml.xpath('//*[@id=$id]//responseparam',
|
for param in self.xml.xpath('//*[@id=$id]//responseparam',
|
||||||
id=self.xml.get('id')):
|
id=self.xml.get('id')):
|
||||||
|
|
||||||
raw_param = param.get("value")
|
raw_param = param.get("value")
|
||||||
params[param.get("name")] = json.loads(contextualize_text(raw_param, self.context))
|
params[param.get("name")] = json.loads(contextualize_text(raw_param, self.context))
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def prepare_inputfield(self):
|
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')):
|
id=self.xml.get('id')):
|
||||||
|
|
||||||
escapedict = {'"': '"'}
|
escapedict = {'"': '"'}
|
||||||
@@ -454,36 +482,36 @@ class JavascriptResponse(LoncapaResponse):
|
|||||||
else:
|
else:
|
||||||
points = 0
|
points = 0
|
||||||
return CorrectMap(self.answer_id, correctness, npoints=points, msg=evaluation)
|
return CorrectMap(self.answer_id, correctness, npoints=points, msg=evaluation)
|
||||||
|
|
||||||
def run_grader(self, submission):
|
def run_grader(self, submission):
|
||||||
if submission is None or submission == '':
|
if submission is None or submission == '':
|
||||||
submission = json.dumps(None)
|
submission = json.dumps(None)
|
||||||
|
|
||||||
grader_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_grader.js'
|
grader_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_grader.js'
|
||||||
outputs = self.call_node([grader_file,
|
outputs = self.call_node([grader_file,
|
||||||
self.grader,
|
self.grader,
|
||||||
json.dumps(self.grader_dependencies),
|
json.dumps(self.grader_dependencies),
|
||||||
submission,
|
submission,
|
||||||
json.dumps(self.problem_state),
|
json.dumps(self.problem_state),
|
||||||
json.dumps(self.params)]).split('\n')
|
json.dumps(self.params)]).split('\n')
|
||||||
|
|
||||||
all_correct = json.loads(outputs[0].strip())
|
all_correct = json.loads(outputs[0].strip())
|
||||||
evaluation = outputs[1].strip()
|
evaluation = outputs[1].strip()
|
||||||
solution = outputs[2].strip()
|
solution = outputs[2].strip()
|
||||||
return (all_correct, evaluation, solution)
|
return (all_correct, evaluation, solution)
|
||||||
|
|
||||||
def get_answers(self):
|
def get_answers(self):
|
||||||
if self.solution is None:
|
if self.solution is None:
|
||||||
(_, _, self.solution) = self.run_grader(None)
|
(_, _, self.solution) = self.run_grader(None)
|
||||||
|
|
||||||
return {self.answer_id: self.solution}
|
return {self.answer_id: self.solution}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
class ChoiceResponse(LoncapaResponse):
|
class ChoiceResponse(LoncapaResponse):
|
||||||
'''
|
"""
|
||||||
This response type is used when the student chooses from a discrete set of
|
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
|
choices. Currently, to be marked correct, all "correct" choices must be
|
||||||
supplied by the student, and no extraneous choices may be included.
|
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,
|
choices must be given names. This behavior seems like a leaky abstraction,
|
||||||
and it'd be nice to change this at some point.
|
and it'd be nice to change this at some point.
|
||||||
|
|
||||||
'''
|
"""
|
||||||
|
|
||||||
response_tag = 'choiceresponse'
|
response_tag = 'choiceresponse'
|
||||||
max_inputfields = 1
|
max_inputfields = 1
|
||||||
@@ -594,7 +622,8 @@ class MultipleChoiceResponse(LoncapaResponse):
|
|||||||
allowed_inputfields = ['choicegroup']
|
allowed_inputfields = ['choicegroup']
|
||||||
|
|
||||||
def setup_response(self):
|
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)
|
# define correct choices (after calling secondary setup)
|
||||||
xml = self.xml
|
xml = self.xml
|
||||||
@@ -609,7 +638,8 @@ class MultipleChoiceResponse(LoncapaResponse):
|
|||||||
for response in self.xml.xpath("choicegroup"):
|
for response in self.xml.xpath("choicegroup"):
|
||||||
rtype = response.get('type')
|
rtype = response.get('type')
|
||||||
if rtype not in ["MultipleChoice"]:
|
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):
|
for choice in list(response):
|
||||||
if choice.get("name") is None:
|
if choice.get("name") is None:
|
||||||
choice.set("name", "choice_" + str(i))
|
choice.set("name", "choice_" + str(i))
|
||||||
@@ -621,8 +651,10 @@ class MultipleChoiceResponse(LoncapaResponse):
|
|||||||
'''
|
'''
|
||||||
grade student response.
|
grade student response.
|
||||||
'''
|
'''
|
||||||
# log.debug('%s: student_answers=%s, correct_choices=%s' % (unicode(self),student_answers,self.correct_choices))
|
# log.debug('%s: student_answers=%s, correct_choices=%s' % (
|
||||||
if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices:
|
# 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')
|
return CorrectMap(self.answer_id, 'correct')
|
||||||
else:
|
else:
|
||||||
return CorrectMap(self.answer_id, 'incorrect')
|
return CorrectMap(self.answer_id, 'incorrect')
|
||||||
@@ -662,10 +694,14 @@ class OptionResponse(LoncapaResponse):
|
|||||||
'''
|
'''
|
||||||
TODO: handle direction and randomize
|
TODO: handle direction and randomize
|
||||||
'''
|
'''
|
||||||
snippets = [{'snippet': '''<optionresponse direction="vertical" randomize="yes">
|
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="Up">
|
||||||
<optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput>
|
<text>The location of the sky</text>
|
||||||
</optionresponse>'''}]
|
</optioninput>
|
||||||
|
<optioninput options="('Up','Down')" correct="Down">
|
||||||
|
<text>The location of the earth</text>
|
||||||
|
</optioninput>
|
||||||
|
</optionresponse>"""}]
|
||||||
|
|
||||||
response_tag = 'optionresponse'
|
response_tag = 'optionresponse'
|
||||||
hint_tag = 'optionhint'
|
hint_tag = 'optionhint'
|
||||||
@@ -721,12 +757,13 @@ class NumericalResponse(LoncapaResponse):
|
|||||||
'''Grade a numeric response '''
|
'''Grade a numeric response '''
|
||||||
student_answer = student_answers[self.answer_id]
|
student_answer = student_answers[self.answer_id]
|
||||||
try:
|
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.
|
# We should catch this explicitly.
|
||||||
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
|
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
|
||||||
# But we'd need to confirm
|
# But we'd need to confirm
|
||||||
except:
|
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))
|
cgi.escape(student_answer))
|
||||||
|
|
||||||
if correct:
|
if correct:
|
||||||
@@ -734,7 +771,7 @@ class NumericalResponse(LoncapaResponse):
|
|||||||
else:
|
else:
|
||||||
return CorrectMap(self.answer_id, 'incorrect')
|
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):
|
def get_answers(self):
|
||||||
return {self.answer_id: self.correct_answer}
|
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>
|
Custom response. The python code to be run should be in <answer>...</answer>
|
||||||
or in a <script>...</script>
|
or in a <script>...</script>
|
||||||
'''
|
'''
|
||||||
snippets = [{'snippet': '''<customresponse>
|
snippets = [{'snippet': """<customresponse>
|
||||||
<text>
|
<text>
|
||||||
<br/>
|
<br/>
|
||||||
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\)
|
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)"):
|
if not(r=="IS*u(t-t0)"):
|
||||||
correct[0] ='incorrect'
|
correct[0] ='incorrect'
|
||||||
</answer>
|
</answer>
|
||||||
</customresponse>'''},
|
</customresponse>"""},
|
||||||
{'snippet': '''<script type="loncapa/python"><![CDATA[
|
{'snippet': """<script type="loncapa/python"><![CDATA[
|
||||||
|
|
||||||
def sympy_check2():
|
def sympy_check2():
|
||||||
messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','<'))
|
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">
|
<customresponse cfn="sympy_check2" type="cs" expect="2.27E-39" dojs="math" size="30" answer="2.27E-39">
|
||||||
<textline size="40" dojs="math" />
|
<textline size="40" dojs="math" />
|
||||||
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
|
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
|
||||||
</customresponse>'''}]
|
</customresponse>"""}]
|
||||||
|
|
||||||
response_tag = 'customresponse'
|
response_tag = 'customresponse'
|
||||||
allowed_inputfields = ['textline', 'textbox']
|
allowed_inputfields = ['textline', 'textbox']
|
||||||
@@ -830,7 +867,8 @@ def sympy_check2():
|
|||||||
|
|
||||||
log.debug('answer_ids=%s' % self.answer_ids)
|
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
|
self.code = None
|
||||||
answer = None
|
answer = None
|
||||||
try:
|
try:
|
||||||
@@ -838,8 +876,9 @@ def sympy_check2():
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
# print "xml = ",etree.tostring(xml,pretty_print=True)
|
# 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
|
# if we have a "cfn" attribute then look for the function specified by cfn, in
|
||||||
# ie the comparison function is defined in the <script>...</script> stanza instead
|
# the problem context ie the comparison function is defined in the
|
||||||
|
# <script>...</script> stanza instead
|
||||||
cfn = xml.get('cfn')
|
cfn = xml.get('cfn')
|
||||||
if cfn:
|
if cfn:
|
||||||
log.debug("cfn = %s" % cfn)
|
log.debug("cfn = %s" % cfn)
|
||||||
@@ -847,13 +886,14 @@ def sympy_check2():
|
|||||||
self.code = self.context[cfn]
|
self.code = self.context[cfn]
|
||||||
else:
|
else:
|
||||||
msg = "%s: can't find cfn %s in context" % (unicode(self), cfn)
|
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)
|
raise LoncapaProblemError(msg)
|
||||||
|
|
||||||
if not self.code:
|
if not self.code:
|
||||||
if answer is None:
|
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"
|
||||||
log.error("[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid)
|
" code checking script! id=%s" % self.myid)
|
||||||
self.code = ''
|
self.code = ''
|
||||||
else:
|
else:
|
||||||
answer_src = answer.get('src')
|
answer_src = answer.get('src')
|
||||||
@@ -870,43 +910,70 @@ def sympy_check2():
|
|||||||
|
|
||||||
log.debug('%s: student_answers=%s' % (unicode(self), student_answers))
|
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:
|
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:
|
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)
|
msg += '\n idset = %s, error = %s' % (idset, err)
|
||||||
log.error(msg)
|
log.error(msg)
|
||||||
raise Exception(msg)
|
raise Exception(msg)
|
||||||
|
|
||||||
# global variable in context which holds the Presentation MathML from dynamic math input
|
# 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 there is only one box, and it's empty, then don't evaluate
|
||||||
if len(idset) == 1 and not submission[0]:
|
if len(idset) == 1 and not submission[0]:
|
||||||
# default to no error message on empty answer (to be consistent with other responsetypes)
|
# default to no error message on empty answer (to be consistent with other
|
||||||
# but allow author to still have the old behavior by setting empty_answer_err attribute
|
# responsetypes) but allow author to still have the old behavior by setting
|
||||||
msg = '<span class="inline-error">No answer entered!</span>' if self.xml.get('empty_answer_err') else ''
|
# 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)
|
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)
|
correct = ['unknown'] * len(idset)
|
||||||
messages = [''] * len(idset)
|
messages = [''] * len(idset)
|
||||||
|
|
||||||
# put these in the context of the check function evaluator
|
# put these in the context of the check function evaluator
|
||||||
# note that this doesn't help the "cfn" version - only the exec version
|
# note that this doesn't help the "cfn" version - only the exec version
|
||||||
self.context.update({'xml': self.xml, # our subtree
|
self.context.update({
|
||||||
'response_id': self.myid, # my ID
|
# our subtree
|
||||||
'expect': self.expect, # expected answer (if given as attribute)
|
'xml': self.xml,
|
||||||
'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
|
# my ID
|
||||||
'dynamath': dynamath, # ordered list of all javascript inputs in our subtree
|
'response_id': self.myid,
|
||||||
'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
|
# expected answer (if given as attribute)
|
||||||
'messages': messages, # the list of messages to be filled in by the check function
|
'expect': self.expect,
|
||||||
'options': self.xml.get('options'), # any options to be passed to the cfn
|
|
||||||
'testdat': 'hello world',
|
# 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
|
# pass self.system.debug to cfn
|
||||||
self.context['debug'] = self.system.DEBUG
|
self.context['debug'] = self.system.DEBUG
|
||||||
@@ -921,8 +988,10 @@ def sympy_check2():
|
|||||||
print "oops in customresponse (code) error %s" % err
|
print "oops in customresponse (code) error %s" % err
|
||||||
print "context = ", self.context
|
print "context = ", self.context
|
||||||
print traceback.format_exc()
|
print traceback.format_exc()
|
||||||
raise StudentInputError("Error: Problem could not be evaluated with your input") # Notify student
|
# Notify student
|
||||||
else: # self.code is not a string; assume its a function
|
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
|
# this is an interface to the Tutor2 check functions
|
||||||
fn = self.code
|
fn = self.code
|
||||||
@@ -958,7 +1027,8 @@ def sympy_check2():
|
|||||||
msg = '<html>' + msg + '</html>'
|
msg = '<html>' + msg + '</html>'
|
||||||
msg = msg.replace('<', '<')
|
msg = msg.replace('<', '<')
|
||||||
#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 = etree.tostring(fromstring_bs(msg),pretty_print=True)
|
||||||
msg = msg.replace(' ', '')
|
msg = msg.replace(' ', '')
|
||||||
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
|
#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):
|
class CodeResponse(LoncapaResponse):
|
||||||
'''
|
"""
|
||||||
Grade student code using an external queueing server, called 'xqueue'
|
Grade student code using an external queueing server, called 'xqueue'
|
||||||
|
|
||||||
Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse:
|
Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse:
|
||||||
system.xqueue = { 'interface': XqueueInterface object,
|
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)
|
'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)
|
(i.e. and not for getting reference answers)
|
||||||
'''
|
"""
|
||||||
|
|
||||||
response_tag = 'coderesponse'
|
response_tag = 'coderesponse'
|
||||||
allowed_inputfields = ['textbox', 'filesubmission']
|
allowed_inputfields = ['textbox', 'filesubmission']
|
||||||
@@ -1046,7 +1117,8 @@ class CodeResponse(LoncapaResponse):
|
|||||||
TODO: Determines whether in synchronous or asynchronous (queued) mode
|
TODO: Determines whether in synchronous or asynchronous (queued) mode
|
||||||
'''
|
'''
|
||||||
xml = self.xml
|
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'])
|
self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename'])
|
||||||
|
|
||||||
# VS[compat]:
|
# 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:
|
# 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
|
# (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
|
# following the ExternalResponse XML format
|
||||||
penv = {}
|
penv = {}
|
||||||
penv['__builtins__'] = globals()['__builtins__']
|
penv['__builtins__'] = globals()['__builtins__']
|
||||||
@@ -1120,10 +1193,12 @@ class CodeResponse(LoncapaResponse):
|
|||||||
self.answer = penv['answer']
|
self.answer = penv['answer']
|
||||||
self.initial_display = penv['initial_display']
|
self.initial_display = penv['initial_display']
|
||||||
except Exception as err:
|
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)
|
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
|
# The XML tagging of grader_payload is pyxserver-specific
|
||||||
grader_payload = '<pyxserver>'
|
grader_payload = '<pyxserver>'
|
||||||
grader_payload += '<tests>' + tests + '</tests>\n'
|
grader_payload += '<tests>' + tests + '</tests>\n'
|
||||||
@@ -1133,14 +1208,16 @@ class CodeResponse(LoncapaResponse):
|
|||||||
|
|
||||||
def get_score(self, student_answers):
|
def get_score(self, student_answers):
|
||||||
try:
|
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:
|
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)))
|
(err, self.answer_id, convert_files_to_filenames(student_answers)))
|
||||||
raise Exception(err)
|
raise Exception(err)
|
||||||
|
|
||||||
# Prepare xqueue request
|
# Prepare xqueue request
|
||||||
#------------------------------------------------------------
|
#------------------------------------------------------------
|
||||||
|
|
||||||
qinterface = self.system.xqueue['interface']
|
qinterface = self.system.xqueue['interface']
|
||||||
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
||||||
@@ -1149,19 +1226,20 @@ class CodeResponse(LoncapaResponse):
|
|||||||
|
|
||||||
# Generate header
|
# Generate header
|
||||||
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
|
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
|
||||||
anonymous_student_id +
|
anonymous_student_id +
|
||||||
self.answer_id)
|
self.answer_id)
|
||||||
xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'],
|
xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'],
|
||||||
lms_key=queuekey,
|
lms_key=queuekey,
|
||||||
queue_name=self.queue_name)
|
queue_name=self.queue_name)
|
||||||
|
|
||||||
# Generate body
|
# Generate body
|
||||||
if is_list_of_files(submission):
|
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:
|
else:
|
||||||
self.context.update({'submission': submission})
|
self.context.update({'submission': submission})
|
||||||
|
|
||||||
contents = self.payload.copy()
|
contents = self.payload.copy()
|
||||||
|
|
||||||
# Metadata related to the student submission revealed to the external grader
|
# Metadata related to the student submission revealed to the external grader
|
||||||
student_info = {'anonymous_student_id': anonymous_student_id,
|
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
|
# Submit request. When successful, 'msg' is the prior length of the queue
|
||||||
if is_list_of_files(submission):
|
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,
|
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||||
body=json.dumps(contents),
|
body=json.dumps(contents),
|
||||||
files_to_upload=submission)
|
files_to_upload=submission)
|
||||||
@@ -1182,44 +1261,51 @@ class CodeResponse(LoncapaResponse):
|
|||||||
|
|
||||||
# State associated with the queueing request
|
# State associated with the queueing request
|
||||||
queuestate = {'key': queuekey,
|
queuestate = {'key': queuekey,
|
||||||
'time': qtime,
|
'time': qtime,}
|
||||||
}
|
|
||||||
|
|
||||||
cmap = CorrectMap()
|
cmap = CorrectMap()
|
||||||
if error:
|
if error:
|
||||||
cmap.set(self.answer_id, queuestate=None,
|
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:
|
else:
|
||||||
# Queueing mechanism flags:
|
# Queueing mechanism flags:
|
||||||
# 1) Backend: Non-null CorrectMap['queuestate'] indicates that the problem has been queued
|
# 1) Backend: Non-null CorrectMap['queuestate'] indicates that
|
||||||
# 2) Frontend: correctness='incomplete' eventually trickles down through inputtypes.textbox
|
# the problem has been queued
|
||||||
# and .filesubmission to inform the browser to poll the LMS
|
# 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)
|
cmap.set(self.answer_id, queuestate=queuestate, correctness='incomplete', msg=msg)
|
||||||
|
|
||||||
return cmap
|
return cmap
|
||||||
|
|
||||||
def update_score(self, score_msg, oldcmap, queuekey):
|
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:
|
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
|
return oldcmap
|
||||||
|
|
||||||
correctness = 'correct' if correct else 'incorrect'
|
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.
|
# Replace 'oldcmap' with new grading results if queuekey matches. If queuekey
|
||||||
# If queuekey does not match, we keep waiting for the score_msg whose key actually matches
|
# does not match, we keep waiting for the score_msg whose key actually matches
|
||||||
if oldcmap.is_right_queuekey(self.answer_id, queuekey):
|
if oldcmap.is_right_queuekey(self.answer_id, queuekey):
|
||||||
# Sanity check on returned points
|
# Sanity check on returned points
|
||||||
if points < 0:
|
if points < 0:
|
||||||
points = 0
|
points = 0
|
||||||
elif points > self.maxpoints[self.answer_id]:
|
elif points > self.maxpoints[self.answer_id]:
|
||||||
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:
|
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
|
return oldcmap
|
||||||
|
|
||||||
@@ -1231,7 +1317,7 @@ class CodeResponse(LoncapaResponse):
|
|||||||
return {self.answer_id: self.initial_display}
|
return {self.answer_id: self.initial_display}
|
||||||
|
|
||||||
def _parse_score_msg(self, score_msg):
|
def _parse_score_msg(self, score_msg):
|
||||||
'''
|
"""
|
||||||
Grader reply is a JSON-dump of the following dict
|
Grader reply is a JSON-dump of the following dict
|
||||||
{ 'correct': True/False,
|
{ 'correct': True/False,
|
||||||
'score': Numeric value (floating point is okay) to assign to answer
|
'score': Numeric value (floating point is okay) to assign to answer
|
||||||
@@ -1242,22 +1328,25 @@ class CodeResponse(LoncapaResponse):
|
|||||||
correct: Correctness of submission (Boolean)
|
correct: Correctness of submission (Boolean)
|
||||||
score: Points to be assigned (numeric, can be float)
|
score: Points to be assigned (numeric, can be float)
|
||||||
msg: Message from grader to display to student (string)
|
msg: Message from grader to display to student (string)
|
||||||
'''
|
"""
|
||||||
fail = (False, False, 0, '')
|
fail = (False, False, 0, '')
|
||||||
try:
|
try:
|
||||||
score_result = json.loads(score_msg)
|
score_result = json.loads(score_msg)
|
||||||
except (TypeError, ValueError):
|
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
|
return fail
|
||||||
if not isinstance(score_result, dict):
|
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
|
return fail
|
||||||
for tag in ['correct', 'score', 'msg']:
|
for tag in ['correct', 'score', 'msg']:
|
||||||
if tag not in score_result:
|
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
|
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.
|
# is safe for the LMS.
|
||||||
# 1) Make sure that the message is valid XML (proper opening/closing tags)
|
# 1) Make sure that the message is valid XML (proper opening/closing tags)
|
||||||
# 2) TODO: Is the message actually HTML?
|
# 2) TODO: Is the message actually HTML?
|
||||||
@@ -1265,11 +1354,12 @@ class CodeResponse(LoncapaResponse):
|
|||||||
try:
|
try:
|
||||||
etree.fromstring(msg)
|
etree.fromstring(msg)
|
||||||
except etree.XMLSyntaxError as err:
|
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 fail
|
||||||
|
|
||||||
return (True, score_result['correct'], score_result['score'], msg)
|
return (True, score_result['correct'], score_result['score'], msg)
|
||||||
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -1325,9 +1415,9 @@ main()
|
|||||||
|
|
||||||
def setup_response(self):
|
def setup_response(self):
|
||||||
xml = self.xml
|
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')
|
answer = xml.find('answer')
|
||||||
if answer is not None:
|
if answer is not None:
|
||||||
answer_src = answer.get('src')
|
answer_src = answer.get('src')
|
||||||
@@ -1335,7 +1425,8 @@ main()
|
|||||||
self.code = self.system.filesystem.open('src/' + answer_src).read()
|
self.code = self.system.filesystem.open('src/' + answer_src).read()
|
||||||
else:
|
else:
|
||||||
self.code = answer.text
|
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']
|
self.code = self.context['script_code']
|
||||||
if not self.code:
|
if not self.code:
|
||||||
msg = '%s: Missing answer script code for externalresponse' % unicode(self)
|
msg = '%s: Missing answer script code for externalresponse' % unicode(self)
|
||||||
@@ -1362,19 +1453,22 @@ main()
|
|||||||
payload.update(extra_payload)
|
payload.update(extra_payload)
|
||||||
|
|
||||||
try:
|
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:
|
except Exception as err:
|
||||||
msg = 'Error %s - cannot connect to external server url=%s' % (err, self.url)
|
msg = 'Error %s - cannot connect to external server url=%s' % (err, self.url)
|
||||||
log.error(msg)
|
log.error(msg)
|
||||||
raise Exception(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()):
|
if (not r.text) or (not r.text.strip()):
|
||||||
raise Exception('Error: no response from external server url=%s' % self.url)
|
raise Exception('Error: no response from external server url=%s' % self.url)
|
||||||
|
|
||||||
try:
|
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:
|
except Exception as err:
|
||||||
msg = 'Error %s - cannot parse response from external server r.text=%s' % (err, r.text)
|
msg = 'Error %s - cannot parse response from external server r.text=%s' % (err, r.text)
|
||||||
log.error(msg)
|
log.error(msg)
|
||||||
@@ -1388,7 +1482,8 @@ main()
|
|||||||
try:
|
try:
|
||||||
submission = [student_answers[k] for k in idset]
|
submission = [student_answers[k] for k in idset]
|
||||||
except Exception as err:
|
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)
|
raise Exception(err)
|
||||||
|
|
||||||
self.context.update({'submission': submission})
|
self.context.update({'submission': submission})
|
||||||
@@ -1401,7 +1496,9 @@ main()
|
|||||||
log.error('Error %s' % err)
|
log.error('Error %s' % err)
|
||||||
if self.system.DEBUG:
|
if self.system.DEBUG:
|
||||||
cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset))))
|
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
|
return cmap
|
||||||
|
|
||||||
ad = rxml.find('awarddetail').text
|
ad = rxml.find('awarddetail').text
|
||||||
@@ -1435,7 +1532,8 @@ main()
|
|||||||
exans[0] = msg
|
exans[0] = msg
|
||||||
|
|
||||||
if not (len(exans) == len(self.answer_ids)):
|
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')
|
raise Exception('Short response from external server')
|
||||||
return dict(zip(self.answer_ids, exans))
|
return dict(zip(self.answer_ids, exans))
|
||||||
|
|
||||||
@@ -1487,11 +1585,14 @@ class FormulaResponse(LoncapaResponse):
|
|||||||
typeslist = []
|
typeslist = []
|
||||||
else:
|
else:
|
||||||
typeslist = ts.split(',')
|
typeslist = ts.split(',')
|
||||||
if 'ci' in typeslist: # Case insensitive
|
if 'ci' in typeslist:
|
||||||
|
# Case insensitive
|
||||||
self.case_sensitive = False
|
self.case_sensitive = False
|
||||||
elif 'cs' in typeslist: # Case sensitive
|
elif 'cs' in typeslist:
|
||||||
|
# Case sensitive
|
||||||
self.case_sensitive = True
|
self.case_sensitive = True
|
||||||
else: # Default
|
else:
|
||||||
|
# Default
|
||||||
self.case_sensitive = False
|
self.case_sensitive = False
|
||||||
|
|
||||||
def get_score(self, student_answers):
|
def get_score(self, student_answers):
|
||||||
@@ -1509,12 +1610,14 @@ class FormulaResponse(LoncapaResponse):
|
|||||||
for i in range(numsamples):
|
for i in range(numsamples):
|
||||||
instructor_variables = self.strip_dict(dict(self.context))
|
instructor_variables = self.strip_dict(dict(self.context))
|
||||||
student_variables = dict()
|
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])
|
value = random.uniform(*ranges[var])
|
||||||
instructor_variables[str(var)] = value
|
instructor_variables[str(var)] = value
|
||||||
student_variables[str(var)] = value
|
student_variables[str(var)] = value
|
||||||
#log.debug('formula: instructor_vars=%s, expected=%s' % (instructor_variables,expected))
|
#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:
|
try:
|
||||||
#log.debug('formula: student_vars=%s, given=%s' % (student_variables,given))
|
#log.debug('formula: student_vars=%s, given=%s' % (student_variables,given))
|
||||||
student_result = evaluator(student_variables,
|
student_result = evaluator(student_variables,
|
||||||
@@ -1540,9 +1643,9 @@ class FormulaResponse(LoncapaResponse):
|
|||||||
keys and all non-numeric values stripped out. All values also
|
keys and all non-numeric values stripped out. All values also
|
||||||
converted to float. Used so we can safely use Python contexts.
|
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 \
|
d = dict([(k, numpy.complex(d[k])) for k in d if type(k) == str and
|
||||||
k.isalnum() and \
|
k.isalnum() and
|
||||||
isinstance(d[k], numbers.Number)])
|
isinstance(d[k], numbers.Number)])
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def check_hint_condition(self, hxml_set, student_answers):
|
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 = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0]
|
||||||
answer_src = answer.get('src')
|
answer_src = answer.get('src')
|
||||||
if answer_src is not None:
|
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:
|
else:
|
||||||
self.code = answer.text
|
self.code = answer.text
|
||||||
|
|
||||||
@@ -1633,17 +1737,19 @@ class ImageResponse(LoncapaResponse):
|
|||||||
|
|
||||||
# parse expected answer
|
# parse expected answer
|
||||||
# TODO: Compile regexp on file load
|
# 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:
|
if not m:
|
||||||
msg = 'Error in problem specification! cannot parse rectangle in %s' % (etree.tostring(self.ielements[aid],
|
msg = 'Error in problem specification! cannot parse rectangle in %s' % (
|
||||||
pretty_print=True))
|
etree.tostring(self.ielements[aid], pretty_print=True))
|
||||||
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
|
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
|
||||||
(llx, lly, urx, ury) = [int(x) for x in m.groups()]
|
(llx, lly, urx, ury) = [int(x) for x in m.groups()]
|
||||||
|
|
||||||
# parse given answer
|
# parse given answer
|
||||||
m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
|
m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
|
||||||
if not m:
|
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()]
|
(gx, gy) = [int(x) for x in m.groups()]
|
||||||
|
|
||||||
# answer is correct if (x,y) is within the specified rectangle
|
# answer is correct if (x,y) is within the specified rectangle
|
||||||
@@ -1660,4 +1766,17 @@ class ImageResponse(LoncapaResponse):
|
|||||||
# TEMPORARY: List of all response subclasses
|
# TEMPORARY: List of all response subclasses
|
||||||
# FIXME: To be replaced by auto-registration
|
# 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
|
Duck typing to check if 'file_to_test' is a File object
|
||||||
'''
|
'''
|
||||||
is_file = True
|
return all(hasattr(file_to_test, method) for method in ['read', 'name'])
|
||||||
for method in ['read', 'name']:
|
|
||||||
if not hasattr(file_to_test, method):
|
|
||||||
is_file = False
|
|
||||||
return is_file
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ dateformat = '%Y%m%d%H%M%S'
|
|||||||
|
|
||||||
def make_hashkey(seed):
|
def make_hashkey(seed):
|
||||||
'''
|
'''
|
||||||
Generate a string key by hashing
|
Generate a string key by hashing
|
||||||
'''
|
'''
|
||||||
h = hashlib.md5()
|
h = hashlib.md5()
|
||||||
h.update(str(seed))
|
h.update(str(seed))
|
||||||
@@ -20,27 +20,27 @@ def make_hashkey(seed):
|
|||||||
|
|
||||||
|
|
||||||
def make_xheader(lms_callback_url, lms_key, queue_name):
|
def make_xheader(lms_callback_url, lms_key, queue_name):
|
||||||
'''
|
"""
|
||||||
Generate header for delivery and reply of queue request.
|
Generate header for delivery and reply of queue request.
|
||||||
|
|
||||||
Xqueue header is a JSON-serialized dict:
|
Xqueue header is a JSON-serialized dict:
|
||||||
{ 'lms_callback_url': url to which xqueue will return the request (string),
|
{ '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)
|
'queue_name': designate a specific queue within xqueue server, e.g. 'MITx-6.00x' (string)
|
||||||
}
|
}
|
||||||
'''
|
"""
|
||||||
return json.dumps({ 'lms_callback_url': lms_callback_url,
|
return json.dumps({ 'lms_callback_url': lms_callback_url,
|
||||||
'lms_key': lms_key,
|
'lms_key': lms_key,
|
||||||
'queue_name': queue_name })
|
'queue_name': queue_name })
|
||||||
|
|
||||||
|
|
||||||
def parse_xreply(xreply):
|
def parse_xreply(xreply):
|
||||||
'''
|
"""
|
||||||
Parse the reply from xqueue. Messages are JSON-serialized dict:
|
Parse the reply from xqueue. Messages are JSON-serialized dict:
|
||||||
{ 'return_code': 0 (success), 1 (fail)
|
{ 'return_code': 0 (success), 1 (fail)
|
||||||
'content': Message from xqueue (string)
|
'content': Message from xqueue (string)
|
||||||
}
|
}
|
||||||
'''
|
"""
|
||||||
try:
|
try:
|
||||||
xreply = json.loads(xreply)
|
xreply = json.loads(xreply)
|
||||||
except ValueError, err:
|
except ValueError, err:
|
||||||
@@ -61,11 +61,11 @@ class XQueueInterface(object):
|
|||||||
self.url = url
|
self.url = url
|
||||||
self.auth = django_auth
|
self.auth = django_auth
|
||||||
self.session = requests.session(auth=requests_auth)
|
self.session = requests.session(auth=requests_auth)
|
||||||
|
|
||||||
def send_to_queue(self, header, body, files_to_upload=None):
|
def send_to_queue(self, header, body, files_to_upload=None):
|
||||||
'''
|
"""
|
||||||
Submit a request to xqueue.
|
Submit a request to xqueue.
|
||||||
|
|
||||||
header: JSON-serialized dict in the format described in 'xqueue_interface.make_xheader'
|
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
|
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
|
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
|
Returns (error_code, msg) where error_code != 0 indicates an error
|
||||||
'''
|
"""
|
||||||
# Attempt to send to queue
|
# Attempt to send to queue
|
||||||
(error, msg) = self._send_to_queue(header, body, files_to_upload)
|
(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()
|
self._login()
|
||||||
if files_to_upload is not None:
|
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)
|
f.seek(0)
|
||||||
(error, msg) = self._send_to_queue(header, body, files_to_upload)
|
(error, msg) = self._send_to_queue(header, body, files_to_upload)
|
||||||
|
|
||||||
@@ -91,18 +93,18 @@ class XQueueInterface(object):
|
|||||||
def _login(self):
|
def _login(self):
|
||||||
payload = { 'username': self.auth['username'],
|
payload = { 'username': self.auth['username'],
|
||||||
'password': self.auth['password'] }
|
'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):
|
def _send_to_queue(self, header, body, files_to_upload):
|
||||||
payload = {'xqueue_header': header,
|
payload = {'xqueue_header': header,
|
||||||
'xqueue_body' : body}
|
'xqueue_body' : body}
|
||||||
files = {}
|
files = {}
|
||||||
if files_to_upload is not None:
|
if files_to_upload is not None:
|
||||||
for f in files_to_upload:
|
for f in files_to_upload:
|
||||||
files.update({ f.name: f })
|
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):
|
def _http_post(self, url, data, files=None):
|
||||||
@@ -111,7 +113,7 @@ class XQueueInterface(object):
|
|||||||
except requests.exceptions.ConnectionError, err:
|
except requests.exceptions.ConnectionError, err:
|
||||||
log.error(err)
|
log.error(err)
|
||||||
return (1, 'cannot connect to server')
|
return (1, 'cannot connect to server')
|
||||||
|
|
||||||
if r.status_code not in [200]:
|
if r.status_code not in [200]:
|
||||||
return (1, 'unexpected HTTP status code [%d]' % r.status_code)
|
return (1, 'unexpected HTTP status code [%d]' % r.status_code)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user