pymoxquizz.py 8.92 KB
Newer Older
1 2
#!/usr/bin/env python
## -*- coding: utf-8 -*-
3 4 5 6 7
"""
A MozQuizz question library for Python.
See http://moxquizz.de/ for the original implementation in TCL.
"""

8 9
from __future__ import unicode_literals, print_function
from io import open
10 11
import re
import sys
12 13 14 15 16 17 18 19


class Question:
    """
    Represents one MoxQuizz question.
    """

    category = None
20 21 22 23
    """
    The question category. Arbitrary text; optional.
    """

24
    question = None
25 26 27 28
    """
    The question. Arbitrary text; required.
    """

29
    answer = None
30 31 32 33 34
    """
    The answer. Arbitrary text; required. Correct answers can also be covered
    by the :attr:`regexp` property.
    """

35
    regexp = None
36 37 38 39 40
    """
    A regular expression that will generate correct answers. Optional. See
    also the :attr:`answer` property.
    """

41
    author = None
42 43 44 45 46 47 48 49 50 51
    """
    The question author. Arbitrary text; optional.
    """

    level = None  # Default: NORMAL (constructor)
    """
    The difficulty level. Value must be from the :attr:`LEVELS` tuple.
    The default value is :attr:`NORMAL`.
    """

52
    comment = None
53 54 55 56 57 58 59 60
    """
    A comment. Arbitrary text; optional.
    """

    score = 1
    """
    The points scored for the correct answer. Integer value; default is 1.
    """
61
    tip = list()
62 63 64 65
    """
    An ordered list of tips (hints) to display to users. Optional.
    """

66
    tipcycle = 0
67 68 69
    """
    Indicates which tip is to be displayed next, if any.
    """
70 71

    TRIVIAL = 1
72 73 74 75
    """
    A value for :attr:`level` that indicates a question of trivial difficulty.
    """

76
    EASY = 2
77 78 79 80
    """
    A value for :attr:`level` that indicates a question of easy difficulty.
    """

81
    NORMAL = 3
82 83 84 85 86
    """
    A value for :attr:`level` that indicates a question of average or normal
    difficulty.
    """

87
    HARD = 4
88 89 90 91
    """
    A value for :attr:`level` that indicates a question of hard difficulty.
    """

92
    EXTREME = 5
93 94 95 96
    """
    A value for :attr:`level` that indicates a question of extreme difficulty
    or obscurity.
    """
97 98

    LEVELS = (TRIVIAL, EASY, NORMAL, HARD, EXTREME)
99 100 101 102
    """
    The available :attr:`level` difficulty values, :attr:`TRIVIAL`, :attr:`EASY`,
    :attr:`NORMAL`, :attr:`HARD` and :attr:`EXTREME`.
    """
103 104

    def __init__(self, attributes_dict):
105 106 107 108 109 110
        """
        Constructor that takes a dictionary of MoxQuizz key-value pairs. Usually
        called from a :class:`QuestionBank`.
        """
        # Set defaults first.
        self.level = self.NORMAL
111 112 113 114
        self.parse(attributes_dict)

    def parse(self, attributes_dict):
        """
115 116
        Populate fields from a dictionary of attributes, usually provided by a
        :class:`QuestionBank` :attr:`~QuestionBank.parse` call.
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
        """

        ## Valid keys:
        #  ----------
        #  Category?                              (should always be on top!)
        #  Question                               (should always stand after Category)
        #  Answer                                 (will be matched if no regexp is provided)
        #  Regexp?                                (use UNIX-style expressions)
        #  Author?                                (the brain behind this question)
        #  Level? [baby|easy|normal|hard|extreme] (difficulty)
        #  Comment?                               (comment line)
        #  Score? [#]                             (credits for answering this question)
        #  Tip*                                   (provide one or more hints)
        #  TipCycle? [#]                          (Specify number of generated tips)

        if 'Question' in attributes_dict.keys():
            self.question = attributes_dict['Question']
        else:
            raise Exception("Cannot instantiate Question: 'Question' attribute required.")

        if 'Category' in attributes_dict.keys():
            self.category = attributes_dict['Category']

        if 'Answer' in attributes_dict.keys():
            self.answer = attributes_dict['Answer']
        else:
            raise Exception("Cannot instantiate Question: 'Answer' attribute required.")

        if 'Regexp' in attributes_dict.keys():
            self.regexp = attributes_dict['Regexp']

        if 'Author' in attributes_dict.keys():
            self.category = attributes_dict['Author']

        if 'Level' in attributes_dict.keys() and attributes_dict['Level'] in self.LEVELS:
152 153 154
            self.level = attributes_dict['Level']
        elif 'Level' in attributes_dict.keys() and attributes_dict['Level'] in QuestionBank.LEVEL_VALUES.keys():
            self.level = QuestionBank.LEVEL_VALUES[attributes_dict['Level']]
155 156 157 158 159 160 161 162 163 164 165 166 167

        if 'Comment' in attributes_dict.keys():
            self.comment = attributes_dict['Comment']

        if 'Score' in attributes_dict.keys():
            self.score = attributes_dict['Score']

        if 'Tip' in attributes_dict.keys():
            self.tip = attributes_dict['Tip']

        if 'Tipcycle' in attributes_dict.keys():
            self.tipcycle = attributes_dict['Tipcycle']

168 169 170
    def attempt(self, answer):
        return (self.answer is not None and self.answer.lower() == answer.lower()) or (
            self.regexp is not None and re.search(self.regexp, answer, re.IGNORECASE) is not None)
171 172 173 174 175 176 177

class QuestionBank:
    """
    Represents a MoxQuizz question bank.
    """

    filename = ''
178 179 180 181
    """
    The path or filename of the question bank file.
    """

182
    questions = list()
183 184 185 186
    """
    A list of :class:`Question` objects, constituting the questions in the
    question bank.
    """
187 188 189 190 191 192 193 194 195 196 197 198 199

    # Case sensitive, to remain backwards-compatible with MoxQuizz.
    KEYS = ('Answer',
            'Author',
            'Category',
            'Comment',
            'Level',
            'Question',
            'Regexp',
            'Score',
            'Tip',
            'Tipcycle',
            )
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
    """
    The valid attributes available in a MoxQuizz question bank file.
    """

    LEVEL_VALUES = {
        'trivial': Question.TRIVIAL,
        'baby': Question.TRIVIAL,
        'easy': Question.EASY,
        'normal': Question.NORMAL,
        'hard': Question.HARD,
        'difficult': Question.HARD,
        'extreme': Question.EXTREME
        }
    """
    Text labels for the :attr:`Question.level` difficulty values.
    """
216 217 218

    def __init__(self, filename):
        """
219
        Constructor, takes a MozQuizz-formatted question bank filename.
220 221 222 223 224 225
        """
        self.filename = filename
        self.questions = self.parse(filename)

    def parse(self, filename):
        """
226 227
        Read a MoxQuizz-formatted question bank file. Returns a ``list`` of
        :class:`Question` objects found in the file.
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
        """
        questions = list()

        with open(filename) as f:
            key = ''
            i = 0

            # new question
            q = dict()
            q['Tip'] = list()

            for line in f:
                line = line.strip()
                i += 1

                # Ignore comments.
                if line.startswith('#'):
                    continue

                # A blank line starts a new question.
                if line == '':
                    # Store the previous question, if valid.
                    if 'Question' in q.keys() and 'Answer' in q.keys():
                        question = Question(q)
                        questions.append(question)

                    # Start a new question.
                    q = dict()
                    q['Tip'] = list()
                    continue

                # Fetch the next parameter.
                try:
                    (key, value) = line.split(':', 1)
262 263
                    key = key.strip()
                    value = value.strip()
264 265 266 267 268 269 270 271 272 273 274 275 276
                except ValueError:
                    print("Unexpected weirdness in MoxQuizz questionbank '%s', line %s." % (self.filename, i))
                    continue
                    # break  # TODO: is it appropriate to bail on broken bank files?

                # Ignore bad parameters.
                if key not in self.KEYS:
                    print("Unexpected key '%s' in MoxQuizz questionbank '%s', line %s." % (key, self.filename, i))
                    continue

                # Enumerate the Tips.
                if key == 'Tip':
                    q['Tip'].append(value.strip())
277 278 279 280 281
                elif key == 'Level':
                    if value not in self.LEVEL_VALUES:
                        print("Unexpected Level value '%s' in MoxQuizz questionbank '%s', line '%s'." % (value, self.filename, i))
                    else:
                        q['Level'] = self.LEVEL_VALUES[value]
282 283 284 285 286 287 288 289
                else:
                    q[key] = value.strip()

        return questions


# A crappy test.
if __name__ == '__main__':
290
    qb = QuestionBank('questions/questions.doctorlard.en')
291 292
    for q in qb.questions:
        print(q.question)
293 294 295 296 297
        if sys.version.startswith('2'):
            a = unicode(raw_input('A: '), 'utf8')
        else:
            a = input('A: ')
        if q.attempt(a):
298 299 300
            print("Correct!")
        else:
            print("Incorrect - the answer is '%s'" % q.answer)