1 # Copyright (C) 2013 W. Trevor King <wking@tremily.us>
3 # This file is part of quizzer.
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
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.
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/>.
18 import logging as _logging
19 import os.path as _os_path
21 import readline as _readline
22 except ImportError as _readline_import_error:
26 from pygments.console import colorize as _colorize
27 except ImportError as e:
28 def _colorize(color_key=None, text=None):
32 from .. import error as _error
33 from .. import question as _question
34 from . import UserInterface as _UserInterface
35 from . import util as _util
38 _LOG = _logging.getLogger(__name__)
41 class QuestionCommandLine (_cmd.Cmd):
43 'Type help or ? to list commands.',
44 'Non-commands will be interpreted as answers.',
45 'Use a blank line to terminate multi-line answers.',
47 intro = '\n'.join(['Welcome to the quizzer shell.'] + _help)
50 def __init__(self, ui):
51 super(QuestionCommandLine, self).__init__()
53 if self.ui.quiz.introduction:
54 self.intro = '\n\n'.join([self.intro, self.ui.quiz.introduction])
58 def get_question(self):
59 self.question = self.ui.get_question(user=self.ui.user)
63 return True # out of questions
68 def postcmd(self, stop, line):
75 self._tempdir.cleanup() # occasionally redundant, but that's ok
80 "Pose a question and prompt"
85 self.ui.colors['question'], self.question.format_prompt()),
88 _colorize(self.ui.colors['prompt'], line)
89 for line in self._extra_ps1_lines())
90 lines.append(_colorize(self.ui.colors['prompt'], self._prompt))
91 self.prompt = '\n'.join(lines)
93 self.prompt = _colorize(self.ui.colors['prompt'], self._prompt)
96 "Just prompt (without the question, e.g. for multi-line answers)"
97 self.prompt = _colorize(self.ui.colors['prompt'], self._prompt)
99 def _extra_ps1_lines(self):
100 for multimedia in self.question.multimedia:
101 for line in self._format_multimedia(multimedia):
102 yield line # for Python 3.3, use PEP 380's `yield from ...`
103 if (isinstance(self.question, _question.ChoiceQuestion) and
104 self.question.display_choices):
105 for line in self._format_choices(question=self.question):
108 def _format_multimedia(self, multimedia):
109 path = self.ui.quiz.multimedia_path(multimedia=multimedia)
110 content_type = multimedia['content-type']
112 self._children.append(_util.mailcap_view(
113 path=path, content_type=content_type, background=True))
114 except NotImplementedError:
115 path = _os_path.abspath(path)
116 yield 'multimedia ({}): {}'.format(content_type, path)
118 def _reap_children(self):
120 for process in self._children:
121 _LOG.debug('poll child process {}'.format(process.pid))
122 if process.poll() is not None:
123 _LOG.debug('process {} returned {}'.format(
124 process.pid, process.returncode))
125 reaped.append(process)
126 for process in reaped:
127 self._children.remove(process)
129 def _format_choices(self, question):
130 for i,choice in enumerate(question.answer):
131 yield '{}) {}'.format(i, choice)
132 yield 'Answer with the index of your choice'
133 if question.accept_all:
135 if question.multiple_answers:
137 yield '{} fill in an alternative answer'.format(conj)
138 if question.multiple_answers:
139 self._separator = ','
140 yield ("Separate multiple answers with the '{}' character"
141 ).format(self._separator)
143 def _process_answer(self, answer):
144 "Back out any mappings suggested by _extra_ps1_lines()"
145 if (isinstance(self.question, _question.ChoiceQuestion) and
146 self.question.display_choices):
147 if self.question.multiple_answers:
149 for a in answer.split(self._separator):
152 answers.append(self.question.answer[i])
153 except (ValueError, IndexError):
159 return self.question.answer[i]
160 except (ValueError, IndexError):
164 def default(self, line):
165 self.answers.append(line)
166 if self.question.multiline:
169 return self._answer()
172 return self._answer()
175 if self.question.multiline:
176 answer = self.answers
178 answer = self.answers[0]
187 kwargs['tempdir'] = self._tempdir
188 answer = self._process_answer(answer=answer)
189 correct,details = self.ui.process_answer(
190 question=self.question, answer=answer, **kwargs)
192 print(_colorize(self.ui.colors['correct'], 'correct\n'))
194 print(_colorize(self.ui.colors['incorrect'], 'incorrect'))
197 self.ui.colors['incorrect'], '{}\n'.format(details)))
200 return self.get_question()
202 def do_answer(self, arg):
203 """Explicitly add a line to your answer
205 This is useful if the line you'd like to add starts with a
206 quizzer-shell command. For example:
208 quizzer? answer help=5
210 return self.default(arg)
212 def do_shell(self, arg):
213 """Run a shell command in the question temporary directory
215 For example, you can spawn an interactive session with:
219 If the question does not allow interactive sessions, this
222 if getattr(self.question, 'allow_interactive', False):
223 if not self._tempdir:
224 self._tempdir = self.question.setup_tempdir()
226 self._tempdir.invoke(
227 interpreter='/bin/sh', text=arg, stdout=None, stderr=None,
228 universal_newlines=False,
229 env=self.question.get_environment())
230 except (KeyboardInterrupt, _error.CommandError) as e:
231 if isinstance(e, KeyboardInterrupt):
232 LOG.warning('KeyboardInterrupt')
235 self._tempdir.cleanup()
238 def do_quit(self, arg):
239 "Stop taking the quiz"
243 def do_skip(self, arg):
244 "Skip the current question, and continue with the quiz"
245 self.ui.stack[self.ui.user].append(self.question)
246 return self.get_question()
248 def do_hint(self, arg):
249 "Show a hint for the current question"
251 print(self.question.format_help())
253 def do_copyright(self, arg):
254 "Print the quiz copyright notice"
255 if self.ui.quiz.copight:
256 print('\n'.join(self.ui.quiz.copyright))
258 print(self.ui.quiz.copyright)
260 def do_help(self, arg):
261 'List available commands with "help" or detailed help with "help cmd"'
263 print('\n'.join(self._help))
264 super(QuestionCommandLine, self).do_help(arg)
267 class CommandLineInterface (_UserInterface):
268 colors = { # listed in pygments.console.light_colors
269 'question': 'turquoise',
278 if self.stack[self.user]:
279 cmd = QuestionCommandLine(ui=self)
282 self._display_results()
284 def _display_results(self):
285 print(_colorize(self.colors['result'], 'results:'))
286 answers = self.answers.get_answers(user=self.user)
287 for question in self.quiz:
288 if question.id in answers:
289 self._display_result(question=question)
291 self._display_totals()
293 def _display_result(self, question):
294 answers = self.answers.get_answers(user=self.user).get(question.id, [])
298 self.colors['question'],
299 question.format_prompt(newline='\n '))))
301 lc = len([a for a in answers if a['correct']])
303 print('answers: {}/{} ({:.2f})'.format(lc, la, float(lc)/la))
304 for answer in answers:
305 if answer['correct']:
308 correct = 'incorrect'
309 correct = _colorize(self.colors[correct], correct)
310 ans = answer['answer']
311 if question.multiline:
312 ans = '\n '.join(ans)
313 print(' you answered: {}'.format(ans))
314 print(' which was: {}'.format(correct))
316 def _display_totals(self):
317 answered = self.answers.get_answered(
318 questions=self.quiz, user=self.user)
319 correctly_answered = self.answers.get_correctly_answered(
320 questions=self.quiz, user=self.user)
322 lc = len(correctly_answered)
323 print('answered {} of {} questions'.format(la, len(self.quiz)))
325 print(('of the answered questions, '
326 '{} ({:.2f}) were answered correctly'
327 ).format(lc, float(lc)/la))