diff --git a/common/lib/capa/capa/calc.py b/common/lib/capa/capa/calc.py index 7979a33d84..40ac14308e 100644 --- a/common/lib/capa/capa/calc.py +++ b/common/lib/capa/capa/calc.py @@ -48,7 +48,7 @@ general_whitespace = re.compile('[^\w]+') def check_variables(string, variables): - ''' Confirm the only variables in string are defined. + '''Confirm the only variables in string are defined. Pyparsing uses a left-to-right parser, which makes the more elegant approach pretty hopeless. @@ -56,7 +56,8 @@ def check_variables(string, variables): achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character undefined_variable = achar + Word(alphanums) undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself()) - varnames = varnames | undefined_variable''' + varnames = varnames | undefined_variable + ''' possible_variables = re.split(general_whitespace, string) # List of all alnums in string bad_variables = list() for v in possible_variables: @@ -71,7 +72,8 @@ def check_variables(string, variables): def evaluator(variables, functions, string, cs=False): - ''' Evaluate an expression. Variables are passed as a dictionary + ''' + Evaluate an expression. Variables are passed as a dictionary from string to value. Unary functions are passed as a dictionary from string to function. Variables must be floats. cs: Case sensitive @@ -108,6 +110,7 @@ def evaluator(variables, functions, string, cs=False): if string.strip() == "": return float('nan') + ops = {"^": operator.pow, "*": operator.mul, "/": operator.truediv, @@ -169,14 +172,19 @@ def evaluator(variables, functions, string, cs=False): def func_parse_action(x): return [all_functions[x[0]](x[1])] - number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) # SI suffixes and percent + # SI suffixes and percent + number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) (dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^") number_part = Word(nums) - inner_number = (number_part + Optional("." + number_part)) | ("." + number_part) # 0.33 or 7 or .34 - number = Optional(minus | plus) + inner_number + \ - Optional(CaselessLiteral("E") + Optional("-") + number_part) + \ - Optional(number_suffix) # 0.33k or -17 + + # 0.33 or 7 or .34 + inner_number = (number_part + Optional("." + number_part)) | ("." + number_part) + + # 0.33k or -17 + number = (Optional(minus | plus) + inner_number + + Optional(CaselessLiteral("E") + Optional("-") + number_part) + + Optional(number_suffix)) number = number.setParseAction(number_parse_action) # Convert to number # Predefine recursive variables @@ -201,9 +209,11 @@ def evaluator(variables, functions, string, cs=False): varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x)) else: varnames = NoMatch() + # Same thing for functions. if len(all_functions) > 0: - funcnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_functions.keys())) + funcnames = sreduce(lambda x, y: x | y, + map(lambda x: CasedLiteral(x), all_functions.keys())) function = funcnames + lpar.suppress() + expr + rpar.suppress() function.setParseAction(func_parse_action) else: 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'): diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index 52411a8e8c..227f85bc8e 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -5,23 +5,26 @@ class CorrectMap(object): - ''' + """ Stores map between answer_id and response evaluation result for each question in a capa problem. The response evaluation result for each answer_id includes (correctness, npoints, msg, hint, hintmode). - correctness : either 'correct' or 'incorrect' - npoints : None, or integer specifying number of points awarded for this answer_id - - msg : string (may have HTML) giving extra message response (displayed below textline or textbox) - - hint : string (may have HTML) giving optional hint (displayed below textline or textbox, above msg) + - msg : string (may have HTML) giving extra message response + (displayed below textline or textbox) + - hint : string (may have HTML) giving optional hint + (displayed below textline or textbox, above msg) - hintmode : one of (None,'on_request','always') criteria for displaying hint - queuestate : Dict {key:'', time:''} where key is a secret string, and time is a string dump of a DateTime object in the format '%Y%m%d%H%M%S'. Is None when not queued Behaves as a dict. - ''' + """ def __init__(self, *args, **kwargs): - self.cmap = dict() # start with empty dict + # start with empty dict + self.cmap = dict() self.items = self.cmap.items self.keys = self.cmap.keys self.set(*args, **kwargs) @@ -33,7 +36,15 @@ class CorrectMap(object): return self.cmap.__iter__() # See the documentation for 'set_dict' for the use of kwargs - def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None, queuestate=None, **kwargs): + def set(self, + answer_id=None, + correctness=None, + npoints=None, + msg='', + hint='', + hintmode=None, + queuestate=None, **kwargs): + if answer_id is not None: self.cmap[answer_id] = {'correctness': correctness, 'npoints': npoints, @@ -56,12 +67,13 @@ class CorrectMap(object): ''' Set internal dict of CorrectMap to provided correct_map dict - correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This means that - when the definition of CorrectMap (e.g. its properties) are altered, existing correct_map dict - not coincide with the newest CorrectMap format as defined by self.set. + correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This + means that when the definition of CorrectMap (e.g. its properties) are altered, + an existing correct_map dict not coincide with the newest CorrectMap format as + defined by self.set. For graceful migration, feed the contents of each correct map to self.set, rather than - making a direct copy of the given correct_map dict. This way, the common keys between + making a direct copy of the given correct_map dict. This way, the common keys between the incoming correct_map dict and the new CorrectMap instance will be written, while mismatched keys will be gracefully ignored. @@ -69,14 +81,20 @@ class CorrectMap(object): If correct_map is a one-level dict, then convert it to the new dict of dicts format. ''' if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict): - self.__init__() # empty current dict - for k in correct_map: self.set(k, correct_map[k]) # create new dict entries + # empty current dict + self.__init__() + + # create new dict entries + for k in correct_map: + self.set(k, correct_map[k]) else: self.__init__() - for k in correct_map: self.set(k, **correct_map[k]) + for k in correct_map: + self.set(k, **correct_map[k]) def is_correct(self, answer_id): - if answer_id in self.cmap: return self.cmap[answer_id]['correctness'] == 'correct' + if answer_id in self.cmap: + return self.cmap[answer_id]['correctness'] == 'correct' return None def is_queued(self, answer_id): @@ -94,14 +112,18 @@ class CorrectMap(object): return npoints elif self.is_correct(answer_id): return 1 - return 0 # if not correct and no points have been assigned, return 0 + # if not correct and no points have been assigned, return 0 + return 0 def set_property(self, answer_id, property, value): - if answer_id in self.cmap: self.cmap[answer_id][property] = value - else: self.cmap[answer_id] = {property: value} + if answer_id in self.cmap: + self.cmap[answer_id][property] = value + else: + self.cmap[answer_id] = {property: value} def get_property(self, answer_id, property, default=None): - if answer_id in self.cmap: return self.cmap[answer_id].get(property, default) + if answer_id in self.cmap: + return self.cmap[answer_id].get(property, default) return default def get_correctness(self, answer_id): diff --git a/common/lib/capa/capa/eia.py b/common/lib/capa/capa/eia.py index b41f205576..f781d46a3f 100644 --- a/common/lib/capa/capa/eia.py +++ b/common/lib/capa/capa/eia.py @@ -1,9 +1,15 @@ -""" Standard resistor codes. +""" +Standard resistor codes. http://en.wikipedia.org/wiki/Electronic_color_code """ E6 = [10, 15, 22, 33, 47, 68] + E12 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82] + E24 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82, 11, 13, 16, 20, 24, 30, 36, 43, 51, 62, 75, 91] + E48 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953] + E96 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976] + E192 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 101, 123, 149, 180, 218, 264, 320, 388, 470, 569, 690, 835, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 104, 126, 152, 184, 223, 271, 328, 397, 481, 583, 706, 856, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 106, 129, 156, 189, 229, 277, 336, 407, 493, 597, 723, 876, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 109, 132, 160, 193, 234, 284, 344, 417, 505, 612, 741, 898, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 111, 135, 164, 198, 240, 291, 352, 427, 517, 626, 759, 920, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 114, 138, 167, 203, 246, 298, 361, 437, 530, 642, 777, 942, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 117, 142, 172, 208, 252, 305, 370, 448, 542, 657, 796, 965, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976, 120, 145, 176, 213, 258, 312, 379, 459, 556, 673, 816, 988] diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 9ae63fb43a..466adcbf01 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -2,7 +2,7 @@ # File: courseware/capa/inputtypes.py # -''' +""" Module containing the problem elements which render into input objects - textline @@ -16,15 +16,16 @@ Module containing the problem elements which render into input objects - optioninput (for option list) - filesubmission (upload a file) -These are matched by *.html files templates/*.html which are mako templates with the actual html. +These are matched by *.html files templates/*.html which are mako templates with the +actual html. -Each input type takes the xml tree as 'element', the previous answer as 'value', and the graded status as 'status' +Each input type takes the xml tree as 'element', the previous answer as 'value', and the +graded status as'status' +""" -''' - -# TODO: rename "state" to "status" for all below -# status is currently the answer for the problem ID for the input element, -# but it will turn into a dict containing both the answer and any associated message for the problem ID for the input element. +# TODO: rename "state" to "status" for all below. status is currently the answer for the +# problem ID for the input element, but it will turn into a dict containing both the +# answer and any associated message for the problem ID for the input element. import logging import re @@ -47,7 +48,8 @@ class SimpleInput():# XModule Type for simple inputs -- plain HTML with a form element ''' - xml_tags = {} # # Maps tags to functions + # Maps tags to functions + xml_tags = {} def __init__(self, system, xml, item_id=None, track_url=None, state=None, use='capa_input'): ''' @@ -69,19 +71,23 @@ class SimpleInput():# XModule self.xml = xml self.tag = xml.tag self.system = system - if not state: state = {} + if not state: + state = {} - ## ID should only come from one place. + ## NOTE: ID should only come from one place. ## If it comes from multiple, we use state first, XML second, and parameter ## third. Since we don't make this guarantee, we can swap this around in ## the future if there's a more logical order. - if item_id: self.id = item_id - if xml.get('id'): self.id = xml.get('id') - if 'id' in state: self.id = state['id'] + if item_id: + self.id = item_id - self.value = '' - if 'value' in state: - self.value = state['value'] + if xml.get('id'): + self.id = xml.get('id') + + if 'id' in state: + self.id = state['id'] + + self.value = state.get('value', '') self.msg = '' feedback = state.get('feedback') @@ -92,6 +98,7 @@ class SimpleInput():# XModule # put hint above msg if to be displayed if self.hintmode == 'always': + # TODO: is the '.' in
below a bug? self.msg = self.hint + ('
' if self.msg else '') + self.msg self.status = 'unanswered' @@ -107,7 +114,8 @@ class SimpleInput():# XModule return ['capa_input', 'capa_transform'] def get_html(self): - return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg) + return self.xml_tags[self.tag](self.xml, self.value, + self.status, self.system.render_template, self.msg) def register_render_function(fn, names=None, cls=SimpleInput): @@ -125,24 +133,26 @@ def register_render_function(fn, names=None, cls=SimpleInput): @register_render_function def optioninput(element, value, status, render_template, msg=''): - ''' + """ Select option input type. Example: The location of the sky - ''' + """ eid = element.get('id') options = element.get('options') if not options: - raise Exception("[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element)) + raise Exception( + "[courseware.capa.inputtypes.optioninput] Missing options specification in " + + etree.tostring(element)) oset = shlex.shlex(options[1:-1]) oset.quotes = "'" oset.whitespace = "," oset = [x[1:-1] for x in list(oset)] - # osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ]) # make dict with IDs - osetdict = [(oset[x], oset[x]) for x in range(len(oset))] # make ordered list with (key,value) same + # make ordered list with (key,value) same + osetdict = [(oset[x], oset[x]) for x in range(len(oset))] # TODO: allow ordering to be randomized context = {'id': eid, @@ -166,26 +176,35 @@ def choicegroup(element, value, status, render_template, msg=''): ''' Radio button inputs: multiple choice or true/false - TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute, - ie random, top, bottom. + TODO: allow order of choices to be randomized, following lon-capa spec. Use + "location" attribute, ie random, top, bottom. ''' eid = element.get('id') if element.get('type') == "MultipleChoice": - type = "radio" + element_type = "radio" elif element.get('type') == "TrueFalse": - type = "checkbox" + element_type = "checkbox" else: - type = "radio" + element_type = "radio" choices = [] for choice in element: if not choice.tag == 'choice': - raise Exception("[courseware.capa.inputtypes.choicegroup] Error only tags should be immediate children of a , found %s instead" % choice.tag) + raise Exception("[courseware.capa.inputtypes.choicegroup] " + "Error: only tags should be immediate children " + "of a , found %s instead" % choice.tag) ctext = "" - ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it? + # TODO: what if choice[0] has math tags in it? + ctext += ''.join([etree.tostring(x) for x in choice]) if choice.text is not None: - ctext += choice.text # TODO: fix order? + # TODO: fix order? + ctext += choice.text choices.append((choice.get("name"), ctext)) - context = {'id': eid, 'value': value, 'state': status, 'input_type': type, 'choices': choices, 'name_array_suffix': ''} + context = {'id': eid, + 'value': value, + 'state': status, + 'input_type': element_type, + 'choices': choices, + 'name_array_suffix': ''} html = render_template("choicegroup.html", context) return etree.XML(html) @@ -196,8 +215,8 @@ def extract_choices(element): Extracts choices for a few input types, such as radiogroup and checkboxgroup. - TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute, - ie random, top, bottom. + TODO: allow order of choices to be randomized, following lon-capa spec. Use + "location" attribute, ie random, top, bottom. ''' choices = [] @@ -226,7 +245,12 @@ def radiogroup(element, value, status, render_template, msg=''): choices = extract_choices(element) - context = {'id': eid, 'value': value, 'state': status, 'input_type': 'radio', 'choices': choices, 'name_array_suffix': '[]'} + context = {'id': eid, + 'value': value, + 'state': status, + 'input_type': 'radio', + 'choices': choices, + 'name_array_suffix': '[]'} html = render_template("choicegroup.html", context) return etree.XML(html) @@ -244,7 +268,12 @@ def checkboxgroup(element, value, status, render_template, msg=''): choices = extract_choices(element) - context = {'id': eid, 'value': value, 'state': status, 'input_type': 'checkbox', 'choices': choices, 'name_array_suffix': '[]'} + context = {'id': eid, + 'value': value, + 'state': status, + 'input_type': 'checkbox', + 'choices': choices, + 'name_array_suffix': '[]'} html = render_template("choicegroup.html", context) return etree.XML(html) @@ -260,49 +289,67 @@ def javascriptinput(element, value, status, render_template, msg='null'): problem_state = element.get('problem_state') display_class = element.get('display_class') display_file = element.get('display_file') - + # Need to provide a value that JSON can parse if there is no # student-supplied value yet. if value == "": value = 'null' - + escapedict = {'"': '"'} value = saxutils.escape(value, escapedict) msg = saxutils.escape(msg, escapedict) - context = {'id': eid, 'params': params, 'display_file': display_file, - 'display_class': display_class, 'problem_state': problem_state, - 'value': value, 'evaluation': msg, + context = {'id': eid, + 'params': params, + 'display_file': display_file, + 'display_class': display_class, + 'problem_state': problem_state, + 'value': value, + 'evaluation': msg, } html = render_template("javascriptinput.html", context) return etree.XML(html) - @register_render_function def textline(element, value, status, render_template, msg=""): ''' Simple text line input, with optional size specification. ''' - if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x - return SimpleInput.xml_tags['textline_dynamath'](element, value, status, render_template, msg) + # TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x + if element.get('math') or element.get('dojs'): + return SimpleInput.xml_tags['textline_dynamath'](element, value, status, + render_template, msg) eid = element.get('id') if eid is None: msg = 'textline has no id: it probably appears outside of a known response type' msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '') raise Exception(msg) + count = int(eid.split('_')[-2]) - 1 # HACK size = element.get('size') - hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden + # if specified, then textline is hidden and id is stored in div of name given by hidden + hidden = element.get('hidden', '') + + # Escape answers with quotes, so they don't crash the system! escapedict = {'"': '"'} - value = saxutils.escape(value, escapedict) # otherwise, answers with quotes in them crashes the system! - context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, 'hidden': hidden, + value = saxutils.escape(value, escapedict) + + context = {'id': eid, + 'value': value, + 'state': status, + 'count': count, + 'size': size, + 'msg': msg, + 'hidden': hidden, 'inline': element.get('inline',''), } + html = render_template("textinput.html", context) try: xhtml = etree.XML(html) except Exception as err: - if True: # TODO needs to be self.system.DEBUG - but can't access system + # TODO: needs to be self.system.DEBUG - but can't access system + if True: log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html) raise return xhtml @@ -313,7 +360,8 @@ def textline(element, value, status, render_template, msg=""): @register_render_function def textline_dynamath(element, value, status, render_template, msg=''): ''' - Text line input with dynamic math display (equation rendered on client in real time during input). + Text line input with dynamic math display (equation rendered on client in real time + during input). ''' # TODO: Make a wrapper for # TODO: Make an AJAX loop to confirm equation is okay in real-time as user types @@ -325,7 +373,8 @@ def textline_dynamath(element, value, status, render_template, msg=''): eid = element.get('id') count = int(eid.split('_')[-2]) - 1 # HACK size = element.get('size') - hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden + # if specified, then textline is hidden and id is stored in div of name given by hidden + hidden = element.get('hidden', '') # Preprocessor to insert between raw input and Mathjax preprocessor = {'class_name': element.get('preprocessorClassName',''), @@ -337,10 +386,14 @@ def textline_dynamath(element, value, status, render_template, msg=''): escapedict = {'"': '"'} value = saxutils.escape(value, escapedict) - context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, - 'msg': msg, 'hidden': hidden, - 'preprocessor': preprocessor, - } + context = {'id': eid, + 'value': value, + 'state': status, + 'count': count, + 'size': size, + 'msg': msg, + 'hidden': hidden, + 'preprocessor': preprocessor,} html = render_template("textinput_dynamath.html", context) return etree.XML(html) @@ -360,15 +413,19 @@ def filesubmission(element, value, status, render_template, msg=''): # Check if problem has been queued queue_len = 0 - if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue + # Flag indicating that the problem has been queued, 'msg' is length of queue + if status == 'incomplete': status = 'queued' queue_len = msg msg = 'Submitted to grader.' - context = { 'id': eid, 'state': status, 'msg': msg, 'value': value, - 'queue_len': queue_len, 'allowed_files': allowed_files, - 'required_files': required_files - } + context = { 'id': eid, + 'state': status, + 'msg': msg, + 'value': value, + 'queue_len': queue_len, + 'allowed_files': allowed_files, + 'required_files': required_files,} html = render_template("filesubmission.html", context) return etree.XML(html) @@ -387,13 +444,17 @@ def textbox(element, value, status, render_template, msg=''): size = element.get('size') rows = element.get('rows') or '30' cols = element.get('cols') or '80' - hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden + # if specified, then textline is hidden and id is stored in div of name given by hidden + hidden = element.get('hidden', '') - if not value: value = element.text # if no student input yet, then use the default input given by the problem + # if no student input yet, then use the default input given by the problem + if not value: + value = element.text # Check if problem has been queued queue_len = 0 - if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue + # Flag indicating that the problem has been queued, 'msg' is length of queue + if status == 'incomplete': status = 'queued' queue_len = msg msg = 'Submitted to grader.' @@ -404,10 +465,18 @@ def textbox(element, value, status, render_template, msg=''): tabsize = element.get('tabsize','4') tabsize = int(tabsize) - context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, - 'mode': mode, 'linenumbers': linenumbers, - 'rows': rows, 'cols': cols, - 'hidden': hidden, 'tabsize': tabsize, + context = {'id': eid, + 'value': value, + 'state': status, + 'count': count, + 'size': size, + 'msg': msg, + 'mode': mode, + 'linenumbers': linenumbers, + 'rows': rows, + 'cols': cols, + 'hidden': hidden, + 'tabsize': tabsize, 'queue_len': queue_len, } html = render_template("textbox.html", context) @@ -475,7 +544,8 @@ def math(element, value, status, render_template, msg=''): # mathstr = mathstr.replace('\\displaystyle','') #else: # isinline = True - # html = render_template("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail}) + # html = render_template("mathstring.html", {'mathstr':mathstr, + # 'isinline':isinline,'tail':element.tail}) html = '%s%s' % (mathstr, saxutils.escape(element.tail)) try: @@ -483,13 +553,14 @@ def math(element, value, status, render_template, msg=''): except Exception as err: if False: # TODO needs to be self.system.DEBUG - but can't access system msg = '

Error %s

' % str(err).replace('<', '<') - msg += '

Failed to construct math expression from

%s

' % html.replace('<', '<') + msg += ('

Failed to construct math expression from

%s

' % + html.replace('<', '<')) msg += "
" log.error(msg) return etree.XML(msg) else: raise - # xhtml.tail = element.tail # don't forget to include the tail! + # xhtml.tail = element.tail # don't forget to include the tail! return xhtml #----------------------------------------------------------------------------- @@ -520,11 +591,13 @@ def solution(element, value, status, render_template, msg=''): @register_render_function def imageinput(element, value, status, render_template, msg=''): ''' - Clickable image as an input field. Element should specify the image source, height, and width, eg - + Clickable image as an input field. Element should specify the image source, height, + and width, e.g. - TODO: showanswer for imageimput does not work yet - need javascript to put rectangle over acceptable area of image. + + TODO: showanswer for imageimput does not work yet - need javascript to put rectangle + over acceptable area of image. ''' eid = element.get('id') src = element.get('src') diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 0909deea3a..ab5eaf950c 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -63,7 +63,7 @@ class StudentInputError(Exception): class LoncapaResponse(object): - ''' + """ Base class for CAPA responsetypes. Each response type (ie a capa question, which is part of a capa problem) is represented as a subclass, which should provide the following methods: @@ -77,19 +77,29 @@ class LoncapaResponse(object): In addition, these methods are optional: - - setup_response : find and note the answer input field IDs for the response; called by __init__ - - check_hint_condition : check to see if the student's answers satisfy a particular condition for a hint to be displayed - - render_html : render this Response as HTML (must return XHTML compliant string) + - setup_response : find and note the answer input field IDs for the response; called + by __init__ + + - check_hint_condition : check to see if the student's answers satisfy a particular + condition for a hint to be displayed + + - render_html : render this Response as HTML (must return XHTML-compliant string) - __unicode__ : unicode representation of this Response Each response type may also specify the following attributes: - - max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None) - - allowed_inputfields : list of allowed input fields (each a string) for this Response - - required_attributes : list of required attributes (each a string) on the main response XML stanza - - hint_tag : xhtml tag identifying hint associated with this response inside hintgroup + - max_inputfields : (int) maximum number of answer input fields (checked in __init__ + if not None) - ''' + - allowed_inputfields : list of allowed input fields (each a string) for this Response + + - required_attributes : list of required attributes (each a string) on the main + response XML stanza + + - hint_tag : xhtml tag identifying hint associated with this response inside + hintgroup + + """ __metaclass__ = abc.ABCMeta # abc = Abstract Base Class response_tag = None @@ -121,26 +131,32 @@ class LoncapaResponse(object): raise LoncapaProblemError(msg) if self.max_inputfields and len(inputfields) > self.max_inputfields: - msg = "%s: cannot have more than %s input fields" % (unicode(self), self.max_inputfields) + msg = "%s: cannot have more than %s input fields" % ( + unicode(self), self.max_inputfields) msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '') raise LoncapaProblemError(msg) for prop in self.required_attributes: if not xml.get(prop): - msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self), prop) + msg = "Error in problem specification: %s missing required attribute %s" % ( + unicode(self), prop) msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '') raise LoncapaProblemError(msg) - self.answer_ids = [x.get('id') for x in self.inputfields] # ordered list of answer_id values for this response + # ordered list of answer_id values for this response + self.answer_ids = [x.get('id') for x in self.inputfields] if self.max_inputfields == 1: - self.answer_id = self.answer_ids[0] # for convenience + # for convenience + self.answer_id = self.answer_ids[0] self.maxpoints = dict() for inputfield in self.inputfields: - maxpoints = inputfield.get('points','1') # By default, each answerfield is worth 1 point + # By default, each answerfield is worth 1 point + maxpoints = inputfield.get('points', '1') self.maxpoints.update({inputfield.get('id'): int(maxpoints)}) - self.default_answer_map = {} # dict for default answer map (provided in input elements) + # dict for default answer map (provided in input elements) + self.default_answer_map = {} for entry in self.inputfields: answer = entry.get('correct_answer') if answer: @@ -163,10 +179,13 @@ class LoncapaResponse(object): - renderer : procedure which produces HTML given an ElementTree ''' - tree = etree.Element('span') # render ourself as a + our content + # render ourself as a + our content + tree = etree.Element('span') for item in self.xml: - item_xhtml = renderer(item) # call provided procedure to do the rendering - if item_xhtml is not None: tree.append(item_xhtml) + # call provided procedure to do the rendering + item_xhtml = renderer(item) + if item_xhtml is not None: + tree.append(item_xhtml) tree.tail = self.xml.tail return tree @@ -192,21 +211,21 @@ class LoncapaResponse(object): Modifies new_cmap, by adding hints to answer_id entries as appropriate. ''' hintgroup = self.xml.find('hintgroup') - if hintgroup is None: return + if hintgroup is None: + return # hint specified by function? hintfn = hintgroup.get('hintfn') if hintfn: ''' - Hint is determined by a function defined in the ''' - snippets = [{'snippet': ''' + snippets = [{'snippet': """
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\) @@ -802,8 +839,8 @@ class CustomResponse(LoncapaResponse): if not(r=="IS*u(t-t0)"): correct[0] ='incorrect' -
'''}, - {'snippet': ''' stanza instead + # if we have a "cfn" attribute then look for the function specified by cfn, in + # the problem context ie the comparison function is defined in the + # stanza instead cfn = xml.get('cfn') if cfn: log.debug("cfn = %s" % cfn) @@ -847,13 +886,14 @@ def sympy_check2(): self.code = self.context[cfn] else: msg = "%s: can't find cfn %s in context" % (unicode(self), cfn) - msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '') + msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', + '') raise LoncapaProblemError(msg) if not self.code: if answer is None: - # raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid - log.error("[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid) + log.error("[courseware.capa.responsetypes.customresponse] missing" + " code checking script! id=%s" % self.myid) self.code = '' else: answer_src = answer.get('src') @@ -870,43 +910,70 @@ def sympy_check2(): log.debug('%s: student_answers=%s' % (unicode(self), student_answers)) - idset = sorted(self.answer_ids) # ordered list of answer id's + # ordered list of answer id's + idset = sorted(self.answer_ids) try: - submission = [student_answers[k] for k in idset] # ordered list of answers + # ordered list of answers + submission = [student_answers[k] for k in idset] except Exception as err: - msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers + msg = ('[courseware.capa.responsetypes.customresponse] error getting' + ' student answer from %s' % student_answers) msg += '\n idset = %s, error = %s' % (idset, err) log.error(msg) raise Exception(msg) # global variable in context which holds the Presentation MathML from dynamic math input - dynamath = [student_answers.get(k + '_dynamath', None) for k in idset] # ordered list of dynamath responses + # ordered list of dynamath responses + dynamath = [student_answers.get(k + '_dynamath', None) for k in idset] # if there is only one box, and it's empty, then don't evaluate if len(idset) == 1 and not submission[0]: - # default to no error message on empty answer (to be consistent with other responsetypes) - # but allow author to still have the old behavior by setting empty_answer_err attribute - msg = 'No answer entered!' if self.xml.get('empty_answer_err') else '' + # default to no error message on empty answer (to be consistent with other + # responsetypes) but allow author to still have the old behavior by setting + # empty_answer_err attribute + msg = ('No answer entered!' + if self.xml.get('empty_answer_err') else '') return CorrectMap(idset[0], 'incorrect', msg=msg) - # NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are not expecting 'unknown's + # NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are + # not expecting 'unknown's correct = ['unknown'] * len(idset) messages = [''] * len(idset) # put these in the context of the check function evaluator # note that this doesn't help the "cfn" version - only the exec version - self.context.update({'xml': self.xml, # our subtree - 'response_id': self.myid, # my ID - 'expect': self.expect, # expected answer (if given as attribute) - 'submission': submission, # ordered list of student answers from entry boxes in our subtree - 'idset': idset, # ordered list of ID's of all entry boxes in our subtree - 'dynamath': dynamath, # ordered list of all javascript inputs in our subtree - 'answers': student_answers, # dict of student's responses, with keys being entry box IDs - 'correct': correct, # the list to be filled in by the check function - 'messages': messages, # the list of messages to be filled in by the check function - 'options': self.xml.get('options'), # any options to be passed to the cfn - 'testdat': 'hello world', - }) + self.context.update({ + # our subtree + 'xml': self.xml, + + # my ID + 'response_id': self.myid, + + # expected answer (if given as attribute) + 'expect': self.expect, + + # ordered list of student answers from entry boxes in our subtree + 'submission': submission, + + # ordered list of ID's of all entry boxes in our subtree + 'idset': idset, + + # ordered list of all javascript inputs in our subtree + 'dynamath': dynamath, + + # dict of student's responses, with keys being entry box IDs + 'answers': student_answers, + + # the list to be filled in by the check function + 'correct': correct, + + # the list of messages to be filled in by the check function + 'messages': messages, + + # any options to be passed to the cfn + 'options': self.xml.get('options'), + 'testdat': 'hello world', + }) # pass self.system.debug to cfn self.context['debug'] = self.system.DEBUG @@ -921,8 +988,10 @@ def sympy_check2(): print "oops in customresponse (code) error %s" % err print "context = ", self.context print traceback.format_exc() - raise StudentInputError("Error: Problem could not be evaluated with your input") # Notify student - else: # self.code is not a string; assume its a function + # Notify student + raise StudentInputError("Error: Problem could not be evaluated with your input") + else: + # self.code is not a string; assume its a function # this is an interface to the Tutor2 check functions fn = self.code @@ -958,7 +1027,8 @@ def sympy_check2(): msg = '' + msg + '' msg = msg.replace('<', '<') #msg = msg.replace('<','<') - msg = etree.tostring(fromstring_bs(msg, convertEntities=None), pretty_print=True) + msg = etree.tostring(fromstring_bs(msg, convertEntities=None), + pretty_print=True) #msg = etree.tostring(fromstring_bs(msg),pretty_print=True) msg = msg.replace(' ', '') #msg = re.sub('(.*)','\\1',msg,flags=re.M|re.DOTALL) # python 2.7 @@ -1022,18 +1092,19 @@ class SymbolicResponse(CustomResponse): class CodeResponse(LoncapaResponse): - ''' + """ Grade student code using an external queueing server, called 'xqueue' Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse: system.xqueue = { 'interface': XqueueInterface object, - 'callback_url': Per-StudentModule callback URL where results are posted (string), + 'callback_url': Per-StudentModule callback URL + where results are posted (string), 'default_queuename': Default queuename to submit request (string) } - External requests are only submitted for student submission grading + External requests are only submitted for student submission grading (i.e. and not for getting reference answers) - ''' + """ response_tag = 'coderesponse' allowed_inputfields = ['textbox', 'filesubmission'] @@ -1046,7 +1117,8 @@ class CodeResponse(LoncapaResponse): TODO: Determines whether in synchronous or asynchronous (queued) mode ''' xml = self.xml - self.url = xml.get('url', None) # TODO: XML can override external resource (grader/queue) URL + # TODO: XML can override external resource (grader/queue) URL + self.url = xml.get('url', None) self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename']) # VS[compat]: @@ -1107,7 +1179,8 @@ class CodeResponse(LoncapaResponse): # Extract 'answer' and 'initial_display' from XML. Note that the code to be exec'ed here is: # (1) Internal edX code, i.e. NOT student submissions, and - # (2) The code should only define the strings 'initial_display', 'answer', 'preamble', 'test_program' + # (2) The code should only define the strings 'initial_display', 'answer', + # 'preamble', 'test_program' # following the ExternalResponse XML format penv = {} penv['__builtins__'] = globals()['__builtins__'] @@ -1120,10 +1193,12 @@ class CodeResponse(LoncapaResponse): self.answer = penv['answer'] self.initial_display = penv['initial_display'] except Exception as err: - log.error("Error in CodeResponse %s: Problem reference code does not define 'answer' and/or 'initial_display' in ..." % err) + log.error("Error in CodeResponse %s: Problem reference code does not define" + " 'answer' and/or 'initial_display' in ..." % err) raise Exception(err) - # Finally, make the ExternalResponse input XML format conform to the generic exteral grader interface + # Finally, make the ExternalResponse input XML format conform to the generic + # exteral grader interface # The XML tagging of grader_payload is pyxserver-specific grader_payload = '' grader_payload += '' + tests + '\n' @@ -1133,14 +1208,16 @@ class CodeResponse(LoncapaResponse): def get_score(self, student_answers): try: - submission = student_answers[self.answer_id] # Note that submission can be a file + # Note that submission can be a file + submission = student_answers[self.answer_id] except Exception as err: - log.error('Error in CodeResponse %s: cannot get student answer for %s; student_answers=%s' % + log.error('Error in CodeResponse %s: cannot get student answer for %s;' + ' student_answers=%s' % (err, self.answer_id, convert_files_to_filenames(student_answers))) raise Exception(err) # Prepare xqueue request - #------------------------------------------------------------ + #------------------------------------------------------------ qinterface = self.system.xqueue['interface'] qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) @@ -1149,19 +1226,20 @@ class CodeResponse(LoncapaResponse): # Generate header queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + - anonymous_student_id + + anonymous_student_id + self.answer_id) xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], lms_key=queuekey, queue_name=self.queue_name) - + # Generate body if is_list_of_files(submission): - self.context.update({'submission': ''}) # TODO: Get S3 pointer from the Queue + # TODO: Get S3 pointer from the Queue + self.context.update({'submission': ''}) else: self.context.update({'submission': submission}) - contents = self.payload.copy() + contents = self.payload.copy() # Metadata related to the student submission revealed to the external grader student_info = {'anonymous_student_id': anonymous_student_id, @@ -1171,7 +1249,8 @@ class CodeResponse(LoncapaResponse): # Submit request. When successful, 'msg' is the prior length of the queue if is_list_of_files(submission): - contents.update({'student_response': ''}) # TODO: Is there any information we want to send here? + # TODO: Is there any information we want to send here? + contents.update({'student_response': ''}) (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents), files_to_upload=submission) @@ -1182,44 +1261,51 @@ class CodeResponse(LoncapaResponse): # State associated with the queueing request queuestate = {'key': queuekey, - 'time': qtime, - } + 'time': qtime,} - cmap = CorrectMap() + cmap = CorrectMap() if error: cmap.set(self.answer_id, queuestate=None, - msg='Unable to deliver your submission to grader. (Reason: %s.) Please try again later.' % msg) + msg='Unable to deliver your submission to grader. (Reason: %s.)' + ' Please try again later.' % msg) else: # Queueing mechanism flags: - # 1) Backend: Non-null CorrectMap['queuestate'] indicates that the problem has been queued - # 2) Frontend: correctness='incomplete' eventually trickles down through inputtypes.textbox - # and .filesubmission to inform the browser to poll the LMS + # 1) Backend: Non-null CorrectMap['queuestate'] indicates that + # the problem has been queued + # 2) Frontend: correctness='incomplete' eventually trickles down + # through inputtypes.textbox and .filesubmission to inform the + # browser to poll the LMS cmap.set(self.answer_id, queuestate=queuestate, correctness='incomplete', msg=msg) return cmap def update_score(self, score_msg, oldcmap, queuekey): - (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) + (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) if not valid_score_msg: - oldcmap.set(self.answer_id, msg='Invalid grader reply. Please contact the course staff.') + oldcmap.set(self.answer_id, + msg='Invalid grader reply. Please contact the course staff.') return oldcmap - + correctness = 'correct' if correct else 'incorrect' - self.context['correct'] = correctness # TODO: Find out how this is used elsewhere, if any + # TODO: Find out how this is used elsewhere, if any + self.context['correct'] = correctness - # Replace 'oldcmap' with new grading results if queuekey matches. - # If queuekey does not match, we keep waiting for the score_msg whose key actually matches + # Replace 'oldcmap' with new grading results if queuekey matches. If queuekey + # does not match, we keep waiting for the score_msg whose key actually matches if oldcmap.is_right_queuekey(self.answer_id, queuekey): - # Sanity check on returned points + # Sanity check on returned points if points < 0: points = 0 elif points > self.maxpoints[self.answer_id]: points = self.maxpoints[self.answer_id] - oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace(' ', ' '), queuestate=None) # Queuestate is consumed + # Queuestate is consumed + oldcmap.set(self.answer_id, npoints=points, correctness=correctness, + msg=msg.replace(' ', ' '), queuestate=None) else: - log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id)) + log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % + (queuekey, self.answer_id)) return oldcmap @@ -1231,7 +1317,7 @@ class CodeResponse(LoncapaResponse): return {self.answer_id: self.initial_display} def _parse_score_msg(self, score_msg): - ''' + """ Grader reply is a JSON-dump of the following dict { 'correct': True/False, 'score': Numeric value (floating point is okay) to assign to answer @@ -1242,22 +1328,25 @@ class CodeResponse(LoncapaResponse): correct: Correctness of submission (Boolean) score: Points to be assigned (numeric, can be float) msg: Message from grader to display to student (string) - ''' + """ fail = (False, False, 0, '') try: score_result = json.loads(score_msg) except (TypeError, ValueError): - log.error("External grader message should be a JSON-serialized dict. Received score_msg = %s" % score_msg) + log.error("External grader message should be a JSON-serialized dict." + " Received score_msg = %s" % score_msg) return fail if not isinstance(score_result, dict): - log.error("External grader message should be a JSON-serialized dict. Received score_result = %s" % score_result) + log.error("External grader message should be a JSON-serialized dict." + " Received score_result = %s" % score_result) return fail for tag in ['correct', 'score', 'msg']: if tag not in score_result: - log.error("External grader message is missing one or more required tags: 'correct', 'score', 'msg'") + log.error("External grader message is missing one or more required" + " tags: 'correct', 'score', 'msg'") return fail - # Next, we need to check that the contents of the external grader message + # Next, we need to check that the contents of the external grader message # is safe for the LMS. # 1) Make sure that the message is valid XML (proper opening/closing tags) # 2) TODO: Is the message actually HTML? @@ -1265,11 +1354,12 @@ class CodeResponse(LoncapaResponse): try: etree.fromstring(msg) except etree.XMLSyntaxError as err: - log.error("Unable to parse external grader message as valid XML: score_msg['msg']=%s" % msg) + log.error("Unable to parse external grader message as valid" + " XML: score_msg['msg']=%s" % msg) return fail - + return (True, score_result['correct'], score_result['score'], msg) - + #----------------------------------------------------------------------------- @@ -1325,9 +1415,9 @@ main() def setup_response(self): xml = self.xml - self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL + # FIXME - hardcoded URL + self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa" - # answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors answer = xml.find('answer') if answer is not None: answer_src = answer.get('src') @@ -1335,7 +1425,8 @@ main() self.code = self.system.filesystem.open('src/' + answer_src).read() else: self.code = answer.text - else: # no stanza; get code from