quiz: Add Quiz.introduction for an optional intro message
[quizzer.git] / quizzer / ui / cli.py
1 # Copyright (C) 2013 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of quizzer.
4 #
5 # quizzer is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # quizzer is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # quizzer.  If not, see <http://www.gnu.org/licenses/>.
16
17 import cmd as _cmd
18 try:
19     import readline as _readline
20 except ImportError as _readline_import_error:
21     _readline = None
22
23 try:
24     from pygments.console import colorize as _colorize
25 except ImportError as e:
26     def _colorize(color_key=None, text=None):
27         return text
28     print(e)
29
30 from . import UserInterface
31
32
33 class QuestionCommandLine (_cmd.Cmd):
34     _help = [
35         'Type help or ? to list commands.',
36         'Non-commands will be interpreted as answers.',
37         'Use a blank line to terminate multi-line answers.',
38         ]
39     intro = '\n'.join(['Welcome to the quizzer shell.'] + _help)
40     _prompt = 'quizzer? '
41
42     def __init__(self, ui):
43         super(QuestionCommandLine, self).__init__()
44         self.ui = ui
45         if self.ui.quiz.introduction:
46             self.intro = '\n\n'.join([self.intro, self.ui.quiz.introduction])
47
48     def preloop(self):
49         self.question = self.ui.get_question()
50         self._reset()
51
52     def _reset(self):
53         self.answers = []
54         self._set_ps1()
55
56     def _set_ps1(self):
57         "Pose a question and prompt"
58         if self.question:
59             self.prompt = '\n{}\n{}'.format(
60                 _colorize(
61                     self.ui.colors['question'], self.question.format_prompt()),
62                 _colorize(self.ui.colors['prompt'], self._prompt))
63         else:
64             self.prompt = _colorize(self.ui.colors['prompt'], self._prompt)
65
66     def _set_ps2(self):
67         "Just prompt (without the question, e.g. for multi-line answers)"
68         self.prompt = _colorize(self.ui.colors['prompt'], self._prompt)
69
70     def default(self, line):
71         self.answers.append(line)
72         if self.question.multiline:
73             self._set_ps2()
74         else:
75             return self._answer()
76
77     def emptyline(self):
78         return self._answer()
79
80     def _answer(self):
81         if self.question.multiline:
82             answer = self.answers
83         elif self.answers:
84             answer = self.answers[0]
85         else:
86             answer = ''
87         correct = self.ui.process_answer(question=self.question, answer=answer)
88         if correct:
89             print(_colorize(self.ui.colors['correct'], 'correct\n'))
90         else:
91             print(_colorize(self.ui.colors['incorrect'], 'incorrect\n'))
92         self.question = self.ui.get_question()
93         if not self.question:
94             return True  # out of questions
95         self._reset()
96
97     def do_answer(self, arg):
98         """Explicitly add a line to your answer
99
100         This is useful if the line you'd like to add starts with a
101         quizzer-shell command.  For example:
102
103           quizzer? answer help=5
104         """
105         return self.default(arg)
106
107     def do_quit(self, arg):
108         "Stop taking the quiz"
109         self._reset()
110         return True
111
112     def do_skip(self, arg):
113         "Skip the current question, and continue with the quiz"
114         self.ui.stack.append(self.question)
115         self.question = self.ui.get_question()
116         if not self.question:
117             return True  # out of questions
118         self._reset()
119
120     def do_hint(self, arg):
121         "Show a hint for the current question"
122         self._reset()
123         print(self.question.format_help())
124
125     def do_help(self, arg):
126         'List available commands with "help" or detailed help with "help cmd"'
127         if not arg:
128             print('\n'.join(self._help))
129         super(QuestionCommandLine, self).do_help(arg)
130
131
132 class CommandLineInterface (UserInterface):
133     colors = {  # listed in pygments.console.light_colors
134         'question': 'turquoise',
135         'prompt': 'blue',
136         'correct': 'green',
137         'incorrect': 'red',
138         'result': 'fuchsia',
139         }
140
141     def run(self):
142         if not self.stack:
143             return
144         cmd = QuestionCommandLine(ui=self)
145         cmd.cmdloop()
146         print()
147
148     def display_results(self):
149         print(_colorize(self.colors['result'], 'results:'))
150         for question in self.quiz:
151             if question.id in self.answers:
152                 self.display_result(question=question)
153                 print()
154         self.display_totals()
155
156     def display_result(self, question):
157         answers = self.answers.get(question.id, [])
158         print('question:')
159         print('  {}'.format(
160             _colorize(
161                 self.colors['question'],
162                 question.format_prompt(newline='\n  '))))
163         la = len(answers)
164         lc = len([a for a in answers if a['correct']])
165         print('answers: {}/{} ({:.2f})'.format(lc, la, float(lc)/la))
166         for answer in answers:
167             if answer['correct']:
168                 correct = 'correct'
169             else:
170                 correct = 'incorrect'
171             correct = _colorize(self.colors[correct], correct)
172             ans = answer['answer']
173             if question.multiline:
174                 ans = '\n                '.join(ans)
175             print('  you answered: {}'.format(ans))
176             print('     which was: {}'.format(correct))
177
178     def display_totals(self):
179         answered = self.answers.get_answered(questions=self.quiz)
180         correctly_answered = self.answers.get_correctly_answered(
181             questions=self.quiz)
182         la = len(answered)
183         lc = len(correctly_answered)
184         print('answered {} of {} questions'.format(la, len(self.quiz)))
185         print(('of the answered questions, {} ({:.2f}) were answered correctly'
186                ).format(lc, float(lc)/la))