MDL-20636 Mostly working conversion of the multichoice question type.

Currently, you get an error on saving the editing form.
This commit is contained in:
Tim Hunt 2011-01-12 19:09:49 +00:00
parent 9481434019
commit c9c989a09a
14 changed files with 1375 additions and 501 deletions

View File

@ -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 +
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 +
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/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 +-

View File

@ -425,6 +425,7 @@ abstract class question_edit_form extends moodleform {
}
}
}
// subclass adds data_preprocessing code here
$question = $this->data_preprocessing($question);
@ -478,6 +479,35 @@ abstract class question_edit_form extends moodleform {
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;

View File

@ -18,7 +18,8 @@
<FIELD NAME="partiallycorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="partiallycorrectfeedback" NEXT="incorrectfeedback"/>
<FIELD NAME="incorrectfeedback" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" COMMENT="Feedback shown for any incorrect response." PREVIOUS="partiallycorrectfeedbackformat" NEXT="incorrectfeedbackformat"/>
<FIELD NAME="incorrectfeedbackformat" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" PREVIOUS="incorrectfeedback" NEXT="answernumbering"/>
<FIELD NAME="answernumbering" TYPE="char" LENGTH="10" NOTNULL="true" DEFAULT="abc" SEQUENCE="false" COMMENT="Indicates how and whether the choices should be numbered." PREVIOUS="incorrectfeedbackformat"/>
<FIELD NAME="answernumbering" TYPE="char" LENGTH="10" NOTNULL="true" DEFAULT="abc" SEQUENCE="false" COMMENT="Indicates how and whether the choices should be numbered." PREVIOUS="incorrectfeedbackformat" NEXT="shownumcorrect"/>
<FIELD NAME="shownumcorrect" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" COMMENT="If true, then when the user gets a multiple-response question partially correct, tell them how many choices they got correct alongside the feedback." PREVIOUS="answernumbering"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="question"/>

View File

@ -109,7 +109,23 @@ function xmldb_qtype_multichoice_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2009021801, 'qtype', 'multichoice');
}
// 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;
}

View File

@ -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 &copy; 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 &copy; 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 @@ class question_edit_multichoice_form extends question_edit_form {
$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;
$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);
// 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);
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';
}
function validation($data, $files) {
public function validation($data, $files) {
$errors = parent::validation($data, $files);
$answers = $data['answer'];
$answercount = 0;
@ -132,27 +86,28 @@ class question_edit_multichoice_form extends question_edit_form {
$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 @@ class question_edit_multichoice_form extends question_edit_form {
}
return $errors;
}
public function qtype() {
return 'multichoice';
}
}

View File

@ -27,13 +27,18 @@ $string['addingmultichoice'] = 'Adding a Multiple choice question';
$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%<br />Instead, they add up to {$a}%';
@ -42,8 +47,6 @@ $string['feedback'] = 'Feedback';
$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%<br />Instead, they add up to {$a}%<br />Do you want to go back and fix this question?';
$string['fractionsnomax'] = 'One of the choices should be 100%, so that it is<br />possible to get a full grade for this question.<br />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['multichoice_link'] = 'question/type/multichoice';
$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.';

View File

@ -0,0 +1,426 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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]);
}
}

View File

@ -1,41 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 @@ class question_multichoice_qtype extends default_questiontype {
$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 @@ class question_multichoice_qtype extends default_questiontype {
}
$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 @@ class question_multichoice_qtype extends default_questiontype {
return $result;
}
}
return true;
}
function delete_question($questionid, $contextid) {
protected function make_question_instance($questiondata) {
question_bank::load_question_definition_classes($this->name());
if ($questiondata->options->single) {
$class = 'qtype_multichoice_single_question';
} else {
$class = 'qtype_multichoice_multi_question';
}
return new $class();
}
protected function make_hint($hint) {
return question_hint_with_parts::load_from_record($hint);
}
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 {
$question->layout = qtype_multichoice_single_question::LAYOUT_VERTICAL;
}
$question->correctfeedback = $questiondata->options->correctfeedback;
$question->partiallycorrectfeedback = $questiondata->options->partiallycorrectfeedback;
$question->incorrectfeedback = $questiondata->options->incorrectfeedback;
$question->shownumcorrect = !$questiondata->options->single && $questiondata->options->shownumcorrect;
$this->initialise_question_answers($question, $questiondata);
}
public 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('' => '');
} else {
$state->responses = array();
}
return true;
}
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;
}
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;
} 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;
}
}
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 = "<input $readonly id=\"$a->id\" $name $checked $type value=\"$aid\" />";
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';
}
$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);
}
include("$CFG->dirroot/question/type/multichoice/display.html");
}
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;
}
}
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;
// mark the state as graded
$state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
return true;
}
// 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;
}
}
} 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) {
public function get_random_guess_score($questiondata) {
$totalfraction = 0;
foreach ($question->options->answers as $answer){
foreach ($questiondata->options->answers as $answer) {
$totalfraction += $answer->fraction;
}
return $totalfraction / count($question->options->answers);
return $totalfraction / count($questiondata->options->answers);
}
function get_possible_responses($questiondata) {
if ($questiondata->options->single) {
$responses = array();
foreach ($questiondata->options->answers as $aid => $answer) {
$responses[$aid] = new question_possible_response($answer->answer,
$answer->fraction);
}
$responses[null] = question_possible_response::no_response();
return array($questiondata->id => $responses);
} else {
$parts = array();
foreach ($questiondata->options->answers as $aid => $answer) {
$parts[$aid] = array($aid =>
new question_possible_response($answer->answer, $answer->fraction));
}
return $parts;
}
}
/**
@ -434,61 +231,12 @@ class question_multichoice_qtype extends default_questiontype {
* 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 '<span class="anun">' . $qnum . '<span class="anumsep">.</span></span> ';
}
/**
* @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 @@ class question_multichoice_qtype extends default_questiontype {
$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());

View File

@ -0,0 +1,294 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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);
}
}

View File

@ -0,0 +1,238 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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()));
}
}

View File

@ -0,0 +1,90 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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));
}
}

View File

@ -0,0 +1,91 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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"/'));
}
}

View File

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

View File

@ -1,6 +1,4 @@
<?php
$plugin->version = 2010090501;
$plugin->version = 2011011200;
$plugin->requires = 2010090501;