#!/usr/bin/env python ## -*- coding: utf-8 -*- """ A MozQuizz question library for Python. See http://moxquizz.de/ for the original implementation in TCL. """ from __future__ import unicode_literals, print_function from io import open import re import sys class Question: """ Represents one MoxQuizz question. """ category = None """ The question category. Arbitrary text; optional. """ question = None """ The question. Arbitrary text; required. """ answer = None """ The answer. Arbitrary text; required. Correct answers can also be covered by the :attr:`regexp` property. """ regexp = None """ A regular expression that will generate correct answers. Optional. See also the :attr:`answer` property. """ author = None """ 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`. """ comment = None """ A comment. Arbitrary text; optional. """ score = 1 """ The points scored for the correct answer. Integer value; default is 1. """ tip = list() """ An ordered list of tips (hints) to display to users. Optional. """ tipcycle = 0 """ Indicates which tip is to be displayed next, if any. """ TRIVIAL = 1 """ A value for :attr:`level` that indicates a question of trivial difficulty. """ EASY = 2 """ A value for :attr:`level` that indicates a question of easy difficulty. """ NORMAL = 3 """ A value for :attr:`level` that indicates a question of average or normal difficulty. """ HARD = 4 """ A value for :attr:`level` that indicates a question of hard difficulty. """ EXTREME = 5 """ A value for :attr:`level` that indicates a question of extreme difficulty or obscurity. """ LEVELS = (TRIVIAL, EASY, NORMAL, HARD, EXTREME) """ The available :attr:`level` difficulty values, :attr:`TRIVIAL`, :attr:`EASY`, :attr:`NORMAL`, :attr:`HARD` and :attr:`EXTREME`. """ def __init__(self, attributes_dict): """ Constructor that takes a dictionary of MoxQuizz key-value pairs. Usually called from a :class:`QuestionBank`. """ # Set defaults first. self.level = self.NORMAL self.parse(attributes_dict) def parse(self, attributes_dict): """ Populate fields from a dictionary of attributes, usually provided by a :class:`QuestionBank` :attr:`~QuestionBank.parse` call. """ ## 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: 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']] 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'] 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) class QuestionBank: """ Represents a MoxQuizz question bank. """ filename = '' """ The path or filename of the question bank file. """ questions = list() """ A list of :class:`Question` objects, constituting the questions in the question bank. """ # Case sensitive, to remain backwards-compatible with MoxQuizz. KEYS = ('Answer', 'Author', 'Category', 'Comment', 'Level', 'Question', 'Regexp', 'Score', 'Tip', 'Tipcycle', ) """ 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. """ def __init__(self, filename): """ Constructor, takes a MozQuizz-formatted question bank filename. """ self.filename = filename self.questions = self.parse(filename) def parse(self, filename): """ Read a MoxQuizz-formatted question bank file. Returns a ``list`` of :class:`Question` objects found in the file. """ 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) key = key.strip() value = value.strip() 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()) 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] else: q[key] = value.strip() return questions # A crappy test. if __name__ == '__main__': qb = QuestionBank('questions.doctorlard.en') for q in qb.questions: print(q.question) if sys.version.startswith('2'): a = unicode(raw_input('A: '), 'utf8') else: a = input('A: ') if q.attempt(a): print("Correct!") else: print("Incorrect - the answer is '%s'" % q.answer)