Moodle  2.2.1
http://www.collinsharper.com
C:/xampp/htdocs/moodle/question/type/multianswer/questiontype.php
Go to the documentation of this file.
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         '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
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 }
 All Data Structures Namespaces Files Functions Variables Enumerations