Merge branch 'MDL-3782_multichoice_multiple' of git://github.com/davosmith/moodle

This commit is contained in:
Andrew Nicols 2016-08-08 14:04:53 +08:00
commit be97f38188
6 changed files with 436 additions and 20 deletions

View 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) {

View 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';

View 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}",

View 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');
}
}

View 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;
}
}

View 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());
}
}