version, __version__)) from e
data = upgrader(data)
self.update(data['answers'])
+ if '' in self:
+ self[None] = self.pop('')
def save(self, **kwargs):
+ answers = dict(self)
+ if None in answers:
+ answers[''] = answers.pop(None)
data = {
'version': __version__,
- 'answers': self,
+ 'answers': answers,
}
with self._open(mode='w', **kwargs) as f:
_json.dump(
data, f, indent=2, separators=(',', ': '), sort_keys=True)
f.write('\n')
- def add(self, question, answer, correct):
- if question.id not in self:
- self[question.id] = []
+ def add(self, question, answer, correct, user=None):
+ if user == '':
+ raise ValueError('the empty string is an invalid username')
+ if user not in self:
+ self[user] = {}
+ if question.id not in self[user]:
+ self[user][question.id] = []
timezone = _datetime.timezone.utc
timestamp = _datetime.datetime.now(tz=timezone).isoformat()
- self[question.id].append({
+ self[user][question.id].append({
'answer': answer,
'correct': correct,
'timestamp': timestamp,
})
- def get_answered(self, questions):
- return [q for q in questions if q.id in self]
+ def get_answers(self, user=None):
+ if user == '':
+ raise ValueError('the empty string is an invalid username')
+ return self.get(user, {})
- def get_unanswered(self, questions):
- return [q for q in questions if q.id not in self]
+ def _get_questions(self, check, questions, user=None):
+ if user == '':
+ raise ValueError('the empty string is an invalid username')
+ answers = self.get_answers(user=user)
+ return [q for q in questions if check(question=q, answers=answers)]
- def get_correctly_answered(self, questions):
- return [q for q in questions
- if True in [a['correct'] for a in self.get(q.id, [])]]
+ def get_answered(self, **kwargs):
+ return self._get_questions(
+ check=lambda question, answers: question.id in answers,
+ **kwargs)
- def get_never_correctly_answered(self, questions):
- return [q for q in questions
- if True not in [a['correct'] for a in self.get(q.id, [])]]
+ def get_unanswered(self, **kwargs):
+ return self._get_questions(
+ check=lambda question, answers: question.id not in answers,
+ **kwargs)
+
+ def get_correctly_answered(self, **kwargs):
+ return self._get_questions(
+ check=lambda question, answers:
+ True in [a['correct'] for a in answers.get(question.id, [])],
+ **kwargs)
+
+ def get_never_correctly_answered(self, **kwargs):
+ return self._get_questions(
+ check=lambda question, answers:
+ True not in [a['correct']
+ for a in answers.get(question.id, [])],
+ **kwargs)
def _upgrade_from_0_1(self, data):
data['version'] = __version__
+ data['answers'] = {'': data['answers']} # add user-id key
return data
_upgrade_from_0_2 = _upgrade_from_0_1
+ _upgrade_from_0_3 = _upgrade_from_0_1
# You should have received a copy of the GNU General Public License along with
# quizzer. If not, see <http://www.gnu.org/licenses/>.
+import collections as _collections
import importlib as _importlib
from .. import answerdb as _answerdb
if stack is None:
stack = self.answers.get_never_correctly_answered(
questions=quiz.leaf_questions())
- self.stack = stack
+ self._stack = stack
+ self.stack = _collections.defaultdict(self._new_stack)
+
+ def _new_stack(self):
+ return list(self._stack) # make a new copy for a new user
def run(self):
raise NotImplementedError()
- def get_question(self):
- if self.stack:
- return self.stack.pop(0)
+ def get_question(self, user=None):
+ if self.stack[user]:
+ return self.stack[user].pop(0)
- def process_answer(self, question, answer, **kwargs):
+ def process_answer(self, question, answer, user=None, **kwargs):
correct,details = question.check(answer=answer, **kwargs)
- self.answers.add(question=question, answer=answer, correct=correct)
+ self.answers.add(
+ question=question, answer=answer, correct=correct, user=user)
if not correct:
- self.stack.insert(0, question)
+ self.stack[user].insert(0, question)
for qid in reversed(question.dependencies):
- self.stack.insert(0, self.quiz.get(id=qid))
+ self.stack[user].insert(0, self.quiz.get(id=qid))
return (correct, details)
self._tempdir = None
def get_question(self):
- self.question = self.ui.get_question()
+ self.question = self.ui.get_question(user=self.ui.user)
if self.question:
self._reset()
else:
def do_skip(self, arg):
"Skip the current question, and continue with the quiz"
- self.ui.stack.append(self.question)
+ self.ui.stack[self.ui.user].append(self.question)
return self.get_question()
def do_hint(self, arg):
}
def run(self):
- if self.stack:
+ self.user = None
+ if self.stack[self.user]:
cmd = QuestionCommandLine(ui=self)
cmd.cmdloop()
print()
def _display_results(self):
print(_colorize(self.colors['result'], 'results:'))
+ answers = self.answers.get_answers(user=self.user)
for question in self.quiz:
- if question.id in self.answers:
+ if question.id in answers:
self._display_result(question=question)
print()
self._display_totals()
def _display_result(self, question):
- answers = self.answers.get(question.id, [])
+ answers = self.answers.get_answers(user=self.user).get(question.id, [])
print('question:')
print(' {}'.format(
_colorize(
print(' which was: {}'.format(correct))
def _display_totals(self):
- answered = self.answers.get_answered(questions=self.quiz)
+ answered = self.answers.get_answered(
+ questions=self.quiz, user=self.user)
correctly_answered = self.answers.get_correctly_answered(
- questions=self.quiz)
+ questions=self.quiz, user=self.user)
la = len(answered)
lc = len(correctly_answered)
print('answered {} of {} questions'.format(la, len(self.quiz)))
(_re.compile('^results/'), self._results),
]
self.setting = 'quizzer'
+ self.user_regexp = _re.compile('^\w+$')
def __call__(self, environ, start_response):
"WSGI entry point"
raise HandlerError(404, 'Not Found')
def _index(self, environ, start_response):
- if self.ui.stack:
- return self._start(environ, start_response)
- else:
- return self._results(environ, start_response)
-
- def _start(self, environ, start_response):
lines = [
'<html>',
' <head>',
if self.ui.quiz.introduction:
lines.append(' <p>{}</p>'.format(self.ui.quiz.introduction))
lines.extend([
- ' <p><a href="question/">Start the quiz</a>.</p>',
+ ' <form name="question" action="../question/" method="post">',
+ ' <p>Username: <input type="text" size="20" name="user">',
+ ' (required, alphanumeric)</p>',
+ ' <input type="submit" value="Start the quiz">',
+ ' </form>',
' </body>',
'</html>',
])
content_type='text/html')
def _results(self, environ, start_response):
+ data = self.post_data(environ)
+ user = data.get('user', '')
+ if not self.user_regexp.match(user):
+ raise HandlerError(303, 'See Other', headers=[('Location', '/')])
lines = [
'<html>',
' <head>',
' <body>',
' <h1>Results</h1>',
]
+ answers = self.ui.answers.get_answers(user=user)
for question in self.ui.quiz:
- if question.id in self.ui.answers:
- lines.extend(self._format_result(question=question))
- lines.extend(self._format_totals())
+ if question.id in answers:
+ lines.extend(self._format_result(question=question, user=user))
+ lines.extend(self._format_totals(user=user))
lines.extend([
' </body>',
'</html>',
environ, start_response, content=content, encoding='utf-8',
content_type='text/html')
- def _format_result(self, question):
- answers = self.ui.answers.get(question.id, [])
+ def _format_result(self, question, user):
+ answers = self.ui.answers.get_answers(user=user).get(question.id, [])
la = len(answers)
lc = len([a for a in answers if a['correct']])
lines = [
lines.append('</ol>')
return lines
- def _format_totals(self):
- answered = self.ui.answers.get_answered(questions=self.ui.quiz)
+ def _format_totals(self, user=None):
+ answered = self.ui.answers.get_answered(
+ questions=self.ui.quiz, user=user)
correctly_answered = self.ui.answers.get_correctly_answered(
- questions=self.ui.quiz)
+ questions=self.ui.quiz, user=user)
la = len(answered)
lc = len(correctly_answered)
return [
data = self.post_data(environ)
else:
data = {}
+ user = data.get('user', '')
+ if not self.user_regexp.match(user):
+ raise HandlerError(303, 'See Other', headers=[('Location', '/')])
question = data.get('question', None)
if not question:
- question = self.ui.get_question()
+ question = self.ui.get_question(user=user)
# put the question back on the stack until it's answered
- self.ui.stack.insert(0, question)
+ self.ui.stack[user].insert(0, question)
if question is None:
- return self._index(environ, start_response)
+ raise HandlerError(
+ 307, 'Temporary Redirect', headers=[('Location', '/results/')])
if question.multiline:
answer_element = (
'<textarea rows="5" cols="60" name="answer"></textarea>')
' <body>',
' <h1>Question</h1>',
' <form name="question" action="../answer/" method="post">',
+ ' <input type="hidden" name="user" value="{}">'.format(user),
' <input type="hidden" name="question" value="{}">'.format(
question.id),
' <p>{}</p>'.format(
def _answer(self, environ, start_response):
data = self.post_data(environ)
+ user = data.get('user', '')
+ if not self.user_regexp.match(user):
+ raise HandlerError(303, 'See Other', headers=[('Location', '/')])
question_id = data.get('question', None)
raw_answer = data.get('answer', None)
if not question_id or not raw_answer:
else:
answer = raw_answer
correct,details = self.ui.process_answer(
- question=question, answer=answer)
+ question=question, answer=answer, user=user)
link_target = '../question/'
if correct:
correct_msg = 'correct'
- self.ui.stack = [q for q in self.ui.stack if q != question]
- if self.ui.stack:
+ self.ui.stack[user] = [q for q in self.ui.stack[user]
+ if q != question]
+ if self.ui.stack[user]:
link_text = 'Next question'
else:
link_text = 'Results'
' <pre>{}</pre>'.format(raw_answer),
' <p>{}</p>'.format(correct_msg),
details or '',
- ' <a href="{}">{}</a>.'.format(link_target, link_text),
+ ' <form name="question" action="{}" method="post">'.format(
+ link_target),
+ ' <input type="hidden" name="user" value="{}">'.format(user),
+ ' <input type="submit" value="{}">'.format(link_text),
+ ' </form>',
' </body>',
'</html>',
'',