diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index 626ad48c36..9a5a15a696 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -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
+ # Convert startouttext and endouttext to proper
+ 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 tags
+ # parse problem XML file into an element tree
+ self.tree = etree.XML(problem_text)
+
+ # handle any 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 ... stanzas
for entry in self.tree.xpath("//" + "|//".join(solution_types)):
answer = etree.tostring(entry)
- if answer: answer_map[entry.get('id')] = contextualize_text(answer, self.context)
+ if answer:
+ answer_map[entry.get('id')] = contextualize_text(answer, self.context)
log.debug('answer_map = %s' % answer_map)
return answer_map
def get_answer_ids(self):
- """Return the IDs of all the responses -- these are the keys used for
+ """
+ Return the IDs of all the responses -- these are the keys used for
the dicts returned by grade_answers and get_question_answers. (Though
- get_question_answers may only return a subset of these."""
+ get_question_answers may only return a subset of these.
+ """
answer_ids = []
for response in self.responders.keys():
results = self.responder_answers[response]
@@ -298,7 +338,8 @@ class LoncapaProblem(object):
file = inc.get('file')
if file is not None:
try:
- ifp = self.system.filestore.open(file) # open using ModuleSystem OSFS filestore
+ # open using ModuleSystem OSFS filestore
+ ifp = self.system.filestore.open(file)
except Exception as err:
log.warning('Error %s in problem xml include: %s' % (
err, etree.tostring(inc, pretty_print=True)))
@@ -311,7 +352,8 @@ class LoncapaProblem(object):
else:
continue
try:
- incxml = etree.XML(ifp.read()) # read in and convert to XML
+ # read in and convert to XML
+ incxml = etree.XML(ifp.read())
except Exception as err:
log.warning('Error %s in problem xml include: %s' % (
err, etree.tostring(inc, pretty_print=True)))
@@ -322,6 +364,7 @@ class LoncapaProblem(object):
raise
else:
continue
+
# insert new XML into tree in place of inlcude
parent = inc.getparent()
parent.insert(parent.index(inc), incxml)
@@ -329,11 +372,13 @@ class LoncapaProblem(object):
log.debug('Included %s into %s' % (file, self.problem_id))
def _extract_system_path(self, script):
- '''
+ """
Extracts and normalizes additional paths for code execution.
For now, there's a default path of data/course/code; this may be removed
at some point.
- '''
+
+ script : ?? (TODO)
+ """
DEFAULT_PATH = ['code']
@@ -351,7 +396,6 @@ class LoncapaProblem(object):
# path is an absolute path or a path relative to the data dir
dir = os.path.join(self.system.filestore.root_path, dir)
abs_dir = os.path.normpath(dir)
- #log.debug("appending to path: %s" % abs_dir)
path.append(abs_dir)
return path
@@ -362,13 +406,20 @@ class LoncapaProblem(object):
context of this problem. Provides ability to randomize problems, and also set
variables for problem answer checking.
- Problem XML goes to Python execution context. Runs everything in script tags
+ Problem XML goes to Python execution context. Runs everything in script tags.
'''
random.seed(self.seed)
- context = {'global_context': global_context} # save global context in here also
- context.update(global_context) # initialize context to have stuff in global_context
- context['__builtins__'] = globals()['__builtins__'] # put globals there also
- context['the_lcp'] = self # pass instance of LoncapaProblem in
+ # save global context in here also
+ context = {'global_context': global_context}
+
+ # initialize context to have stuff in global_context
+ context.update(global_context)
+
+ # put globals there also
+ context['__builtins__'] = globals()['__builtins__']
+
+ # pass instance of LoncapaProblem in
+ context['the_lcp'] = self
context['script_code'] = ''
self._execute_scripts(tree.findall('.//script'), context)
@@ -395,12 +446,14 @@ class LoncapaProblem(object):
code = script.text
XMLESC = {"'": "'", """: '"'}
code = unescape(code, XMLESC)
- context['script_code'] += code # store code source in context
+ # store code source in context
+ context['script_code'] += code
try:
- exec code in context, context # use "context" for global context; thus defs in code are global within code
+ # use "context" for global context; thus defs in code are global within code
+ exec code in context, context
except Exception as err:
log.exception("Error while execing script code: " + code)
- msg = "Error while executing script code: %s" % str(err).replace('<','<')
+ msg = "Error while executing script code: %s" % str(err).replace('<','<')
raise responsetypes.LoncapaProblemError(msg)
finally:
sys.path = original_path
@@ -415,7 +468,8 @@ class LoncapaProblem(object):
Used by get_html.
'''
- if problemtree.tag == 'script' and problemtree.get('type') and 'javascript' in problemtree.get('type'):
+ if (problemtree.tag == 'script' and problemtree.get('type')
+ and 'javascript' in problemtree.get('type')):
# leave javascript intact.
return problemtree
@@ -453,21 +507,26 @@ class LoncapaProblem(object):
}
},
use='capa_input')
- return render_object.get_html() # function(problemtree, value, status, msg) # render the special response (textline, schematic,...)
+ # function(problemtree, value, status, msg)
+ # render the special response (textline, schematic,...)
+ return render_object.get_html()
- if problemtree in self.responders: # let each Response render itself
+ # let each Response render itself
+ if problemtree in self.responders:
return self.responders[problemtree].render_html(self._extract_html)
tree = etree.Element(problemtree.tag)
for item in problemtree:
- item_xhtml = self._extract_html(item) # nothing special: recurse
+ # render child recursively
+ item_xhtml = self._extract_html(item)
if item_xhtml is not None:
tree.append(item_xhtml)
if tree.tag in html_transforms:
tree.tag = html_transforms[problemtree.tag]['tag']
else:
- for (key, value) in problemtree.items(): # copy attributes over if not innocufying
+ # copy attributes over if not innocufying
+ for (key, value) in problemtree.items():
tree.set(key, value)
tree.text = problemtree.text
@@ -490,31 +549,41 @@ class LoncapaProblem(object):
self.responders = {}
for response in tree.xpath('//' + "|//".join(response_tag_dict)):
response_id_str = self.problem_id + "_" + str(response_id)
- response.set('id', response_id_str) # create and save ID for this response
+ # create and save ID for this response
+ response.set('id', response_id_str)
response_id += 1
answer_id = 1
- inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]),
+ inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x
+ for x in (entry_types + solution_types)]),
id=response_id_str)
- for entry in inputfields: # assign one answer_id for each entry_type or solution_type
+
+ # assign one answer_id for each entry_type or solution_type
+ for entry in inputfields:
entry.attrib['response_id'] = str(response_id)
entry.attrib['answer_id'] = str(answer_id)
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
answer_id = answer_id + 1
- responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
- self.responders[response] = responder # save in list in self
+ # instantiate capa Response
+ responder = response_tag_dict[response.tag](response, inputfields,
+ self.context, self.system)
+ # save in list in self
+ self.responders[response] = responder
- # get responder answers (do this only once, since there may be a performance cost, eg with externalresponse)
+ # get responder answers (do this only once, since there may be a performance cost,
+ # eg with externalresponse)
self.responder_answers = {}
for response in self.responders.keys():
try:
self.responder_answers[response] = self.responders[response].get_answers()
except:
- log.debug('responder %s failed to properly return get_answers()' % self.responders[response]) # FIXME
+ log.debug('responder %s failed to properly return get_answers()',
+ self.responders[response]) # FIXME
raise
- # ... may not be associated with any specific response; give IDs for those separately
+ # ... 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'):