Skip to content

Commit

Permalink
added support for multiple-answers questions
Browse files Browse the repository at this point in the history
  • Loading branch information
gpoore committed May 20, 2020
1 parent 770912d commit 0a1594d
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 60 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

## v0.3.0 (2020-??-??)

* Added support for multiple-answers questions.
* Added support for comments at the top level of quiz files (outside Markdown
content like questions, choices, or feedback). HTML comments within
Markdown are now stripped and no longer appear in the final QTI file (#2).
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ numbers in scientific notation.

## Examples

text2qti allows quick and efficient quiz creation. Example plain-text quiz
question that can be converted to QTI and then imported by Canvas:
text2qti allows quick and efficient quiz creation. Example
**multiple-choice** plain-text quiz question that can be converted to QTI and
then imported by Canvas:

```
1. What is 2+3?
Expand Down Expand Up @@ -51,6 +52,17 @@ b) 1
... Feedback for this particular answer.
```

**Multiple-answers questions** use `[]` or `[ ]` for incorrect answers and
`[*]` for correct answers.

```
1. Which of the following are dinosaurs?
[ ] Woolly mammoth
[*] Tyrannosaurus rex
[*] Triceratops
[ ] Smilodon fatalis
```

**Numerical questions** are indicated by an equals sign followed by one or
more spaces or tabs followed by the numerical answer. Acceptable answers can
be designated as a range of the form `[<min>, <max>]` or as a correct answer
Expand Down
128 changes: 91 additions & 37 deletions text2qti/quiz.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
# regex patterns for parsing quiz content
start_patterns = {
'question': r'\d+\.',
'correct_choice': r'\*[a-zA-Z]\)',
'incorrect_choice': r'[a-zA-Z]\)',
'mctf_correct_choice': r'\*[a-zA-Z]\)',
'mctf_incorrect_choice': r'[a-zA-Z]\)',
'multans_correct_choice': r'\[\*\]',
'multans_incorrect_choice': r'\[ ?\]',
'feedback': r'\.\.\.',
'correct_feedback': r'\+',
'incorrect_feedback': r'\-',
Expand Down Expand Up @@ -141,25 +143,6 @@ def __init__(self, text: str, *, md: Markdown):

_no_feedback_question_types = set(['essay_question'])

def append_correct_choice(self, text: str):
if self.type is not None:
raise Text2qtiError(f'Question type "{self.type}" does not support choices')
choice = Choice(text, correct=True, question_hash_digest=self.hash_digest, md=self.md)
if choice.choice_html_xml in self._choice_set:
raise Text2qtiError('Duplicate choice for question')
self._choice_set.add(choice.choice_html_xml)
self.choices.append(choice)
self.correct_choices += 1

def append_incorrect_choice(self, text: str):
if self.type is not None:
raise Text2qtiError(f'Question type "{self.type}" does not support choices')
choice = Choice(text, correct=False, question_hash_digest=self.hash_digest, md=self.md)
if choice.choice_html_xml in self._choice_set:
raise Text2qtiError('Duplicate choice for question')
self._choice_set.add(choice.choice_html_xml)
self.choices.append(choice)

def append_feedback(self, text: str):
if self.type is not None:
if self.type in self._no_feedback_question_types:
Expand Down Expand Up @@ -197,6 +180,52 @@ def append_incorrect_feedback(self, text: str):
self.incorrect_feedback_raw = text
self.incorrect_feedback_html_xml = self.md.md_to_html_xml(text)

def append_mctf_correct_choice(self, text: str):
if self.type is not None:
raise Text2qtiError(f'Question type "{self.type}" does not support multiple choice')
choice = Choice(text, correct=True, question_hash_digest=self.hash_digest, md=self.md)
if choice.choice_html_xml in self._choice_set:
raise Text2qtiError('Duplicate choice for question')
self._choice_set.add(choice.choice_html_xml)
self.choices.append(choice)
self.correct_choices += 1

def append_mctf_incorrect_choice(self, text: str):
if self.type is not None:
raise Text2qtiError(f'Question type "{self.type}" does not support multiple choice')
choice = Choice(text, correct=False, question_hash_digest=self.hash_digest, md=self.md)
if choice.choice_html_xml in self._choice_set:
raise Text2qtiError('Duplicate choice for question')
self._choice_set.add(choice.choice_html_xml)
self.choices.append(choice)

def append_multans_correct_choice(self, text: str):
if self.type is None:
self.type = 'multiple_answers_question'
if self.choices:
raise Text2qtiError(f'Question type "{self.type}" is not compatible with existing choices')
elif self.type != 'multiple_answers_question':
raise Text2qtiError(f'Question type "{self.type}" does not support multiple answers')
choice = Choice(text, correct=True, question_hash_digest=self.hash_digest, md=self.md)
if choice.choice_html_xml in self._choice_set:
raise Text2qtiError('Duplicate choice for question')
self._choice_set.add(choice.choice_html_xml)
self.choices.append(choice)
self.correct_choices += 1

def append_multans_incorrect_choice(self, text: str):
if self.type is None:
self.type = 'multiple_answers_question'
if self.choices:
raise Text2qtiError(f'Question type "{self.type}" is not compatible with existing choices')
elif self.type != 'multiple_answers_question':
raise Text2qtiError(f'Question type "{self.type}" does not support multiple answers')
choice = Choice(text, correct=False, question_hash_digest=self.hash_digest, md=self.md)
if choice.choice_html_xml in self._choice_set:
raise Text2qtiError('Duplicate choice for question')
self._choice_set.add(choice.choice_html_xml)
self.choices.append(choice)

def append_essay(self, text: str):
if text:
# The essay response indicator consumes its entire line, leaving
Expand Down Expand Up @@ -278,6 +307,15 @@ def finalize(self):
raise Text2qtiError('Question must specify a correct choice')
if self.correct_choices > 1:
raise Text2qtiError('Question must specify only one correct choice')
elif self.type == 'multiple_answers_question':
# There must be at least one choice for the type to be set, so
# don't need to check for zero choices
if len(self.choices) < 2:
raise Text2qtiError('Question must provide more than one choice')
if self.correct_choices < 1:
raise Text2qtiError('Question must specify a correct choice')




class Group(object):
Expand Down Expand Up @@ -573,22 +611,6 @@ def append_question(self, text: str):
if self._current_group is not None:
self._current_group.append_question(question)

def append_correct_choice(self, text: str):
if not self.questions_and_delims:
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim = self.questions_and_delims[-1]
if not isinstance(last_question_or_delim, Question):
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim.append_correct_choice(text)

def append_incorrect_choice(self, text: str):
if not self.questions_and_delims:
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim = self.questions_and_delims[-1]
if not isinstance(last_question_or_delim, Question):
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim.append_incorrect_choice(text)

def append_feedback(self, text: str):
if not self.questions_and_delims:
raise Text2qtiError('Cannot have feedback without a question')
Expand All @@ -613,6 +635,38 @@ def append_incorrect_feedback(self, text: str):
raise Text2qtiError('Cannot have feedback without a question')
last_question_or_delim.append_incorrect_feedback(text)

def append_mctf_correct_choice(self, text: str):
if not self.questions_and_delims:
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim = self.questions_and_delims[-1]
if not isinstance(last_question_or_delim, Question):
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim.append_mctf_correct_choice(text)

def append_mctf_incorrect_choice(self, text: str):
if not self.questions_and_delims:
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim = self.questions_and_delims[-1]
if not isinstance(last_question_or_delim, Question):
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim.append_mctf_incorrect_choice(text)

def append_multans_correct_choice(self, text: str):
if not self.questions_and_delims:
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim = self.questions_and_delims[-1]
if not isinstance(last_question_or_delim, Question):
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim.append_multans_correct_choice(text)

def append_multans_incorrect_choice(self, text: str):
if not self.questions_and_delims:
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim = self.questions_and_delims[-1]
if not isinstance(last_question_or_delim, Question):
raise Text2qtiError('Cannot have a choice without a question')
last_question_or_delim.append_multans_incorrect_choice(text)

def append_essay(self, text: str):
if not self.questions_and_delims:
raise Text2qtiError('Cannot have an essay response without a question')
Expand Down
2 changes: 1 addition & 1 deletion text2qti/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-

from .fmtversion import get_version_plus_info
__version__, __version_info__ = get_version_plus_info(0, 3, 0, 'dev', 1)
__version__, __version_info__ = get_version_plus_info(0, 3, 0, 'dev', 2)
Loading

0 comments on commit 0a1594d

Please sign in to comment.