import yaml import random import re import subprocess # Example usage: # # pool = QuestionPool() # pool.add_from_file('filename.yaml') # pool.add_from_files(['file1.yaml', 'file1.yaml']) # # test = [] # for q in pool.values(): # test.append(create_question(q)) # # test[0]['answer'] = 42 # grade = test[0].correct() # =========================================================================== class QuestionsPool(dict): '''This class contains base questions read from files, but which are not ready yet. They have to be instantiated separatly for each student.''' #------------------------------------------------------------------------ def add_from_file(self, filename): try: with open(filename, 'r') as f: questions = yaml.load(f) except(FileNotFoundError): print(' * Questions file "%s" not found. Ignoring...' % filename) return # add defaults if missing from sources for i, q in enumerate(questions): # filename and index (number in the file, 0 based) q['filename'] = filename q['index'] = i # ref (if missing, add 'filename.yaml:3') q['ref'] = str(q.get('ref', filename + ':' + str(i))) # type (default type is just '') q['type'] = str(q.get('type', '')) # add question to the pool self[q['ref']] = q #------------------------------------------------------------------------ def add_from_files(self, file_list): for filename in file_list: self.add_from_file(filename) #============================================================================ # Question Factory # given a dictionary returns a question instance. def create_question(q): '''To create a question, q must be a dictionary with at least the following keys defined: filename ref type The remaing keys depend on the type of question. ''' types = { 'information': QuestionInformation, 'radio' : QuestionRadio, 'checkbox' : QuestionCheckbox, 'text' : QuestionText, 'text_regex': QuestionTextRegex, 'textarea' : QuestionTextArea, '' : QuestionInformation, # default } # create instance of given type try: questiontype = types[q['type']] except KeyError: print(' * unsupported question type in "%s:%s".' % (q['filename'], q['ref'])) questiontype = Question # create question instance and return return questiontype(q) # =========================================================================== class Question(dict): ''' Classes derived from this base class are meant to instantiate a question to a student. Instances can shuffle options, or automatically generate questions. ''' pass # =========================================================================== class QuestionRadio(Question): '''An instance of QuestionRadio will always have the keys: type (str) text (str) options (list of strings) shuffle (bool, default True) correct (list of floats) discount (bool, default True) answer (None or an actual answer) ''' #------------------------------------------------------------------------ def __init__(self, q): # create key/values as given in q super().__init__(q) self['text'] = self.get('text', '') # generate an order for the options, e.g. [0,1,2,3,4] n = len(self['options']) perm = list(range(n)) # shuffle the order, e.g. [2,1,4,0,3] if self.get('shuffle', True): self['shuffle'] = True random.shuffle(perm) else: self['shuffle'] = False # sort options in the given order options = [None] * n # will contain list with shuffled options for i, v in enumerate(self['options']): options[perm[i]] = str(v) # always convert to string self['options'] = options # default correct option is the first one if 'correct' not in self: self['correct'] = 0 # correct can be either an integer with the correct option # or a list of degrees of correction 0..1 # always convert to list, e.g. [0,0,1,0,0] if isinstance(self['correct'], int): correct = [0.0] * n correct[self['correct']] = 1.0 self['correct'] = correct # sort correct in the given order correct = [None] * n for i, v in enumerate(self['correct']): correct[perm[i]] = float(v) self['correct'] = correct self['discount'] = bool(self.get('discount', True)) self['answer'] = None #------------------------------------------------------------------------ # can return negative values for wrong answers def correct(self): if self['answer'] is None: x = 0.0 # zero points if no answer given else: x = self['correct'][int(self['answer'])] if self['discount']: n = len(self['options']) # number of options x_aver = sum(self['correct']) / n x = (x - x_aver) / (1.0 - x_aver) self['grade'] = x return x # =========================================================================== class QuestionCheckbox(Question): '''An instance of QuestionCheckbox will always have the keys: type (str) text (str) options (list of strings) shuffle (bool, default True) correct (list of floats) discount (bool, default True) answer (None or an actual answer) ''' #------------------------------------------------------------------------ def __init__(self, q): # create key/values as given in q super().__init__(q) self['text'] = self.get('text', '') # generate an order for the options, e.g. [0,1,2,3,4] n = len(self['options']) perm = list(range(n)) # shuffle the order, e.g. [2,1,4,0,3] if self.get('shuffle', True): self['shuffle'] = True random.shuffle(perm) else: self['shuffle'] = False # sort options in the given order options = [None] * n # will contain list with shuffled options for i, v in enumerate(self['options']): options[perm[i]] = str(v) # always convert to string self['options'] = options # default is to give zero to all options [0,0,...,0] if 'correct' not in self: self['correct'] = [0.0] * n # sort correct in the given order correct = [None] * n for i, v in enumerate(self['correct']): correct[perm[i]] = float(v) self['correct'] = correct self['discount'] = bool(self.get('discount', True)) self['answer'] = None #------------------------------------------------------------------------ # can return negative values for wrong answers def correct(self): if self['answer'] is None: # not answered self['grade'] = 0.0 else: # answered x = 0.0 if self['discount']: for i, p in enumerate(self['correct']): x += p if str(i) in self['answer'] else -p self['grade'] = x / sum(abs(p) for p in self['correct']) else: for i, p in enumerate(self['correct']): x += p if str(i) in self['answer'] else 0.0 self['grade'] = x / sum(abs(p) for p in self['correct']) return self['grade'] # =========================================================================== class QuestionText(Question): '''An instance of QuestionCheckbox will always have the keys: type (str) text (str) correct (list of str) answer (None or an actual answer) ''' #------------------------------------------------------------------------ def __init__(self, q): # create key/values as given in q super().__init__(q) self['text'] = self.get('text', '') # make sure its always a list of possible correct answers if isinstance(self['correct'], str): self['correct'] = [self['correct']] self['answer'] = None #------------------------------------------------------------------------ # can return negative values for wrong answers def correct(self): if self['answer'] is None: # not answered self['grade'] = 0.0 else: # answered self['grade'] = 1.0 if self['answer'] in self['correct'] else 0.0 return self['grade'] # =========================================================================== class QuestionTextRegex(Question): '''An instance of QuestionCheckbox will always have the keys: type (str) text (str) correct (str with regex) answer (None or an actual answer) ''' #------------------------------------------------------------------------ def __init__(self, q): # create key/values as given in q super().__init__(q) self['text'] = self.get('text', '') self['answer'] = None #------------------------------------------------------------------------ # can return negative values for wrong answers def correct(self): if self['answer'] is None: # not answered self['grade'] = 0.0 else: # answered self['grade'] = 1.0 if re.match(self['correct'], self['answer']) else 0.0 return self['grade'] # =========================================================================== class QuestionTextArea(Question): '''An instance of QuestionCheckbox will always have the keys: type (str) text (str) correct (str with script to run) answer (None or an actual answer) ''' #------------------------------------------------------------------------ def __init__(self, q): # create key/values as given in q super().__init__(q) self['text'] = self.get('text', '') self['answer'] = None #------------------------------------------------------------------------ # can return negative values for wrong answers def correct(self): if self['answer'] is None: # not answered self['grade'] = 0.0 else: # answered # The correction program expects data from stdin and prints the result to stdout. # The result should be a string that can be parsed to a float. p = subprocess.Popen([self['correct']], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) try: value = p.communicate(input=self['answer'].encode('utf-8'), timeout=5)[0].decode('utf-8') # esta a dar erro! except subprocess.TimeoutExpired: p.kill() # p.communicate() # FIXME parece que este communicate obriga a ficar ate ao final do processo, mas nao consigo repetir o comportamento num script simples... self['grade'] = 0.0 # student gets a zero if timout occurs # In case the correction program returns a non float value, we assume an error # has occurred (for instance, invalid input). We just assume its the student's # fault and give him Zero. try: self['grade'] = float(value) except (ValueError): cherrypy.log.error('While checking answer, process %s returned a non float value: %s' % (self['correct'], value), 'APPLICATION') self['grade'] = 0.0 # Example script to validade answers: # import sys # s = sys.stdin.read() # if s=='Alibaba': # print(1.0) # else: # print(0.0) # exit(0) return self['grade'] # =========================================================================== class QuestionInformation(Question): '''An instance of QuestionCheckbox will always have the keys: type (str) text (str) correct (str with regex) answer (None or an actual answer) ''' #------------------------------------------------------------------------ def __init__(self, q): # create key/values as given in q super().__init__(q) self['text'] = self.get('text', '') self['answer'] = None self['points'] = 0.0 #------------------------------------------------------------------------ # can return negative values for wrong answers def correct(self): self['grade'] = 1.0 # always "correct" but points should be zero! return self['grade']