diff --git a/question/todo/diffstat.txt b/question/todo/diffstat.txt index 804ebe92daa81..bc91606a9c552 100644 --- a/question/todo/diffstat.txt +++ b/question/todo/diffstat.txt @@ -335,30 +335,30 @@ DONE question/type/essay/version.php | 2 - question/type/match/simpletest/testwalkthrough.php | 474 ++++ question/type/match/version.php | 4 +- - question/type/missingtype/display.html | 24 - - question/type/missingtype/edit_missingtype_form.php | 102 +- - question/type/missingtype/lang/en_utf8/qtype_missingtype.php | 10 + - question/type/missingtype/question.php | 86 + - question/type/missingtype/questiontype.php | 125 +- - question/type/missingtype/renderer.php | 36 + - question/type/missingtype/simpletest/testmissingtype.php | 125 + - - question/type/multianswer/db/upgrade.php | 1 - - question/type/multianswer/edit_multianswer_form.php | 11 +- - question/type/multianswer/questiontype.php | 61 +- - question/type/multianswer/version.php | 2 - - question/type/multichoice/db/install.xml | 3 +- - question/type/multichoice/db/upgrade.php | 16 +- - question/type/multichoice/display.html | 38 - - question/type/multichoice/edit_multichoice_form.php | 88 +- - question/type/multichoice/lang/en_utf8/qtype_multichoice.php | 42 + - question/type/multichoice/question.php | 367 +++ - question/type/multichoice/questiontype.php | 399 +-- - question/type/multichoice/renderer.php | 313 +++ - question/type/multichoice/simpletest/testquestion.php | 236 ++ - question/type/multichoice/simpletest/testquestiontype.php | 90 + - question/type/multichoice/simpletest/testwalkthrough.php | 91 + - question/type/multichoice/version.php | 6 +- +DONE question/type/missingtype/display.html | 24 - +DONE question/type/missingtype/edit_missingtype_form.php | 102 +- +DONE question/type/missingtype/lang/en_utf8/qtype_missingtype.php | 10 + +DONE question/type/missingtype/question.php | 86 + +DONE question/type/missingtype/questiontype.php | 125 +- +DONE question/type/missingtype/renderer.php | 36 + +DONE question/type/missingtype/simpletest/testmissingtype.php | 125 + + +DONE question/type/multianswer/db/upgrade.php | 1 - +DONE question/type/multianswer/edit_multianswer_form.php | 11 +- +DONE question/type/multianswer/questiontype.php | 61 +- +DONE question/type/multianswer/version.php | 2 - +DONE question/type/multichoice/db/install.xml | 3 +- +DONE question/type/multichoice/db/upgrade.php | 16 +- +DONE question/type/multichoice/display.html | 38 - +DONE question/type/multichoice/edit_multichoice_form.php | 88 +- +DONE question/type/multichoice/lang/en_utf8/qtype_multichoice.php | 42 + +DONE question/type/multichoice/question.php | 367 +++ +DONE question/type/multichoice/questiontype.php | 399 +-- +DONE question/type/multichoice/renderer.php | 313 +++ +DONE question/type/multichoice/simpletest/testquestion.php | 236 ++ +DONE question/type/multichoice/simpletest/testquestiontype.php | 90 + +DONE question/type/multichoice/simpletest/testwalkthrough.php | 91 + +DONE question/type/multichoice/version.php | 6 +- question/type/numerical/db/upgrade.php | 2 - question/type/numerical/edit_numerical_form.php | 48 +- diff --git a/question/type/edit_question_form.php b/question/type/edit_question_form.php index 5419fb48afad5..82f55da0728bf 100644 --- a/question/type/edit_question_form.php +++ b/question/type/edit_question_form.php @@ -425,6 +425,7 @@ public function set_data($question) { } } } + // subclass adds data_preprocessing code here $question = $this->data_preprocessing($question); @@ -478,6 +479,35 @@ protected function data_preprocessing_answers($question) { return $question; } + protected function data_preprocessing_combined_feedback($question, $withshownumcorrect = false) { + if (empty($question->options)) { + return $question; + } + + foreach (array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback') as $feedbackname) { + $draftid = file_get_submitted_draft_itemid($feedbackname); + $question->$feedbackname = array(); + $question->$feedbackname['text'] = file_prepare_draft_area( + $draftid, // draftid + $this->context->id, // context + 'qtype_multichoice', // component + $feedbackname, // filarea + !empty($question->id) ? (int) $question->id : null, // itemid + $this->fileoptions, // options + $question->options->$feedbackname // text + ); + $feedbackformat = $feedbackname . 'format'; + $question->$feedbackname['format'] = $question->options->$feedbackformat; + $question->$feedbackname['itemid'] = $draftid; + } + + if ($withshownumcorrect) { + $question->shownumcorrect = $question->options->shownumcorrect; + } + + return $question; + } + protected function data_preprocessing_hints($question, $withclearwrong = false, $withshownumpartscorrect = false) { if (empty($question->hints)) { return $question; diff --git a/question/type/multichoice/db/install.xml b/question/type/multichoice/db/install.xml index da93831b65c7f..3516b9ebcc375 100644 --- a/question/type/multichoice/db/install.xml +++ b/question/type/multichoice/db/install.xml @@ -18,7 +18,8 @@ - + + diff --git a/question/type/multichoice/db/upgrade.php b/question/type/multichoice/db/upgrade.php index 3c8eed3a33fc9..b3ce830ef05f0 100644 --- a/question/type/multichoice/db/upgrade.php +++ b/question/type/multichoice/db/upgrade.php @@ -109,7 +109,23 @@ function xmldb_qtype_multichoice_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2009021801, 'qtype', 'multichoice'); } - return true; -} + // Add new shownumcorrect field. If this is true, then when the user gets a + // multiple-response question partially correct, tell them how many choices + // they got correct alongside the feedback. + if ($oldversion < 2011011200) { + // Define field shownumcorrect to be added to question_multichoice + $table = new xmldb_table('question_multichoice'); + $field = new xmldb_field('shownumcorrect', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'answernumbering'); + // Launch add field shownumcorrect + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // multichoice savepoint reached + upgrade_plugin_savepoint(true, 2011011200, 'qtype', 'multichoice'); + } + + return true; +} diff --git a/question/type/multichoice/edit_multichoice_form.php b/question/type/multichoice/edit_multichoice_form.php index 44efd4bb1068a..a1cf2e58c3551 100644 --- a/question/type/multichoice/edit_multichoice_form.php +++ b/question/type/multichoice/edit_multichoice_form.php @@ -18,17 +18,19 @@ defined('MOODLE_INTERNAL') || die(); /** - * Defines the editing form for the multichoice question type. + * Defines the editing form for the multiple choice question type. * + * @package qtype + * @subpackage multichoice * @copyright © 2007 Jamie Pratt - * @author Jamie Pratt me@jamiep.org - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questionbank - * @subpackage questiontypes + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** - * multiple choice editing form definition. + * Multiple choice editing form definition. + * + * @copyright © 2007 Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class question_edit_multichoice_form extends question_edit_form { /** @@ -36,7 +38,7 @@ class question_edit_multichoice_form extends question_edit_form { * * @param object $mform the form being built. */ - function definition_inner(&$mform) { + protected function definition_inner($mform) { global $QTYPES; $menu = array(get_string('answersingleno', 'qtype_multichoice'), get_string('answersingleyes', 'qtype_multichoice')); @@ -47,84 +49,36 @@ function definition_inner(&$mform) { $mform->addHelpButton('shuffleanswers', 'shuffleanswers', 'qtype_multichoice'); $mform->setDefault('shuffleanswers', 1); - $numberingoptions = $QTYPES[$this->qtype()]->get_numbering_styles(); - $menu = array(); - foreach ($numberingoptions as $numberingoption) { - $menu[$numberingoption] = get_string('answernumbering' . $numberingoption, 'qtype_multichoice'); - } - $mform->addElement('select', 'answernumbering', get_string('answernumbering', 'qtype_multichoice'), $menu); + $mform->addElement('select', 'answernumbering', get_string('answernumbering', 'qtype_multichoice'), + qtype_multichoice::get_numbering_styles()); $mform->setDefault('answernumbering', 'abc'); -/* $mform->addElement('static', 'answersinstruct', get_string('choices', 'qtype_multichoice'), get_string('fillouttwochoices', 'qtype_multichoice')); - $mform->closeHeaderBefore('answersinstruct'); -*/ $creategrades = get_grade_options(); $this->add_per_answer_fields($mform, get_string('choiceno', 'qtype_multichoice', '{no}'), $creategrades->gradeoptionsfull, max(5, QUESTION_NUMANS_START)); - $mform->addElement('header', 'overallfeedbackhdr', get_string('overallfeedback', 'qtype_multichoice')); - - foreach (array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback') as $feedbackname) { - $mform->addElement('editor', $feedbackname, get_string($feedbackname, 'qtype_multichoice'), - array('rows' => 10), $this->editoroptions); - $mform->setType($feedbackname, PARAM_RAW); - } + $this->add_combined_feedback_fields(true); + $mform->disabledIf('shownumcorrect', 'single', 'eq', 1); + $this->add_interactive_settings(true, true); } function data_preprocessing($question) { - if (isset($question->options)){ - $answers = $question->options->answers; - if (count($answers)) { - $key = 0; - foreach ($answers as $answer){ - $default_values['answer['.$key.']'] = $answer->answer; - $default_values['fraction['.$key.']'] = $answer->fraction; - - // prepare question text - $draftid = file_get_submitted_draft_itemid('feedback['.$key.']'); - $default_values['feedback['.$key.']'] = array(); - $default_values['feedback['.$key.']']['text'] = file_prepare_draft_area($draftid, $this->context->id, 'question', 'answerfeedback', empty($answer->id)?null:(int)$answer->id, $this->fileoptions, $answer->feedback); - $default_values['feedback['.$key.']']['format'] = $answer->feedbackformat; - $default_values['feedback['.$key.']']['itemid'] = $draftid; - $key++; - } - } - $default_values['single'] = $question->options->single; - $default_values['answernumbering'] = $question->options->answernumbering; - $default_values['shuffleanswers'] = $question->options->shuffleanswers; - - // prepare feedback editor to display files in draft area - foreach (array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback') as $feedbackname) { - $draftid = file_get_submitted_draft_itemid($feedbackname); - $text = $question->options->$feedbackname; - $feedbackformat = $feedbackname . 'format'; - $format = $question->options->$feedbackformat; - $default_values[$feedbackname] = array(); - $default_values[$feedbackname]['text'] = file_prepare_draft_area( - $draftid, // draftid - $this->context->id, // context - 'qtype_multichoice', // component - $feedbackname, // filarea - !empty($question->id)?(int)$question->id:null, // itemid - $this->fileoptions, // options - $text // text - ); - $default_values[$feedbackname]['format'] = $format; - $default_values[$feedbackname]['itemid'] = $draftid; - } - // prepare files code block ends - - $question = (object)((array)$question + $default_values); + $question = parent::data_preprocessing($question); + $question = $this->data_preprocessing_answers($question, true); + $question = $this->data_preprocessing_combined_feedback($question, true); + $question = $this->data_preprocessing_hints($question); + + if (!empty($question->options)) { + $question->single = $question->options->single; + $question->shuffleanswers = $question->options->shuffleanswers; + $question->answernumbering = $question->options->answernumbering; } - return $question; - } - function qtype() { - return 'multichoice'; + return $question; } - function validation($data, $files) { + public function validation($data, $files) { $errors = parent::validation($data, $files); $answers = $data['answer']; $answercount = 0; @@ -132,27 +86,28 @@ function validation($data, $files) { $totalfraction = 0; $maxfraction = -1; - foreach ($answers as $key => $answer){ + foreach ($answers as $key => $answer) { //check no of choices $trimmedanswer = trim($answer); - if (!empty($trimmedanswer)){ - $answercount++; + if (empty($trimmedanswer)) { + continue; } + + $answercount++; + //check grades - if ($answer != '') { - if ($data['fraction'][$key] > 0) { - $totalfraction += $data['fraction'][$key]; - } - if ($data['fraction'][$key] > $maxfraction) { - $maxfraction = $data['fraction'][$key]; - } + if ($data['fraction'][$key] > 0) { + $totalfraction += $data['fraction'][$key]; + } + if ($data['fraction'][$key] > $maxfraction) { + $maxfraction = $data['fraction'][$key]; } } - if ($answercount==0){ + if ($answercount == 0) { $errors['answer[0]'] = get_string('notenoughanswers', 'qtype_multichoice', 2); $errors['answer[1]'] = get_string('notenoughanswers', 'qtype_multichoice', 2); - } elseif ($answercount==1){ + } elseif ($answercount == 1) { $errors['answer[1]'] = get_string('notenoughanswers', 'qtype_multichoice', 2); } @@ -172,4 +127,8 @@ function validation($data, $files) { } return $errors; } + + public function qtype() { + return 'multichoice'; + } } diff --git a/question/type/multichoice/lang/en/qtype_multichoice.php b/question/type/multichoice/lang/en/qtype_multichoice.php index bc00a317edf57..e569525bf962e 100644 --- a/question/type/multichoice/lang/en/qtype_multichoice.php +++ b/question/type/multichoice/lang/en/qtype_multichoice.php @@ -27,13 +27,18 @@ $string['addmorechoiceblanks'] = 'Blanks for {no} more choices'; $string['answerhowmany'] = 'One or multiple answers?'; $string['answernumbering'] = 'Number the choices?'; +$string['answernumbering123'] = '1., 2., 3., ...'; $string['answernumberingabc'] = 'a., b., c., ...'; $string['answernumberingABCD'] = 'A., B., C., ...'; +$string['answernumberingiii'] = 'i., ii., iii., ...'; +$string['answernumberingIIII'] = 'I., II., III., ...'; $string['answernumberingnone'] = 'No numbering'; -$string['answernumbering123'] = '1., 2., 3., ...'; $string['answersingleno'] = 'Multiple answers allowed'; $string['answersingleyes'] = 'One answer only'; +$string['choiceno'] = 'Choice {$a}'; +$string['choices'] = 'Available choices'; $string['clozeaid'] = 'Enter missing word'; +$string['correctansweris'] = 'The correct answer is: {$a}.'; $string['correctfeedback'] = 'For any correct response'; $string['editingmultichoice'] = 'Editing a Multiple choice question'; $string['errfractionsaddwrong'] = 'The positive grades you have chosen do not add up to 100%
Instead, they add up to {$a}%'; @@ -42,8 +47,6 @@ $string['fillouttwochoices'] = 'You must fill out at least two choices. Choices left blank will not be used.'; $string['fractionsaddwrong'] = 'The positive grades you have chosen do not add up to 100%
Instead, they add up to {$a}%
Do you want to go back and fix this question?'; $string['fractionsnomax'] = 'One of the choices should be 100%, so that it is
possible to get a full grade for this question.
Do you want to go back and fix this question?'; -$string['choiceno'] = 'Choice {$a}'; -$string['choices'] = 'Available choices'; $string['incorrectfeedback'] = 'For any incorrect response'; $string['multichoice'] = 'Multiple choice'; $string['multichoice_help'] = 'In response to a question (that may include a image) the respondent chooses from multiple answers. There are two types of multiple choice questions - one answer and multiple answer.'; @@ -51,10 +54,15 @@ $string['multichoicesummary'] = 'Allows the selection of a single or multiple responses from a pre-defined list.'; $string['notenoughanswers'] = 'This type of question requires at least {$a} choices'; $string['overallcorrectfeedback'] = 'Feedback for any correct response'; -$string['overallfeedback'] = 'Overall Feedback'; +$string['overallfeedback'] = 'Overall feedback'; $string['overallincorrectfeedback'] = 'Feedback for any incorrect response'; $string['overallpartiallycorrectfeedback'] = 'Feedback for any partially correct response'; $string['partiallycorrectfeedback'] = 'For any partially correct response'; +$string['pleaseselectananswer'] = 'Please select an answer.'; +$string['pleaseselectatleastoneanswer'] = 'Please select at least one answer.'; +$string['selectmulti'] = 'Select one or more:'; +$string['selectone'] = 'Select one:'; $string['shuffleanswers'] = 'Shuffle the choices?'; $string['shuffleanswers_help'] = 'If enabled, the order of the answers is randomly shuffled for each attempt, provided that "Shuffle within questions" in the quiz settings is also enabled.'; $string['singleanswer'] = 'Choose one answer.'; +$string['toomanyselected'] = 'You have selected too many options.'; diff --git a/question/type/multichoice/question.php b/question/type/multichoice/question.php new file mode 100644 index 0000000000000..f3fa6afa4a045 --- /dev/null +++ b/question/type/multichoice/question.php @@ -0,0 +1,426 @@ +. + + +/** + * Multiple choice question definition classes. + * + * @package qtype_multichoice + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * Base class for multiple choice questions. The parts that are common to + * single select and multiple select. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class qtype_multichoice_base extends question_graded_automatically { + const LAYOUT_DROPDOWN = 0; + const LAYOUT_VERTICAL = 1; + const LAYOUT_HORIZONTAL = 2; + + public $answers; + + public $shuffleanswers; + public $answernumbering; + public $layout = self::LAYOUT_VERTICAL; + public $correctfeedback; + public $partiallycorrectfeedback; + public $incorrectfeedback; + + protected $order = null; + + public function init_first_step(question_attempt_step $step) { + if ($step->has_qt_var('_order')) { + $this->order = explode(',', $step->get_qt_var('_order')); + } else { + $this->order = array_keys($this->answers); + if ($this->shuffleanswers) { + shuffle($this->order); + } + $step->set_qt_var('_order', implode(',', $this->order)); + } + } + + public function get_question_summary() { + $question = $this->html_to_text($this->questiontext); + $choices = array(); + foreach ($this->order as $ansid) { + $choices[] = $this->html_to_text($this->answers[$ansid]->answer); + } + return $question . ': ' . implode('; ', $choices); + } + + public function get_order(question_attempt $qa) { + $this->init_order($qa); + return $this->order; + } + + protected function init_order(question_attempt $qa) { + if (is_null($this->order)) { + $this->order = explode(',', $qa->get_step(0)->get_qt_var('_order')); + } + } + + abstract public function get_response(question_attempt $qa); + + abstract public function is_choice_selected($response, $value); + + function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { + $itemid = reset($args); + + if ($component == 'qtype_multichoice' && in_array($filearea, + array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) { + $state = $qa->get_state(); + + if (!$state->is_finished()) { + $response = $qa->get_last_qt_data(); + if (!$this->is_gradable_response($response)) { + return false; + } + list($notused, $state) = $qa->get_question()->grade_response($response); + } + + return $options->feedback && $state == $filearea; + + } else if ($component == 'question' && $filearea == 'answerfeedback') { + $answerid = reset($args); // itemid is answer id. + $response = $this->get_response($qa); + $isselected = false; + foreach ($this->order as $value => $ansid) { + if ($ansid == $answerid) { + $isselected = $this->is_choice_selected($response, $value); + break; + } + } + // $options->suppresschoicefeedback is a hack specific to the + // oumultiresponse question type. It would be good to refactor to + // avoid refering to it here. + return $options->feedback && empty($options->suppresschoicefeedback) && + $isselected; + + } else { + return parent::check_file_access($qa, $options, $component, $filearea, $args, $forcedownload); + } + } +} + + +/** + * Represents a multiple choice question where only one choice should be selected. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoice_single_question extends qtype_multichoice_base { + public function get_renderer() { + global $PAGE; // TODO get rid of this global. + return $PAGE->get_renderer('qtype_multichoice', 'single'); + } + + public function get_min_fraction() { + $minfraction = 0; + foreach ($this->answers as $ans) { + $minfraction = min($minfraction, $ans->fraction); + } + return $minfraction; + } + + /** + * Return an array of the question type variables that could be submitted + * as part of a question of this type, with their types, so they can be + * properly cleaned. + * @return array variable name => PARAM_... constant. + */ + public function get_expected_data() { + return array('answer' => PARAM_INT); + } + + public function summarise_response(array $response) { + if (!array_key_exists('answer', $response) || + !array_key_exists($response['answer'], $this->order)) { + return null; + } + $ansid = $this->order[$response['answer']]; + return $this->html_to_text($this->answers[$ansid]->answer); + } + + public function classify_response(array $response) { + if (!array_key_exists('answer', $response) || + !array_key_exists($response['answer'], $this->order)) { + return array($this->id => question_classified_response::no_response()); + } + $choiceid = $this->order[$response['answer']]; + $ans = $this->answers[$choiceid]; + return array($this->id => new question_classified_response($choiceid, + $this->html_to_text($ans->answer), $ans->fraction)); + } + + public function get_correct_response() { + foreach ($this->order as $key => $answerid) { + if (question_state::graded_state_for_fraction( + $this->answers[$answerid]->fraction)->is_correct()) { + return array('answer' => $key); + } + } + return array(); + } + + public function is_same_response(array $prevresponse, array $newresponse) { + return question_utils::arrays_same_at_key($prevresponse, $newresponse, 'answer'); + } + + public function is_complete_response(array $response) { + return array_key_exists('answer', $response); + } + + public function is_gradable_response(array $response) { + return $this->is_complete_response($response); + } + + public function grade_response(array $response) { + if (array_key_exists('answer', $response) && + array_key_exists($response['answer'], $this->order)) { + $fraction = $this->answers[$this->order[$response['answer']]]->fraction; + } else { + $fraction = 0; + } + return array($fraction, question_state::graded_state_for_fraction($fraction)); + } + + public function get_validation_error(array $response) { + if ($this->is_gradable_response($response)) { + return ''; + } + return get_string('pleaseselectananswer', 'qtype_multichoice'); + } + + public function get_response(question_attempt $qa) { + return $qa->get_last_qt_var('answer', -1); + } + + public function is_choice_selected($response, $value) { + return $response == $value; + } +} + + +/** + * Represents a multiple choice question where multiple choices can be selected. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoice_multi_question extends qtype_multichoice_base { + public function get_renderer() { + global $PAGE; // TODO get rid of this global. + return $PAGE->get_renderer('qtype_multichoice', 'multi'); + } + + public function get_min_fraction() { + return 0; + } + + public function clear_wrong_from_response(array $response) { + foreach ($this->order as $key => $ans) { + if (array_key_exists($this->field($key), $response) && + question_state::graded_state_for_fraction( + $this->answers[$ans]->fraction)->is_incorrect()) { + $response[$this->field($key)] = 0; + } + } + return $response; + } + + public function get_num_parts_right(array $response) { + $numright = 0; + foreach ($this->order as $key => $ans) { + $fieldname = $this->field($key); + if (!array_key_exists($fieldname, $response) || !$response[$fieldname]) { + continue; + } + + if (!question_state::graded_state_for_fraction( + $this->answers[$ans]->fraction)->is_incorrect()) { + $numright += 1; + } + } + return array($numright, count($this->order)); + } + + /** + * @param integer $key choice number + * @return string the question-type variable name. + */ + protected function field($key) { + return 'choice' . $key; + } + + public function get_expected_data() { + $expected = array(); + foreach ($this->order as $key => $notused) { + $expected[$this->field($key)] = PARAM_BOOL; + } + return $expected; + } + + public function summarise_response(array $response) { + $selectedchoices = array(); + foreach ($this->order as $key => $ans) { + $fieldname = $this->field($key); + if (array_key_exists($fieldname, $response) && $response[$fieldname]) { + $selectedchoices[] = $this->html_to_text($this->answers[$ans]->answer); + } + } + if (empty($selectedchoices)) { + return null; + } + return implode('; ', $selectedchoices); + } + + public function classify_response(array $response) { + $selectedchoices = array(); + foreach ($this->order as $key => $ansid) { + $fieldname = $this->field($key); + if (array_key_exists($fieldname, $response) && $response[$fieldname]) { + $selectedchoices[$ansid] = 1; + } + } + $choices = array(); + foreach ($this->answers as $ansid => $ans) { + if (isset($selectedchoices[$ansid])) { + $choices[$ansid] = new question_classified_response($ansid, + $this->html_to_text($ans->answer), $ans->fraction); + } + } + return $choices; + } + + public function get_correct_response() { + $response = array(); + foreach ($this->order as $key => $ans) { + if (!question_state::graded_state_for_fraction( + $this->answers[$ans]->fraction)->is_incorrect()) { + $response[$this->field($key)] = 1; + } + } + return $response; + } + + public function is_same_response(array $prevresponse, array $newresponse) { + foreach ($this->order as $key => $notused) { + $fieldname = $this->field($key); + if (!question_utils::arrays_same_at_key($prevresponse, $newresponse, $fieldname)) { + return false; + } + } + return true; + } + + public function is_complete_response(array $response) { + foreach ($this->order as $key => $notused) { + if (!empty($response[$this->field($key)])) { + return true; + } + } + return false; + } + + public function is_gradable_response(array $response) { + return $this->is_complete_response($response); + } + + /** + * @param array $response responses, as returned by {@link question_attempt_step::get_qt_data()}. + * @return integer the number of choices that were selected. in this response. + */ + public function get_num_selected_choices(array $response) { + $numselected = 0; + foreach ($response as $key => $value) { + if (!empty($value)) { + $numselected += 1; + } + } + return $numselected; + } + + /** + * @return integer the number of choices that are correct. + */ + public function get_num_correct_choices() { + $numcorrect = 0; + foreach ($this->answers as $ans) { + if (!question_state::graded_state_for_fraction($ans->fraction)->is_incorrect()) { + $numcorrect += 1; + } + } + return $numcorrect; + } + + public function grade_response(array $response) { + $fraction = 0; + foreach ($this->order as $key => $ansid) { + if (!empty($response[$this->field($key)])) { + $fraction += $this->answers[$ansid]->fraction; + } + } + $fraction = min(max(0, $fraction), 1.0); + return array($fraction, question_state::graded_state_for_fraction($fraction)); + } + + public function get_validation_error(array $response) { + if ($this->is_gradable_response($response)) { + return ''; + } + return get_string('pleaseselectatleastoneanswer', 'qtype_multichoice'); + } + + /** + * Disable those hint settings that we don't want when the student has selected + * more choices than the number of right choices. This avoids giving the game away. + * @param question_hint_with_parts $hint a hint. + */ + protected function disable_hint_settings_when_too_many_selected(question_hint_with_parts $hint) { + $hint->clearwrong = false; + } + + public function get_hint($hintnumber, question_attempt $qa) { + $hint = parent::get_hint($hintnumber, $qa); + if (is_null($hint)) { + return $hint; + } + + if ($this->get_num_selected_choices($qa->get_last_qt_data()) > + $this->get_num_correct_choices()) { + $hint = clone($hint); + $this->disable_hint_settings_when_too_many_selected($hint); + } + return $hint; + } + + public function get_response(question_attempt $qa) { + return $qa->get_last_qt_data(); + } + + public function is_choice_selected($response, $value) { + return !empty($response['choice' . $value]); + } +} diff --git a/question/type/multichoice/questiontype.php b/question/type/multichoice/questiontype.php index 02f8087ec8cad..1374e06a46a75 100644 --- a/question/type/multichoice/questiontype.php +++ b/question/type/multichoice/questiontype.php @@ -1,41 +1,47 @@ . + + /** * The questiontype class for the multiple choice question type. * - * Note, This class contains some special features in order to make the - * question type embeddable within a multianswer (cloze) question - * - * @package questionbank - * @subpackage questiontypes + * @package qtype + * @subpackage multichoice + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class question_multichoice_qtype extends default_questiontype { - function name() { - return 'multichoice'; - } - - function get_question_options(&$question) { +/** + * The multiple choice question type. + * + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoice extends question_type { + public function get_question_options($question) { global $DB, $OUTPUT; - // Get additional information from database - // and attach it to the question object - if (!$question->options = $DB->get_record('question_multichoice', array('question' => $question->id))) { - echo $OUTPUT->notification('Error: Missing question options for multichoice question'.$question->id.'!'); - return false; - } - - list ($usql, $params) = $DB->get_in_or_equal(explode(',', $question->options->answers)); - if (!$question->options->answers = $DB->get_records_select('question_answers', "id $usql", $params, 'id')) { - echo $OUTPUT->notification('Error: Missing question answers for multichoice question'.$question->id.'!'); - return false; - } - - return true; + $question->options = $DB->get_record('question_multichoice', array('question' => $question->id), '*', MUST_EXIST); + parent::get_question_options($question); } - function save_question_options($question) { + public function save_question_options($question) { global $DB; $context = $question->context; - $result = new stdClass; + $result = new stdClass(); $oldanswers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC'); @@ -106,7 +112,7 @@ function save_question_options($question) { $options = $DB->get_record('question_multichoice', array('question' => $question->id)); if (!$options) { - $options = new stdClass; + $options = new stdClass(); $options->question = $question->id; $options->correctfeedback = ''; $options->partiallycorrectfeedback = ''; @@ -121,6 +127,7 @@ function save_question_options($question) { } $options->answernumbering = $question->answernumbering; $options->shuffleanswers = $question->shuffleanswers; + $options->shownumcorrect = !empty($question->shownumcorrect); $options->correctfeedback = $this->import_or_save_files($question->correctfeedback, $context, 'qtype_multichoice', 'correctfeedback', $question->id); $options->correctfeedbackformat = $question->correctfeedback['format']; @@ -146,286 +153,76 @@ function save_question_options($question) { return $result; } } - - return true; - } - - function delete_question($questionid, $contextid) { - global $DB; - $DB->delete_records('question_multichoice', array('question' => $questionid)); - - parent::delete_question($questionid, $contextid); } - function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) { - // create an array of answerids ??? why so complicated ??? - $answerids = array_values(array_map(create_function('$val', - 'return $val->id;'), $question->options->answers)); - // Shuffle the answers if required - if (!empty($cmoptions->shuffleanswers) and !empty($question->options->shuffleanswers)) { - $answerids = swapshuffle($answerids); - } - $state->options->order = $answerids; - // Create empty responses - if ($question->options->single) { - $state->responses = array('' => ''); + protected function make_question_instance($questiondata) { + question_bank::load_question_definition_classes($this->name()); + if ($questiondata->options->single) { + $class = 'qtype_multichoice_single_question'; } else { - $state->responses = array(); + $class = 'qtype_multichoice_multi_question'; } - return true; + return new $class(); } - - function restore_session_and_responses(&$question, &$state) { - // The serialized format for multiple choice quetsions - // is an optional comma separated list of answer ids (the order of the - // answers) followed by a colon, followed by another comma separated - // list of answer ids, which are the radio/checkboxes that were - // ticked. - // E.g. 1,3,2,4:2,4 means that the answers were shown in the order - // 1, 3, 2 and then 4 and the answers 2 and 4 were checked. - - $pos = strpos($state->responses[''], ':'); - if (false === $pos) { // No order of answers is given, so use the default - $state->options->order = array_keys($question->options->answers); - } else { // Restore the order of the answers - $state->options->order = explode(',', substr($state->responses[''], 0, $pos)); - $state->responses[''] = substr($state->responses[''], $pos + 1); - } - // Restore the responses - // This is done in different ways if only a single answer is allowed or - // if multiple answers are allowed. For single answers the answer id is - // saved in $state->responses[''], whereas for the multiple answers case - // the $state->responses array is indexed by the answer ids and the - // values are also the answer ids (i.e. key = value). - if (empty($state->responses[''])) { // No previous responses - $state->responses = array('' => ''); - } else { - if ($question->options->single) { - $state->responses = array('' => $state->responses['']); - } else { - // Get array of answer ids - $state->responses = explode(',', $state->responses['']); - // Create an array indexed by these answer ids - $state->responses = array_flip($state->responses); - // Set the value of each element to be equal to the index - array_walk($state->responses, create_function('&$a, $b', - '$a = $b;')); - } - } - return true; - } - - function save_session_and_responses(&$question, &$state) { - global $DB; - // Bundle the answer order and the responses into the legacy answer - // field. - // The serialized format for multiple choice quetsions - // is (optionally) a comma separated list of answer ids - // followed by a colon, followed by another comma separated - // list of answer ids, which are the radio/checkboxes that were - // ticked. - // E.g. 1,3,2,4:2,4 means that the answers were shown in the order - // 1, 3, 2 and then 4 and the answers 2 and 4 were checked. - $responses = implode(',', $state->options->order) . ':'; - $responses .= implode(',', $state->responses); - - // Set the legacy answer field - $DB->set_field('question_states', 'answer', $responses, array('id' => $state->id)); - return true; + protected function make_hint($hint) { + return question_hint_with_parts::load_from_record($hint); } - function get_correct_responses(&$question, &$state) { - if ($question->options->single) { - foreach ($question->options->answers as $answer) { - if (((int) $answer->fraction) === 1) { - return array('' => $answer->id); - } - } - return null; + protected function initialise_question_instance(question_definition $question, $questiondata) { + parent::initialise_question_instance($question, $questiondata); + $question->shuffleanswers = $questiondata->options->shuffleanswers; + $question->answernumbering = $questiondata->options->answernumbering; + if (!empty($questiondata->options->layout)) { + $question->layout = $questiondata->options->layout; } else { - $responses = array(); - foreach ($question->options->answers as $answer) { - if (((float) $answer->fraction) > 0.0) { - $responses[$answer->id] = (string) $answer->id; - } - } - return empty($responses) ? null : $responses; + $question->layout = qtype_multichoice_single_question::LAYOUT_VERTICAL; } - } - - function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) { - global $CFG; - - // required by file api - $context = $this->get_context_by_category_id($question->category); - $component = 'qtype_' . $question->qtype; - - $answers = &$question->options->answers; - $correctanswers = $this->get_correct_responses($question, $state); - $readonly = empty($options->readonly) ? '' : 'disabled="disabled"'; - - $formatoptions = new stdClass; - $formatoptions->noclean = true; - $formatoptions->para = false; - - // Print formulation - $questiontext = format_text($question->questiontext, $question->questiontextformat, - $formatoptions, $cmoptions->course); - $answerprompt = ($question->options->single) ? get_string('singleanswer', 'quiz') : - get_string('multipleanswers', 'quiz'); - - // Print each answer in a separate row - foreach ($state->options->order as $key => $aid) { - $answer = &$answers[$aid]; - $checked = ''; - $chosen = false; - - if ($question->options->single) { - $type = 'type="radio"'; - $name = "name=\"{$question->name_prefix}\""; - if (isset($state->responses['']) and $aid == $state->responses['']) { - $checked = 'checked="checked"'; - $chosen = true; - } - } else { - $type = ' type="checkbox" '; - $name = "name=\"{$question->name_prefix}{$aid}\""; - if (isset($state->responses[$aid])) { - $checked = 'checked="checked"'; - $chosen = true; - } - } - - $a = new stdClass; - $a->id = $question->name_prefix . $aid; - $a->class = ''; - $a->feedbackimg = ''; - // Print the control - $a->control = "id\" $name $checked $type value=\"$aid\" />"; + $question->correctfeedback = $questiondata->options->correctfeedback; + $question->partiallycorrectfeedback = $questiondata->options->partiallycorrectfeedback; + $question->incorrectfeedback = $questiondata->options->incorrectfeedback; + $question->shownumcorrect = !$questiondata->options->single && $questiondata->options->shownumcorrect; - if ($options->correct_responses && $answer->fraction > 0) { - $a->class = question_get_feedback_class(1); - } - if (($options->feedback && $chosen) || $options->correct_responses) { - if ($type == ' type="checkbox" ') { - $a->feedbackimg = question_get_feedback_image($answer->fraction > 0 ? 1 : 0, $chosen && $options->feedback); - } else { - $a->feedbackimg = question_get_feedback_image($answer->fraction, $chosen && $options->feedback); - } - } - - // Print the answer text - $a->text = $this->number_in_style($key, $question->options->answernumbering) . - format_text($answer->answer, $answer->answerformat, $formatoptions, $cmoptions->course); - - // Print feedback if feedback is on - if (($options->feedback || $options->correct_responses) && $checked) { - // feedback for each answer - $a->feedback = quiz_rewrite_question_urls($answer->feedback, 'pluginfile.php', $context->id, 'question', 'answerfeedback', array($state->attempt, $state->question), $answer->id); - $a->feedback = format_text($a->feedback, $answer->feedbackformat, $formatoptions, $cmoptions->course); - } else { - $a->feedback = ''; - } - - $anss[] = clone($a); - } - - $feedback = ''; - if ($options->feedback) { - if ($state->raw_grade >= $question->maxgrade/1.01) { - $feedback = $question->options->correctfeedback; - $feedbacktype = 'correctfeedback'; - } else if ($state->raw_grade > 0) { - $feedback = $question->options->partiallycorrectfeedback; - $feedbacktype = 'partiallycorrectfeedback'; - } else { - $feedback = $question->options->incorrectfeedback; - $feedbacktype = 'incorrectfeedback'; - } + $this->initialise_question_answers($question, $questiondata); + } - $feedback = quiz_rewrite_question_urls($feedback, 'pluginfile.php', $context->id, $component, $feedbacktype, array($state->attempt, $state->question), $question->id); - $feedbackformat = $feedbacktype . 'format'; - $feedback = format_text($feedback, $question->options->$feedbackformat, $formatoptions, $cmoptions->course); - } + public function delete_question($questionid, $contextid) { + global $DB; + $DB->delete_records('question_multichoice', array('question' => $questionid)); - include("$CFG->dirroot/question/type/multichoice/display.html"); + parent::delete_question($questionid, $contextid); } - function compare_responses($question, $state, $teststate) { - if ($question->options->single) { - if (!empty($state->responses[''])) { - return $state->responses[''] == $teststate->responses['']; - } else { - return empty($teststate->response['']); - } - } else { - foreach ($question->options->answers as $ansid => $notused) { - if (empty($state->responses[$ansid]) != empty($teststate->responses[$ansid])) { - return false; - } - } - return true; + public function get_random_guess_score($questiondata) { + $totalfraction = 0; + foreach ($questiondata->options->answers as $answer) { + $totalfraction += $answer->fraction; } + return $totalfraction / count($questiondata->options->answers); } - function grade_responses(&$question, &$state, $cmoptions) { - $state->raw_grade = 0; - if($question->options->single) { - $response = reset($state->responses); - if ($response) { - $state->raw_grade = $question->options->answers[$response]->fraction; - } - } else { - foreach ($state->responses as $response) { - if ($response) { - $state->raw_grade += $question->options->answers[$response]->fraction; - } - } - } - - // Make sure we don't assign negative or too high marks - $state->raw_grade = min(max((float) $state->raw_grade, - 0.0), 1.0) * $question->maxgrade; - - // Apply the penalty for this attempt - $state->penalty = $question->penalty * $question->maxgrade; + function get_possible_responses($questiondata) { + if ($questiondata->options->single) { + $responses = array(); - // mark the state as graded - $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE; + foreach ($questiondata->options->answers as $aid => $answer) { + $responses[$aid] = new question_possible_response($answer->answer, + $answer->fraction); + } - return true; - } + $responses[null] = question_possible_response::no_response(); + return array($questiondata->id => $responses); + } else { + $parts = array(); - // ULPGC ecastro - function get_actual_response($question, $state) { - $answers = $question->options->answers; - $responses = array(); - if (!empty($state->responses)) { - foreach ($state->responses as $aid =>$rid){ - if (!empty($answers[$rid])) { - $responses[] = $answers[$rid]->answer; - } + foreach ($questiondata->options->answers as $aid => $answer) { + $parts[$aid] = array($aid => + new question_possible_response($answer->answer, $answer->fraction)); } - } else { - $responses[] = ''; - } - return $responses; - } - /** - * @param object $question - * @return mixed either a integer score out of 1 that the average random - * guess by a student might give or an empty string which means will not - * calculate. - */ - function get_random_guess_score($question) { - $totalfraction = 0; - foreach ($question->options->answers as $answer){ - $totalfraction += $answer->fraction; + return $parts; } - return $totalfraction / count($question->options->answers); } /** @@ -434,61 +231,12 @@ function get_random_guess_score($question) { * language file, and a case in the switch statement in number_in_style, * and it should be listed in the definition of this column in install.xml. */ - function get_numbering_styles() { - return array('abc', 'ABCD', '123', 'none'); - } - - function number_html($qnum) { - return '' . $qnum . '. '; - } - - /** - * @param int $num The number, starting at 0. - * @param string $style The style to render the number in. One of the ones returned by $numberingoptions. - * @return string the number $num in the requested style. - */ - function number_in_style($num, $style) { - switch($style) { - case 'abc': - return $this->number_html(chr(ord('a') + $num)); - case 'ABCD': - return $this->number_html(chr(ord('A') + $num)); - case '123': - return $this->number_html(($num + 1)); - case 'none': - return ''; - default: - return 'ERR'; + public static function get_numbering_styles() { + $styles = array(); + foreach (array('abc', 'ABCD', '123', 'iii', 'IIII', 'none') as $numberingoption) { + $styles[$numberingoption] = get_string('answernumbering' . $numberingoption, 'qtype_multichoice'); } - } - - /** - * Runs all the code required to set up and save an essay question for testing purposes. - * Alternate DB table prefix may be used to facilitate data deletion. - */ - function generate_test($name, $courseid = null) { - global $DB; - list($form, $question) = parent::generate_test($name, $courseid); - $question->category = $form->category; - $form->questiontext = "How old is the sun?"; - $form->generalfeedback = "General feedback"; - $form->penalty = 0.1; - $form->single = 1; - $form->shuffleanswers = 1; - $form->answernumbering = 'abc'; - $form->noanswers = 3; - $form->answer = array('Ancient', '5 billion years old', '4.5 billion years old'); - $form->fraction = array(0.3, 0.9, 1); - $form->feedback = array('True, but lacking in accuracy', 'Close, but no cigar!', 'Yep, that is it!'); - $form->correctfeedback = 'Excellent!'; - $form->incorrectfeedback = 'Nope!'; - $form->partiallycorrectfeedback = 'Not bad'; - - if ($courseid) { - $course = $DB->get_record('course', array('id' => $courseid)); - } - - return $this->save_question($question, $form); + return $styles; } function move_files($questionid, $oldcontextid, $newcontextid) { @@ -514,39 +262,4 @@ protected function delete_files($questionid, $contextid) { $fs->delete_area_files($contextid, 'qtype_multichoice', 'partiallycorrectfeedback', $questionid); $fs->delete_area_files($contextid, 'qtype_multichoice', 'incorrectfeedback', $questionid); } - - function check_file_access($question, $state, $options, $contextid, $component, - $filearea, $args) { - $itemid = reset($args); - - if (empty($question->maxgrade)) { - $question->maxgrade = $question->defaultgrade; - } - - if (in_array($filearea, array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) { - $result = $options->feedback && ($itemid == $question->id); - if (!$result) { - return false; - } - if ($state->raw_grade >= $question->maxgrade/1.01) { - $feedbacktype = 'correctfeedback'; - } else if ($state->raw_grade > 0) { - $feedbacktype = 'partiallycorrectfeedback'; - } else { - $feedbacktype = 'incorrectfeedback'; - } - if ($feedbacktype != $filearea) { - return false; - } - return true; - } else if ($component == 'question' && $filearea == 'answerfeedback') { - return $options->feedback && (array_key_exists($itemid, $question->options->answers)); - } else { - return parent::check_file_access($question, $state, $options, $contextid, $component, - $filearea, $args); - } - } } - -// Register this question type with the question bank. -question_register_questiontype(new question_multichoice_qtype()); diff --git a/question/type/multichoice/renderer.php b/question/type/multichoice/renderer.php new file mode 100644 index 0000000000000..aef9399ec0898 --- /dev/null +++ b/question/type/multichoice/renderer.php @@ -0,0 +1,294 @@ +. + + +/** + * Multiple choice question renderer classes. + * + * @package qtype_multichoice + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * Base class for generating the bits of output common to multiple choice + * single and multiple questions. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedback_renderer { + abstract protected function get_input_type(); + + abstract protected function get_input_name(question_attempt $qa, $value); + + abstract protected function get_input_value($value); + + abstract protected function get_input_id(question_attempt $qa, $value); + + /** + * Whether a choice should be considered right, wrong or partially right. + * @param question_answer $ans representing one of the choices. + * @return fload 1.0, 0.0 or something in between, respectively. + */ + abstract protected function is_right(question_answer $ans); + + abstract protected function prompt(); + + public function formulation_and_controls(question_attempt $qa, + question_display_options $options) { + + $question = $qa->get_question(); + $order = $question->get_order($qa); + $response = $question->get_response($qa); + + $inputname = $qa->get_qt_field_name('answer'); + $inputattributes = array( + 'type' => $this->get_input_type(), + 'name' => $inputname, + ); + + if ($options->readonly) { + $inputattributes['disabled'] = 'disabled'; + } + + $radiobuttons = array(); + $feedbackimg = array(); + $feedback = array(); + $classes = array(); + foreach ($order as $value => $ansid) { + $ans = $question->answers[$ansid]; + $inputattributes['name'] = $this->get_input_name($qa, $value); + $inputattributes['value'] = $this->get_input_value($value); + $inputattributes['id'] = $this->get_input_id($qa, $value); + $isselected = $question->is_choice_selected($response, $value); + if ($isselected) { + $inputattributes['checked'] = 'checked'; + } else { + unset($inputattributes['checked']); + } + $hidden = ''; + if (!$options->readonly && $this->get_input_type() == 'checkbox') { + $hidden = html_writer::empty_tag('input', array( + 'type' => 'hidden', + 'name' => $inputattributes['name'], + 'value' => 0, + )); + } + $radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) . + html_writer::tag('label', $this->number_in_style($value, $question->answernumbering) . + $question->format_text($ans->answer, $qa, + 'question', 'answer', $ansid), array('for' => $inputattributes['id'])); + + // $options->suppresschoicefeedback is a hack specific to the + // oumultiresponse question type. It would be good to refactor to + // avoid refering to it here. + if ($options->feedback && empty($options->suppresschoicefeedback) && + $isselected && trim($ans->feedback)) { + $feedback[] = html_writer::tag('div', + $question->format_text($ans->feedback, $qa, 'question', 'answerfeedback', $ansid), + array('class' => 'specificfeedback')); + } else { + $feedback[] = ''; + } + $class = 'r' . ($value % 2); + if ($options->correctness && $isselected) { + $feedbackimg[] = $this->feedback_image($this->is_right($ans)); + $class .= ' ' . $this->feedback_class($this->is_right($ans)); + } else { + $feedbackimg[] = ''; + } + $classes[] = $class; + } + + $result = ''; + $result .= html_writer::tag('div', $question->format_questiontext($qa), + array('class' => 'qtext')); + + $result .= html_writer::start_tag('div', array('class' => 'ablock')); + $result .= html_writer::tag('div', $this->prompt(), array('class' => 'prompt')); + + $result .= html_writer::start_tag('div', array('class' => 'answer')); + foreach ($radiobuttons as $key => $radio) { + $result .= html_writer::tag('span', $radio . ' ' . $feedbackimg[$key] . $feedback[$key], + array('class' => $classes[$key])) . "\n"; + } + $result .= html_writer::end_tag('div'); // answer + + $result .= html_writer::end_tag('div'); // ablock + + if ($qa->get_state() == question_state::$invalid) { + $result .= html_writer::nonempty_tag('div', + $question->get_validation_error($qa->get_last_qt_data()), + array('class' => 'validationerror')); + } + + return $result; + } + + protected function number_html($qnum) { + return $qnum . '. '; + } + + /** + * @param int $num The number, starting at 0. + * @param string $style The style to render the number in. One of the + * options returned by {@link qtype_multichoice:;get_numbering_styles()}. + * @return string the number $num in the requested style. + */ + protected function number_in_style($num, $style) { + switch($style) { + case 'abc': + $number = chr(ord('a') + $num); + break; + case 'ABCD': + $number = chr(ord('A') + $num); + break; + case '123': + $number = $num + 1; + break; + case 'iii': + $number = question_utils::int_to_roman($num + 1); + break; + case 'IIII': + $number = strtoupper(question_utils::int_to_roman($num + 1)); + break; + case 'none': + return ''; + default: + return 'ERR'; + } + return $this->number_html($number); + } + + public function specific_feedback(question_attempt $qa) { + return $this->combined_feedback($qa); + } +} + + +/** + * Subclass for generating the bits of output specific to multiple choice + * single questions. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base { + protected function get_input_type() { + return 'radio'; + } + + protected function get_input_name(question_attempt $qa, $value) { + return $qa->get_qt_field_name('answer'); + } + + protected function get_input_value($value) { + return $value; + } + + protected function get_input_id(question_attempt $qa, $value) { + return $qa->get_qt_field_name('answer' . $value); + } + + protected function is_right(question_answer $ans) { + return $ans->fraction; + } + + protected function prompt() { + return get_string('selectone', 'qtype_multichoice'); + } + + public function correct_response(question_attempt $qa) { + $question = $qa->get_question(); + + foreach ($question->answers as $ansid => $ans) { + if (question_state::graded_state_for_fraction($ans->fraction) == + question_state::$gradedright) { + return get_string('correctansweris', 'qtype_multichoice', + $question->format_text($ans->answer, $qa, 'question', 'answer', $ansid)); + } + } + + return ''; + } +} + +/** + * Subclass for generating the bits of output specific to multiple choice + * multi=select questions. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoice_multi_renderer extends qtype_multichoice_renderer_base { + protected function get_input_type() { + return 'checkbox'; + } + + protected function get_input_name(question_attempt $qa, $value) { + return $qa->get_qt_field_name('choice' . $value); + } + + protected function get_input_value($value) { + return 1; + } + + protected function get_input_id(question_attempt $qa, $value) { + return $this->get_input_name($qa, $value); + } + + protected function is_right(question_answer $ans) { + if ($ans->fraction > 0) { + return 1; + } else { + return 0; + } + } + + protected function prompt() { + return get_string('selectmulti', 'qtype_multichoice'); + } + + public function correct_response(question_attempt $qa) { + $question = $qa->get_question(); + + $right = array(); + foreach ($question->answers as $ansid => $ans) { + if ($ans->fraction > 0) { + $right[] = $question->format_text($ans->answer, $qa, 'question', 'answer', $ansid); + } + } + + if (!empty($right)) { + return get_string('correctansweris', 'qtype_multichoice', + implode(', ', $right)); + + } + return ''; + } + + protected function num_parts_correct(question_attempt $qa) { + if ($qa->get_question()->get_num_selected_choices($qa->get_last_qt_data()) > + $qa->get_question()->get_num_correct_choices()) { + return get_string('toomanyselected', 'qtype_multichoice'); + } + + return parent::num_parts_correct($qa); + } +} diff --git a/question/type/multichoice/simpletest/testquestion.php b/question/type/multichoice/simpletest/testquestion.php new file mode 100644 index 0000000000000..7dbca7d22d407 --- /dev/null +++ b/question/type/multichoice/simpletest/testquestion.php @@ -0,0 +1,238 @@ +. + + +/** + * Unit tests for the multiple choice question definition classes. + * + * @package qtype_multichoice + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot . '/question/engine/simpletest/helpers.php'); + + +/** + * Unit tests for the multiple choice, multiple response question definition class. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoice_single_question_test extends UnitTestCase { + + public function test_get_expected_data() { + $question = test_question_maker::make_a_multichoice_single_question(); + $this->assertEqual(array('answer' => PARAM_INT), $question->get_expected_data()); + } + + public function test_is_complete_response() { + $question = test_question_maker::make_a_multichoice_single_question(); + + $this->assertFalse($question->is_complete_response(array())); + $this->assertTrue($question->is_complete_response(array('answer' => '0'))); + $this->assertTrue($question->is_complete_response(array('answer' => '2'))); + } + + public function test_is_gradable_response() { + $question = test_question_maker::make_a_multichoice_single_question(); + + $this->assertFalse($question->is_gradable_response(array())); + $this->assertTrue($question->is_gradable_response(array('answer' => '0'))); + $this->assertTrue($question->is_gradable_response(array('answer' => '2'))); + } + + public function test_grading() { + $question = test_question_maker::make_a_multichoice_single_question(); + $question->shuffleanswers = false; + $question->init_first_step(new question_attempt_step()); + + $this->assertEqual(array(1, question_state::$gradedright), + $question->grade_response(array('answer' => 0))); + $this->assertEqual(array(-0.3333333, question_state::$gradedwrong), + $question->grade_response(array('answer' => 1))); + $this->assertEqual(array(-0.3333333, question_state::$gradedwrong), + $question->grade_response(array('answer' => 2))); + } + + public function test_grading_rounding_three_right() { + question_bank::load_question_definition_classes('multichoice'); + $mc = new qtype_multichoice_multi_question(); + test_question_maker::initialise_a_question($mc); + $mc->name = 'Odd numbers'; + $mc->questiontext = 'Which are the odd numbers?'; + $mc->generalfeedback = '1, 3 and 5 are the odd numbers.'; + $mc->qtype = question_bank::get_qtype('multichoice'); + + $mc->shuffleanswers = 0; + $mc->answernumbering = 'abc'; + + test_question_maker::set_standard_combined_feedback_fields($mc); + + $mc->answers = array( + 11 => new question_answer('1', 0.3333333, ''), + 12 => new question_answer('2', -1, ''), + 13 => new question_answer('3', 0.3333333, ''), + 14 => new question_answer('4', -1, ''), + 15 => new question_answer('5', 0.3333333, ''), + 16 => new question_answer('6', -1, ''), + ); + + $mc->init_first_step(new question_attempt_step()); + + list($grade, $state) = $mc->grade_response( + array('choice0' => 1, 'choice2' => 1, 'choice4' => 1)); + $this->assertWithinMargin(1, $grade, 0.000001); + $this->assertEqual(question_state::$gradedright, $state); + } + + public function test_get_correct_response() { + $question = test_question_maker::make_a_multichoice_single_question(); + $question->shuffleanswers = false; + $question->init_first_step(new question_attempt_step()); + + $this->assertEqual(array('answer' => 0), + $question->get_correct_response()); + } + + public function test_summarise_response() { + $mc = test_question_maker::make_a_multichoice_single_question(); + $mc->shuffleanswers = false; + $mc->init_first_step(new question_attempt_step()); + + $summary = $mc->summarise_response(array('answer' => 0), + test_question_maker::get_a_qa($mc)); + + $this->assertEqual('A', $summary); + } + + public function test_classify_response() { + $mc = test_question_maker::make_a_multichoice_single_question(); + $mc->shuffleanswers = false; + $mc->init_first_step(new question_attempt_step()); + + $this->assertEqual(array( + $mc->id => new question_classified_response(14, 'B', -0.3333333), + ), $mc->classify_response(array('answer' => 1))); + + $this->assertEqual(array( + $mc->id => question_classified_response::no_response(), + ), $mc->classify_response(array())); + } +} + + +/** + * Unit tests for the multiple choice, single response question definition class. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoice_multi_question_test extends UnitTestCase { + + public function test_get_expected_data() { + $question = test_question_maker::make_a_multichoice_multi_question(); + $question->init_first_step(new question_attempt_step()); + + $this->assertEqual(array('choice0' => PARAM_BOOL, 'choice1' => PARAM_BOOL, + 'choice2' => PARAM_BOOL, 'choice3' => PARAM_BOOL), $question->get_expected_data()); + } + + public function test_is_complete_response() { + $question = test_question_maker::make_a_multichoice_multi_question(); + $question->init_first_step(new question_attempt_step()); + + $this->assertFalse($question->is_complete_response(array())); + $this->assertFalse($question->is_complete_response( + array('choice0' => '0', 'choice1' => '0', 'choice2' => '0', 'choice3' => '0'))); + $this->assertTrue($question->is_complete_response(array('choice1' => '1'))); + $this->assertTrue($question->is_complete_response( + array('choice0' => '1', 'choice1' => '1', 'choice2' => '1', 'choice3' => '1'))); + } + + public function test_is_gradable_response() { + $question = test_question_maker::make_a_multichoice_multi_question(); + $question->init_first_step(new question_attempt_step()); + + $this->assertFalse($question->is_gradable_response(array())); + $this->assertFalse($question->is_gradable_response( + array('choice0' => '0', 'choice1' => '0', 'choice2' => '0', 'choice3' => '0'))); + $this->assertTrue($question->is_gradable_response(array('choice1' => '1'))); + $this->assertTrue($question->is_gradable_response( + array('choice0' => '1', 'choice1' => '1', 'choice2' => '1', 'choice3' => '1'))); + } + + public function test_grading() { + $question = test_question_maker::make_a_multichoice_multi_question(); + $question->shuffleanswers = false; + $question->init_first_step(new question_attempt_step()); + + $this->assertEqual(array(1, question_state::$gradedright), + $question->grade_response(array('choice0' => '1', 'choice2' => '1'))); + $this->assertEqual(array(0.5, question_state::$gradedpartial), + $question->grade_response(array('choice0' => '1'))); + $this->assertEqual(array(0, question_state::$gradedwrong), + $question->grade_response(array('choice0' => '1', 'choice1' => '1', 'choice2' => '1'))); + $this->assertEqual(array(0, question_state::$gradedwrong), + $question->grade_response(array('choice1' => '1'))); + } + + public function test_get_correct_response() { + $question = test_question_maker::make_a_multichoice_multi_question(); + $question->shuffleanswers = false; + $question->init_first_step(new question_attempt_step()); + + $this->assertEqual(array('choice0' => '1', 'choice2' => '1'), + $question->get_correct_response()); + } + + public function test_get_question_summary() { + $mc = test_question_maker::make_a_multichoice_single_question(); + $mc->init_first_step(new question_attempt_step()); + + $qsummary = $mc->get_question_summary(); + + $this->assertPattern('/' . preg_quote($mc->questiontext) . '/', $qsummary); + foreach ($mc->answers as $answer) { + $this->assertPattern('/' . preg_quote($answer->answer) . '/', $qsummary); + } + } + + public function test_summarise_response() { + $mc = test_question_maker::make_a_multichoice_multi_question(); + $mc->shuffleanswers = false; + $mc->init_first_step(new question_attempt_step()); + + $summary = $mc->summarise_response(array('choice1' => 1, 'choice2' => 1), + test_question_maker::get_a_qa($mc)); + + $this->assertEqual('B; C', $summary); + } + + public function test_classify_response() { + $mc = test_question_maker::make_a_multichoice_multi_question(); + $mc->shuffleanswers = false; + $mc->init_first_step(new question_attempt_step()); + + $this->assertEqual(array( + 13 => new question_classified_response(13, 'A', 0.5), + 14 => new question_classified_response(14, 'B', -1.0), + ), $mc->classify_response(array('choice0' => 1, 'choice1' => 1))); + + $this->assertEqual(array(), $mc->classify_response(array())); + } +} diff --git a/question/type/multichoice/simpletest/testquestiontype.php b/question/type/multichoice/simpletest/testquestiontype.php new file mode 100644 index 0000000000000..5edb7fad4f2f3 --- /dev/null +++ b/question/type/multichoice/simpletest/testquestiontype.php @@ -0,0 +1,90 @@ +. + + +/** + * Unit tests for the mulitple choice question definition class. + * + * @package qtype_multichoice + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot . '/question/type/multichoice/questiontype.php'); + +/** + * Unit tests for the multiple choice question definition class. + * + * @copyright 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multichoice_test extends UnitTestCase { + var $qtype; + + public function setUp() { + $this->qtype = new qtype_multichoice(); + } + + public function tearDown() { + $this->qtype = null; + } + + public function test_name() { + $this->assertEqual($this->qtype->name(), 'multichoice'); + } + + protected function get_test_question_data() { + $q = new stdClass; + $q->id = 1; + $q->options->single = true; + $q->options->answers[1] = (object) array('answer' => 'frog', 'fraction' => 1); + $q->options->answers[2] = (object) array('answer' => 'toad', 'fraction' => 0); + + return $q; + } + + public function test_can_analyse_responses() { + $this->assertTrue($this->qtype->can_analyse_responses()); + } + + public function test_get_random_guess_score() { + $q = $this->get_test_question_data(); + $this->assertEqual(0.5, $this->qtype->get_random_guess_score($q)); + } + + public function test_get_possible_responses_single() { + $q = $this->get_test_question_data(); + $responses = $this->qtype->get_possible_responses($q); + + $this->assertEqual(array( + $q->id => array( + 1 => new question_possible_response('frog', 1), + 2 => new question_possible_response('toad', 0), + null => question_possible_response::no_response(), + )), $this->qtype->get_possible_responses($q)); + } + + public function test_get_possible_responses_multi() { + $q = $this->get_test_question_data(); + $q->options->single = false; + + $this->assertEqual(array( + 1 => array(1 => new question_possible_response('frog', 1)), + 2 => array(2 => new question_possible_response('toad', 0)), + ), $this->qtype->get_possible_responses($q)); + } +} diff --git a/question/type/multichoice/simpletest/testwalkthrough.php b/question/type/multichoice/simpletest/testwalkthrough.php new file mode 100644 index 0000000000000..7c673918345da --- /dev/null +++ b/question/type/multichoice/simpletest/testwalkthrough.php @@ -0,0 +1,91 @@ +. + + +/** + * This file contains tests that walk mutichoice questions through various behaviours. + * + * Note, there are already lots of tests of the multichoice type in the behaviour + * tests. (Search for test_question_maker::make_a_multichoice.) This file only + * contains a few additional tests for problems that were found during testing. + * + * @package qtype_multichoice + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once($CFG->dirroot . '/question/engine/lib.php'); +require_once($CFG->dirroot . '/question/engine/simpletest/helpers.php'); + +class qtype_multichoice_walkthrough_test extends qbehaviour_walkthrough_test_base { + public function test_deferredfeedback_feedback_multichoice_single() { + + // Create a true-false question with correct answer true. + $mc = test_question_maker::make_a_multichoice_single_question(); + $mc->shuffleanswers = false; + $mc->answers[14]->fraction = 0.1; // Make one of the choices partially right. + $rightindex = 0; + + $this->start_attempt_at_question($mc, 'deferredfeedback', 3); + $this->process_submission(array('answer' => $rightindex)); + + // Verify. + $this->check_current_state(question_state::$complete); + $this->check_current_mark(null); + $this->check_current_output( + $this->get_contains_mc_radio_expectation($rightindex, true, true), + $this->get_contains_mc_radio_expectation($rightindex + 1, true, false), + $this->get_contains_mc_radio_expectation($rightindex + 2, true, false), + $this->get_does_not_contain_correctness_expectation(), + $this->get_does_not_contain_feedback_expectation()); + + // Finish the attempt. + $this->quba->finish_all_questions(); + + // Verify. + $this->check_current_state(question_state::$gradedright); + $this->check_current_mark(3); + $this->check_current_output( + $this->get_contains_mc_radio_expectation($rightindex, false, true), + $this->get_contains_correct_expectation(), + new PatternExpectation('/class="r0 correct"/'), + new PatternExpectation('/class="r1"/')); + } + + public function test_deferredfeedback_feedback_multichoice_multi() { + // Create a true-false question with correct answer true. + $mc = test_question_maker::make_a_multichoice_multi_question(); + $mc->shuffleanswers = false; + + $this->start_attempt_at_question($mc, 'deferredfeedback', 2); + $this->process_submission($mc->get_correct_response()); + $this->quba->finish_all_questions(); + + // Verify. + $this->check_current_state(question_state::$gradedright); + $this->check_current_mark(2); + $this->check_current_output( + $this->get_contains_mc_checkbox_expectation('choice0', false, true), + $this->get_contains_mc_checkbox_expectation('choice1', false, false), + $this->get_contains_mc_checkbox_expectation('choice2', false, true), + $this->get_contains_mc_checkbox_expectation('choice3', false, false), + $this->get_contains_correct_expectation(), + new PatternExpectation('/class="r0 correct"/'), + new PatternExpectation('/class="r1"/')); + } +} diff --git a/question/type/multichoice/styles.css b/question/type/multichoice/styles.css new file mode 100644 index 0000000000000..311d9cb8c7628 --- /dev/null +++ b/question/type/multichoice/styles.css @@ -0,0 +1,10 @@ +/* TODO */ +.que.multichoice .answer .specificfeedback { + display: inline; + padding: 0 0.7em; + background: #FFF3BF; +} +.que.multichoice .answer .specificfeedback * { + display: inline; + background: #FFF3BF; +} diff --git a/question/type/multichoice/version.php b/question/type/multichoice/version.php index 9061bb2e5ebf2..f5abde65759f6 100644 --- a/question/type/multichoice/version.php +++ b/question/type/multichoice/version.php @@ -1,6 +1,4 @@ version = 2010090501; +$plugin->version = 2011011200; $plugin->requires = 2010090501; - -