See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Defines the editing form for the multi-answer question type. 19 * 20 * @package qtype 21 * @subpackage multianswer 22 * @copyright 2007 Jamie Pratt me@jamiep.org 23 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 require_once($CFG->dirroot . '/question/type/numerical/questiontype.php'); 30 31 32 /** 33 * Form for editing multi-answer questions. 34 * 35 * @copyright 2007 Jamie Pratt me@jamiep.org 36 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License 37 */ 38 class qtype_multianswer_edit_form extends question_edit_form { 39 40 // The variable $questiondisplay will contain the qtype_multianswer_extract_question from 41 // the questiontext. 42 public $questiondisplay; 43 // The variable $savedquestiondisplay will contain the qtype_multianswer_extract_question 44 // from the questiontext in database. 45 public $savedquestion; 46 public $savedquestiondisplay; 47 /** @var bool this question is used in quiz */ 48 public $usedinquiz = false; 49 /** @var bool the qtype has been changed */ 50 public $qtypechange = false; 51 /** @var integer number of questions that have been deleted */ 52 public $negativediff = 0; 53 /** @var integer number of quiz that used this question */ 54 public $nbofquiz = 0; 55 /** @var integer number of attempts that used this question */ 56 public $nbofattempts = 0; 57 public $confirm = 0; 58 public $reload = false; 59 /** @var qtype_numerical_answer_processor used when validating numerical answers. */ 60 protected $ap = null; 61 62 63 public function __construct($submiturl, $question, $category, $contexts, $formeditable = true) { 64 $this->regenerate = true; 65 $this->reload = optional_param('reload', false, PARAM_BOOL); 66 67 $this->usedinquiz = false; 68 69 if (isset($question->id) && $question->id != 0) { 70 // TODO MDL-43779 should not have quiz-specific code here. 71 $this->savedquestiondisplay = fullclone($question); 72 $questiondata = question_bank::load_question($question->id); 73 $this->nbofquiz = \qbank_usage\helper::get_question_entry_usage_count($questiondata); 74 $this->usedinquiz = $this->nbofquiz > 0; 75 $this->nbofattempts = \qbank_usage\helper::get_question_attempts_count_in_quiz((int)$question->id); 76 } 77 78 parent::__construct($submiturl, $question, $category, $contexts, $formeditable); 79 } 80 81 protected function definition_inner($mform) { 82 $mform->addElement('hidden', 'reload', 1); 83 $mform->setType('reload', PARAM_INT); 84 85 // Remove meaningless defaultmark field. 86 $mform->removeElement('defaultmark'); 87 $this->confirm = optional_param('confirm', false, PARAM_BOOL); 88 89 // Display the questions from questiontext. 90 if ($questiontext = optional_param_array('questiontext', false, PARAM_RAW)) { 91 $this->questiondisplay = fullclone(qtype_multianswer_extract_question($questiontext)); 92 93 } else { 94 if (!$this->reload && !empty($this->savedquestiondisplay->id)) { 95 // Use database data as this is first pass 96 // question->id == 0 so no stored datasets. 97 $this->questiondisplay = fullclone($this->savedquestiondisplay); 98 foreach ($this->questiondisplay->options->questions as $subquestion) { 99 if (!empty($subquestion)) { 100 $subquestion->answer = array(''); 101 foreach ($subquestion->options->answers as $ans) { 102 $subquestion->answer[] = $ans->answer; 103 } 104 } 105 } 106 } else { 107 $this->questiondisplay = ""; 108 } 109 } 110 111 if (isset($this->savedquestiondisplay->options->questions) && 112 is_array($this->savedquestiondisplay->options->questions)) { 113 $countsavedsubquestions = 0; 114 foreach ($this->savedquestiondisplay->options->questions as $subquestion) { 115 if (!empty($subquestion)) { 116 $countsavedsubquestions++; 117 } 118 } 119 } else { 120 $countsavedsubquestions = 0; 121 } 122 if ($this->reload) { 123 if (isset($this->questiondisplay->options->questions) && 124 is_array($this->questiondisplay->options->questions)) { 125 $countsubquestions = 0; 126 foreach ($this->questiondisplay->options->questions as $subquestion) { 127 if (!empty($subquestion)) { 128 $countsubquestions++; 129 } 130 } 131 } else { 132 $countsubquestions = 0; 133 } 134 } else { 135 $countsubquestions = $countsavedsubquestions; 136 } 137 138 $mform->addElement('submit', 'analyzequestion', 139 get_string('decodeverifyquestiontext', 'qtype_multianswer')); 140 $mform->registerNoSubmitButton('analyzequestion'); 141 if ($this->reload) { 142 for ($sub = 1; $sub <= $countsubquestions; $sub++) { 143 144 if (isset($this->questiondisplay->options->questions[$sub]->qtype)) { 145 $this->editas[$sub] = $this->questiondisplay->options->questions[$sub]->qtype; 146 } else { 147 $this->editas[$sub] = optional_param('sub_'.$sub.'_qtype', 'unknown type', PARAM_PLUGIN); 148 } 149 150 $storemess = ''; 151 if (isset($this->savedquestiondisplay->options->questions[$sub]->qtype) && 152 $this->savedquestiondisplay->options->questions[$sub]->qtype != 153 $this->questiondisplay->options->questions[$sub]->qtype && 154 $this->savedquestiondisplay->options->questions[$sub]->qtype != 'subquestion_replacement') { 155 $this->qtypechange = true; 156 $storemess = ' ' . html_writer::tag('span', get_string( 157 'storedqtype', 'qtype_multianswer', question_bank::get_qtype_name( 158 $this->savedquestiondisplay->options->questions[$sub]->qtype)), 159 array('class' => 'error')); 160 } 161 $mform->addElement('header', 'subhdr'.$sub, get_string('questionno', 'question', 162 '{#'.$sub.'}').' '.question_bank::get_qtype_name( 163 $this->questiondisplay->options->questions[$sub]->qtype).$storemess); 164 165 $mform->addElement('static', 'sub_'.$sub.'_questiontext', 166 get_string('questiondefinition', 'qtype_multianswer')); 167 168 if (isset ($this->questiondisplay->options->questions[$sub]->questiontext)) { 169 $mform->setDefault('sub_'.$sub.'_questiontext', 170 $this->questiondisplay->options->questions[$sub]->questiontext['text']); 171 } 172 173 $mform->addElement('static', 'sub_'.$sub.'_defaultmark', 174 get_string('defaultmark', 'question')); 175 $mform->setDefault('sub_'.$sub.'_defaultmark', 176 $this->questiondisplay->options->questions[$sub]->defaultmark); 177 178 if ($this->questiondisplay->options->questions[$sub]->qtype == 'shortanswer') { 179 $mform->addElement('static', 'sub_'.$sub.'_usecase', 180 get_string('casesensitive', 'qtype_shortanswer')); 181 } 182 183 if ($this->questiondisplay->options->questions[$sub]->qtype == 'multichoice') { 184 $mform->addElement('static', 'sub_'.$sub.'_layout', 185 get_string('layout', 'qtype_multianswer')); 186 $mform->addElement('static', 'sub_'.$sub.'_shuffleanswers', 187 get_string('shuffleanswers', 'qtype_multichoice')); 188 } 189 190 foreach ($this->questiondisplay->options->questions[$sub]->answer as $key => $ans) { 191 $mform->addElement('static', 'sub_'.$sub.'_answer['.$key.']', 192 get_string('answer', 'question')); 193 194 if ($this->questiondisplay->options->questions[$sub]->qtype == 'numerical' && 195 $key == 0) { 196 $mform->addElement('static', 'sub_'.$sub.'_tolerance['.$key.']', 197 get_string('acceptederror', 'qtype_numerical')); 198 } 199 200 $mform->addElement('static', 'sub_'.$sub.'_fraction['.$key.']', 201 get_string('gradenoun')); 202 203 $mform->addElement('static', 'sub_'.$sub.'_feedback['.$key.']', 204 get_string('feedback', 'question')); 205 } 206 } 207 208 $this->negativediff = $countsavedsubquestions - $countsubquestions; 209 if (($this->negativediff > 0) ||$this->qtypechange || 210 ($this->usedinquiz && $this->negativediff != 0)) { 211 $mform->addElement('header', 'additemhdr', 212 get_string('warningquestionmodified', 'qtype_multianswer')); 213 } 214 if ($this->negativediff > 0) { 215 $mform->addElement('static', 'alert1', "<strong>". 216 get_string('questiondeleted', 'qtype_multianswer')."</strong>", 217 get_string('questionsless', 'qtype_multianswer', $this->negativediff)); 218 } 219 if ($this->qtypechange) { 220 $mform->addElement('static', 'alert1', "<strong>". 221 get_string('questiontypechanged', 'qtype_multianswer')."</strong>", 222 get_string('questiontypechangedcomment', 'qtype_multianswer')); 223 } 224 } 225 if ($this->usedinquiz) { 226 if ($this->negativediff < 0) { 227 $diff = $countsubquestions - $countsavedsubquestions; 228 $mform->addElement('static', 'alert1', "<strong>". 229 get_string('questionsadded', 'qtype_multianswer')."</strong>", 230 "<strong>".get_string('questionsmore', 'qtype_multianswer', $diff). 231 "</strong>"); 232 } 233 $a = new stdClass(); 234 $a->nb_of_quiz = $this->nbofquiz; 235 $a->nb_of_attempts = $this->nbofattempts; 236 $mform->addElement('header', 'additemhdr2', 237 get_string('questionusedinquiz', 'qtype_multianswer', $a)); 238 $mform->addElement('static', 'alertas', 239 get_string('youshouldnot', 'qtype_multianswer')); 240 } 241 if (($this->negativediff > 0 || $this->usedinquiz && 242 ($this->negativediff > 0 || $this->negativediff < 0 || $this->qtypechange)) && 243 $this->reload) { 244 $mform->addElement('header', 'additemhdr', 245 get_string('questionsaveasedited', 'qtype_multianswer')); 246 $mform->addElement('checkbox', 'confirm', '', 247 get_string('confirmquestionsaveasedited', 'qtype_multianswer')); 248 $mform->setDefault('confirm', 0); 249 } else { 250 $mform->addElement('hidden', 'confirm', 0); 251 $mform->setType('confirm', PARAM_BOOL); 252 } 253 254 $this->add_interactive_settings(true, true); 255 } 256 257 258 public function set_data($question) { 259 global $DB; 260 $defaultvalues = array(); 261 if (isset($question->id) and $question->id and $question->qtype && 262 $question->questiontext) { 263 264 foreach ($question->options->questions as $key => $wrapped) { 265 if (!empty($wrapped)) { 266 // The old way of restoring the definitions is kept to gradually 267 // update all multianswer questions. 268 if (empty($wrapped->questiontext)) { 269 $parsableanswerdef = '{' . $wrapped->defaultmark . ':'; 270 switch ($wrapped->qtype) { 271 case 'multichoice': 272 $parsableanswerdef .= 'MULTICHOICE:'; 273 break; 274 case 'shortanswer': 275 $parsableanswerdef .= 'SHORTANSWER:'; 276 break; 277 case 'numerical': 278 $parsableanswerdef .= 'NUMERICAL:'; 279 break; 280 case 'subquestion_replacement': 281 continue 2; 282 default: 283 throw new \moodle_exception('unknownquestiontype', 'question', '', 284 $wrapped->qtype); 285 } 286 $separator = ''; 287 foreach ($wrapped->options->answers as $subanswer) { 288 $parsableanswerdef .= $separator 289 . '%' . round(100 * $subanswer->fraction) . '%'; 290 if (is_array($subanswer->answer)) { 291 $parsableanswerdef .= $subanswer->answer['text']; 292 } else { 293 $parsableanswerdef .= $subanswer->answer; 294 } 295 if (!empty($wrapped->options->tolerance)) { 296 // Special for numerical answers. 297 $parsableanswerdef .= ":{$wrapped->options->tolerance}"; 298 // We only want tolerance for the first alternative, it will 299 // be applied to all of the alternatives. 300 unset($wrapped->options->tolerance); 301 } 302 if ($subanswer->feedback) { 303 $parsableanswerdef .= "#{$subanswer->feedback}"; 304 } 305 $separator = '~'; 306 } 307 $parsableanswerdef .= '}'; 308 // Fix the questiontext fields of old questions. 309 $DB->set_field('question', 'questiontext', $parsableanswerdef, 310 array('id' => $wrapped->id)); 311 } else { 312 $parsableanswerdef = str_replace('&#', '&\#', $wrapped->questiontext); 313 } 314 $question->questiontext = str_replace("{#$key}", $parsableanswerdef, 315 $question->questiontext); 316 } 317 } 318 } 319 320 // Set default to $questiondisplay questions elements. 321 if ($this->reload) { 322 if (isset($this->questiondisplay->options->questions)) { 323 $subquestions = fullclone($this->questiondisplay->options->questions); 324 if (count($subquestions)) { 325 $sub = 1; 326 foreach ($subquestions as $subquestion) { 327 $prefix = 'sub_'.$sub.'_'; 328 329 // Validate parameters. 330 $answercount = 0; 331 $maxgrade = false; 332 $maxfraction = -1; 333 if ($subquestion->qtype == 'shortanswer') { 334 switch ($subquestion->usecase) { 335 case '1': 336 $defaultvalues[$prefix.'usecase'] = 337 get_string('caseyes', 'qtype_shortanswer'); 338 break; 339 case '0': 340 default : 341 $defaultvalues[$prefix.'usecase'] = 342 get_string('caseno', 'qtype_shortanswer'); 343 } 344 } 345 346 if ($subquestion->qtype == 'multichoice') { 347 $defaultvalues[$prefix.'layout'] = $subquestion->layout; 348 if ($subquestion->single == 1) { 349 switch ($subquestion->layout) { 350 case '0': 351 $defaultvalues[$prefix.'layout'] = 352 get_string('layoutselectinline', 'qtype_multianswer'); 353 break; 354 case '1': 355 $defaultvalues[$prefix.'layout'] = 356 get_string('layoutvertical', 'qtype_multianswer'); 357 break; 358 case '2': 359 $defaultvalues[$prefix.'layout'] = 360 get_string('layouthorizontal', 'qtype_multianswer'); 361 break; 362 default: 363 $defaultvalues[$prefix.'layout'] = 364 get_string('layoutundefined', 'qtype_multianswer'); 365 } 366 } else { 367 switch ($subquestion->layout) { 368 case '1': 369 $defaultvalues[$prefix.'layout'] = 370 get_string('layoutmultiple_vertical', 'qtype_multianswer'); 371 break; 372 case '2': 373 $defaultvalues[$prefix.'layout'] = 374 get_string('layoutmultiple_horizontal', 'qtype_multianswer'); 375 break; 376 default: 377 $defaultvalues[$prefix.'layout'] = 378 get_string('layoutundefined', 'qtype_multianswer'); 379 } 380 } 381 if ($subquestion->shuffleanswers ) { 382 $defaultvalues[$prefix.'shuffleanswers'] = get_string('yes', 'moodle'); 383 } else { 384 $defaultvalues[$prefix.'shuffleanswers'] = get_string('no', 'moodle'); 385 } 386 } 387 foreach ($subquestion->answer as $key => $answer) { 388 if ($subquestion->qtype == 'numerical' && $key == 0) { 389 $defaultvalues[$prefix.'tolerance['.$key.']'] = 390 $subquestion->tolerance[0]; 391 } 392 if (is_array($answer)) { 393 $answer = $answer['text']; 394 } 395 $trimmedanswer = trim($answer); 396 if ($trimmedanswer !== '') { 397 $answercount++; 398 if ($subquestion->qtype == 'numerical' && 399 !(qtype_numerical::is_valid_number($trimmedanswer) || $trimmedanswer == '*')) { 400 $this->_form->setElementError($prefix.'answer['.$key.']', 401 get_string('answermustbenumberorstar', 402 'qtype_numerical')); 403 } 404 if ($subquestion->fraction[$key] == 1) { 405 $maxgrade = true; 406 } 407 if ($subquestion->fraction[$key] > $maxfraction) { 408 $maxfraction = $subquestion->fraction[$key]; 409 } 410 // For 'multiresponse' we are OK if there is at least one fraction > 0. 411 if ($subquestion->qtype == 'multichoice' && $subquestion->single == 0 && 412 $subquestion->fraction[$key] > 0) { 413 $maxgrade = true; 414 } 415 } 416 417 $defaultvalues[$prefix.'answer['.$key.']'] = 418 htmlspecialchars($answer, ENT_COMPAT); 419 } 420 if ($answercount == 0) { 421 if ($subquestion->qtype == 'multichoice') { 422 $this->_form->setElementError($prefix.'answer[0]', 423 get_string('notenoughanswers', 'qtype_multichoice', 2)); 424 } else { 425 $this->_form->setElementError($prefix.'answer[0]', 426 get_string('notenoughanswers', 'question', 1)); 427 } 428 } 429 if ($maxgrade == false) { 430 $this->_form->setElementError($prefix.'fraction[0]', 431 get_string('fractionsnomax', 'question')); 432 } 433 foreach ($subquestion->feedback as $key => $answer) { 434 435 $defaultvalues[$prefix.'feedback['.$key.']'] = 436 htmlspecialchars ($answer['text'], ENT_COMPAT); 437 } 438 foreach ($subquestion->fraction as $key => $answer) { 439 $defaultvalues[$prefix.'fraction['.$key.']'] = $answer; 440 } 441 442 $sub++; 443 } 444 } 445 } 446 } 447 $defaultvalues['alertas'] = "<strong>".get_string('questioninquiz', 'qtype_multianswer'). 448 "</strong>"; 449 450 if ($defaultvalues != "") { 451 $question = (object)((array)$question + $defaultvalues); 452 } 453 $question = $this->data_preprocessing_hints($question, true, true); 454 parent::set_data($question); 455 } 456 457 public function validation($data, $files) { 458 $errors = parent::validation($data, $files); 459 460 $questiondisplay = qtype_multianswer_extract_question($data['questiontext']); 461 462 $errors = array_merge($errors, qtype_multianswer_validate_question($questiondisplay)); 463 464 if (($this->negativediff > 0 || $this->usedinquiz && 465 ($this->negativediff > 0 || $this->negativediff < 0 || 466 $this->qtypechange)) && !$this->confirm) { 467 $errors['confirm'] = 468 get_string('confirmsave', 'qtype_multianswer', $this->negativediff); 469 } 470 471 return $errors; 472 } 473 474 public function qtype() { 475 return 'multianswer'; 476 } 477 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body