|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00027 defined('MOODLE_INTERNAL') || die(); 00028 00029 require_once($CFG->dirroot . '/question/type/multichoice/question.php'); 00030 00031 00038 class qtype_multianswer extends question_type { 00039 00040 public function can_analyse_responses() { 00041 return false; 00042 } 00043 00044 public function get_question_options($question) { 00045 global $DB, $OUTPUT; 00046 00047 // Get relevant data indexed by positionkey from the multianswers table 00048 $sequence = $DB->get_field('question_multianswer', 'sequence', 00049 array('question' => $question->id), '*', MUST_EXIST); 00050 00051 $wrappedquestions = $DB->get_records_list('question', 'id', 00052 explode(',', $sequence), 'id ASC'); 00053 00054 // We want an array with question ids as index and the positions as values 00055 $sequence = array_flip(explode(',', $sequence)); 00056 array_walk($sequence, create_function('&$val', '$val++;')); 00057 00058 // If a question is lost, the corresponding index is null 00059 // so this null convention is used to test $question->options->questions 00060 // before using the values. 00061 // first all possible questions from sequence are nulled 00062 // then filled with the data if available in $wrappedquestions 00063 foreach ($sequence as $seq) { 00064 $question->options->questions[$seq] = ''; 00065 } 00066 00067 foreach ($wrappedquestions as $wrapped) { 00068 question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped); 00069 // for wrapped questions the maxgrade is always equal to the defaultmark, 00070 // there is no entry in the question_instances table for them 00071 $wrapped->maxmark = $wrapped->defaultmark; 00072 $question->options->questions[$sequence[$wrapped->id]] = $wrapped; 00073 } 00074 00075 $question->hints = $DB->get_records('question_hints', 00076 array('questionid' => $question->id), 'id ASC'); 00077 00078 return true; 00079 } 00080 00081 public function save_question_options($question) { 00082 global $DB; 00083 $result = new stdClass(); 00084 00085 // This function needs to be able to handle the case where the existing set of wrapped 00086 // questions does not match the new set of wrapped questions so that some need to be 00087 // created, some modified and some deleted 00088 // Unfortunately the code currently simply overwrites existing ones in sequence. This 00089 // will make re-marking after a re-ordering of wrapped questions impossible and 00090 // will also create difficulties if questiontype specific tables reference the id. 00091 00092 // First we get all the existing wrapped questions 00093 if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence', 00094 array('question' => $question->id))) { 00095 $oldwrappedquestions = array(); 00096 } else { 00097 $oldwrappedquestions = $DB->get_records_list('question', 'id', 00098 explode(',', $oldwrappedids), 'id ASC'); 00099 } 00100 00101 $sequence = array(); 00102 foreach ($question->options->questions as $wrapped) { 00103 if (!empty($wrapped)) { 00104 // if we still have some old wrapped question ids, reuse the next of them 00105 00106 if (is_array($oldwrappedquestions) && 00107 $oldwrappedquestion = array_shift($oldwrappedquestions)) { 00108 $wrapped->id = $oldwrappedquestion->id; 00109 if ($oldwrappedquestion->qtype != $wrapped->qtype) { 00110 switch ($oldwrappedquestion->qtype) { 00111 case 'multichoice': 00112 $DB->delete_records('question_multichoice', 00113 array('question' => $oldwrappedquestion->id)); 00114 break; 00115 case 'shortanswer': 00116 $DB->delete_records('question_shortanswer', 00117 array('question' => $oldwrappedquestion->id)); 00118 break; 00119 case 'numerical': 00120 $DB->delete_records('question_numerical', 00121 array('question' => $oldwrappedquestion->id)); 00122 break; 00123 default: 00124 throw new moodle_exception('qtypenotrecognized', 00125 'qtype_multianswer', '', $oldwrappedquestion->qtype); 00126 $wrapped->id = 0; 00127 } 00128 } 00129 } else { 00130 $wrapped->id = 0; 00131 } 00132 } 00133 $wrapped->name = $question->name; 00134 $wrapped->parent = $question->id; 00135 $previousid = $wrapped->id; 00136 // save_question strips this extra bit off the category again. 00137 $wrapped->category = $question->category . ',1'; 00138 $wrapped = question_bank::get_qtype($wrapped->qtype)->save_question( 00139 $wrapped, clone($wrapped)); 00140 $sequence[] = $wrapped->id; 00141 if ($previousid != 0 && $previousid != $wrapped->id) { 00142 // for some reasons a new question has been created 00143 // so delete the old one 00144 question_delete_question($previousid); 00145 } 00146 } 00147 00148 // Delete redundant wrapped questions 00149 if (is_array($oldwrappedquestions) && count($oldwrappedquestions)) { 00150 foreach ($oldwrappedquestions as $oldwrappedquestion) { 00151 question_delete_question($oldwrappedquestion->id); 00152 } 00153 } 00154 00155 if (!empty($sequence)) { 00156 $multianswer = new stdClass(); 00157 $multianswer->question = $question->id; 00158 $multianswer->sequence = implode(',', $sequence); 00159 if ($oldid = $DB->get_field('question_multianswer', 'id', 00160 array('question' => $question->id))) { 00161 $multianswer->id = $oldid; 00162 $DB->update_record('question_multianswer', $multianswer); 00163 } else { 00164 $DB->insert_record('question_multianswer', $multianswer); 00165 } 00166 } 00167 00168 $this->save_hints($question); 00169 } 00170 00171 public function save_question($authorizedquestion, $form) { 00172 $question = qtype_multianswer_extract_question($form->questiontext); 00173 if (isset($authorizedquestion->id)) { 00174 $question->id = $authorizedquestion->id; 00175 } 00176 00177 $question->category = $authorizedquestion->category; 00178 $form->defaultmark = $question->defaultmark; 00179 $form->questiontext = $question->questiontext; 00180 $form->questiontextformat = 0; 00181 $form->options = clone($question->options); 00182 unset($question->options); 00183 return parent::save_question($question, $form); 00184 } 00185 00186 public function delete_question($questionid, $contextid) { 00187 global $DB; 00188 $DB->delete_records('question_multianswer', array('question' => $questionid)); 00189 00190 parent::delete_question($questionid, $contextid); 00191 } 00192 00193 protected function initialise_question_instance($question, $questiondata) { 00194 parent::initialise_question_instance($question, $questiondata); 00195 00196 $bits = preg_split('/\{#(\d+)\}/', $question->questiontext, 00197 null, PREG_SPLIT_DELIM_CAPTURE); 00198 $question->textfragments[0] = array_shift($bits); 00199 $i = 1; 00200 while (!empty($bits)) { 00201 $question->places[$i] = array_shift($bits); 00202 $question->textfragments[$i] = array_shift($bits); 00203 $i += 1; 00204 } 00205 00206 foreach ($questiondata->options->questions as $key => $subqdata) { 00207 $subqdata->contextid = $questiondata->contextid; 00208 $question->subquestions[$key] = question_bank::make_question($subqdata); 00209 $question->subquestions[$key]->maxmark = $subqdata->defaultmark; 00210 if (isset($subqdata->options->layout)) { 00211 $question->subquestions[$key]->layout = $subqdata->options->layout; 00212 } 00213 } 00214 } 00215 00216 public function get_random_guess_score($questiondata) { 00217 $fractionsum = 0; 00218 $fractionmax = 0; 00219 foreach ($questiondata->options->questions as $key => $subqdata) { 00220 $fractionmax += $subqdata->defaultmark; 00221 $fractionsum += question_bank::get_qtype( 00222 $subqdata->qtype)->get_random_guess_score($subqdata); 00223 } 00224 return $fractionsum / $fractionmax; 00225 } 00226 } 00227 00228 00229 // ANSWER_ALTERNATIVE regexes 00230 define('ANSWER_ALTERNATIVE_FRACTION_REGEX', 00231 '=|%(-?[0-9]+)%'); 00232 // for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C 00233 define('ANSWER_ALTERNATIVE_ANSWER_REGEX', 00234 '.+?(?<!\\\\|&|&)(?=[~#}]|$)'); 00235 define('ANSWER_ALTERNATIVE_FEEDBACK_REGEX', 00236 '.*?(?<!\\\\)(?=[~}]|$)'); 00237 define('ANSWER_ALTERNATIVE_REGEX', 00238 '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' . 00239 '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' . 00240 '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?'); 00241 00242 // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX 00243 define('ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION', 2); 00244 define('ANSWER_ALTERNATIVE_REGEX_FRACTION', 1); 00245 define('ANSWER_ALTERNATIVE_REGEX_ANSWER', 3); 00246 define('ANSWER_ALTERNATIVE_REGEX_FEEDBACK', 5); 00247 00248 // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used 00249 // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER 00250 define('NUMBER_REGEX', 00251 '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)'); 00252 define('NUMERICAL_ALTERNATIVE_REGEX', 00253 '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$'); 00254 00255 // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX 00256 define('NUMERICAL_CORRECT_ANSWER', 1); 00257 define('NUMERICAL_ABS_ERROR_MARGIN', 6); 00258 00259 // Remaining ANSWER regexes 00260 define('ANSWER_TYPE_DEF_REGEX', 00261 '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' . 00262 '(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)'); 00263 define('ANSWER_START_REGEX', 00264 '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):'); 00265 00266 define('ANSWER_REGEX', 00267 ANSWER_START_REGEX 00268 . '(' . ANSWER_ALTERNATIVE_REGEX 00269 . '(~' 00270 . ANSWER_ALTERNATIVE_REGEX 00271 . ')*)\}'); 00272 00273 // Parenthesis positions for singulars in ANSWER_REGEX 00274 define('ANSWER_REGEX_NORM', 1); 00275 define('ANSWER_REGEX_ANSWER_TYPE_NUMERICAL', 3); 00276 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE', 4); 00277 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR', 5); 00278 define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL', 6); 00279 define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER', 7); 00280 define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8); 00281 define('ANSWER_REGEX_ALTERNATIVES', 9); 00282 00283 function qtype_multianswer_extract_question($text) { 00284 // $text is an array [text][format][itemid] 00285 $question = new stdClass(); 00286 $question->qtype = 'multianswer'; 00287 $question->questiontext = $text; 00288 $question->generalfeedback['text'] = ''; 00289 $question->generalfeedback['format'] = FORMAT_HTML; 00290 $question->generalfeedback['itemid'] = ''; 00291 00292 $question->options->questions = array(); 00293 $question->defaultmark = 0; // Will be increased for each answer norm 00294 00295 for ($positionkey = 1; 00296 preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs); 00297 ++$positionkey) { 00298 $wrapped = new stdClass(); 00299 $wrapped->generalfeedback['text'] = ''; 00300 $wrapped->generalfeedback['format'] = FORMAT_HTML; 00301 $wrapped->generalfeedback['itemid'] = ''; 00302 if (isset($answerregs[ANSWER_REGEX_NORM])&& $answerregs[ANSWER_REGEX_NORM]!== '') { 00303 $wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM]; 00304 } else { 00305 $wrapped->defaultmark = '1'; 00306 } 00307 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) { 00308 $wrapped->qtype = 'numerical'; 00309 $wrapped->multiplier = array(); 00310 $wrapped->units = array(); 00311 $wrapped->instructions['text'] = ''; 00312 $wrapped->instructions['format'] = FORMAT_HTML; 00313 $wrapped->instructions['itemid'] = ''; 00314 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) { 00315 $wrapped->qtype = 'shortanswer'; 00316 $wrapped->usecase = 0; 00317 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) { 00318 $wrapped->qtype = 'shortanswer'; 00319 $wrapped->usecase = 1; 00320 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) { 00321 $wrapped->qtype = 'multichoice'; 00322 $wrapped->single = 1; 00323 $wrapped->shuffleanswers = 1; 00324 $wrapped->answernumbering = 0; 00325 $wrapped->correctfeedback['text'] = ''; 00326 $wrapped->correctfeedback['format'] = FORMAT_HTML; 00327 $wrapped->correctfeedback['itemid'] = ''; 00328 $wrapped->partiallycorrectfeedback['text'] = ''; 00329 $wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML; 00330 $wrapped->partiallycorrectfeedback['itemid'] = ''; 00331 $wrapped->incorrectfeedback['text'] = ''; 00332 $wrapped->incorrectfeedback['format'] = FORMAT_HTML; 00333 $wrapped->incorrectfeedback['itemid'] = ''; 00334 $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN; 00335 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) { 00336 $wrapped->qtype = 'multichoice'; 00337 $wrapped->single = 1; 00338 $wrapped->shuffleanswers = 0; 00339 $wrapped->answernumbering = 0; 00340 $wrapped->correctfeedback['text'] = ''; 00341 $wrapped->correctfeedback['format'] = FORMAT_HTML; 00342 $wrapped->correctfeedback['itemid'] = ''; 00343 $wrapped->partiallycorrectfeedback['text'] = ''; 00344 $wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML; 00345 $wrapped->partiallycorrectfeedback['itemid'] = ''; 00346 $wrapped->incorrectfeedback['text'] = ''; 00347 $wrapped->incorrectfeedback['format'] = FORMAT_HTML; 00348 $wrapped->incorrectfeedback['itemid'] = ''; 00349 $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL; 00350 } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) { 00351 $wrapped->qtype = 'multichoice'; 00352 $wrapped->single = 1; 00353 $wrapped->shuffleanswers = 0; 00354 $wrapped->answernumbering = 0; 00355 $wrapped->correctfeedback['text'] = ''; 00356 $wrapped->correctfeedback['format'] = FORMAT_HTML; 00357 $wrapped->correctfeedback['itemid'] = ''; 00358 $wrapped->partiallycorrectfeedback['text'] = ''; 00359 $wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML; 00360 $wrapped->partiallycorrectfeedback['itemid'] = ''; 00361 $wrapped->incorrectfeedback['text'] = ''; 00362 $wrapped->incorrectfeedback['format'] = FORMAT_HTML; 00363 $wrapped->incorrectfeedback['itemid'] = ''; 00364 $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL; 00365 } else { 00366 print_error('unknownquestiontype', 'question', '', $answerregs[2]); 00367 return false; 00368 } 00369 00370 // Each $wrapped simulates a $form that can be processed by the 00371 // respective save_question and save_question_options methods of the 00372 // wrapped questiontypes 00373 $wrapped->answer = array(); 00374 $wrapped->fraction = array(); 00375 $wrapped->feedback = array(); 00376 $wrapped->questiontext['text'] = $answerregs[0]; 00377 $wrapped->questiontext['format'] = FORMAT_HTML; 00378 $wrapped->questiontext['itemid'] = ''; 00379 $answerindex = 0; 00380 00381 $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES]; 00382 while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) { 00383 if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) { 00384 $wrapped->fraction["$answerindex"] = '1'; 00385 } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) { 00386 $wrapped->fraction["$answerindex"] = .01 * $percentile; 00387 } else { 00388 $wrapped->fraction["$answerindex"] = '0'; 00389 } 00390 if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) { 00391 $feedback = html_entity_decode( 00392 $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8'); 00393 $feedback = str_replace('\}', '}', $feedback); 00394 $wrapped->feedback["$answerindex"]['text'] = str_replace('\#', '#', $feedback); 00395 $wrapped->feedback["$answerindex"]['format'] = FORMAT_HTML; 00396 $wrapped->feedback["$answerindex"]['itemid'] = ''; 00397 } else { 00398 $wrapped->feedback["$answerindex"]['text'] = ''; 00399 $wrapped->feedback["$answerindex"]['format'] = FORMAT_HTML; 00400 $wrapped->feedback["$answerindex"]['itemid'] = ''; 00401 00402 } 00403 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL]) 00404 && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~', 00405 $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) { 00406 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER]; 00407 if (array_key_exists(NUMERICAL_ABS_ERROR_MARGIN, $numregs)) { 00408 $wrapped->tolerance["$answerindex"] = 00409 $numregs[NUMERICAL_ABS_ERROR_MARGIN]; 00410 } else { 00411 $wrapped->tolerance["$answerindex"] = 0; 00412 } 00413 } else { // Tolerance can stay undefined for non numerical questions 00414 // Undo quoting done by the HTML editor. 00415 $answer = html_entity_decode( 00416 $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8'); 00417 $answer = str_replace('\}', '}', $answer); 00418 $wrapped->answer["$answerindex"] = str_replace('\#', '#', $answer); 00419 if ($wrapped->qtype == 'multichoice') { 00420 $wrapped->answer["$answerindex"] = array( 00421 'text' => $wrapped->answer["$answerindex"], 00422 'format' => FORMAT_HTML, 00423 'itemid' => ''); 00424 } 00425 } 00426 $tmp = explode($altregs[0], $remainingalts, 2); 00427 $remainingalts = $tmp[1]; 00428 $answerindex++; 00429 } 00430 00431 $question->defaultmark += $wrapped->defaultmark; 00432 $question->options->questions[$positionkey] = clone($wrapped); 00433 $question->questiontext['text'] = implode("{#$positionkey}", 00434 explode($answerregs[0], $question->questiontext['text'], 2)); 00435 } 00436 return $question; 00437 }