From 775456b987c6504b4772f987bbbca2fb77196106 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 8 Oct 2012 10:54:59 -0400 Subject: [PATCH] formatting cleanups in capa_problem.py --- common/lib/capa/capa/capa_problem.py | 181 ++++++++++++++++++--------- 1 file changed, 125 insertions(+), 56 deletions(-) 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'):