Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 global $SESSION, $CFG, $DB; 65 $this->regenerate = true; 66 $this->reload = optional_param('reload', false, PARAM_BOOL); 67 68 $this->usedinquiz = false; 69 70 if (isset($question->id) && $question->id != 0) { 71 // TODO MDL-43779 should not have quiz-specific code here. 72 $this->savedquestiondisplay = fullclone($question); 73 $this->nbofquiz = $DB->count_records('quiz_slots', array('questionid' => $question->id)); 74 $this->usedinquiz = $this->nbofquiz > 0; 75 $this->nbofattempts = $DB->count_records_sql(" 76 SELECT count(1) 77 FROM {quiz_slots} slot 78 JOIN {quiz_attempts} quiza ON quiza.quiz = slot.quizid 79 WHERE slot.questionid = ? 80 AND quiza.preview = 0", array($question->id)); 81 } 82 83 parent::__construct($submiturl, $question, $category, $contexts, $formeditable); 84 } 85 86 protected function definition_inner($mform) { 87 $mform->addElement('hidden', 'reload', 1); 88 $mform->setType('reload', PARAM_INT); 89 90 // Remove meaningless defaultmark field. 91 $mform->removeElement('defaultmark'); 92 $this->confirm = optional_param('confirm', false, PARAM_BOOL); 93 94 // Display the questions from questiontext. 95 if ($questiontext = optional_param_array('questiontext', false, PARAM_RAW)) { 96 $this->questiondisplay = fullclone(qtype_multianswer_extract_question($questiontext)); 97 98 } else { 99 if (!$this->reload && !empty($this->savedquestiondisplay->id)) { 100 // Use database data as this is first pass 101 // question->id == 0 so no stored datasets. 102 $this->questiondisplay = fullclone($this->savedquestiondisplay); 103 foreach ($this->questiondisplay->options->questions as $subquestion) { 104 if (!empty($subquestion)) { 105 $subquestion->answer = array(''); 106 foreach ($subquestion->options->answers as $ans) { 107 $subquestion->answer[] = $ans->answer; 108 } 109 } 110 } 111 } else { 112 $this->questiondisplay = ""; 113 } 114 } 115 116 if (isset($this->savedquestiondisplay->options->questions) && 117 is_array($this->savedquestiondisplay->options->questions)) { 118 $countsavedsubquestions = 0; 119 foreach ($this->savedquestiondisplay->options->questions as $subquestion) { 120 if (!empty($subquestion)) { 121 $countsavedsubquestions++; 122 } 123 } 124 } else { 125 $countsavedsubquestions = 0; 126 } 127 if ($this->reload) { 128 if (isset($this->questiondisplay->options->questions) && 129 is_array($this->questiondisplay->options->questions)) { 130 $countsubquestions = 0; 131 foreach ($this->questiondisplay->options->questions as $subquestion) { 132 if (!empty($subquestion)) { 133 $countsubquestions++; 134 } 135 } 136 } else { 137 $countsubquestions = 0; 138 } 139 } else { 140 $countsubquestions = $countsavedsubquestions; 141 } 142 143 $mform->addElement('submit', 'analyzequestion', 144 get_string('decodeverifyquestiontext', 'qtype_multianswer')); 145 $mform->registerNoSubmitButton('analyzequestion'); 146 if ($this->reload) { 147 for ($sub = 1; $sub <= $countsubquestions; $sub++) { 148 149 if (isset($this->questiondisplay->options->questions[$sub]->qtype)) { 150 $this->editas[$sub] = $this->questiondisplay->options->questions[$sub]->qtype; 151 } else { 152 $this->editas[$sub] = optional_param('sub_'.$sub.'_qtype', 'unknown type', PARAM_PLUGIN); 153 } 154 155 $storemess = ''; 156 if (isset($this->savedquestiondisplay->options->questions[$sub]->qtype) && 157 $this->savedquestiondisplay->options->questions[$sub]->qtype != 158 $this->questiondisplay->options->questions[$sub]->qtype && 159 $this->savedquestiondisplay->options->questions[$sub]->qtype != 'subquestion_replacement') { 160 $this->qtypechange = true; 161 $storemess = ' ' . html_writer::tag('span', get_string( 162 'storedqtype', 'qtype_multianswer', question_bank::get_qtype_name( 163 $this->savedquestiondisplay->options->questions[$sub]->qtype)), 164 array('class' => 'error')); 165 } 166 $mform->addElement('header', 'subhdr'.$sub, get_string('questionno', 'question', 167 '{#'.$sub.'}').' '.question_bank::get_qtype_name( 168 $this->questiondisplay->options->questions[$sub]->qtype).$storemess); 169 170 $mform->addElement('static', 'sub_'.$sub.'_questiontext', 171 get_string('questiondefinition', 'qtype_multianswer')); 172 173 if (isset ($this->questiondisplay->options->questions[$sub]->questiontext)) { 174 $mform->setDefault('sub_'.$sub.'_questiontext', 175 $this->questiondisplay->options->questions[$sub]->questiontext['text']); 176 } 177 178 $mform->addElement('static', 'sub_'.$sub.'_defaultmark', 179 get_string('defaultmark', 'question')); 180 $mform->setDefault('sub_'.$sub.'_defaultmark', 181 $this->questiondisplay->options->questions[$sub]->defaultmark); 182 183 if ($this->questiondisplay->options->questions[$sub]->qtype == 'shortanswer') { 184 $mform->addElement('static', 'sub_'.$sub.'_usecase', 185 get_string('casesensitive', 'qtype_shortanswer')); 186 } 187 188 if ($this->questiondisplay->options->questions[$sub]->qtype == 'multichoice') { 189 $mform->addElement('static', 'sub_'.$sub.'_layout', 190 get_string('layout', 'qtype_multianswer')); 191 $mform->addElement('static', 'sub_'.$sub.'_shuffleanswers', 192 get_string('shuffleanswers', 'qtype_multichoice')); 193 } 194 195 foreach ($this->questiondisplay->options->questions[$sub]->answer as $key => $ans) { 196 $mform->addElement('static', 'sub_'.$sub.'_answer['.$key.']', 197 get_string('answer', 'question')); 198 199 if ($this->questiondisplay->options->questions[$sub]->qtype == 'numerical' && 200 $key == 0) { 201 $mform->addElement('static', 'sub_'.$sub.'_tolerance['.$key.']', 202 get_string('acceptederror', 'qtype_numerical')); 203 } 204 205 $mform->addElement('static', 'sub_'.$sub.'_fraction['.$key.']', 206 get_string('gradenoun')); 207 208 $mform->addElement('static', 'sub_'.$sub.'_feedback['.$key.']', 209 get_string('feedback', 'question')); 210 } 211 } 212 213 $this->negativediff = $countsavedsubquestions - $countsubquestions; 214 if (($this->negativediff > 0) ||$this->qtypechange || 215 ($this->usedinquiz && $this->negativediff != 0)) { 216 $mform->addElement('header', 'additemhdr', 217 get_string('warningquestionmodified', 'qtype_multianswer')); 218 } 219 if ($this->negativediff > 0) { 220 $mform->addElement('static', 'alert1', "<strong>". 221 get_string('questiondeleted', 'qtype_multianswer')."</strong>", 222 get_string('questionsless', 'qtype_multianswer', $this->negativediff)); 223 } 224 if ($this->qtypechange) { 225 $mform->addElement('static', 'alert1', "<strong>". 226 get_string('questiontypechanged', 'qtype_multianswer')."</strong>", 227 get_string('questiontypechangedcomment', 'qtype_multianswer')); 228 } 229 } 230 if ($this->usedinquiz) { 231 if ($this->negativediff < 0) { 232 $diff = $countsubquestions - $countsavedsubquestions; 233 $mform->addElement('static', 'alert1', "<strong>". 234 get_string('questionsadded', 'qtype_multianswer')."</strong>", 235 "<strong>".get_string('questionsmore', 'qtype_multianswer', $diff). 236 "</strong>"); 237 } 238 $a = new stdClass(); 239 $a->nb_of_quiz = $this->nbofquiz; 240 $a->nb_of_attempts = $this->nbofattempts; 241 $mform->addElement('header', 'additemhdr2', 242 get_string('questionusedinquiz', 'qtype_multianswer', $a)); 243 $mform->addElement('static', 'alertas', 244 get_string('youshouldnot', 'qtype_multianswer')); 245 } 246 if (($this->negativediff > 0 || $this->usedinquiz && 247 ($this->negativediff > 0 || $this->negativediff < 0 || $this->qtypechange)) && 248 $this->reload) { 249 $mform->addElement('header', 'additemhdr', 250 get_string('questionsaveasedited', 'qtype_multianswer')); 251 $mform->addElement('checkbox', 'confirm', '', 252 get_string('confirmquestionsaveasedited', 'qtype_multianswer')); 253 $mform->setDefault('confirm', 0); 254 } else { 255 $mform->addElement('hidden', 'confirm', 0); 256 $mform->setType('confirm', PARAM_BOOL); 257 } 258 259 $this->add_interactive_settings(true, true); 260 } 261 262 263 public function set_data($question) { 264 global $DB; 265 $defaultvalues = array(); 266 if (isset($question->id) and $question->id and $question->qtype && 267 $question->questiontext) { 268 269 foreach ($question->options->questions as $key => $wrapped) { 270 if (!empty($wrapped)) { 271 // The old way of restoring the definitions is kept to gradually 272 // update all multianswer questions. 273 if (empty($wrapped->questiontext)) { 274 $parsableanswerdef = '{' . $wrapped->defaultmark . ':'; 275 switch ($wrapped->qtype) { 276 case 'multichoice': 277 $parsableanswerdef .= 'MULTICHOICE:'; 278 break; 279 case 'shortanswer': 280 $parsableanswerdef .= 'SHORTANSWER:'; 281 break; 282 case 'numerical': 283 $parsableanswerdef .= 'NUMERICAL:'; 284 break; 285 case 'subquestion_replacement': 286 continue 2; 287 default: 288 print_error('unknownquestiontype', 'question', '', 289 $wrapped->qtype); 290 } 291 $separator = ''; 292 foreach ($wrapped->options->answers as $subanswer) { 293 $parsableanswerdef .= $separator 294 . '%' . round(100 * $subanswer->fraction) . '%'; 295 if (is_array($subanswer->answer)) { 296 $parsableanswerdef .= $subanswer->answer['text']; 297 } else { 298 $parsableanswerdef .= $subanswer->answer; 299 } 300 if (!empty($wrapped->options->tolerance)) { 301 // Special for numerical answers. 302 $parsableanswerdef .= ":{$wrapped->options->tolerance}"; 303 // We only want tolerance for the first alternative, it will 304 // be applied to all of the alternatives. 305 unset($wrapped->options->tolerance); 306 } 307 if ($subanswer->feedback) { 308 $parsableanswerdef .= "#{$subanswer->feedback}"; 309 } 310 $separator = '~'; 311 } 312 $parsableanswerdef .= '}'; 313 // Fix the questiontext fields of old questions. 314 $DB->set_field('question', 'questiontext', $parsableanswerdef, 315 array('id' => $wrapped->id)); 316 } else { 317 $parsableanswerdef = str_replace('&#', '&\#', $wrapped->questiontext); 318 } 319 $question->questiontext = str_replace("{#$key}", $parsableanswerdef, 320 $question->questiontext); 321 } 322 } 323 } 324 325 // Set default to $questiondisplay questions elements. 326 if ($this->reload) { 327 if (isset($this->questiondisplay->options->questions)) { 328 $subquestions = fullclone($this->questiondisplay->options->questions); 329 if (count($subquestions)) { 330 $sub = 1; 331 foreach ($subquestions as $subquestion) { 332 $prefix = 'sub_'.$sub.'_'; 333 334 // Validate parameters. 335 $answercount = 0; 336 $maxgrade = false; 337 $maxfraction = -1; 338 if ($subquestion->qtype == 'shortanswer') { 339 switch ($subquestion->usecase) { 340 case '1': 341 $defaultvalues[$prefix.'usecase'] = 342 get_string('caseyes', 'qtype_shortanswer'); 343 break; 344 case '0': 345 default : 346 $defaultvalues[$prefix.'usecase'] = 347 get_string('caseno', 'qtype_shortanswer'); 348 } 349 } 350 351 if ($subquestion->qtype == 'multichoice') { 352 $defaultvalues[$prefix.'layout'] = $subquestion->layout; 353 if ($subquestion->single == 1) { 354 switch ($subquestion->layout) { 355 case '0': 356 $defaultvalues[$prefix.'layout'] = 357 get_string('layoutselectinline', 'qtype_multianswer'); 358 break; 359 case '1': 360 $defaultvalues[$prefix.'layout'] = 361 get_string('layoutvertical', 'qtype_multianswer'); 362 break; 363 case '2': 364 $defaultvalues[$prefix.'layout'] = 365 get_string('layouthorizontal', 'qtype_multianswer'); 366 break; 367 default: 368 $defaultvalues[$prefix.'layout'] = 369 get_string('layoutundefined', 'qtype_multianswer'); 370 } 371 } else { 372 switch ($subquestion->layout) { 373 case '1': 374 $defaultvalues[$prefix.'layout'] = 375 get_string('layoutmultiple_vertical', 'qtype_multianswer'); 376 break; 377 case '2': 378 $defaultvalues[$prefix.'layout'] = 379 get_string('layoutmultiple_horizontal', 'qtype_multianswer'); 380 break; 381 default: 382 $defaultvalues[$prefix.'layout'] = 383 get_string('layoutundefined', 'qtype_multianswer'); 384 } 385 } 386 if ($subquestion->shuffleanswers ) { 387 $defaultvalues[$prefix.'shuffleanswers'] = get_string('yes', 'moodle'); 388 } else { 389 $defaultvalues[$prefix.'shuffleanswers'] = get_string('no', 'moodle'); 390 } 391 } 392 foreach ($subquestion->answer as $key => $answer) { 393 if ($subquestion->qtype == 'numerical' && $key == 0) { 394 $defaultvalues[$prefix.'tolerance['.$key.']'] = 395 $subquestion->tolerance[0]; 396 } 397 if (is_array($answer)) { 398 $answer = $answer['text']; 399 } 400 $trimmedanswer = trim($answer); 401 if ($trimmedanswer !== '') { 402 $answercount++; 403 if ($subquestion->qtype == 'numerical' && 404 !(qtype_numerical::is_valid_number($trimmedanswer) || $trimmedanswer == '*')) { 405 $this->_form->setElementError($prefix.'answer['.$key.']', 406 get_string('answermustbenumberorstar', 407 'qtype_numerical')); 408 } 409 if ($subquestion->fraction[$key] == 1) { 410 $maxgrade = true; 411 } 412 if ($subquestion->fraction[$key] > $maxfraction) { 413 $maxfraction = $subquestion->fraction[$key]; 414 } 415 // For 'multiresponse' we are OK if there is at least one fraction > 0. 416 if ($subquestion->qtype == 'multichoice' && $subquestion->single == 0 && 417 $subquestion->fraction[$key] > 0) { 418 $maxgrade = true; 419 } 420 } 421 422 $defaultvalues[$prefix.'answer['.$key.']'] = 423 htmlspecialchars($answer); 424 } 425 if ($answercount == 0) { 426 if ($subquestion->qtype == 'multichoice') { 427 $this->_form->setElementError($prefix.'answer[0]', 428 get_string('notenoughanswers', 'qtype_multichoice', 2)); 429 } else { 430 $this->_form->setElementError($prefix.'answer[0]', 431 get_string('notenoughanswers', 'question', 1)); 432 } 433 } 434 if ($maxgrade == false) { 435 $this->_form->setElementError($prefix.'fraction[0]', 436 get_string('fractionsnomax', 'question')); 437 } 438 foreach ($subquestion->feedback as $key => $answer) { 439 440 $defaultvalues[$prefix.'feedback['.$key.']'] = 441 htmlspecialchars ($answer['text']); 442 } 443 foreach ($subquestion->fraction as $key => $answer) { 444 $defaultvalues[$prefix.'fraction['.$key.']'] = $answer; 445 } 446 447 $sub++; 448 } 449 } 450 } 451 } 452 $defaultvalues['alertas'] = "<strong>".get_string('questioninquiz', 'qtype_multianswer'). 453 "</strong>"; 454 455 if ($defaultvalues != "") { 456 $question = (object)((array)$question + $defaultvalues); 457 } 458 $question = $this->data_preprocessing_hints($question, true, true); 459 parent::set_data($question); 460 } 461 462 public function validation($data, $files) { 463 $errors = parent::validation($data, $files); 464 465 $questiondisplay = qtype_multianswer_extract_question($data['questiontext']); 466 467 $errors = array_merge($errors, qtype_multianswer_validate_question($questiondisplay)); 468 469 if (($this->negativediff > 0 || $this->usedinquiz && 470 ($this->negativediff > 0 || $this->negativediff < 0 || 471 $this->qtypechange)) && !$this->confirm) { 472 $errors['confirm'] = 473 get_string('confirmsave', 'qtype_multianswer', $this->negativediff); 474 } 475 476 return $errors; 477 } 478 479 public function qtype() { 480 return 'multianswer'; 481 } 482 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body