Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

451 lines (402 sloc) 20.349 kb
<?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/>.
/**
* Question type class for the multi-answer question type.
*
* @package qtype
* @subpackage multianswer
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/multichoice/question.php');
/**
* The multi-answer question type class.
*
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
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
$sequence = $DB->get_field('question_multianswer', 'sequence',
array('question' => $question->id), '*', MUST_EXIST);
$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
// 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
foreach ($sequence as $seq) {
$question->options->questions[$seq] = '';
}
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;
}
$question->hints = $DB->get_records('question_hints',
array('questionid' => $question->id), 'id ASC');
return true;
}
public function save_question_options($question) {
global $DB;
$result = new stdClass();
// This function needs to be able to handle the case where the existing set of wrapped
// questions does not match the new set of wrapped questions so that some need to be
// created, some modified and some deleted
// Unfortunately the code currently simply overwrites existing ones in sequence. This
// will make re-marking after a re-ordering of wrapped questions impossible and
// 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))) {
$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)) {
// if we still have some old wrapped question ids, reuse the next of them
if (is_array($oldwrappedquestions) &&
$oldwrappedquestion = array_shift($oldwrappedquestions)) {
$wrapped->id = $oldwrappedquestion->id;
if ($oldwrappedquestion->qtype != $wrapped->qtype) {
switch ($oldwrappedquestion->qtype) {
case 'multichoice':
$DB->delete_records('question_multichoice',
array('question' => $oldwrappedquestion->id));
break;
case 'shortanswer':
$DB->delete_records('question_shortanswer',
array('question' => $oldwrappedquestion->id));
break;
case 'numerical':
$DB->delete_records('question_numerical',
array('question' => $oldwrappedquestion->id));
break;
default:
throw new moodle_exception('qtypenotrecognized',
'qtype_multianswer', '', $oldwrappedquestion->qtype);
$wrapped->id = 0;
}
}
} else {
$wrapped->id = 0;
}
}
$wrapped->name = $question->name;
$wrapped->parent = $question->id;
$previousid = $wrapped->id;
// 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
// so delete the old one
question_delete_question($previousid);
}
}
// Delete redundant wrapped questions
if (is_array($oldwrappedquestions) && count($oldwrappedquestions)) {
foreach ($oldwrappedquestions as $oldwrappedquestion) {
question_delete_question($oldwrappedquestion->id);
}
}
if (!empty($sequence)) {
$multianswer = new stdClass();
$multianswer->question = $question->id;
$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);
} else {
$DB->insert_record('question_multianswer', $multianswer);
}
}
$this->save_hints($question);
}
public function save_question($authorizedquestion, $form) {
$question = qtype_multianswer_extract_question($form->questiontext);
if (isset($authorizedquestion->id)) {
$question->id = $authorizedquestion->id;
}
$question->category = $authorizedquestion->category;
$form->defaultmark = $question->defaultmark;
$form->questiontext = $question->questiontext;
$form->questiontextformat = 0;
$form->options = clone($question->options);
unset($question->options);
return parent::save_question($question, $form);
}
public function delete_question($questionid, $contextid) {
global $DB;
$DB->delete_records('question_multianswer', array('question' => $questionid));
parent::delete_question($questionid, $contextid);
}
protected function initialise_question_instance($question, $questiondata) {
parent::initialise_question_instance($question, $questiondata);
$bits = preg_split('/\{#(\d+)\}/', $question->questiontext,
null, PREG_SPLIT_DELIM_CAPTURE);
$question->textfragments[0] = array_shift($bits);
$i = 1;
while (!empty($bits)) {
$question->places[$i] = array_shift($bits);
$question->textfragments[$i] = array_shift($bits);
$i += 1;
}
foreach ($questiondata->options->questions as $key => $subqdata) {
$subqdata->contextid = $questiondata->contextid;
$question->subquestions[$key] = question_bank::make_question($subqdata);
$question->subquestions[$key]->maxmark = $subqdata->defaultmark;
if (isset($subqdata->options->layout)) {
$question->subquestions[$key]->layout = $subqdata->options->layout;
}
}
}
public function get_random_guess_score($questiondata) {
$fractionsum = 0;
$fractionmax = 0;
foreach ($questiondata->options->questions as $key => $subqdata) {
$fractionmax += $subqdata->defaultmark;
$fractionsum += question_bank::get_qtype(
$subqdata->qtype)->get_random_guess_score($subqdata);
}
return $fractionsum / $fractionmax;
}
public function move_files($questionid, $oldcontextid, $newcontextid) {
parent::move_files($questionid, $oldcontextid, $newcontextid);
$this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
}
protected function delete_files($questionid, $contextid) {
parent::delete_files($questionid, $contextid);
$this->delete_files_in_hints($questionid, $contextid);
}
}
// ANSWER_ALTERNATIVE regexes
define('ANSWER_ALTERNATIVE_FRACTION_REGEX',
'=|%(-?[0-9]+)%');
// for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
define('ANSWER_ALTERNATIVE_ANSWER_REGEX',
'.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
define('ANSWER_ALTERNATIVE_FEEDBACK_REGEX',
'.*?(?<!\\\\)(?=[~}]|$)');
define('ANSWER_ALTERNATIVE_REGEX',
'(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
'(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
'(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
// Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
define('ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION', 2);
define('ANSWER_ALTERNATIVE_REGEX_FRACTION', 1);
define('ANSWER_ALTERNATIVE_REGEX_ANSWER', 3);
define('ANSWER_ALTERNATIVE_REGEX_FEEDBACK', 5);
// NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
// for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
define('NUMBER_REGEX',
'-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
define('NUMERICAL_ALTERNATIVE_REGEX',
'^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
// Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
define('NUMERICAL_CORRECT_ANSWER', 1);
define('NUMERICAL_ABS_ERROR_MARGIN', 6);
// Remaining ANSWER regexes
define('ANSWER_TYPE_DEF_REGEX',
'(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' .
'(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)');
define('ANSWER_START_REGEX',
'\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
define('ANSWER_REGEX',
ANSWER_START_REGEX
. '(' . ANSWER_ALTERNATIVE_REGEX
. '(~'
. ANSWER_ALTERNATIVE_REGEX
. ')*)\}');
// Parenthesis positions for singulars in ANSWER_REGEX
define('ANSWER_REGEX_NORM', 1);
define('ANSWER_REGEX_ANSWER_TYPE_NUMERICAL', 3);
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE', 4);
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR', 5);
define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL', 6);
define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER', 7);
define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8);
define('ANSWER_REGEX_ALTERNATIVES', 9);
function qtype_multianswer_extract_question($text) {
// $text is an array [text][format][itemid]
$question = new stdClass();
$question->qtype = 'multianswer';
$question->questiontext = $text;
$question->generalfeedback['text'] = '';
$question->generalfeedback['format'] = FORMAT_HTML;
$question->generalfeedback['itemid'] = '';
$question->options->questions = array();
$question->defaultmark = 0; // Will be increased for each answer norm
for ($positionkey = 1;
preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs);
++$positionkey) {
$wrapped = new stdClass();
$wrapped->generalfeedback['text'] = '';
$wrapped->generalfeedback['format'] = FORMAT_HTML;
$wrapped->generalfeedback['itemid'] = '';
if (isset($answerregs[ANSWER_REGEX_NORM])&& $answerregs[ANSWER_REGEX_NORM]!== '') {
$wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM];
} else {
$wrapped->defaultmark = '1';
}
if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
$wrapped->qtype = 'numerical';
$wrapped->multiplier = array();
$wrapped->units = array();
$wrapped->instructions['text'] = '';
$wrapped->instructions['format'] = FORMAT_HTML;
$wrapped->instructions['itemid'] = '';
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
$wrapped->qtype = 'shortanswer';
$wrapped->usecase = 0;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
$wrapped->qtype = 'shortanswer';
$wrapped->usecase = 1;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
$wrapped->qtype = 'multichoice';
$wrapped->single = 1;
$wrapped->shuffleanswers = 1;
$wrapped->answernumbering = 0;
$wrapped->correctfeedback['text'] = '';
$wrapped->correctfeedback['format'] = FORMAT_HTML;
$wrapped->correctfeedback['itemid'] = '';
$wrapped->partiallycorrectfeedback['text'] = '';
$wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML;
$wrapped->partiallycorrectfeedback['itemid'] = '';
$wrapped->incorrectfeedback['text'] = '';
$wrapped->incorrectfeedback['format'] = FORMAT_HTML;
$wrapped->incorrectfeedback['itemid'] = '';
$wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
$wrapped->qtype = 'multichoice';
$wrapped->single = 1;
$wrapped->shuffleanswers = 0;
$wrapped->answernumbering = 0;
$wrapped->correctfeedback['text'] = '';
$wrapped->correctfeedback['format'] = FORMAT_HTML;
$wrapped->correctfeedback['itemid'] = '';
$wrapped->partiallycorrectfeedback['text'] = '';
$wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML;
$wrapped->partiallycorrectfeedback['itemid'] = '';
$wrapped->incorrectfeedback['text'] = '';
$wrapped->incorrectfeedback['format'] = FORMAT_HTML;
$wrapped->incorrectfeedback['itemid'] = '';
$wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
} else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
$wrapped->qtype = 'multichoice';
$wrapped->single = 1;
$wrapped->shuffleanswers = 0;
$wrapped->answernumbering = 0;
$wrapped->correctfeedback['text'] = '';
$wrapped->correctfeedback['format'] = FORMAT_HTML;
$wrapped->correctfeedback['itemid'] = '';
$wrapped->partiallycorrectfeedback['text'] = '';
$wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML;
$wrapped->partiallycorrectfeedback['itemid'] = '';
$wrapped->incorrectfeedback['text'] = '';
$wrapped->incorrectfeedback['format'] = FORMAT_HTML;
$wrapped->incorrectfeedback['itemid'] = '';
$wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
} else {
print_error('unknownquestiontype', 'question', '', $answerregs[2]);
return false;
}
// Each $wrapped simulates a $form that can be processed by the
// respective save_question and save_question_options methods of the
// wrapped questiontypes
$wrapped->answer = array();
$wrapped->fraction = array();
$wrapped->feedback = array();
$wrapped->questiontext['text'] = $answerregs[0];
$wrapped->questiontext['format'] = FORMAT_HTML;
$wrapped->questiontext['itemid'] = '';
$answerindex = 0;
$remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
$wrapped->fraction["$answerindex"] = '1';
} else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) {
$wrapped->fraction["$answerindex"] = .01 * $percentile;
} else {
$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 = str_replace('\}', '}', $feedback);
$wrapped->feedback["$answerindex"]['text'] = str_replace('\#', '#', $feedback);
$wrapped->feedback["$answerindex"]['format'] = FORMAT_HTML;
$wrapped->feedback["$answerindex"]['itemid'] = '';
} else {
$wrapped->feedback["$answerindex"]['text'] = '';
$wrapped->feedback["$answerindex"]['format'] = FORMAT_HTML;
$wrapped->feedback["$answerindex"]['itemid'] = '';
}
if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
&& preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~',
$altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
$wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
if (array_key_exists(NUMERICAL_ABS_ERROR_MARGIN, $numregs)) {
$wrapped->tolerance["$answerindex"] =
$numregs[NUMERICAL_ABS_ERROR_MARGIN];
} else {
$wrapped->tolerance["$answerindex"] = 0;
}
} 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 = str_replace('\}', '}', $answer);
$wrapped->answer["$answerindex"] = str_replace('\#', '#', $answer);
if ($wrapped->qtype == 'multichoice') {
$wrapped->answer["$answerindex"] = array(
'text' => $wrapped->answer["$answerindex"],
'format' => FORMAT_HTML,
'itemid' => '');
}
}
$tmp = explode($altregs[0], $remainingalts, 2);
$remainingalts = $tmp[1];
$answerindex++;
}
$question->defaultmark += $wrapped->defaultmark;
$question->options->questions[$positionkey] = clone($wrapped);
$question->questiontext['text'] = implode("{#$positionkey}",
explode($answerregs[0], $question->questiontext['text'], 2));
}
return $question;
}
Jump to Line
Something went wrong with that request. Please try again.