From ab50232b7e1c7d05bc73e5b849a17eabf2c852b2 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 23 May 2011 16:45:12 +0100 Subject: [PATCH] MDL-27413 start conversion of the multianswer qtype to the new question engine. --- question/engine/questionattemptstep.php | 158 +++++ .../db/simpletest/testupgradelibnewqe.php | 41 ++ question/type/multianswer/db/upgradelib.php | 106 +++ .../multianswer/edit_multianswer_form.php | 24 +- .../multianswer/lang/en/qtype_multianswer.php | 1 + question/type/multianswer/question.php | 212 ++++++ question/type/multianswer/questiontype.php | 294 +++----- question/type/multianswer/renderer.php | 641 ++++++++++++++++++ 8 files changed, 1260 insertions(+), 217 deletions(-) create mode 100644 question/type/multianswer/db/simpletest/testupgradelibnewqe.php create mode 100644 question/type/multianswer/db/upgradelib.php create mode 100644 question/type/multianswer/question.php create mode 100644 question/type/multianswer/renderer.php diff --git a/question/engine/questionattemptstep.php b/question/engine/questionattemptstep.php index a288012e857..eb7d549b4f0 100644 --- a/question/engine/questionattemptstep.php +++ b/question/engine/questionattemptstep.php @@ -458,3 +458,161 @@ class question_null_step { return null; } } + + +/** + * This is an adapter class that wraps a {@link question_attempt_step} and + * modifies the get/set_*_data methods so that they operate only on the parts + * that belong to a particular subquestion, as indicated by an extra prefix. + * + * @copyright 2010 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_attempt_step_subquestion_adapter extends question_attempt_step { + /** @var question_attempt_step the step we are wrapping. */ + protected $realstep; + /** @var string the exta prefix on fields we work with. */ + protected $extraprefix; + + /** + * Constructor. + * @param question_attempt_step $realqas the step to wrap. (Can be null if you + * just want to call add/remove.prefix.) + * @param unknown_type $extraprefix the extra prefix that is used for date fields. + */ + public function __construct($realqas, $extraprefix) { + $this->realqas = $realqas; + $this->extraprefix = $extraprefix; + } + + /** + * Add the extra prefix to a field name. + * @param string $field the plain field name. + * @return string the field name with the extra bit of prefix added. + */ + public function add_prefix($field) { + if (substr($field, 0, 2) === '!_') { + return '-_' . $this->extraprefix . substr($field, 2); + } else if (substr($field, 0, 1) === '-') { + return '-' . $this->extraprefix . substr($field, 1); + } else if (substr($field, 0, 1) === '_') { + return '_' . $this->extraprefix . substr($field, 1); + } else { + return $this->extraprefix . $field; + } + } + + /** + * Remove the extra prefix from a field name if it is present. + * @param string $field the extended field name. + * @return string the field name with the extra bit of prefix removed, or + * null if the extre prefix was not present. + */ + public function remove_prefix($field) { + if (preg_match('~^(-?_?)' . preg_quote($this->extraprefix) . '(.*)$~', $field, $matches)) { + return $matches[1] . $matches[2]; + } else { + return null; + } + } + + /** + * Filter some data to keep only those entries where the key contains + * extraprefix, and remove the extra prefix from the reutrned arrary. + * @param array $data some of the data stored in this step. + * @return array the data with the keys ajusted using {@link remove_prefix()}. + */ + public function filter_array($data) { + $result = array(); + foreach ($data as $fullname => $value) { + if ($name = $this->remove_prefix($fullname)) { + $result[$name] = $value; + } + } + return $result; + } + + public function get_state() { + return $this->realqas->get_state(); + } + + public function set_state($state) { + throw new coding_exception('Cannot modify a question_attempt_step_subquestion_adapter.'); + } + + public function get_fraction() { + return $this->realqas->get_fraction(); + } + + public function set_fraction($fraction) { + throw new coding_exception('Cannot modify a question_attempt_step_subquestion_adapter.'); + } + + public function get_user_id() { + return $this->realqas->get_user_id; + } + + public function get_timecreated() { + return $this->realqas->get_timecreated(); + } + + public function has_qt_var($name) { + return $this->realqas->has_qt_var($this->add_prefix($name)); + } + + public function get_qt_var($name) { + return $this->realqas->get_qt_var($this->add_prefix($name)); + } + + public function set_qt_var($name, $value) { + return $this->realqas->set_qt_var($this->add_prefix($name), $value); + } + + public function get_qt_data() { + return $this->filter_array($this->realqas->get_qt_data()); + } + + public function has_behaviour_var($name) { + return $this->realqas->has_im_var($this->add_prefix($name)); + } + + public function get_behaviour_var($name) { + return $this->realqas->get_im_var($this->add_prefix($name)); + } + + public function set_behaviour_var($name, $value) { + return $this->realqas->set_im_var($this->add_prefix($name), $value); + } + + public function get_behaviour_data() { + return $this->filter_array($this->realqas->get_behaviour_data()); + } + + public function get_submitted_data() { + return $this->filter_array($this->realqas->get_submitted_data()); + } + + public function get_all_data() { + return $this->filter_array($this->realqas->get_all_data()); + } + + public function get_qt_files($name, $contextid) { + throw new coding_exception('No attempt has yet been made to implement files support in ' . + 'question_attempt_step_subquestion_adapter.'); + } + + public function prepare_response_files_draft_itemid($name, $contextid) { + throw new coding_exception('No attempt has yet been made to implement files support in ' . + 'question_attempt_step_subquestion_adapter.'); + } + + public function prepare_response_files_draft_itemid_with_text($name, $contextid, $text) { + throw new coding_exception('No attempt has yet been made to implement files support in ' . + 'question_attempt_step_subquestion_adapter.'); + } + + public function rewrite_response_pluginfile_urls($text, $contextid, $name, $extras) { + throw new coding_exception('No attempt has yet been made to implement files support in ' . + 'question_attempt_step_subquestion_adapter.'); + } +} diff --git a/question/type/multianswer/db/simpletest/testupgradelibnewqe.php b/question/type/multianswer/db/simpletest/testupgradelibnewqe.php new file mode 100644 index 00000000000..0f1367667ec --- /dev/null +++ b/question/type/multianswer/db/simpletest/testupgradelibnewqe.php @@ -0,0 +1,41 @@ +. + +/** + * Tests of the upgrade to the new Moodle question engine for attempts at + * multianswer questions. + * + * @package qtype + * @subpackage multianswer + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/question/engine/upgrade/simpletest/helper.php'); + + +/** + * Testing the upgrade of multianswer question attempts. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multianswer_attempt_upgrader_test extends question_attempt_upgrader_test_base { + // TODO +} diff --git a/question/type/multianswer/db/upgradelib.php b/question/type/multianswer/db/upgradelib.php new file mode 100644 index 00000000000..9db4d122a7d --- /dev/null +++ b/question/type/multianswer/db/upgradelib.php @@ -0,0 +1,106 @@ +. + +/** + * Upgrade library code for the multianswer question type. + * + * @package qtype + * @subpackage multianswer + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Class for converting attempt data for multianswer questions when upgrading + * attempts to the new question engine. + * + * This class is used by the code in question/engine/upgrade/upgradelib.php. + * + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multianswer_qe2_attempt_updater extends question_qtype_attempt_updater { + // TODO + public function right_answer() { + foreach ($this->question->options->answers as $ans) { + if ($ans->fraction > 0.999) { + return $ans->answer; + } + } + } + +// public function restore_session_and_responses(&$question, &$state) { +// $responses = explode(',', $state->responses['']); +// $state->responses = array(); +// foreach ($responses as $response) { +// $tmp = explode("-", $response); +// // restore encoded characters +// $state->responses[$tmp[0]] = str_replace(array(",", "-"), +// array(",", "-"), $tmp[1]); +// } +// return true; +// } +// +// public function save_session_and_responses(&$question, &$state) { +// global $DB; +// $responses = $state->responses; +// // encode - (hyphen) and , (comma) to - because they are used as +// // delimiters +// array_walk($responses, create_function('&$val, $key', +// '$val = str_replace(array(",", "-"), array(",", "-"), $val); +// $val = "$key-$val";')); +// $responses = implode(',', $responses); +// +// // Set the legacy answer field +// $DB->set_field('question_states', 'answer', $responses, array('id' => $state->id)); +// return true; +// } + + public function response_summary($state) { + if (is_numeric($state->answer)) { + if (array_key_exists($state->answer, $this->question->options->answers)) { + return $this->question->options->answers[$state->answer]->answer; + } else { + $this->logger->log_assumption("Dealing with a place where the + student selected a choice that was later deleted for + true/false question {$this->question->id}"); + return null; + } + } else { + return null; + } + } + + public function was_answered($state) { + return !empty($state->answer); + } + + public function set_first_step_data_elements($state, &$data) { + } + + public function supply_missing_first_step_data(&$data) { + } + + public function set_data_elements_for_step($state, &$data) { + if (is_numeric($state->answer)) { + $data['answer'] = (int) ($state->answer == $this->question->options->trueanswer); + } + } +} diff --git a/question/type/multianswer/edit_multianswer_form.php b/question/type/multianswer/edit_multianswer_form.php index cd35c521579..415ff62163e 100644 --- a/question/type/multianswer/edit_multianswer_form.php +++ b/question/type/multianswer/edit_multianswer_form.php @@ -33,7 +33,7 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 2007 Jamie Pratt me@jamiep.org * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ -class question_edit_multianswer_form extends question_edit_form { +class qtype_multianswer_edit_form extends question_edit_form { // $questiondisplay will contain the qtype_multianswer_extract_question from // the questiontext @@ -83,8 +83,8 @@ class question_edit_multianswer_form extends question_edit_form { $mform->addElement('hidden', 'reload', 1); $mform->setType('reload', PARAM_INT); - // Remove meaningless defaultgrade field. - $mform->removeElement('defaultgrade'); + // Remove meaningless defaultmark field. + $mform->removeElement('defaultmark'); $this->confirm = optional_param('confirm', '0', PARAM_RAW); // display the questions from questiontext; @@ -145,9 +145,9 @@ class question_edit_multianswer_form extends question_edit_form { $mform->addElement('html', '
'); for ($sub = 1; $sub <= $countsubquestions; $sub++) { - $this->editas[$sub] = 'unknown type'; + $this->editas[$sub] = 'unknown type'; if (isset($this->questiondisplay->options->questions[$sub]->qtype)) { - $this->editas[$sub] = $this->questiondisplay->options->questions[$sub]->qtype; + $this->editas[$sub] = $this->questiondisplay->options->questions[$sub]->qtype; } else if (optional_param('sub_'.$sub."_".'qtype', '', PARAM_RAW) != '') { $this->editas[$sub] = optional_param('sub_'.$sub."_".'qtype', '', PARAM_RAW); } @@ -175,14 +175,14 @@ class question_edit_multianswer_form extends question_edit_form { $this->questiondisplay->options->questions[$sub]->questiontext['text']); } - $mform->addElement('static', 'sub_'.$sub."_".'defaultgrade', - get_string('defaultgrade', 'question')); - $mform->setDefault('sub_'.$sub."_".'defaultgrade', - $this->questiondisplay->options->questions[$sub]->defaultgrade); + $mform->addElement('static', 'sub_'.$sub."_".'defaultmark', + get_string('defaultmark', 'question')); + $mform->setDefault('sub_'.$sub."_".'defaultmark', + $this->questiondisplay->options->questions[$sub]->defaultmark); if ($this->questiondisplay->options->questions[$sub]->qtype == 'shortanswer') { $mform->addElement('static', 'sub_'.$sub."_".'usecase', - get_string('casesensitive', 'question')); + get_string('casesensitive', 'qtype_shortanswer')); } if ($this->questiondisplay->options->questions[$sub]->qtype == 'multichoice') { @@ -198,7 +198,7 @@ class question_edit_multianswer_form extends question_edit_form { if ($this->questiondisplay->options->questions[$sub]->qtype == 'numerical' && $key == 0) { $mform->addElement('static', 'sub_'.$sub."_".'tolerance['.$key.']', - get_string('acceptederror', 'quiz')); + get_string('acceptederror', 'qtype_numerical')); } $mform->addElement('static', 'sub_'.$sub."_".'fraction['.$key.']', @@ -270,7 +270,7 @@ class question_edit_multianswer_form extends question_edit_form { // The old way of restoring the definitions is kept to gradually // update all multianswer questions if (empty($wrapped->questiontext)) { - $parsableanswerdef = '{' . $wrapped->defaultgrade . ':'; + $parsableanswerdef = '{' . $wrapped->defaultmark . ':'; switch ($wrapped->qtype) { case 'multichoice': $parsableanswerdef .= 'MULTICHOICE:'; diff --git a/question/type/multianswer/lang/en/qtype_multianswer.php b/question/type/multianswer/lang/en/qtype_multianswer.php index f25f5ef72b1..fffd3cec9e8 100644 --- a/question/type/multianswer/lang/en/qtype_multianswer.php +++ b/question/type/multianswer/lang/en/qtype_multianswer.php @@ -60,6 +60,7 @@ $string['questionsaveasedited'] = 'The question will be saved as edited'; $string['questiontypechanged'] = 'Question type changed'; $string['questiontypechangedcomment'] = 'At least one question type has been changed.
Did you add, delete or move a question?
Look ahead.'; $string['questionusedinquiz'] = 'This question is used in {$a->nb_of_quiz} quiz(s), total attempt(s) : {$a->nb_of_attempts} '; +$string['subqresponse'] = 'part {$a->i}: {$a->response}'; $string['unknownquestiontypeofsubquestion'] = 'Unknown question type: {$a->type} of question part # {$a->sub}'; $string['warningquestionmodified'] = 'WARNING'; $string['youshouldnot'] = 'YOU SHOULD NOT'; diff --git a/question/type/multianswer/question.php b/question/type/multianswer/question.php new file mode 100644 index 00000000000..2a0c194bcee --- /dev/null +++ b/question/type/multianswer/question.php @@ -0,0 +1,212 @@ +. + + +/** + * Multianswer question definition class. + * + * @package qtype + * @subpackage multianswer + * @copyright 2010 Pierre Pichet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot . '/question/type/shortanswer/question.php'); +require_once($CFG->dirroot . '/question/type/numerical/question.php'); +require_once($CFG->dirroot . '/question/type/multichoice/question.php'); + + +/** + * Represents a multianswer question. + * + * A multi-answer question is made of of several subquestions of various types. + * You can think of it as an application of the composite pattern to qusetion + * types. + * + * @copyright 2010 Pierre Pichet + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qtype_multianswer_question extends question_graded_automatically { + /** @var array of question_graded_automatically. */ + public $subquestions = array(); + + /** + * Get a question_attempt_step_subquestion_adapter + * @param question_attempt_step $step the step to adapt. + * @param int $i the subquestion index. + * @return question_attempt_step_subquestion_adapter. + */ + protected function get_substep($step, $i) { + return new question_attempt_step_subquestion_adapter($step, 'sub' . $i . '_'); + } + + public function start_attempt(question_attempt_step $step) { + foreach ($this->subquestions as $i => $subq) { + $subq->start_attempt($this->get_substep($step, $i)); + + } + } + + public function apply_attempt_state(question_attempt_step $step) { + foreach ($this->subquestions as $i => $subq) { + $subq->apply_attempt_state($this->get_substep($step, $i)); + + } + } + + // TODO get_question_summary ??? + + public function get_min_fraction() { + $fractionsum = 0; + $fractionmax = 0; + foreach ($this->subquestions as $i => $subq) { + $fractionmax += $subq->defaultmark; + $fractionsum += $subq->defaultmark * $subq->get_min_fraction(); + } + return $fractionsum / $fractionmax; + } + + public function get_expected_data() { + $expected = array(); + foreach ($this->subquestions as $i => $subq) { + $substep = $this->get_substep(null, $i); + foreach ($subq->get_expected_data() as $name => $type) { + $expected[$substep->add_prefix($name)] = $type; + } + } + return $expected; + } + + public function get_correct_response() { + $right = array(); + foreach ($this->subquestions as $i => $subq) { + $substep = $this->get_substep(null, $i); + foreach ($subq->get_expected_data() as $name => $type) { + $right[$substep->add_prefix($name)] = $type; + } + } + return $right; + } + + public function is_complete_response(array $response) { + foreach ($this->subquestions as $i => $subq) { + $substep = $this->get_substep(null, $i); + if (!$subq->is_complete_response($substep->filter_array($response))) { + return false; + } + } + return true; + } + + public function is_gradable_response(array $response) { + foreach ($this->subquestions as $i => $subq) { + $substep = $this->get_substep(null, $i); + if ($subq->is_gradable_response($substep->filter_array($response))) { + return true; + } + } + return false; + } + + public function is_same_response(array $prevresponse, array $newresponse) { + foreach ($this->subquestions as $i => $subq) { + $substep = $this->get_substep(null, $i); + if (!$subq->is_same_response($substep->filter_array($prevresponse), + $substep->filter_array($newresponse))) { + return false; + } + } + return true; + } + + public function get_validation_error(array $response) { + $errors = array(); + foreach ($this->subquestions as $i => $subq) { + $substep = $this->get_substep(null, $i); + $errors[] = $subq->get_validation_error($substep->filter_array($response)); + } + return implode('
', $errors); + } + + /** + * Used by grade_response to combine the states of the subquestions. + * The combined state is accumulates in $overallstate. That will be right + * if all the separate states are right; and wrong if all the separate states + * are wrong, otherwise, it will be partially right. + * @param question_state $overallstate the result so far. + * @param question_state $newstate the new state to add to the combination. + * @return question_state the new combined state. + */ + protected function combine_states($overallstate, $newstate) { + if (is_null($overallstate)) { + return $newstate; + } else if ($overallstate == question_state::$gaveup && + $newstate == question_state::$gaveup) { + return question_state::$gaveup; + } else if ($overallstate == question_state::$gaveup && + $newstate == question_state::$gradedwrong) { + return question_state::$gradedwrong; + } else if ($overallstate == question_state::$gradedwrong && + $newstate == question_state::$gaveup) { + return question_state::$gradedwrong; + } else if ($overallstate == question_state::$gradedwrong && + $newstate == question_state::$gradedwrong) { + return question_state::$gradedwrong; + } else if ($overallstate == question_state::$gradedright && + $newstate == question_state::$gradedright) { + return question_state::$gradedright; + } else { + return question_state::$gradedpartial; + } + } + + public function grade_response(array $response) { + $overallstate = null; + $fractionsum = 0; + $fractionmax = 0; + foreach ($this->subquestions as $i => $subq) { + $fractionmax += $subq->defaultmark; + $substep = $this->get_substep(null, $i); + $subresp = $substep->filter_array($response); + if (!$subq->is_gradable_response($subresp)) { + $overallstate = $this->combine_states($overallstate, question_state::$gaveup); + } else { + list($subfraction, $newstate) = $subq->grade_response($subresp); + $fractionsum += $subfraction * $subq->defaultmark; + $overallstate = $this->combine_states($overallstate, $newstate); + } + } + return array($fractionsum / $fractionmax, $overallstate); + } + + public function summarise_response(array $response) { + $summary = array(); + foreach ($this->subquestions as $i => $subq) { + $substep = $this->get_substep(null, $i); + $a = new stdClass(); + $a->i = $i; + $a->response = $subq->summarise_response($substep->filter_array($response)); + $summary[] = get_string('subqresponse', 'qtype_multianswer', $a); + } + + return implode('; ', $summary); + } + + public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { + // TODO + } +} diff --git a/question/type/multianswer/questiontype.php b/question/type/multianswer/questiontype.php index 4de517efd09..46bc7b5c1c0 100644 --- a/question/type/multianswer/questiontype.php +++ b/question/type/multianswer/questiontype.php @@ -33,56 +33,44 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class embedded_cloze_qtype extends question_type { - - public function name() { - return 'multianswer'; - } - +class qtype_multianswer extends question_type { public function requires_qtypes() { return array('shortanswer', 'numerical', 'multichoice'); } + public function can_analyse_responses() { + return false; + } + public function get_question_options($question) { global $DB, $OUTPUT; // Get relevant data indexed by positionkey from the multianswers table - if (!$sequence = $DB->get_field('question_multianswer', 'sequence', array('question' => $question->id))) { - echo $OUTPUT->notification(get_string('noquestions', 'qtype_multianswer', $question->name)); - $question->options->questions['1'] = ''; - return true; - } + $sequence = $DB->get_field('question_multianswer', 'sequence', + array('question' => $question->id), '*', MUST_EXIST); - $wrappedquestions = $DB->get_records_list('question', 'id', explode(',', $sequence), 'id ASC'); + $wrappedquestions = $DB->get_records_list('question', 'id', + explode(',', $sequence), 'id ASC'); // We want an array with question ids as index and the positions as values $sequence = array_flip(explode(',', $sequence)); array_walk($sequence, create_function('&$val', '$val++;')); - //If a question is lost, the corresponding index is null + + // If a question is lost, the corresponding index is null // so this null convention is used to test $question->options->questions // before using the values. // first all possible questions from sequence are nulled // then filled with the data if available in $wrappedquestions - $nbvaliquestion = 0; foreach ($sequence as $seq) { $question->options->questions[$seq] = ''; } - if (isset($wrappedquestions) && is_array($wrappedquestions)) { - foreach ($wrappedquestions as $wrapped) { - if (!$QTYPES[$wrapped->qtype]->get_question_options($wrapped)) { - echo $OUTPUT->notification("Unable to get options for questiontype {$wrapped->qtype} (id={$wrapped->id})"); - } else { - // for wrapped questions the maxgrade is always equal to the defaultgrade, - // there is no entry in the question_instances table for them - $wrapped->maxgrade = $wrapped->defaultgrade; - $nbvaliquestion++; - // ??? Why do we need a clone here? - $question->options->questions[$sequence[$wrapped->id]] = clone($wrapped); - } - } - } - if ($nbvaliquestion == 0) { - echo $OUTPUT->notification(get_string('noquestions', 'qtype_multianswer', $question->name)); + + foreach ($wrappedquestions as $wrapped) { + question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped); + // for wrapped questions the maxgrade is always equal to the defaultmark, + // there is no entry in the question_instances table for them + $wrapped->maxmark = $wrapped->defaultmark; + $question->options->questions[$sequence[$wrapped->id]] = $wrapped; } return true; @@ -100,11 +88,13 @@ class embedded_cloze_qtype extends question_type { // will also create difficulties if questiontype specific tables reference the id. // First we get all the existing wrapped questions - if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence', array('question' => $question->id))) { + if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence', + array('question' => $question->id))) { $oldwrappedquestions = array(); } else { $oldwrappedquestions = $DB->get_records_list('question', 'id', explode(',', $oldwrappedids), 'id ASC'); } + $sequence = array(); foreach ($question->options->questions as $wrapped) { if (!empty($wrapped)) { @@ -115,16 +105,20 @@ class embedded_cloze_qtype extends question_type { if ($oldwrappedquestion->qtype != $wrapped->qtype) { switch ($oldwrappedquestion->qtype) { case 'multichoice': - $DB->delete_records('question_multichoice', array('question' => $oldwrappedquestion->id)); + $DB->delete_records('question_multichoice', + array('question' => $oldwrappedquestion->id)); break; case 'shortanswer': - $DB->delete_records('question_shortanswer', array('question' => $oldwrappedquestion->id)); + $DB->delete_records('question_shortanswer', + array('question' => $oldwrappedquestion->id)); break; case 'numerical': - $DB->delete_records('question_numerical', array('question' => $oldwrappedquestion->id)); + $DB->delete_records('question_numerical', + array('question' => $oldwrappedquestion->id)); break; default: - print_error('qtypenotrecognized', 'qtype_multianswer', '', $oldwrappedquestion->qtype); + throw new moodle_exception('qtypenotrecognized', + 'qtype_multianswer', '', $oldwrappedquestion->qtype); $wrapped->id = 0; } } @@ -135,8 +129,10 @@ class embedded_cloze_qtype extends question_type { $wrapped->name = $question->name; $wrapped->parent = $question->id; $previousid = $wrapped->id; - $wrapped->category = $question->category . ',1'; // save_question strips this extra bit off again. - $wrapped = $QTYPES[$wrapped->qtype]->save_question($wrapped, clone($wrapped)); + // save_question strips this extra bit off the category again. + $wrapped->category = $question->category . ',1'; + $wrapped = question_bank::get_qtype($wrapped->qtype)->save_question( + $wrapped, clone($wrapped)); $sequence[] = $wrapped->id; if ($previousid != 0 && $previousid != $wrapped->id) { // for some reasons a new question has been created @@ -158,9 +154,9 @@ class embedded_cloze_qtype extends question_type { $multianswer->sequence = implode(',', $sequence); if ($oldid = $DB->get_field('question_multianswer', 'id', array('question' => $question->id))) { $multianswer->id = $oldid; - $DB->update_record("question_multianswer", $multianswer); + $DB->update_record('question_multianswer', $multianswer); } else { - $DB->insert_record("question_multianswer", $multianswer); + $DB->insert_record('question_multianswer', $multianswer); } } } @@ -172,7 +168,7 @@ class embedded_cloze_qtype extends question_type { } $question->category = $authorizedquestion->category; - $form->defaultgrade = $question->defaultgrade; + $form->defaultmark = $question->defaultmark; $form->questiontext = $question->questiontext; $form->questiontextformat = 0; $form->options = clone($question->options); @@ -180,63 +176,19 @@ class embedded_cloze_qtype extends question_type { return parent::save_question($question, $form); } - public function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) { - $state->responses = array(); - foreach ($question->options->questions as $key => $wrapped) { - $state->responses[$key] = ''; - } - return true; - } - - public function restore_session_and_responses(&$question, &$state) { - $responses = explode(',', $state->responses['']); - $state->responses = array(); - foreach ($responses as $response) { - $tmp = explode("-", $response); - // restore encoded characters - $state->responses[$tmp[0]] = str_replace(array(",", "-"), - array(",", "-"), $tmp[1]); - } - return true; - } - - public function save_session_and_responses(&$question, &$state) { - global $DB; - $responses = $state->responses; - // encode - (hyphen) and , (comma) to - because they are used as - // delimiters - array_walk($responses, create_function('&$val, $key', - '$val = str_replace(array(",", "-"), array(",", "-"), $val); - $val = "$key-$val";')); - $responses = implode(',', $responses); - - // Set the legacy answer field - $DB->set_field('question_states', 'answer', $responses, array('id' => $state->id)); - return true; - } - public function delete_question($questionid, $contextid) { global $DB; - $DB->delete_records("question_multianswer", array("question" => $questionid)); + $DB->delete_records('question_multianswer', array('question' => $questionid)); parent::delete_question($questionid, $contextid); } - public function get_possible_responses(&$question) { - $responses = array(); - foreach ($question->options->questions as $key => $wrapped) { - if (!empty($wrapped)) { - if ($correct = $QTYPES[$wrapped->qtype]->get_possible_responses($wrapped)) { - $responses += $correct; - } else { - // if there is no correct answer to this subquestion then there - // can not be a correct answer to the whole question either, so - // we have to return null. - return null; - } - } + protected function initialise_question_instance($question, $questiondata) { + parent::initialise_question_instance($question, $questiondata); + foreach ($questiondata->options->questions as $key => $subqdata) { + $subqdata->contextid = $questiondata->contextid; + $question->subquestions[$key] = question_bank::make_question($subqdata); } - return $responses; } public function get_html_head_contributions(&$question, &$state) { @@ -298,7 +250,7 @@ class embedded_cloze_qtype extends question_type { $correctanswer = ''; $strfeedbackwrapped = $strfeedback; $testedstate = clone($state); - if ($correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) { + if ($correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) { if ($options->readonly && $options->correct_responses) { $delimiter = ''; if ($correctanswers) { @@ -434,7 +386,7 @@ class embedded_cloze_qtype extends question_type { echo $feedbackimg; } else if ($wrapped->options->layout == 1 || $wrapped->options->layout == 2) { $ordernumber = 0; - $anss = Array(); + $anss = array(); foreach ($answers as $mcanswer) { $ordernumber++; $checked = ''; @@ -546,129 +498,56 @@ class embedded_cloze_qtype extends question_type { echo '
'; } - public function compare_responses($question, $state, $teststate) { - foreach ($question->options->questions as $key => $wrapped) { - if (empty($wrapped)) { - continue; - } - - $stateforquestion = clone($state); - if (isset($state->responses[$key])) { - $stateforquestion->responses[''] = $state->responses[$key]; - } else { - $stateforquestion->responses[''] = ''; - } - - $teststateforquestion = clone($teststate); - if (isset($teststate->responses[$key])) { - $teststateforquestion->responses[''] = $teststate->responses[$key]; - } else { - $teststateforquestion->responses[''] = ''; - } - - if ($wrapped->qtype == 'numerical') { - // Use shortanswer - if (!$QTYPES['shortanswer']->compare_responses($wrapped, - $stateforquestion, $teststateforquestion)) { - return false; - } - } else { - if (!$QTYPES[$wrapped->qtype]->compare_responses($wrapped, - $stateforquestion, $teststateforquestion)) { - return false; - } - } + public function get_random_guess_score($questiondata) { + $fractionsum = 0; + $fractionmax = 0; + foreach ($questiondata->options->questions as $key => $subqdata) { + $fractionmax += $subqdata->maxmark; + $fractionsum += question_bank::get_qtype( + $subqdata->qtype)->get_random_guess_score($subqdata); } - - return true; + return $fractionsum / $fractionmax; } - - public function grade_responses(&$question, &$state, $cmoptions) { - $teststate = clone($state); - $state->raw_grade = 0; - foreach ($question->options->questions as $key => $wrapped) { - if (!empty($wrapped)) { - if (isset($state->responses[$key])) { - $state->responses[$key] = $state->responses[$key]; - } else { - $state->responses[$key] = ''; - } - $teststate->responses = array('' => $state->responses[$key]); - $teststate->raw_grade = 0; - if (false === $QTYPES[$wrapped->qtype]->grade_responses($wrapped, $teststate, $cmoptions)) { - return false; - } - $state->raw_grade += $teststate->raw_grade; - } - } - $state->raw_grade /= $question->defaultgrade; - $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0) - * $question->maxgrade; - - if (empty($state->raw_grade)) { - $state->raw_grade = 0.0; - } - $state->penalty = $question->penalty * $question->maxgrade; - - // mark the state as graded - $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE; - - return true; - } - - /** - * @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. - */ - public function get_random_guess_score($question) { - $totalfraction = 0; - foreach (array_keys($question->options->questions) as $key) { - $totalfraction += question_get_random_guess_score($question->options->questions[$key]); - } - return $totalfraction / count($question->options->questions); - } - } + // ANSWER_ALTERNATIVE regexes -define("ANSWER_ALTERNATIVE_FRACTION_REGEX", +define('ANSWER_ALTERNATIVE_FRACTION_REGEX', '=|%(-?[0-9]+)%'); // for the syntax '(?generalfeedback['itemid'] = ''; $question->options->questions = array(); - $question->defaultgrade = 0; // Will be increased for each answer norm + $question->defaultmark = 0; // Will be increased for each answer norm - for ($positionkey = 1; preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs); ++$positionkey) { + for ($positionkey = 1; + preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs); + ++$positionkey) { $wrapped = new stdClass(); $wrapped->generalfeedback['text'] = ''; $wrapped->generalfeedback['format'] = '1'; $wrapped->generalfeedback['itemid'] = ''; if (isset($answerregs[ANSWER_REGEX_NORM])&& $answerregs[ANSWER_REGEX_NORM]!== '') { - $wrapped->defaultgrade = $answerregs[ANSWER_REGEX_NORM]; + $wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM]; } else { - $wrapped->defaultgrade = '1'; + $wrapped->defaultmark = '1'; } if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) { $wrapped->qtype = 'numerical'; @@ -789,7 +670,8 @@ function qtype_multianswer_extract_question($text) { $wrapped->fraction["$answerindex"] = '0'; } if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) { - $feedback = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8'); + $feedback = html_entity_decode( + $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8'); $feedback = str_replace('\}', '}', $feedback); $wrapped->feedback["$answerindex"]['text'] = str_replace('\#', '#', $feedback); $wrapped->feedback["$answerindex"]['format'] = '1'; @@ -801,7 +683,8 @@ function qtype_multianswer_extract_question($text) { } if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL]) - && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~', $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) { + && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~', + $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) { $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER]; if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) { $wrapped->tolerance["$answerindex"] = @@ -811,7 +694,8 @@ function qtype_multianswer_extract_question($text) { } } else { // Tolerance can stay undefined for non numerical questions // Undo quoting done by the HTML editor. - $answer = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8'); + $answer = html_entity_decode( + $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8'); $answer = str_replace('\}', '}', $answer); $wrapped->answer["$answerindex"] = str_replace('\#', '#', $answer); } @@ -820,7 +704,7 @@ function qtype_multianswer_extract_question($text) { $answerindex++; } - $question->defaultgrade += $wrapped->defaultgrade; + $question->defaultmark += $wrapped->defaultmark; $question->options->questions[$positionkey] = clone($wrapped); $question->questiontext['text'] = implode("{#$positionkey}", explode($answerregs[0], $question->questiontext['text'], 2)); diff --git a/question/type/multianswer/renderer.php b/question/type/multianswer/renderer.php new file mode 100644 index 00000000000..ce5c3a26e9f --- /dev/null +++ b/question/type/multianswer/renderer.php @@ -0,0 +1,641 @@ + . + + +/** + * Multianswer question renderer classes. + * Handle shortanswer, numerical and various multichoice subquestions + * + * @package qtype_multianswer + * @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 multianswer + * (Cloze) questions. + * This render the main question text and transfer to the subquestions + * the task of display their input elements and status + * feedback, grade, correct answer(s) + * + * @copyright © 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + class qtype_multianswer_renderer extends qtype_renderer { + + public function formulation_and_controls(question_attempt $qa, + question_display_options $options) { + + $question = $qa->get_question(); + + $result = ''; + + $qtextremaining = $question->format_questiontext(); + + $strfeedback = get_string('feedback', 'quiz'); + + // The regex will recognize text snippets of type {#X} + // where the X can be any text not containg } or white-space characters. + + while (ereg('\{#([^[:space:]}]*)}', $qtextremaining, $regs)) { + $qtextsplits = explode($regs[0], $qtextremaining, 2); + $result .= $qtextsplits[0]; + // $result .= ""; // MDL-7497 + + } else { + if(! isset($question->subquestions[$positionkey])){ + $result .= $regs[0]; //.""; + }else { // + $result .= '
'.get_string('questionnotfound','qtype_multianswer',$positionkey).'
'; + } + } + } // end while + + // Print the final piece of question text: + $result .= $qtextremaining; + + return $result; + } + + + public function correct_response(question_attempt $qa) { + return '' ; + } + +} + + +/** + * Subclass for generating the bits of output specific to shortanswer + * subquestions. + * + * @copyright © 2009 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + require_once($CFG->dirroot . '/question/type/shortanswer/renderer.php'); + class qtype_multianswer_shortanswer_renderer extends qtype_shortanswer_renderer { + /** + * function normally part of core_question_renderer + * that is copy here + */ + + public function correct_response(question_attempt $qa) { + $questiontot = $qa->get_question(); + $subquestion = $questiontot->subquestions[$qa->subquestionindex]; + $answer = reset($subquestion->get_answers()); + if (!$answer) { + return ''; + } + return get_string('correctansweris', 'qtype_multianswer', s($answer->answer)); + } + + public function formulation_and_controls(question_attempt $qa, + question_display_options $options) { + $questiontot = $qa->get_question(); + $subquestion = $questiontot->subquestions[$qa->subquestionindex]; + $answername = $subquestion->fieldid.'answer' ; + $response = $qa->get_last_qt_var($answername); + $inputname = $qa->get_qt_field_name($answername); + $size = 1 ; + foreach ($subquestion->answers as $answer) { + if (strlen(trim($answer->answer)) > $size ){ + $size = strlen(trim($answer->answer)); + } + + } + if (strlen(trim($response))> $size ){ + $size = strlen(trim($response))+1; + } + $size = round($size + rand(0,$size*0.15)); + $size > 60 ? $size = 60 : $size = $size; + $inputattributes = array( + 'type' => 'text', + 'name' => $inputname, + 'value' => $response, + 'id' => $inputname, + 'size' => $size, + ); + // readonly cannot by put in input + if ($options->readonly) { + $inputattributes['readonly'] = 'readonly'; + + } + $class = ''; + $feedbackimg = ''; + // Determine feedback popup if any + $popup = ''; + $feedback = '' ; + $fraction = 0 ; + + if ($options->feedback) { + $answer = $subquestion->get_matching_answer(array('answer' => $response)); + if ($answer) { + $inputattributes['class'] = question_get_feedback_class($answer->fraction); + $feedbackimg = question_get_feedback_image($answer->fraction); + $fraction = $answer->fraction ; + if ($answer->feedback) { + // $feedback .= $subquestion->format_text(htmlspecialchars($answer->feedback, ENT_QUOTES )); + $feedback .= $subquestion->format_text($answer->feedback ); + } + } else { + $inputattributes['class'] = question_get_feedback_class(0); + $feedbackimg = question_get_feedback_image(0); + } + } + $readonly =''; + if ($options->readonly) { + $inputattributes['readonly'] = 'readonly'; + $readonly = 'readonly="readonly"'; + } + // determine popup + // answer feedback (specific)i.e if options->feedback already set + // subquestion status correctness or Finished validator if correctness + // Correct response + // marks + if ($options->feedback) { + $strfeedbackwrapped = 'Response Status'; + $subfraction = '' ; + if ($options->correctness ) { + if ( ! $answer ){ + $state = $qa->get_state(); + $state = question_state::$invalid; + $strfeedbackwrapped .= ":".$state->default_string()."" ; + $feedback = "".$subquestion->get_validation_error(array('answer' => $response)) .""; + }else { + $state = $qa->get_state(); + $state = question_state::graded_state_for_fraction($fraction); + $strfeedbackwrapped .= ":".$state->default_string(); + } + } + + if ($options->correctresponse ) { + $feedback .= "
".$this->correct_response( $qa);// + } + if ($options->marks ) { + $res = $subquestion->grade_response(array('answer'=>$response)); // fraction=>state + $subfraction = $res[0]; + $subgrade= $subfraction * $subquestion->defaultmark ; + $feedback .= "
".$questiontot->mark_summary($options, $subquestion->defaultmark , $subgrade ); + + } + $feedback = str_replace("'","\'",$feedback); + $feedback = str_replace('"',"\'",$feedback); + $strfeedbackwrapped = str_replace("'"," ",$strfeedbackwrapped); + $strfeedbackwrapped = str_replace('"',"\'",$strfeedbackwrapped); + $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ". + " onmouseout=\"return nd();\" "; + } //if feedback + + $result = ''; + $result .= ""; // MDL-7497 + + return $result; + } + +} + +/** + * As multianswer have specific display requirements for multichoice display + * a new class was defined although largely following the multichoice one + */ + +abstract class qtype_multianswer_multichoice_renderer_base extends qtype_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); + + abstract protected function is_choice_selected($response, $value); + + abstract protected function is_right(question_answer $ans); + + abstract protected function get_response(question_attempt $qa); + + + + public function specific_feedback(question_attempt $qa) { + return ''; + } + + public function formulation_and_controls(question_attempt $qa, + question_display_options $options) { + + $questiontot = $qa->get_question(); + $subquestion = $questiontot->subquestions[$qa->subquestionindex]; + $order = $subquestion->get_order($qa); //array_keys($question->answers); // + $response = $this->get_response($qa); + $inputattributes = array( + 'type' => $this->get_input_type(), + ); + + if ($options->readonly) { + $inputattributes['disabled'] = 'disabled'; + } + $radiobuttons = array(); + $feedbackimg = array(); + $feedback = array(); + $classes = array(); + $totfraction = 0 ; + $nullresponse = true ; + foreach ($order as $value => $ansid) { + $ans = $subquestion->answers[$ansid]; + $inputattributes['name'] = $this->get_input_name($qa, $value); + // echo "

name $value name".$inputattributes['name']."

"; + $inputattributes['value'] = $this->get_input_value($value); + $inputattributes['id'] = $this->get_input_id($qa, $value); + if ($subquestion->single) { + $isselected = $this->is_choice_selected($response, $value); + } else { + $isselected = $this->is_choice_selected($response,$value) ; //$subquestion->field( $value)); + } + if ($isselected) { + $inputattributes['checked'] = 'checked'; + $totfraction += $ans->fraction ; + $nullresponse = false ; + } else { + unset($inputattributes['checked']); + } + $radiobuttons[] = html_writer::empty_tag('input', $inputattributes) . + html_writer::tag('label', $subquestion->format_text($ans->answer), array('for' => $inputattributes['id'])); + + if (($options->feedback || $options->correctresponse) && $response !== -1) { + $feedbackimg[] = question_get_feedback_image($this->is_right($ans), $isselected && $options->feedback); + } else { + $feedbackimg[] = ''; + } + if (($options->feedback || $options->correctresponse) && $isselected) { + $feedback[] = $subquestion->format_text($ans->feedback); + } else { + $feedback[] = ''; + } + $class = 'r' . ($value % 2); + if ($options->correctresponse && $ans->fraction > 0) { + $class .= ' ' . question_get_feedback_class($ans->fraction); + } + $classes[] = $class; + } + + $result = '' ; + + $answername = 'answer' ; + if ($subquestion->layout == 1 ){ + $result .= html_writer::start_tag('div', array('class' => 'ablock')); + + $result .= html_writer::start_tag('table', array('class' => $answername)); + foreach ($radiobuttons as $key => $radio) { + $result .= html_writer::start_tag('tr', array('class' => $answername)); + $result .= html_writer::start_tag('td', array('class' => $answername)); + $result .= html_writer::tag('span',$radio . $feedbackimg[$key] . $feedback[$key], array('class' => $classes[$key])) . "\n"; + $result .= html_writer::end_tag('td'); + $result .= html_writer::end_tag('tr'); + } + $result .= html_writer::end_tag('table'); // answer + + $result .= html_writer::end_tag('div'); // ablock + } + if ($subquestion->layout == 2 ){ + $result .= html_writer::start_tag('div', array('class' => 'ablock')); + $result .= html_writer::start_tag('table', array('class' => $answername)); + $result .= html_writer::start_tag('tr', array('class' => $answername)); + foreach ($radiobuttons as $key => $radio) { + $result .= html_writer::start_tag('td', array('class' => $answername)); + $result .= html_writer::tag('span',$radio . $feedbackimg[$key] . $feedback[$key] + , array('class' => $classes[$key])) . "\n"; + $result .= html_writer::end_tag('td'); + } + $result .= html_writer::end_tag('tr'); + $result .= html_writer::end_tag('table'); // answer + + $result .= html_writer::end_tag('div'); // ablock + + } + if ($options->feedback ) { + $result .= html_writer::start_tag('div', array('class' => 'outcome')); + + if ($options->correctness ) { + if ( $nullresponse ){ + $state = $qa->get_state(); + $state = question_state::$invalid; + $result1 = $state->default_string(); + $result .= html_writer::nonempty_tag('div',$result1, + array('class' => 'validationerror')); + $result1 = ($subquestion->single) ? get_string('singleanswer', 'quiz') : get_string('multipleanswers', 'quiz'); + $result .= html_writer::nonempty_tag('div', $result1, + array('class' => 'validationerror')) + ; + }else { + $state = $qa->get_state(); + $state = question_state::graded_state_for_fraction($totfraction); + $result1 = $state->default_string(); + $result .= html_writer::nonempty_tag('div', $result1, + array('class' => 'outcome')); + } + } + + + if ($options->correctresponse ) { + $result1 = $this->correct_response($qa); + $result .= html_writer::nonempty_tag('div',$result1, array('class' => 'outcome')) + ; + } + if ($options->marks ) { + $subgrade= $totfraction * $subquestion->defaultmark ; + $result .= $questiontot->mark_summary($options, $subquestion->defaultmark , $subgrade ); + } + + if ($qa->get_state() == question_state::$invalid) { + $result .= html_writer::nonempty_tag('div', array('class' => 'validationerror'), + $subquestion->get_validation_error($qa->get_last_qt_data())); + } + $result .= html_writer::end_tag('div'); + + } + return $result; + } + + +} + + +class qtype_multianswer_multichoice_single_renderer extends qtype_multianswer_multichoice_renderer_base { + protected function get_input_type() { + return 'radio'; + } + + protected function is_choice_selected($response, $value) { + return $response == $value ; + } + protected function is_right(question_answer $ans) { + return $ans->fraction > 0.9999999; + } + protected function get_input_name(question_attempt $qa, $value) { + $questiontot = $qa->get_question(); + $subquestion = $questiontot->subquestions[$qa->subquestionindex]; + $answername = $subquestion->fieldid.'answer'; + return $qa->get_qt_field_name($answername); + } + protected function get_input_value($value) { + return $value; + } + + protected function get_input_id(question_attempt $qa, $value) { + $questiontot = $qa->get_question(); + $subquestion = $questiontot->subquestions[$qa->subquestionindex]; + $answername = $subquestion->fieldid.'answer'; + return $qa->get_qt_field_name($answername); + } + + protected function get_response(question_attempt $qa) { + $questiontot = $qa->get_question(); + $subquestion = $questiontot->subquestions[$qa->subquestionindex]; + return $qa->get_last_qt_var($subquestion->fieldid.'answer', -1); + + } + public function correct_response(question_attempt $qa) { + $questiontot = $qa->get_question(); + $subquestion = $questiontot->subquestions[$qa->subquestionindex]; + + foreach ($subquestion->answers as $ans) { + if ($ans->fraction > 0.9999999) { + return get_string('correctansweris', 'qtype_multichoice', + $subquestion->format_text($ans->answer)); + } + } + + return ''; + } + +} +class qtype_multianswer_multichoice_single_inline_renderer extends qtype_multianswer_multichoice_single_renderer { + protected function get_input_type() { + return 'select'; + } + + public function formulation_and_controls(question_attempt $qa, + question_display_options $options) { + $questiontot = $qa->get_question(); + $subquestion = $questiontot->subquestions[$qa->subquestionindex]; + $answers = $subquestion->answers; + $correctanswers = $subquestion->get_correct_response(); + foreach($correctanswers as $key=> $value){ + $correct = $value ; + } + $order = $subquestion->get_order($qa); + $response = $this->get_response($qa); + $currentanswer = $response ; + $answername = $subquestion->fieldid.'answer'; + $inputname = $qa->get_qt_field_name($answername); + $inputattributes = array( + 'type' => $this->get_input_type(), + 'name' => $inputname, + ); + + if ($options->readonly) { + $inputattributes['disabled'] = 'disabled'; + $readonly = 'disabled ="disabled"'; + } + $choices = array(); + $popup = ''; + $feedback = '' ; + $answer = '' ; + $classes = 'control'; + $feedbackimage = ''; + $fraction = 0 ; + $chosen = 0 ; + + foreach ($order as $value => $ansid) { + $mcanswer = $subquestion->answers[$ansid]; + $choices[$value] = strip_tags($mcanswer->answer); + $selected = ''; + $isselected = false ; + if( $response != ''){ + $isselected = $this->is_choice_selected($response, $value); + } + if ($isselected) { + $chosen = $value ; + $answer = $mcanswer ; + $fraction = $mcanswer->fraction ; + $selected = ' selected="selected"'; + } + } + if ($options->feedback) { + if ($answer) { + $classes .= ' ' . question_get_feedback_class($fraction); + $feedbackimage = question_get_feedback_image($answer->fraction); + if ($answer->feedback) { + $feedback .= $subquestion->format_text($answer->feedback); + } + } else { + $classes .= ' ' . question_get_feedback_class(0); + $feedbackimage = question_get_feedback_image(0); + } + } + // determine popup + // answer feedback (specific)i.e if options->feedback already set + // subquestion status correctness or Finished validator if correctness + // Correct response + // marks + $strfeedbackwrapped = 'Response Status'; + if ($options->feedback ) { + $feedback = get_string('feedback', 'quiz').":".$feedback."
"; + + if ($options->correctness ) { + if ( ! $answer ){ + $state = $qa->get_state(); + $state = question_state::$invalid; + $strfeedbackwrapped .= ":".$state->default_string()."" ; + $feedback = "".get_string('singleanswer', 'quiz') ."
"; + }else { + $state = $qa->get_state(); + $state = question_state::graded_state_for_fraction($fraction); + $strfeedbackwrapped .= ":".$state->default_string(); + } + } + + + if ($options->correctresponse ) { + $feedback .= $this->correct_response($qa)."
"; + } + if ($options->marks ) { + $subgrade= $fraction * $subquestion->defaultmark ; + $feedback .= $questiontot->mark_summary($options, $subquestion->defaultmark , $subgrade ); + } + + $feedback .= ''; + } + + if ($options->feedback ) { + // need to replace ' and " as they could break the popup string + // as the text comes from database, slashes have been removed + // addslashes will not work as it keeps the " + // HTML ' for ' does not work + $feedback = str_replace("'","\'",$feedback); + $feedback = str_replace('"',"\'",$feedback); + $strfeedbackwrapped = str_replace("'","\'",$strfeedbackwrapped); + $strfeedbackwrapped = str_replace('"',"\'",$strfeedbackwrapped); + + $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ". + " onmouseout=\"return nd();\" "; + } + $result = ''; + + $result .= ""; + $result .= html_writer::start_tag('span', array('class' => $classes), ''); + + $result .= + choose_from_menu($choices, $inputname, $chosen, + ' ', '', '', true, $options->readonly) . $feedbackimage ; + $result .= html_writer::end_tag('span'); + $result .= html_writer::end_tag('span'); + + + return $result; + } + + protected function format_choices($question) { + $choices = array(); + foreach ($question->get_choice_order() as $key => $choiceid) { + $choices[$key] = strip_tags($question->format_text($question->choices[$choiceid])); + } + return $choices; + } + + +} +class qtype_multianswer_multichoice_multi_renderer extends qtype_multianswer_multichoice_renderer_base { + protected function get_input_type() { + return 'checkbox'; + } + + protected function get_input_name(question_attempt $qa, $value) { + $questiontot = $qa->get_question(); + $subquestion = $questiontot->subquestions[$qa->subquestionindex]; + return $qa->get_qt_field_name($subquestion->fieldid.'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 get_response(question_attempt $qa) { + $responses = $qa->get_last_qt_data(); + $questiontot = $qa->get_question(); + $subresponses =$questiontot->decode_subquestion_responses($responses); + if( isset($subresponses[$qa->subquestionindex])) { + return $subresponses[$qa->subquestionindex] ; + }else{ + return ''; + } + } + + protected function is_choice_selected($response, $value) { + return isset($response['choice'.$value]); + } + + protected function is_right(question_answer $ans) { + return $ans->fraction > 0; + } + + public function correct_response(question_attempt $qa) { + $questiontot = $qa->get_question(); + $subquestion = $questiontot->subquestions[$qa->subquestionindex]; + + $right = array(); + foreach ($subquestion->answers as $ans) { + if ($ans->fraction > 0) { + $right[] = $subquestion->format_text($ans->answer); + } + } + + if (!empty($right)) { + return get_string('correctansweris', 'qtype_multichoice', + implode(', ', $right)); + + } + return ''; + } + + + +}