question: Add the ChoiceQuestion.multiple_answer attribute
authorW. Trevor King <wking@tremily.us>
Fri, 22 Mar 2013 21:29:44 +0000 (17:29 -0400)
committerW. Trevor King <wking@tremily.us>
Fri, 22 Mar 2013 21:29:44 +0000 (17:29 -0400)
This adds support for multiple choice questions where you are allowed
to select one or more of the possible choices.  In web forms, this is
usually done via checkboxes.

quizzer/question.py
quizzer/ui/cli.py
quizzer/ui/wsgi.py

index 95d902284e56032bb5ad98f3bb8e67d4b690dc2e..393379574548e2934ee8c0491b8afa0f2a6249d0 100644 (file)
@@ -115,16 +115,20 @@ class NormalizedStringQuestion (Question):
 class ChoiceQuestion (Question):
     _state_attributes = Question._state_attributes + [
         'display_choices',
+        'multiple_answers',
         ]
 
     def __setstate__(self, state):
-        for key in ['display_choices']:
+        for key in ['display_choices', 'multiple_answers']:
             if key not in state:
                 state[key] = False
         super(ChoiceQuestion, self).__setstate__(state)
 
     def _check(self, answer):
-        correct = answer in self.answer
+        if self.multiple_answers and not isinstance(answer, str):
+            correct = min([a in self.answer for a in answer])
+        else:
+            correct = answer in self.answer
         details = None
         if not correct:
             details = 'answer ({}) is not in list of expected values'.format(
index 14ae67f97242254d4d70b7b86d20fcb73c2360ab..ba26b328fd66179d2c59615050e5529335788571 100644 (file)
@@ -90,19 +90,37 @@ class QuestionCommandLine (_cmd.Cmd):
                 self.question.display_choices):
             for i,choice in enumerate(self.question.answer):
                 yield '{}) {}'.format(i, choice)
+            yield 'Answer with the index of your choice'
             if self.question.accept_all:
-                yield 'or fill in something else'
+                conj = 'or'
+                if self.question.multiple_answers:
+                    conj = 'and/or'
+                yield '{} fill in an alternative answer'.format(conj)
+            if self.question.multiple_answers:
+                self._separator = ','
+                yield ("Separate multiple answers with the '{}' character"
+                       ).format(self._separator)
         return []
 
     def _process_answer(self, answer):
         "Back out any mappings suggested by _extra_ps1_lines()"
         if (isinstance(self.question, _question.ChoiceQuestion) and
                 self.question.display_choices):
-            try:
-                a = int(answer)
-                return self.question.answer[a]
-            except (ValueError, IndexError):
-                pass
+            if self.question.multiple_answers:
+                answers = []
+                for a in answer.split(self._separator):
+                    try:
+                        i = int(a)
+                        answers.append(self.question.answer[i])
+                    except (ValueError, IndexError):
+                        answers.append(a)
+                return answers
+            else:
+                try:
+                    i = int(answer)
+                    return self.question.answer[i]
+                except (ValueError, IndexError):
+                    pass
         return answer
 
     def default(self, line):
index 91f43210f8d190ffe8fe0e47207852f3bfeec542..4c7af2d645dfe01e57112238845324653d256c25 100644 (file)
@@ -244,13 +244,18 @@ class QuestionApp (WSGI_DataObject):
                 307, 'Temporary Redirect', headers=[('Location', '/results/')])
         if (isinstance(question, _question.ChoiceQuestion) and
                 question.display_choices):
+            if question.multiple_answers:
+                itype = 'checkbox'
+            else:
+                itype = 'radio'
             choices = [
-                ('<input type="radio" name="answer" value="{0}"/>{0}<br/>'
-                 ).format(answer)
+                ('<input type="{0}" name="answer" value="{1}"/>{1}<br/>'
+                 ).format(itype, answer)
                 for answer in question.answer]
             if question.accept_all:
                 choices.extend([
-                    '<input type="radio" name="answer" value="answer-other"/>',
+                    ('<input type="{}" name="answer" value="answer-other"/>'
+                     ).format(itype),
                     '<input type="text" size="60" name="answer-other"/>'])
             answer_element = '\n'.join(choices)
         elif question.multiline:
@@ -300,9 +305,15 @@ class QuestionApp (WSGI_DataObject):
             raise HandlerError(404, 'Not Found') from e
         if (isinstance(question, _question.ChoiceQuestion) and
                 question.display_choices and
-                question.accept_all and
-                raw_answer == 'answer-other'):
-            answer = print_answer = data.get('answer-other', None)
+                question.accept_all):
+            if raw_answer == 'answer-other':
+                answer = print_answer = data.get('answer-other', None)
+            elif 'answer-other' in raw_answer:
+                i = raw_answer.index('answer-other')
+                raw_answer[i] = data.get('answer-other', None)
+                answer = print_answer = raw_answer
+            else:
+                answer = print_answer = raw_answer
         elif question.multiline:
             answer = raw_answer.splitlines()
             print_answer = raw_answer