Commit 1300035e by Victor Shnayder

formatting cleanups in capa_problem.py

parent ed8620dc
......@@ -3,8 +3,9 @@
#
# Nomenclature:
#
# A capa Problem is a collection of text and capa Response questions. Each Response may have one or more
# Input entry fields. The capa Problem may include a solution.
# A capa Problem is a collection of text and capa Response questions.
# Each Response may have one or more Input entry fields.
# The capa problem may include a solution.
#
'''
Main module which shows problems (of "capa" type).
......@@ -42,9 +43,23 @@ import responsetypes
# dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
entry_types = ['textline', 'schematic', 'textbox', 'imageinput', 'optioninput', 'choicegroup', 'radiogroup', 'checkboxgroup', 'filesubmission', 'javascriptinput']
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
response_properties = ["codeparam", "responseparam", "answer"] # these get captured as student responses
# Different ways students can input code
entry_types = ['textline',
'schematic',
'textbox',
'imageinput',
'optioninput',
'choicegroup',
'radiogroup',
'checkboxgroup',
'filesubmission',
'javascriptinput',]
# extra things displayed after "show answers" is pressed
solution_types = ['solution']
# these get captured as student responses
response_properties = ["codeparam", "responseparam", "answer"]
# special problem tags which should be turned into innocuous HTML
html_transforms = {'problem': {'tag': 'div'},
......@@ -83,7 +98,8 @@ class LoncapaProblem(object):
- id (string): identifier for this problem; often a filename (no spaces)
- state (dict): student state
- seed (int): random number generator seed (int)
- system (ModuleSystem): ModuleSystem instance which provides OS, rendering, and user context
- system (ModuleSystem): ModuleSystem instance which provides OS,
rendering, and user context
'''
......@@ -107,19 +123,24 @@ class LoncapaProblem(object):
if not self.seed:
self.seed = struct.unpack('i', os.urandom(4))[0]
problem_text = re.sub("startouttext\s*/", "text", problem_text) # Convert startouttext and endouttext to proper <text></text>
# Convert startouttext and endouttext to proper <text></text>
problem_text = re.sub("startouttext\s*/", "text", problem_text)
problem_text = re.sub("endouttext\s*/", "/text", problem_text)
self.problem_text = problem_text
self.tree = etree.XML(problem_text) # parse problem XML file into an element tree
self._process_includes() # handle any <include file="foo"> tags
# parse problem XML file into an element tree
self.tree = etree.XML(problem_text)
# handle any <include file="foo"> tags
self._process_includes()
# construct script processor context (eg for customresponse problems)
self.context = self._extract_context(self.tree, seed=self.seed)
# pre-parse the XML tree: modifies it to add ID's and perform some in-place transformations
# this also creates the dict (self.responders) of Response instances for each question in the problem.
# the dict has keys = xml subtree of Response, values = Response instance
# Pre-parse the XML tree: modifies it to add ID's and perform some in-place
# transformations. This also creates the dict (self.responders) of Response
# instances for each question in the problem. The dict has keys = xml subtree of
# Response, values = Response instance
self._preprocess_problem(self.tree)
if not self.student_answers: # True when student_answers is an empty dict
......@@ -134,6 +155,9 @@ class LoncapaProblem(object):
self.done = False
def set_initial_display(self):
"""
Set the student's answers to the responders' initial displays, if specified.
"""
initial_answers = dict()
for responder in self.responders.values():
if hasattr(responder, 'get_initial_display'):
......@@ -145,9 +169,11 @@ class LoncapaProblem(object):
return u"LoncapaProblem ({0})".format(self.problem_id)
def get_state(self):
''' Stored per-user session data neeeded to:
'''
Stored per-user session data neeeded to:
1) Recreate the problem
2) Populate any student answers. '''
2) Populate any student answers.
'''
return {'seed': self.seed,
'student_answers': self.student_answers,
......@@ -156,7 +182,7 @@ class LoncapaProblem(object):
def get_max_score(self):
'''
Return maximum score for this problem.
Return the maximum score for this problem.
'''
maxscore = 0
for response, responder in self.responders.iteritems():
......@@ -164,11 +190,11 @@ class LoncapaProblem(object):
return maxscore
def get_score(self):
'''
"""
Compute score for this problem. The score is the number of points awarded.
Returns a dictionary {'score': integer, from 0 to get_max_score(),
'total': get_max_score()}.
'''
"""
correct = 0
for key in self.correct_map:
try:
......@@ -204,22 +230,25 @@ class LoncapaProblem(object):
def is_queued(self):
'''
Returns True if any part of the problem has been submitted to an external queue
(e.g. for grading.)
'''
return any(self.correct_map.is_queued(answer_id) for answer_id in self.correct_map)
def get_recentmost_queuetime(self):
'''
Returns a DateTime object that represents the timestamp of the most recent queueing request, or None if not queued
Returns a DateTime object that represents the timestamp of the most recent
queueing request, or None if not queued
'''
if not self.is_queued():
return None
# Get a list of timestamps of all queueing requests, then convert it to a DateTime object
queuetime_strs = [self.correct_map.get_queuetime_str(answer_id)
for answer_id in self.correct_map
for answer_id in self.correct_map
if self.correct_map.is_queued(answer_id)]
queuetimes = [datetime.strptime(qt_str, xqueue_interface.dateformat) for qt_str in queuetime_strs]
queuetimes = [datetime.strptime(qt_str, xqueue_interface.dateformat)
for qt_str in queuetime_strs]
return max(queuetimes)
......@@ -235,14 +264,20 @@ class LoncapaProblem(object):
Calls the Response for each question in this problem, to do the actual grading.
'''
# if answers include File objects, convert them to filenames.
self.student_answers = convert_files_to_filenames(answers)
oldcmap = self.correct_map # old CorrectMap
newcmap = CorrectMap() # start new with empty CorrectMap
# old CorrectMap
oldcmap = self.correct_map
# start new with empty CorrectMap
newcmap = CorrectMap()
# log.debug('Responders: %s' % self.responders)
for responder in self.responders.values(): # Call each responsetype instance to do actual grading
if 'filesubmission' in responder.allowed_inputfields: # File objects are passed only if responsetype
# explicitly allows for file submissions
# Call each responsetype instance to do actual grading
for responder in self.responders.values():
# File objects are passed only if responsetype explicitly allows for file
# submissions
if 'filesubmission' in responder.allowed_inputfields:
results = responder.evaluate_answers(answers, oldcmap)
else:
results = responder.evaluate_answers(convert_files_to_filenames(answers), oldcmap)
......@@ -252,28 +287,33 @@ class LoncapaProblem(object):
return newcmap
def get_question_answers(self):
"""Returns a dict of answer_ids to answer values. If we cannot generate
"""
Returns a dict of answer_ids to answer values. If we cannot generate
an answer (this sometimes happens in customresponses), that answer_id is
not included. Called by "show answers" button JSON request
(see capa_module)
"""
# dict of (id, correct_answer)
answer_map = dict()
for response in self.responders.keys():
results = self.responder_answers[response]
answer_map.update(results) # dict of (id,correct_answer)
answer_map.update(results)
# include solutions from <solution>...</solution> stanzas
for entry in self.tree.xpath("//" + "|//".join(solution_types)):
answer = etree.tostring(entry)
if answer: answer_map[entry.get('id')] = contextualize_text(answer, self.context)
if answer:
answer_map[entry.get('id')] = contextualize_text(answer, self.context)
log.debug('answer_map = %s' % answer_map)
return answer_map
def get_answer_ids(self):
"""Return the IDs of all the responses -- these are the keys used for
"""
Return the IDs of all the responses -- these are the keys used for
the dicts returned by grade_answers and get_question_answers. (Though
get_question_answers may only return a subset of these."""
get_question_answers may only return a subset of these.
"""
answer_ids = []
for response in self.responders.keys():
results = self.responder_answers[response]
......@@ -298,7 +338,8 @@ class LoncapaProblem(object):
file = inc.get('file')
if file is not None:
try:
ifp = self.system.filestore.open(file) # open using ModuleSystem OSFS filestore
# open using ModuleSystem OSFS filestore
ifp = self.system.filestore.open(file)
except Exception as err:
log.warning('Error %s in problem xml include: %s' % (
err, etree.tostring(inc, pretty_print=True)))
......@@ -311,7 +352,8 @@ class LoncapaProblem(object):
else:
continue
try:
incxml = etree.XML(ifp.read()) # read in and convert to XML
# read in and convert to XML
incxml = etree.XML(ifp.read())
except Exception as err:
log.warning('Error %s in problem xml include: %s' % (
err, etree.tostring(inc, pretty_print=True)))
......@@ -322,6 +364,7 @@ class LoncapaProblem(object):
raise
else:
continue
# insert new XML into tree in place of inlcude
parent = inc.getparent()
parent.insert(parent.index(inc), incxml)
......@@ -329,11 +372,13 @@ class LoncapaProblem(object):
log.debug('Included %s into %s' % (file, self.problem_id))
def _extract_system_path(self, script):
'''
"""
Extracts and normalizes additional paths for code execution.
For now, there's a default path of data/course/code; this may be removed
at some point.
'''
script : ?? (TODO)
"""
DEFAULT_PATH = ['code']
......@@ -351,7 +396,6 @@ class LoncapaProblem(object):
# path is an absolute path or a path relative to the data dir
dir = os.path.join(self.system.filestore.root_path, dir)
abs_dir = os.path.normpath(dir)
#log.debug("appending to path: %s" % abs_dir)
path.append(abs_dir)
return path
......@@ -362,13 +406,20 @@ class LoncapaProblem(object):
context of this problem. Provides ability to randomize problems, and also set
variables for problem answer checking.
Problem XML goes to Python execution context. Runs everything in script tags
Problem XML goes to Python execution context. Runs everything in script tags.
'''
random.seed(self.seed)
context = {'global_context': global_context} # save global context in here also
context.update(global_context) # initialize context to have stuff in global_context
context['__builtins__'] = globals()['__builtins__'] # put globals there also
context['the_lcp'] = self # pass instance of LoncapaProblem in
# save global context in here also
context = {'global_context': global_context}
# initialize context to have stuff in global_context
context.update(global_context)
# put globals there also
context['__builtins__'] = globals()['__builtins__']
# pass instance of LoncapaProblem in
context['the_lcp'] = self
context['script_code'] = ''
self._execute_scripts(tree.findall('.//script'), context)
......@@ -395,12 +446,14 @@ class LoncapaProblem(object):
code = script.text
XMLESC = {"&apos;": "'", "&quot;": '"'}
code = unescape(code, XMLESC)
context['script_code'] += code # store code source in context
# store code source in context
context['script_code'] += code
try:
exec code in context, context # use "context" for global context; thus defs in code are global within code
# use "context" for global context; thus defs in code are global within code
exec code in context, context
except Exception as err:
log.exception("Error while execing script code: " + code)
msg = "Error while executing script code: %s" % str(err).replace('<','&lt;')
msg = "Error while executing script code: %s" % str(err).replace('<','&lt;')
raise responsetypes.LoncapaProblemError(msg)
finally:
sys.path = original_path
......@@ -415,7 +468,8 @@ class LoncapaProblem(object):
Used by get_html.
'''
if problemtree.tag == 'script' and problemtree.get('type') and 'javascript' in problemtree.get('type'):
if (problemtree.tag == 'script' and problemtree.get('type')
and 'javascript' in problemtree.get('type')):
# leave javascript intact.
return problemtree
......@@ -453,21 +507,26 @@ class LoncapaProblem(object):
}
},
use='capa_input')
return render_object.get_html() # function(problemtree, value, status, msg) # render the special response (textline, schematic,...)
# function(problemtree, value, status, msg)
# render the special response (textline, schematic,...)
return render_object.get_html()
if problemtree in self.responders: # let each Response render itself
# let each Response render itself
if problemtree in self.responders:
return self.responders[problemtree].render_html(self._extract_html)
tree = etree.Element(problemtree.tag)
for item in problemtree:
item_xhtml = self._extract_html(item) # nothing special: recurse
# render child recursively
item_xhtml = self._extract_html(item)
if item_xhtml is not None:
tree.append(item_xhtml)
if tree.tag in html_transforms:
tree.tag = html_transforms[problemtree.tag]['tag']
else:
for (key, value) in problemtree.items(): # copy attributes over if not innocufying
# copy attributes over if not innocufying
for (key, value) in problemtree.items():
tree.set(key, value)
tree.text = problemtree.text
......@@ -490,31 +549,41 @@ class LoncapaProblem(object):
self.responders = {}
for response in tree.xpath('//' + "|//".join(response_tag_dict)):
response_id_str = self.problem_id + "_" + str(response_id)
response.set('id', response_id_str) # create and save ID for this response
# create and save ID for this response
response.set('id', response_id_str)
response_id += 1
answer_id = 1
inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]),
inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x
for x in (entry_types + solution_types)]),
id=response_id_str)
for entry in inputfields: # assign one answer_id for each entry_type or solution_type
# assign one answer_id for each entry_type or solution_type
for entry in inputfields:
entry.attrib['response_id'] = str(response_id)
entry.attrib['answer_id'] = str(answer_id)
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
answer_id = answer_id + 1
responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
self.responders[response] = responder # save in list in self
# instantiate capa Response
responder = response_tag_dict[response.tag](response, inputfields,
self.context, self.system)
# save in list in self
self.responders[response] = responder
# get responder answers (do this only once, since there may be a performance cost, eg with externalresponse)
# get responder answers (do this only once, since there may be a performance cost,
# eg with externalresponse)
self.responder_answers = {}
for response in self.responders.keys():
try:
self.responder_answers[response] = self.responders[response].get_answers()
except:
log.debug('responder %s failed to properly return get_answers()' % self.responders[response]) # FIXME
log.debug('responder %s failed to properly return get_answers()',
self.responders[response]) # FIXME
raise
# <solution>...</solution> may not be associated with any specific response; give IDs for those separately
# <solution>...</solution> may not be associated with any specific response; give
# IDs for those separately
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
solution_id = 1
for solution in tree.findall('.//solution'):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment