diff --git a/questionfactory.py b/questionfactory.py new file mode 100644 index 0000000..240de73 --- /dev/null +++ b/questionfactory.py @@ -0,0 +1,169 @@ +# 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 +# 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 +# QuestionInformation - not a question, just a box with content + +# base +from os import path +from tools import load_yaml, run_script + +import logging + + + + +from questions import Question, 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 -- libgit2 0.21.2