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