def run(self): """ process the fillintheblank directive and generate html for output. :param self: :return: Nodes resulting from this directive. ... """ super(FillInTheBlank, self).run() TEMPLATE_START = """ <div class="%(divclass)s"> <div data-component="fillintheblank" id="%(divid)s" %(optional)s> """ TEMPLATE_END = """ <script type="application/json"> %(json)s </script> </div> </div> """ addQuestionToDB(self) fitbNode = FITBNode(self.options, rawsource=self.block_text) fitbNode.source, fitbNode.line = self.state_machine.get_source_and_line( self.lineno ) fitbNode.template_start = TEMPLATE_START fitbNode.template_end = TEMPLATE_END self.updateContent() self.state.nested_parse(self.content, self.content_offset, fitbNode) env = self.state.document.settings.env self.options["divclass"] = env.config.fitb_div_class # Expected _`structure`, with assigned variable names and transformations made: # # .. code-block:: # :number-lines: # # fitbNode = FITBNode() # Item 1 of problem text # ... # Item n of problem text # feedback_bullet_list = bullet_list() <-- The last element in fitbNode. # feedback_list_item = list_item() <-- Feedback for the first blank. # feedback_field_list = field_list() # feedback_field = field() # feedback_field_name = field_name() <-- Contains an answer. # feedback_field_body = field_body() <-- Contains feedback for this answer. # feedback_field = field() <-- Another answer/feedback pair. # feedback_list_item = bullet_item() <-- Feedback for the second blank. # ...etc. ... # # This becomes a data structure: # # .. code-block:: # :number-lines: # # self.feedbackArray = [ # [ # blankArray # { # blankFeedbackDict: feedback 1 # "regex" : feedback_field_name # (An answer, as a regex; # "regexFlags" : "x" # "i" if ``:casei:`` was specified, otherwise "".) OR # "number" : [min, max] # a range of correct numeric answers. # "feedback": feedback_field_body (after being rendered as HTML) # Provides feedback for this answer. # }, # { # Feedback 2 # Same as above. # } # ], # [ # Blank 2, same as above. # ] # ] # # ...and a transformed node structure: # # .. code-block:: # :number-lines: # # fitbNode = FITBNode() # Item 1 of problem text # ... # Item n of problem text # FITBFeedbackNode(), which contains all the nodes in blank 1's feedback_field_body # ... # FITBFeedbackNode(), which contains all the nodes in blank n's feedback_field_body # self.assert_has_content() feedback_bullet_list = fitbNode.pop() if not isinstance(feedback_bullet_list, nodes.bullet_list): raise self.error( "On line {}, the last item in a fill-in-the-blank question must be a bulleted list.".format( get_node_line(feedback_bullet_list) ) ) for feedback_list_item in feedback_bullet_list.children: assert isinstance(feedback_list_item, nodes.list_item) feedback_field_list = feedback_list_item[0] if len(feedback_list_item) != 1 or not isinstance( feedback_field_list, nodes.field_list ): raise self.error( "On line {}, each list item in a fill-in-the-blank problems must contain only one item, a field list.".format( get_node_line(feedback_list_item) ) ) blankArray = [] for feedback_field in feedback_field_list: assert isinstance(feedback_field, nodes.field) feedback_field_name = feedback_field[0] assert isinstance(feedback_field_name, nodes.field_name) feedback_field_name_raw = feedback_field_name.rawsource # See if this is a number, optinonally followed by a tolerance. try: # Parse the number. In Python 3 syntax, this would be ``str_num, *list_tol = feedback_field_name_raw.split()``. tmp = feedback_field_name_raw.split() str_num = tmp[0] list_tol = tmp[1:] num = ast.literal_eval(str_num) assert isinstance(num, Number) # If no tolerance is given, use a tolarance of 0. if len(list_tol) == 0: tol = 0 else: assert len(list_tol) == 1 tol = ast.literal_eval(list_tol[0]) assert isinstance(tol, Number) # We have the number and a tolerance. Save that. blankFeedbackDict = {"number": [num - tol, num + tol]} except (SyntaxError, ValueError, AssertionError): # We can't parse this as a number, so assume it's a regex. regex = ( # The given regex must match the entire string, from the beginning (which may be preceded by whitespaces) ... r"^\s*" + # ... to the contents (where a single space in the provided pattern is treated as one or more whitespaces in the student's answer) ... feedback_field_name.rawsource.replace(" ", r"\s+") # ... to the end (also with optional spaces). + r"\s*$" ) blankFeedbackDict = { "regex": regex, "regexFlags": "i" if "casei" in self.options else "", } # Test out the regex to make sure it compiles without an error. try: re.compile(regex) except Exception as ex: raise self.error( 'Error when compiling regex "{}": {}.'.format( regex, str(ex) ) ) blankArray.append(blankFeedbackDict) feedback_field_body = feedback_field[1] assert isinstance(feedback_field_body, nodes.field_body) # Append feedback for this answer to the end of the fitbNode. ffn = FITBFeedbackNode( feedback_field_body.rawsource, *feedback_field_body.children, **feedback_field_body.attributes ) ffn.blankFeedbackDict = blankFeedbackDict fitbNode += ffn # Add all the feedback for this blank to the feedbackArray. fitbNode.feedbackArray.append(blankArray) maybeAddToAssignment(self) return [fitbNode]
def run(self): """ process the fillintheblank directive and generate html for output. :param self: :return: Nodes resulting from this directive. ... """ super(FillInTheBlank, self).run() TEMPLATE_START = ''' <div class="%(divclass)s"> <div data-component="fillintheblank" id="%(divid)s"> ''' TEMPLATE_END = ''' <script type="application/json"> %(json)s </script> </div> </div> ''' addQuestionToDB(self) fitbNode = FITBNode(self.options, rawsource=self.block_text) fitbNode.source, fitbNode.line = self.state_machine.get_source_and_line(self.lineno) fitbNode.template_start = TEMPLATE_START fitbNode.template_end = TEMPLATE_END self.updateContent() self.state.nested_parse(self.content, self.content_offset, fitbNode) env = self.state.document.settings.env self.options['divclass'] = env.config.fitb_div_class # Expected _`structure`, with assigned variable names and transformations made: # # .. code-block:: # :number-lines: # # fitbNode = FITBNode() # Item 1 of problem text # ... # Item n of problem text # feedback_bullet_list = bullet_list() <-- The last element in fitbNode. # feedback_list_item = list_item() <-- Feedback for the first blank. # feedback_field_list = field_list() # feedback_field = field() # feedback_field_name = field_name() <-- Contains an answer. # feedback_field_body = field_body() <-- Contains feedback for this answer. # feedback_field = field() <-- Another answer/feedback pair. # feedback_list_item = bullet_item() <-- Feedback for the second blank. # ...etc. ... # # This becomes a data structure: # # .. code-block:: # :number-lines: # # self.feedbackArray = [ # [ # blankArray # { # blankFeedbackDict: feedback 1 # "regex" : feedback_field_name # (An answer, as a regex; # "regexFlags" : "x" # "i" if ``:casei:`` was specified, otherwise "".) OR # "number" : [min, max] # a range of correct numeric answers. # "feedback": feedback_field_body (after being rendered as HTML) # Provides feedback for this answer. # }, # { # Feedback 2 # Same as above. # } # ], # [ # Blank 2, same as above. # ] # ] # # ...and a transformed node structure: # # .. code-block:: # :number-lines: # # fitbNode = FITBNode() # Item 1 of problem text # ... # Item n of problem text # FITBFeedbackNode(), which contains all the nodes in blank 1's feedback_field_body # ... # FITBFeedbackNode(), which contains all the nodes in blank n's feedback_field_body # self.assert_has_content() feedback_bullet_list = fitbNode.pop() if not isinstance(feedback_bullet_list, nodes.bullet_list): raise self.error('On line {}, the last item in a fill-in-the-blank question must be a bulleted list.'.format(get_node_line(feedback_bullet_list))) for feedback_list_item in feedback_bullet_list.children: assert isinstance(feedback_list_item, nodes.list_item) feedback_field_list = feedback_list_item[0] if len(feedback_list_item) != 1 or not isinstance(feedback_field_list, nodes.field_list): raise self.error('On line {}, each list item in a fill-in-the-blank problems must contain only one item, a field list.'.format(get_node_line(feedback_list_item))) blankArray = [] for feedback_field in feedback_field_list: assert isinstance(feedback_field, nodes.field) feedback_field_name = feedback_field[0] assert isinstance(feedback_field_name, nodes.field_name) feedback_field_name_raw = feedback_field_name.rawsource # See if this is a number, optinonally followed by a tolerance. try: # Parse the number. In Python 3 syntax, this would be ``str_num, *list_tol = feedback_field_name_raw.split()``. tmp = feedback_field_name_raw.split() str_num = tmp[0] list_tol = tmp[1:] num = ast.literal_eval(str_num) assert isinstance(num, Number) # If no tolerance is given, use a tolarance of 0. if len(list_tol) == 0: tol = 0 else: assert len(list_tol) == 1 tol = ast.literal_eval(list_tol[0]) assert isinstance(tol, Number) # We have the number and a tolerance. Save that. blankFeedbackDict = {"number": [num - tol, num + tol]} except (SyntaxError, ValueError, AssertionError): # We can't parse this as a number, so assume it's a regex. blankFeedbackDict = { 'regex': # The given regex must match the entire string, from the beginning (which may be preceeded by spaces) ... '^ *' + # ... to the contents (where a single space in the provided pattern is treated as one or more spaces in the student's anwer) ... feedback_field_name.rawsource.replace(' ', ' +') # ... to the end (also with optional whitespace). + ' *$', 'regexFlags': 'i' if 'casei' in self.options else '', } blankArray.append(blankFeedbackDict) feedback_field_body = feedback_field[1] assert isinstance(feedback_field_body, nodes.field_body) # Append feedback for this asnwer to the end of the fitbNode. ffn = FITBFeedbackNode(feedback_field_body.rawsource, *feedback_field_body.children, **feedback_field_body.attributes) ffn.blankFeedbackDict = blankFeedbackDict fitbNode += ffn # Add all the feedback for this blank to the feedbackArray. fitbNode.feedbackArray.append(blankArray) return [fitbNode]