Skip to content
This repository was archived by the owner on Jun 7, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,8 @@ bower_componenets/
# Files produced by runestone builds.
runestone/build_info
runestone/*/test/build_info
**/sphinx_settings.json

# IDEs
.vscode/
**/sphinx-enki-info.txt
13 changes: 6 additions & 7 deletions runestone/assess/assessbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#
__author__ = 'bmiller'

from runestone.common.runestonedirective import RunestoneDirective, RunestoneNode
from runestone.common.runestonedirective import RunestoneDirective

_base_js_escapes = (
('\\', r'\u005C'),
Expand Down Expand Up @@ -71,17 +71,16 @@ def getNumber(self):


def run(self):

self.options['qnumber'] = self.getNumber()

self.options['divid'] = self.arguments[0]

if self.content[0][:2] == '..': # first line is a directive
self.content[0] = self.options['qnumber'] + ': \n\n' + self.content[0]
else:
self.content[0] = self.options['qnumber'] + ': ' + self.content[0]

if self.content:
if self.content[0][:2] == '..': # first line is a directive
self.content[0] = self.options['qnumber'] + ': \n\n' + self.content[0]
else:
self.content[0] = self.options['qnumber'] + ': ' + self.content[0]

if 'iscode' in self.options:
self.options['bodytext'] = '<pre>' + "\n".join(self.content) + '</pre>'
else:
Expand Down
12 changes: 6 additions & 6 deletions runestone/assess/js/mchoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ MultipleChoice.prototype.renderMCFormOpts = function () {
label.appendChild(input);
label.appendChild(labelspan);
//$(label).attr("for", optid);
$(labelspan).html(this.answerList[k].content);
$(labelspan).html(String.fromCharCode(65 + j) + '. ' + this.answerList[k].content);

// create the object to store in optionArray
var optObj = {
Expand Down Expand Up @@ -369,7 +369,7 @@ MultipleChoice.prototype.getSubmittedOpts = function () {
if (buttonObjs[i].checked) {
given = buttonObjs[i].value;
this.givenArray.push(given);
this.feedbackString += given + ": " + this.feedbackList[i] + "<br />";
this.feedbackString += '<li value="' + (i + 1) + '">' + this.feedbackList[i] + "</li>";
this.givenlog += given + ",";
this.singlefeedback = this.feedbackList[i];
}
Expand Down Expand Up @@ -416,12 +416,12 @@ MultipleChoice.prototype.renderMCMAFeedBack = function () {
var feedbackText = this.feedbackString;

if (numCorrect === numNeeded && numNeeded === numGiven) {
$(this.feedBackDiv).html("Correct! <br />" + feedbackText);
$(this.feedBackDiv).html('Correct.<ol type="A">' + feedbackText + "</ul>");
$(this.feedBackDiv).attr("class", "alert alert-success");
} else {
$(this.feedBackDiv).html("Incorrect. " + "You gave " + numGiven +
" " + answerStr + " and got " + numCorrect + " correct of " +
numNeeded + " needed.<br /> " + feedbackText);
numNeeded + ' needed.<ol type="A">' + feedbackText + "</ul>");
$(this.feedBackDiv).attr("class", "alert alert-danger");
}
};
Expand Down Expand Up @@ -459,13 +459,13 @@ MultipleChoice.prototype.logMCMFsubmission = function () {

MultipleChoice.prototype.renderMCMFFeedback = function (correct, feedbackText) {
if (correct) {
$(this.feedBackDiv).html("Correct! " + feedbackText);
$(this.feedBackDiv).html(feedbackText);
$(this.feedBackDiv).attr("class", "alert alert-success");
} else {
if (feedbackText == null) {
feedbackText = "";
}
$(this.feedBackDiv).html("Incorrect. " + feedbackText);
$(this.feedBackDiv).html(feedbackText);
$(this.feedBackDiv).attr("class", "alert alert-danger");
}
};
Expand Down
38 changes: 21 additions & 17 deletions runestone/assess/multiplechoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@

from docutils import nodes
from docutils.parsers.rst import directives
from .assessbase import *
from .assessbase import Assessment
from runestone.common.runestonedirective import RunestoneNode, get_node_line
from runestone.server.componentdb import addQuestionToDB, addHTMLToDB


Expand Down Expand Up @@ -61,7 +62,7 @@ def depart_mc_node(self,node):
if 'answer_' in k:
x,label = k.split('_')
node.mc_options['alabel'] = label
node.mc_options['atext'] = "(" +k[-1].upper() + ") " + node.mc_options[k]
node.mc_options['atext'] = node.mc_options[k]
currFeedback = "feedback_" + label
node.mc_options['feedtext'] = node.mc_options.get(currFeedback,"") #node.mc_options[currFeedback]
if label in node.mc_options['correct']:
Expand Down Expand Up @@ -94,8 +95,8 @@ class MChoice(Assessment):
The syntax for a multiple-choice question is:

.. mchoice:: uniqueid
:multiple_answers: boolean [optional]. Implied if ``:correct:`` contains a list.
:random: boolean [optional]
:multiple_answers: [optional]. Implied if ``:correct:`` contains a list.
:random: [optional]

The following arguments supply answers and feedback. See below for an alternative method of specification.

Expand All @@ -120,7 +121,7 @@ class MChoice(Assessment):

..

- +Text for answer A. The leading ``+`` indicates this answer is correct. Prefix all correct answers with a ``+``.
- CText for answer A. The leading ``C`` indicates this answer is correct. Prefix all correct answers with a ``C``.

Your text may be multiple paragraphs, including `images <http://www.sphinx-doc.org/en/stable/rest.html#images>`_
and any other `inline <http://www.sphinx-doc.org/en/stable/rest.html#inline-markup>`_ or block markup. For example: :math:`\sqrt(2)/2`. As earlier, if your feedback contains an unordered list, end it with a comment.
Expand All @@ -134,10 +135,10 @@ class MChoice(Assessment):
This may also span multiple paragraphs and include any markup.
However, there can be only one item in this unordered list.

- \+Text for answer B. This answer is incorrect, instead showing how to display a ``+`` at the beginning of an answer without marking it as a correct answer.
- \CText for answer B. This answer is incorrect, instead showing how to display a ``C`` at the beginning of an answer without marking it as a correct answer.

- Feedback for answer B.
- Text for answer C. This answer is also incorrect. Note that the empty line between a sublist and a list may be omitted.
- C ``Text`` for answer C. This answer is correct. Note that the empty line between a sublist and a list may be omitted. Placing a space after the ``C`` allows the following text to be treated as an `inline literal <http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-literals>`_.

- Feedback for answer C. However, the empty line is required between a list and a sublist.

Expand All @@ -149,7 +150,7 @@ class MChoice(Assessment):
optional_arguments = 1
final_argument_whitespace = True
has_content = True
option_spec = RunestoneDirective.option_spec.copy()
option_spec = Assessment.option_spec.copy()
option_spec.update({'answer_a':directives.unchanged,
'answer_b':directives.unchanged,
'answer_c':directives.unchanged,
Expand Down Expand Up @@ -223,7 +224,7 @@ def run(self):
# ...and so on...
#
# See if the last item is a list. If so, and questions/answers weren't specified as options, assume it contains questions and answers.
answers_bullet_list = mcNode[-1]
answers_bullet_list = mcNode[-1] if len(mcNode) else None
if isinstance(answers_bullet_list, nodes.bullet_list) and ('answer_a' not in self.options and ('correct' not in self.options)):
# Accumulate the correct answers.
correct_answers = []
Expand All @@ -232,7 +233,7 @@ def run(self):
for answer_list_item in answers_bullet_list:
assert isinstance(answer_list_item, nodes.list_item)

# Look for a correct answer: An initial ``+``. In this case, the expected structure is:
# Look for a correct answer: An initial ``C``. In this case, the expected structure is:
#
# .. code-block::
# :number-lines:
Expand All @@ -244,19 +245,20 @@ def run(self):
possible_paragraph = answer_list_item[0]
if isinstance(possible_paragraph, nodes.paragraph):
possible_Text = possible_paragraph[0]
if isinstance(possible_Text, nodes.Text) and possible_Text.rawsource.startswith('+'):
if isinstance(possible_Text, nodes.Text) and possible_Text.rawsource.strip().startswith('C'):
# This is a correct answer.
#
# Remove the +. While a simple statement like ``possible_Text.rawsource = possible_Text.rawsource[1:]`` might seem the right approach, it doesn't work: the ``__new__`` method for Text nodes does something weird.
possible_paragraph[0] = nodes.Text(possible_Text.rawsource[1:])
possible_paragraph[0] = nodes.Text(possible_Text.rawsource.replace('C', '', 1))
# Record this in the list of correct answers.
correct_answers.append(chr(answer_list_item.parent.index(answer_list_item) + ord('a')))

# Look for the feedback for this answer -- the last child of this answer list item.
feedback_bullet_list = answer_list_item[-1]
assert isinstance(feedback_bullet_list, nodes.bullet_list)
# It should have just one item (the feedback itself).
assert len(feedback_bullet_list) == 1
if ((not isinstance(feedback_bullet_list, nodes.bullet_list) or
# It should have just one item (the feedback itself).
(len(feedback_bullet_list) != 1))):
raise self.error('On line {}, a single-item list must be nested under each answer.'.format(get_node_line(feedback_bullet_list)))

# Change the feedback list item (which is currently a generic list_item) to our special node class (a FeedbackListItem).
feedback_list_item = feedback_bullet_list[0]
Expand All @@ -274,6 +276,9 @@ def run(self):
# Store the correct answers.
self.options['correct'] = ','.join(correct_answers)

# Check that a correct answer was provided.
if not self.options.get('correct'):
raise self.error('No correct answer specified.')
return [mcNode]


Expand Down Expand Up @@ -314,14 +319,13 @@ def visit_answer_list_item(self, node):
label = chr(node.parent.index(node) + ord('a'))
# Update dict for formatting the HTML.
mcNode.mc_options['alabel'] = label
mcNode.mc_options['letter'] = label.upper()
if label in mcNode.mc_options['correct']:
mcNode.mc_options['is_correct'] = 'data-correct'
else:
mcNode.mc_options['is_correct'] = ''

# Format the HTML.
self.body.append('<li data-component="answer" %(is_correct)s id="%(divid)s_opt_%(alabel)s">(%(letter)s) ' % mcNode.mc_options)
self.body.append('<li data-component="answer" %(is_correct)s id="%(divid)s_opt_%(alabel)s">' % mcNode.mc_options)

# Although the feedback for an answer is given as a sublist, the HTML is just a list. So, let the feedback list item close this list.
def depart_answer_list_item(self, node):
Expand Down
108 changes: 94 additions & 14 deletions runestone/assess/test/_sources/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,10 @@
Testing: Multiple Choice and Multiple Answer Questions
======================================================

.. Here is were you specify the content and order of your new book.

.. Each section heading (e.g. "SECTION 1: A Random Section") will be
a heading in the table of contents. Source files that should be
generated and included in that section should be placed on individual
lines, with one line separating the first source filename and the
:maxdepth: line.

.. Sources can also be included from subfolders of this directory.
(e.g. "DataStructures/queues.rst").


Multiple Answer
---------------

===============
Old style
---------
.. mchoice:: question1
:multiple_answers:
:correct: a, c
Expand All @@ -31,6 +20,97 @@ Multiple Answer

Which colors might be found in a rainbow (check all)?

New style
---------
.. mchoice:: question1_new

Which colors might be found in a rainbow (check all)?

- Cred

- Red it is.

- brown

- Not brown.

- Cblue

- Blue it is.

- gray

- Not gray.


Test error handling
^^^^^^^^^^^^^^^^^^^
.. mchoice:: error1_no_content

.. mchoice:: error2

No list is provided.

.. mchoice:: error3

A list with missing sublists.

- COne

- Yes.

- Two


.. mchoice:: error4

A list with extra sublists.

- COne

- Yes.
- OK.

- Two

- No.

.. This just produces a confused question. The auto-numbering in the base classes prepends ``Q-x`` to ``- COne``, which means it's no longer a list. There's no easy way to detect this, without rewriting the way question numbers are prepended.

.. mchoice:: error5_only_list_is_provided

- COne

- Yes.

- Two

- No.

.. mchoice:: error6

A list with something else instead of sublists.

- COne

Not a sublist.

- Two

- No


.. mchoice:: error7

No correct answers.

- One

- No.

- Two

- Nope.

Multiple Choice
---------------
Expand Down
Loading