ui.cli: `skip` shifts the current question to the back of the stack
[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
46     def preloop(self):
47         self.question = self.ui.get_question()
48         self._reset()
49
50     def _reset(self):
51         self.answers = []
52         self._set_ps1()
53
54     def _set_ps1(self):
55         "Pose a question and prompt"
56         if self.question:
57             self.prompt = '\n{}\n{}'.format(
58                 _colorize(
59                     self.ui.colors['question'], self.question.format_prompt()),
60                 _colorize(self.ui.colors['prompt'], self._prompt))
61         else:
62             self.prompt = _colorize(self.ui.colors['prompt'], self._prompt)
63
64     def _set_ps2(self):
65         "Just prompt (without the question, e.g. for multi-line answers)"
66         self.prompt = _colorize(self.ui.colors['prompt'], self._prompt)
67
68     def default(self, line):
69         self.answers.append(line)
70         if self.question.multiline:
71             self._set_ps2()
72         else:
73             return self._answer()
74
75     def emptyline(self):
76         return self._answer()
77
78     def _answer(self):
79         if self.question.multiline:
80             answer = self.answers
81         elif self.answers:
82             answer = self.answers[0]
83         else:
84             answer = ''
85         correct = self.ui.process_answer(question=self.question, answer=answer)
86         if correct:
87             print(_colorize(self.ui.colors['correct'], 'correct\n'))
88         else:
89             print(_colorize(self.ui.colors['incorrect'], 'incorrect\n'))
90         self.question = self.ui.get_question()
91         if not self.question:
92             return True  # out of questions
93         self._reset()
94
95     def do_answer(self, arg):
96         """Explicitly add a line to your answer
97
98         This is useful if the line you'd like to add starts with a
99         quizzer-shell command.  For example:
100
101           quizzer? answer help=5
102         """
103         return self.default(arg)
104
105     def do_quit(self, arg):
106         "Stop taking the quiz"
107         self._reset()
108         return True
109
110     def do_skip(self, arg):
111         "Skip the current question, and continue with the quiz"
112         self.ui.stack.append(self.question)
113         self.question = self.ui.get_question()
114         if not self.question:
115             return True  # out of questions
116         self._reset()
117
118     def do_hint(self, arg):
119         "Show a hint for the current question"
120         self._reset()
121         print(self.question.format_help())
122
123     def do_help(self, arg):
124         'List available commands with "help" or detailed help with "help cmd"'
125         if not arg:
126             print('\n'.join(self._help))
127         super(QuestionCommandLine, self).do_help(arg)
128
129
130 class CommandLineInterface (UserInterface):
131     colors = {  # listed in pygments.console.light_colors
132         'question': 'turquoise',
133         'prompt': 'blue',
134         'correct': 'green',
135         'incorrect': 'red',
136         'result': 'fuchsia',
137         }
138
139     def run(self):
140         if not self.stack:
141             return
142         cmd = QuestionCommandLine(ui=self)
143         cmd.cmdloop()
144         print()
145
146     def display_results(self):
147         print(_colorize(self.colors['result'], 'results:'))
148         for question in self.quiz:
149             if question.id in self.answers:
150                 self.display_result(question=question)
151                 print()
152         self.display_totals()
153
154     def display_result(self, question):
155         answers = self.answers.get(question.id, [])
156         print('question:')
157         print('  {}'.format(
158             _colorize(
159                 self.colors['question'],
160                 question.format_prompt(newline='\n  '))))
161         la = len(answers)
162         lc = len([a for a in answers if a['correct']])
163         print('answers: {}/{} ({:.2f})'.format(lc, la, float(lc)/la))
164         for answer in answers:
165             if answer['correct']:
166                 correct = 'correct'
167             else:
168                 correct = 'incorrect'
169             correct = _colorize(self.colors[correct], correct)
170             ans = answer['answer']
171             if question.multiline:
172                 ans = '\n                '.join(ans)
173             print('  you answered: {}'.format(ans))
174             print('     which was: {}'.format(correct))
175
176     def display_totals(self):
177         answered = self.answers.get_answered(questions=self.quiz)
178         correctly_answered = self.answers.get_correctly_answered(
179             questions=self.quiz)
180         la = len(answered)
181         lc = len(correctly_answered)
182         print('answered {} of {} questions'.format(la, len(self.quiz)))
183         print(('of the answered questions, {} ({:.2f}) were answered correctly'
184                ).format(lc, float(lc)/la))