# We start with an empty QuestionFactory() that will be populated with # question generators that we can load from YAML files. # To generate an instance of a question we use the method generate(ref) where # the argument is the reference of the question we wish to produce. # # Example: # # # read everything from question files # factory = QuestionFactory() # factory.load_files(['file1.yaml', 'file1.yaml'], '/path/to') # # question = factory.generate('some_ref') # # # experiment answering one question and correct it # question['answer'] = 42 # insert answer # grade = question.correct() # correct answer # An instance of an actual question is an object that inherits from Question() # # Question - base class inherited by other classes # QuestionInformation - not a question, just a box with content # QuestionRadio - single choice from a list of options # QuestionCheckbox - multiple choice, equivalent to multiple true/false # QuestionText - line of text compared to a list of acceptable answers # QuestionTextRegex - line of text matched against a regular expression # QuestionTextArea - corrected by an external program # QuestionNumericInterval - line of text parsed as a float # base from os import path from tools import load_yaml, run_script import logging from questions import QuestionRadio, QuestionCheckbox, QuestionText, QuestionTextRegex, QuestionNumericInterval, QuestionTextArea, QuestionInformation # setup logger for this module logger = logging.getLogger(__name__) # =========================================================================== class QuestionFactoryException(Exception): pass # =========================================================================== # This class contains a pool of questions generators from which particular # Question() instances are generated using QuestionsFactory.generate(ref). # =========================================================================== class QuestionFactory(dict): _types = { 'radio' : QuestionRadio, 'checkbox' : QuestionCheckbox, 'text' : QuestionText, 'text-regex': QuestionTextRegex, 'numeric-interval': QuestionNumericInterval, 'textarea' : QuestionTextArea, # -- informative panels -- 'information': QuestionInformation, 'info': QuestionInformation, 'warning' : QuestionInformation, 'warn': QuestionInformation, 'alert' : QuestionInformation, 'success' : QuestionInformation, } # ----------------------------------------------------------------------- def __init__(self): super().__init__() # ----------------------------------------------------------------------- # Add single question provided in a dictionary. # After this, each question will have at least 'ref' and 'type' keys. # ----------------------------------------------------------------------- def add_question(self, question): # if missing defaults to ref='/path/file.yaml:3' question.setdefault('ref', f'{question["filename"]}:{question["index"]}') if question['ref'] in self: logger.error(f'Duplicate reference "{question["ref"]}" replaces the original.') question.setdefault('type', 'information') self[question['ref']] = question logger.debug(f'Added question "{question["ref"]}" to the pool.') # ----------------------------------------------------------------------- # load single YAML questions file # ----------------------------------------------------------------------- def load_file(self, pathfile, questions_dir=''): # questions_dir is a base directory # pathfile is a path of a file under the questions_dir # For example, if # pathfile = 'math/questions.yaml' # questions_dir = '/home/john/questions' # then the complete path is # fullpath = '/home/john/questions/math/questions.yaml' fullpath = path.normpath(path.join(questions_dir, pathfile)) (dirname, filename) = path.split(fullpath) questions = load_yaml(fullpath, default=[]) for i, q in enumerate(questions): try: q.update({ 'filename': filename, 'path': dirname, 'index': i # position in the file, 0 based }) except AttributeError: logger.error(f'Question {pathfile}:{i} is not a dictionary. Skipped!') else: self.add_question(q) logger.info(f'Loaded {len(self)} questions from "{pathfile}".') # ----------------------------------------------------------------------- # load multiple YAML question files # ----------------------------------------------------------------------- def load_files(self, files, questions_dir=''): for filename in files: self.load_file(filename, questions_dir) # ----------------------------------------------------------------------- # Given a ref returns an instance of a descendent of Question(), # i.e. a question object (radio, checkbox, ...). # ----------------------------------------------------------------------- def generate(self, ref): # Shallow copy so that script generated questions will not replace # the original generators try: q = self[ref].copy() except KeyError: #FIXME exception type? logger.error(f'Can\'t find question "{ref}".') raise QuestionFactoryException() # If question is of generator type, an external program will be run # which will print a valid question in yaml format to stdout. This # output is then converted to a dictionary and `q` becomes that dict. if q['type'] == 'generator': logger.debug('Running script to generate question "{0}".'.format(q['ref'])) q.setdefault('arg', '') # optional arguments will be sent to stdin script = path.normpath(path.join(q['path'], q['script'])) out = run_script(script=script, stdin=q['arg']) try: q.update(out) except: q.update({ 'type': 'alert', 'title': 'Erro interno', 'text': 'Ocorreu um erro a gerar esta pergunta.' }) # The generator was replaced by a question but not yet instantiated # Finally we create an instance of Question() try: qinstance = self._types[q['type']](q) # instance with correct class except KeyError as e: logger.error(f'Unknown type "{q["type"]}" in "{q["filename"]}:{q["ref"]}".') raise e except: logger.error(f'Failed to create question "{q["ref"]}" from file "{q["filename"]}".') raise else: logger.debug(f'Generated question "{ref}".') return qinstance