factory.py 3.73 KB
# QFactory is a class that can generate question instances, e.g. by shuffling
# options, running a script to generate the question, etc.
#
# To generate an instance of a question we use the method generate() where
# the argument is the reference of the question we wish to produce.
#
# Example:
#
#   # read question from file
#   qdict = tools.load_yaml(filename)
#   qfactory = QFactory(qdict)
#   question = qfactory.generate()
#
#   # experiment answering one question and correct it
#   question.updateAnswer('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
import logging

# project
from tools import run_script
from questions import QuestionInformation, QuestionRadio, QuestionCheckbox, QuestionText, QuestionTextRegex, QuestionTextArea, QuestionNumericInterval

# setup logger for this module
logger = logging.getLogger(__name__)



# ===========================================================================
# Question Factory
# ===========================================================================
class QFactory(object):
    # Depending on the type of question, a different question class will be
    # instantiated. All these classes derive from the base class `Question`.
    _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, question_dict):
        self.question = question_dict

    # -----------------------------------------------------------------------
    # Given a ref returns an instance of a descendent of Question(),
    # i.e. a question object (radio, checkbox, ...).
    # -----------------------------------------------------------------------
    def generate(self):
        logger.debug(f'Generating "{self.question["ref"]}"...')
        # Shallow copy so that script generated questions will not replace
        # the original generators
        q = self.question.copy()

        # 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 yaml parsed into a dictionary `q`.
        if q['type'] == 'generator':
            logger.debug(f' \\_ Running "{q["script"]}".')
            q.setdefault('arg', '') # optional arguments will be sent to stdin
            script = path.join(q['path'], q['script'])
            out = run_script(script=script, stdin=q['arg'])
            q.update(out)

        # 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'Failed to generate question "{q["ref"]}"')
            raise e
        else:
            return qinstance