MDL-3782 qtype_multianswer: support 'multiple-answer' subquestions
authorDavo Smith <git@davosmith.co.uk>
Tue, 28 Jun 2016 11:38:23 +0000 (12:38 +0100)
committerDavo Smith <git@davosmith.co.uk>
Wed, 3 Aug 2016 07:04:49 +0000 (08:04 +0100)
question/type/multianswer/edit_multianswer_form.php
question/type/multianswer/lang/en/qtype_multianswer.php
question/type/multianswer/questiontype.php
question/type/multianswer/renderer.php
question/type/multianswer/tests/helper.php
question/type/multianswer/tests/walkthrough_test.php

index 91b86a9..80e12ed 100644 (file)
@@ -347,22 +347,38 @@ class qtype_multianswer_edit_form extends question_edit_form {
 
                         if ($subquestion->qtype == 'multichoice') {
                             $defaultvalues[$prefix.'layout'] = $subquestion->layout;
-                            switch ($subquestion->layout) {
-                                case '0':
-                                    $defaultvalues[$prefix.'layout'] =
+                            if ($subquestion->single == 1) {
+                                switch ($subquestion->layout) {
+                                    case '0':
+                                        $defaultvalues[$prefix.'layout'] =
                                             get_string('layoutselectinline', 'qtype_multianswer');
-                                    break;
-                                case '1':
-                                    $defaultvalues[$prefix.'layout'] =
+                                        break;
+                                    case '1':
+                                        $defaultvalues[$prefix.'layout'] =
                                             get_string('layoutvertical', 'qtype_multianswer');
-                                    break;
-                                case '2':
-                                    $defaultvalues[$prefix.'layout'] =
+                                        break;
+                                    case '2':
+                                        $defaultvalues[$prefix.'layout'] =
                                             get_string('layouthorizontal', 'qtype_multianswer');
-                                    break;
-                                default:
-                                    $defaultvalues[$prefix.'layout'] =
+                                        break;
+                                    default:
+                                        $defaultvalues[$prefix.'layout'] =
                                             get_string('layoutundefined', 'qtype_multianswer');
+                                }
+                            } else {
+                                switch ($subquestion->layout) {
+                                    case '1':
+                                        $defaultvalues[$prefix.'layout'] =
+                                            get_string('layoutmultiple_vertical', 'qtype_multianswer');
+                                        break;
+                                    case '2':
+                                        $defaultvalues[$prefix.'layout'] =
+                                            get_string('layoutmultiple_horizontal', 'qtype_multianswer');
+                                        break;
+                                    default:
+                                        $defaultvalues[$prefix.'layout'] =
+                                            get_string('layoutundefined', 'qtype_multianswer');
+                                }
                             }
                             if ($subquestion->shuffleanswers ) {
                                 $defaultvalues[$prefix.'shuffleanswers'] = get_string('yes', 'moodle');
@@ -393,6 +409,11 @@ class qtype_multianswer_edit_form extends question_edit_form {
                                 if ($subquestion->fraction[$key] > $maxfraction) {
                                     $maxfraction = $subquestion->fraction[$key];
                                 }
+                                // For 'multiresponse' we are OK if there is at least one fraction > 0.
+                                if ($subquestion->qtype == 'multichoice' && $subquestion->single == 0 &&
+                                    $subquestion->fraction[$key] > 0) {
+                                    $maxgrade = true;
+                                }
                             }
 
                             $defaultvalues[$prefix.'answer['.$key.']'] =
@@ -484,6 +505,11 @@ class qtype_multianswer_edit_form extends question_edit_form {
                             if ($subquestion->fraction[$key] > $maxfraction) {
                                 $maxfraction = $subquestion->fraction[$key];
                             }
+                            // For 'multiresponse' we are OK if there is at least one fraction > 0.
+                            if ($subquestion->qtype == 'multichoice' && $subquestion->single == 0 &&
+                                $subquestion->fraction[$key] > 0) {
+                                $maxgrade = true;
+                            }
                         }
                     }
                     if ($answercount == 0) {
index 5c42080..ddbe942 100644 (file)
@@ -30,6 +30,8 @@ $string['correctanswerandfeedback'] = 'Correct answer and feedback';
 $string['decodeverifyquestiontext'] = 'Decode and verify the question text';
 $string['layout'] = 'Layout';
 $string['layouthorizontal'] = 'Horizontal row of radio-buttons';
+$string['layoutmultiple_horizontal'] = 'Horizontal row of checkboxes';
+$string['layoutmultiple_vertical'] = 'Vertical column of checkboxes';
 $string['layoutselectinline'] = 'Dropdown menu in-line in the text';
 $string['layoutundefined'] = 'Undefined layout';
 $string['layoutvertical'] = 'Vertical column of radio buttons';
index e1acd61..17274b4 100644 (file)
@@ -279,7 +279,8 @@ define('NUMERICAL_ABS_ERROR_MARGIN', 6);
 define('ANSWER_TYPE_DEF_REGEX',
         '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' .
         '(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)|' .
-        '(MULTICHOICE_S|MCS)|(MULTICHOICE_VS|MCVS)|(MULTICHOICE_HS|MCHS)');
+        '(MULTICHOICE_S|MCS)|(MULTICHOICE_VS|MCVS)|(MULTICHOICE_HS|MCHS)|'.
+        '(MULTIRESPONSE|MR)|(MULTIRESPONSE_H|MRH)|(MULTIRESPONSE_S|MRS)|(MULTIRESPONSE_HS|MRHS)');
 define('ANSWER_START_REGEX',
        '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
 
@@ -301,7 +302,11 @@ define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8);
 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED', 9);
 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED', 10);
 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED', 11);
-define('ANSWER_REGEX_ALTERNATIVES', 12);
+define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE', 12);
+define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL', 13);
+define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED', 14);
+define('ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED', 15);
+define('ANSWER_REGEX_ALTERNATIVES', 16);
 
 /**
  * Initialise subquestion fields that are constant across all MULTICHOICE
@@ -387,6 +392,26 @@ function qtype_multianswer_extract_question($text) {
             qtype_multianswer_initialise_multichoice_subquestion($wrapped);
             $wrapped->shuffleanswers = 1;
             $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
+        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE])) {
+            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
+            $wrapped->single = 0;
+            $wrapped->shuffleanswers = 0;
+            $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
+        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL])) {
+            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
+            $wrapped->single = 0;
+            $wrapped->shuffleanswers = 0;
+            $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
+        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_SHUFFLED])) {
+            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
+            $wrapped->single = 0;
+            $wrapped->shuffleanswers = 1;
+            $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
+        } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTIRESPONSE_HORIZONTAL_SHUFFLED])) {
+            qtype_multianswer_initialise_multichoice_subquestion($wrapped);
+            $wrapped->single = 0;
+            $wrapped->shuffleanswers = 1;
+            $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
         } else {
             print_error('unknownquestiontype', 'question', '', $answerregs[2]);
             return false;
@@ -403,12 +428,14 @@ function qtype_multianswer_extract_question($text) {
         $wrapped->questiontext['itemid'] = '';
         $answerindex = 0;
 
+        $hasspecificfraction = false;
         $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
         while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/s', $remainingalts, $altregs)) {
             if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
                 $wrapped->fraction["{$answerindex}"] = '1';
             } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) {
                 $wrapped->fraction["{$answerindex}"] = .01 * $percentile;
+                $hasspecificfraction = true;
             } else {
                 $wrapped->fraction["{$answerindex}"] = '0';
             }
@@ -453,6 +480,26 @@ function qtype_multianswer_extract_question($text) {
             $answerindex++;
         }
 
+        // Fix the score for multichoice_multi questions (as positive scores should add up to 1, not have a maximum of 1).
+        if (isset($wrapped->single) && $wrapped->single == 0) {
+            $total = 0;
+            foreach ($wrapped->fraction as $idx => $fraction) {
+                if ($fraction > 0) {
+                    $total += $fraction;
+                }
+            }
+            if ($total) {
+                foreach ($wrapped->fraction as $idx => $fraction) {
+                    if ($fraction > 0) {
+                        $wrapped->fraction[$idx] = $fraction / $total;
+                    } else if (!$hasspecificfraction) {
+                        // If no specific fractions are given, set incorrect answers to each cancel out one correct answer.
+                        $wrapped->fraction[$idx] = -(1.0 / $total);
+                    }
+                }
+            }
+        }
+
         $question->defaultmark += $wrapped->defaultmark;
         $question->options->questions[$positionkey] = clone($wrapped);
         $question->questiontext['text'] = implode("{#$positionkey}",
index baa76ce..7d54c58 100644 (file)
@@ -84,12 +84,20 @@ class qtype_multianswer_renderer extends qtype_renderer {
         if ($subtype == 'numerical' || $subtype == 'shortanswer') {
             $subrenderer = 'textfield';
         } else if ($subtype == 'multichoice') {
-            if ($subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) {
-                $subrenderer = 'multichoice_inline';
-            } else if ($subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL) {
-                $subrenderer = 'multichoice_horizontal';
+            if ($subq instanceof qtype_multichoice_multi_question) {
+                if ($subq->layout == qtype_multichoice_base::LAYOUT_VERTICAL) {
+                    $subrenderer = 'multiresponse_vertical';
+                } else {
+                    $subrenderer = 'multiresponse_horizontal';
+                }
             } else {
-                $subrenderer = 'multichoice_vertical';
+                if ($subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) {
+                    $subrenderer = 'multichoice_inline';
+                } else if ($subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL) {
+                    $subrenderer = 'multichoice_horizontal';
+                } else {
+                    $subrenderer = 'multichoice_vertical';
+                }
             }
         } else {
             throw new coding_exception('Unexpected subquestion type.', $subq);
@@ -470,3 +478,187 @@ class qtype_multianswer_multichoice_horizontal_renderer
                 html_writer::end_tag('table');
     }
 }
+
+/**
+ * Class qtype_multianswer_multiresponse_renderer
+ *
+ * @copyright  2016 Davo Smith, Synergy Learning
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_multianswer_multiresponse_vertical_renderer extends qtype_multianswer_subq_renderer_base {
+
+    /**
+     * Output the content of the subquestion.
+     *
+     * @param question_attempt $qa
+     * @param question_display_options $options
+     * @param int $index
+     * @param question_graded_automatically $subq
+     * @return string
+     */
+    public function subquestion(question_attempt $qa, question_display_options $options,
+                                $index, question_graded_automatically $subq) {
+
+        if (!$subq instanceof qtype_multichoice_multi_question) {
+            throw new coding_exception('Expecting subquestion of type qtype_multichoice_multi_question');
+        }
+
+        $fieldprefix = 'sub' . $index . '_';
+        $fieldname = $fieldprefix . 'choice';
+
+        // Extract the responses that related to this question + strip off the prefix.
+        $fieldprefixlen = strlen($fieldprefix);
+        $response = [];
+        foreach ($qa->get_last_qt_data() as $name => $val) {
+            if (substr($name, 0, $fieldprefixlen) == $fieldprefix) {
+                $name = substr($name, $fieldprefixlen);
+                $response[$name] = $val;
+            }
+        }
+
+        $basename = $qa->get_qt_field_name($fieldname);
+        $inputattributes = array(
+            'type' => 'checkbox',
+            'value' => 1,
+        );
+        if ($options->readonly) {
+            $inputattributes['disabled'] = 'disabled';
+        }
+
+        $result = $this->all_choices_wrapper_start();
+
+        // Calculate the total score (as we need to know if choices should be marked as 'correct' or 'partial').
+        $fraction = 0;
+        foreach ($subq->get_order($qa) as $value => $ansid) {
+            $ans = $subq->answers[$ansid];
+            if ($subq->is_choice_selected($response, $value)) {
+                $fraction += $ans->fraction;
+            }
+        }
+        // Display 'correct' answers as correct, if we are at 100%, otherwise mark them as 'partial'.
+        $answerfraction = ($fraction > 0.999) ? 1.0 : 0.5;
+
+        foreach ($subq->get_order($qa) as $value => $ansid) {
+            $ans = $subq->answers[$ansid];
+
+            $name = $basename.$value;
+            $inputattributes['name'] = $name;
+            $inputattributes['id'] = $name;
+
+            $isselected = $subq->is_choice_selected($response, $value);
+            if ($isselected) {
+                $inputattributes['checked'] = 'checked';
+            } else {
+                unset($inputattributes['checked']);
+            }
+
+            $class = 'r' . ($value % 2);
+            if ($options->correctness && $isselected) {
+                $thisfrac = ($ans->fraction > 0) ? $answerfraction : 0;
+                $feedbackimg = $this->feedback_image($thisfrac);
+                $class .= ' ' . $this->feedback_class($thisfrac);
+            } else {
+                $feedbackimg = '';
+            }
+
+            $result .= $this->choice_wrapper_start($class);
+            $result .= html_writer::empty_tag('input', $inputattributes);
+            $result .= html_writer::tag('label', $subq->format_text($ans->answer,
+                                                                    $ans->answerformat, $qa, 'question', 'answer', $ansid),
+                                        array('for' => $inputattributes['id']));
+            $result .= $feedbackimg;
+
+            if ($options->feedback && $isselected && trim($ans->feedback)) {
+                $result .= html_writer::tag('div',
+                                            $subq->format_text($ans->feedback, $ans->feedbackformat,
+                                                               $qa, 'question', 'answerfeedback', $ansid),
+                                            array('class' => 'specificfeedback'));
+            }
+
+            $result .= $this->choice_wrapper_end();
+        }
+
+        $result .= $this->all_choices_wrapper_end();
+
+        $feedback = array();
+        if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
+            $subq->maxmark > 0) {
+            $a = new stdClass();
+            $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
+            $a->max = format_float($subq->maxmark, $options->markdp);
+
+            $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
+        }
+
+        if ($options->rightanswer) {
+            $correct = [];
+            foreach ($subq->answers as $ans) {
+                if (question_state::graded_state_for_fraction($ans->fraction) == question_state::$gradedpartial) {
+                    $correct[] = $subq->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ans->id);
+                }
+            }
+            $correct = '<ul><li>'.implode('</li><li>', $correct).'</li></ul>';
+            $feedback[] = get_string('correctansweris', 'qtype_multichoice', $correct);
+        }
+
+        $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
+
+        return $result;
+    }
+
+    /**
+     * @param string $class class attribute value.
+     * @return string HTML to go before each choice.
+     */
+    protected function choice_wrapper_start($class) {
+        return html_writer::start_tag('div', array('class' => $class));
+    }
+
+    /**
+     * @return string HTML to go after each choice.
+     */
+    protected function choice_wrapper_end() {
+        return html_writer::end_tag('div');
+    }
+
+    /**
+     * @return string HTML to go before all the choices.
+     */
+    protected function all_choices_wrapper_start() {
+        return html_writer::start_tag('div', array('class' => 'answer'));
+    }
+
+    /**
+     * @return string HTML to go after all the choices.
+     */
+    protected function all_choices_wrapper_end() {
+        return html_writer::end_tag('div');
+    }
+}
+
+/**
+ * Render an embedded multiple-response question horizontally.
+ *
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_multianswer_multiresponse_horizontal_renderer
+    extends qtype_multianswer_multiresponse_vertical_renderer {
+
+    protected function choice_wrapper_start($class) {
+        return html_writer::start_tag('td', array('class' => $class));
+    }
+
+    protected function choice_wrapper_end() {
+        return html_writer::end_tag('td');
+    }
+
+    protected function all_choices_wrapper_start() {
+        return html_writer::start_tag('table', array('class' => 'answer')) .
+        html_writer::start_tag('tbody') . html_writer::start_tag('tr');
+    }
+
+    protected function all_choices_wrapper_end() {
+        return html_writer::end_tag('tr') . html_writer::end_tag('tbody') .
+        html_writer::end_tag('table');
+    }
+}
index 1ba3408..cc19a48 100644 (file)
@@ -37,7 +37,7 @@ require_once($CFG->dirroot . '/question/type/multianswer/question.php');
  */
 class qtype_multianswer_test_helper extends question_test_helper {
     public function get_test_questions() {
-        return array('twosubq', 'fourmc', 'numericalzero', 'dollarsigns');
+        return array('twosubq', 'fourmc', 'numericalzero', 'dollarsigns', 'multiple');
     }
 
     /**
@@ -387,4 +387,96 @@ class qtype_multianswer_test_helper extends question_test_helper {
         return $q;
     }
 
+    /**
+     * Makes a multianswer question with multichoice_multiple questions in it.
+     * @return qtype_multianswer_question
+     */
+    public function make_multianswer_question_multiple() {
+        question_bank::load_question_definition_classes('multianswer');
+        $q = new qtype_multianswer_question();
+        test_question_maker::initialise_a_question($q);
+        $q->name = 'Multichoice multiple';
+        $q->questiontext = 'Please select the fruits {#1} and vegetables {#2}';
+        $q->generalfeedback = 'You should know which foods are fruits or vegetables.';
+        $q->qtype = question_bank::get_qtype('multianswer');
+
+        $q->textfragments = array(
+            'Please select the fruits ',
+            ' and vegetables ',
+            ''
+        );
+        $q->places = array('1' => '1', '2' => '2');
+
+        // Multiple-choice subquestion.
+        question_bank::load_question_definition_classes('multichoice');
+        $mc = new qtype_multichoice_multi_question();
+        test_question_maker::initialise_a_question($mc);
+        $mc->name = 'Multianswer 1';
+        $mc->questiontext = '{1:MULTIRESPONSE:=Apple#Good~%-50%Burger~%-50%Hot dog#Not a fruit~%-50%Pizza' .
+            '~=Orange#Correct~=Banana}';
+        $mc->questiontextformat = FORMAT_HTML;
+        $mc->generalfeedback = '';
+        $mc->generalfeedbackformat = FORMAT_HTML;
+
+        $mc->shuffleanswers = 0;
+        $mc->answernumbering = 'none';
+        $mc->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
+        $mc->single = 0;
+
+        $mc->answers = array(
+            16 => new question_answer(16, 'Apple', 0.3333333,
+                                      'Good', FORMAT_HTML),
+            17 => new question_answer(17, 'Burger', -0.5,
+                                      '', FORMAT_HTML),
+            18 => new question_answer(18, 'Hot dog', -0.5,
+                                      'Not a fruit', FORMAT_HTML),
+            19 => new question_answer(19, 'Pizza', -0.5,
+                                      '', FORMAT_HTML),
+            20 => new question_answer(20, 'Orange', 0.3333333,
+                                      'Correct', FORMAT_HTML),
+            21 => new question_answer(21, 'Banana', 0.3333333,
+                                      '', FORMAT_HTML),
+        );
+        $mc->qtype = question_bank::get_qtype('multichoice');
+        $mc->maxmark = 1;
+
+        // Multiple-choice subquestion.
+        question_bank::load_question_definition_classes('multichoice');
+        $mc2 = new qtype_multichoice_multi_question();
+        test_question_maker::initialise_a_question($mc2);
+        $mc2->name = 'Multichoice 2';
+        $mc2->questiontext = '{1:MULTIRESPONSE:=Raddish#Good~%-50%Chocolate~%-50%Biscuit#Not a vegetable~%-50%Cheese' .
+            '~=Carrot#Correct}';
+        $mc2->questiontextformat = FORMAT_HTML;
+        $mc2->generalfeedback = '';
+        $mc2->generalfeedbackformat = FORMAT_HTML;
+
+        $mc2->shuffleanswers = 0;
+        $mc2->answernumbering = 'none';
+        $mc2->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
+        $mc2->single = 0;
+
+        $mc2->answers = array(
+            22 => new question_answer(22, 'Raddish', 0.5,
+                                      'Good', FORMAT_HTML),
+            23 => new question_answer(23, 'Chocolate', -0.5,
+                                      '', FORMAT_HTML),
+            24 => new question_answer(24, 'Biscuit', -0.5,
+                                      'Not a vegetable', FORMAT_HTML),
+            25 => new question_answer(25, 'Cheese', -0.5,
+                                      '', FORMAT_HTML),
+            26 => new question_answer(26, 'Carrot', 0.5,
+                                      'Correct', FORMAT_HTML),
+        );
+        $mc2->qtype = question_bank::get_qtype('multichoice');
+        $mc2->maxmark = 1;
+
+        $q->subquestions = array(
+            1 => $mc,
+            2 => $mc2,
+        );
+
+        return $q;
+    }
+
 }
index a7b7e84..e29f6a2 100644 (file)
@@ -465,4 +465,61 @@ class qtype_multianswer_walkthrough_test extends qbehaviour_walkthrough_test_bas
                 $this->get_contains_correct_expectation(),
                 new question_no_pattern_expectation('/class="control\b[^"]*\bpartiallycorrect"/'));
     }
+
+    public function test_deferred_feedback_multiple() {
+
+        // Create a multianswer question.
+        $q = test_question_maker::make_question('multianswer', 'multiple');
+        $this->start_attempt_at_question($q, 'deferredfeedback', 2);
+
+        // Check the initial state.
+        $this->check_current_state(question_state::$todo);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+            $this->get_contains_marked_out_of_summary(),
+            $this->get_does_not_contain_feedback_expectation(),
+            $this->get_does_not_contain_validation_error_expectation());
+
+        // Save in incomplete answer.
+        $this->process_submission(array('sub1_choice0' => '1', 'sub1_choice1' => '1',
+                                        'sub1_choice2' => '', 'sub1_choice3' => '',
+                                        'sub1_choice4' => '', 'sub1_choice5' => '1',
+                                        ));
+
+        // Verify.
+        $this->check_current_state(question_state::$invalid);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+            $this->get_contains_marked_out_of_summary(),
+            $this->get_does_not_contain_feedback_expectation(),
+            $this->get_contains_validation_error_expectation());
+
+        // Save a partially correct answer.
+        $this->process_submission(array('sub1_choice0' => '1', 'sub1_choice1' => '',
+                                        'sub1_choice2' => '', 'sub1_choice3' => '',
+                                        'sub1_choice4' => '1', 'sub1_choice5' => '1',
+                                        'sub2_choice0' => '', 'sub2_choice1' => '',
+                                        'sub2_choice2' => '', 'sub2_choice3' => '',
+                                        'sub2_choice4' => '1',
+                                  ));
+
+        // Verify.
+        $this->check_current_state(question_state::$complete);
+        $this->check_current_mark(null);
+        $this->check_current_output(
+            $this->get_contains_marked_out_of_summary(),
+            $this->get_does_not_contain_feedback_expectation(),
+            $this->get_does_not_contain_validation_error_expectation());
+
+        // Now submit all and finish.
+        $this->finish();
+
+        // Verify.
+        $this->check_current_state(question_state::$gradedpartial);
+        $this->check_current_mark(1.5);
+        $this->check_current_output(
+            $this->get_contains_mark_summary(1.5),
+            $this->get_contains_partcorrect_expectation(),
+            $this->get_does_not_contain_validation_error_expectation());
+    }
 }