MDL-38711 qtype_multianswer: missing setType param
[moodle.git] / question / type / multianswer / edit_multianswer_form.php
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/>.
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  */
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
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 {
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     public $used_in_quiz = false;
48     public $qtype_change = false;
49     public $negative_diff = 0;
50     public $nb_of_quiz = 0;
51     public $nb_of_attempts = 0;
52     public $confirm = 0;
53     public $reload = false;
54     /** @var qtype_numerical_answer_processor used when validating numerical answers. */
55     protected $ap = null;
58     public function __construct($submiturl, $question, $category, $contexts, $formeditable = true) {
59         global $SESSION, $CFG, $DB;
60         $this->regenerate = true;
61         $this->reload = optional_param('reload', false, PARAM_BOOL);
63         $this->used_in_quiz = false;
65         if (isset($question->id) && $question->id != 0) {
66             $this->savedquestiondisplay = fullclone($question);
67             if ($list = $DB->get_records('quiz_question_instances',
68                     array('question' => $question->id))) {
69                 foreach ($list as $key => $li) {
70                     $this->nb_of_quiz ++;
71                     if ($att = $DB->get_records('quiz_attempts',
72                             array('quiz' => $li->quiz, 'preview' => '0'))) {
73                         $this->nb_of_attempts += count($att);
74                         $this->used_in_quiz = true;
75                     }
76                 }
77             }
78         }
80         parent::__construct($submiturl, $question, $category, $contexts, $formeditable);
81     }
83     protected function definition_inner($mform) {
84         $mform->addElement('hidden', 'reload', 1);
85         $mform->setType('reload', PARAM_INT);
87         // Remove meaningless defaultmark field.
88         $mform->removeElement('defaultmark');
89         $this->confirm = optional_param('confirm', false, PARAM_BOOL);
91         // Make questiontext a required field for this question type.
92         $mform->addRule('questiontext', null, 'required', null, 'client');
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));
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         }
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         }
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++) {
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                 }
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->qtype_change = true;
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'));
164                 }
166                 $mform->addElement('header', 'subhdr'.$sub, get_string('questionno', 'question',
167                        '{#'.$sub.'}').'&nbsp;'.question_bank::get_qtype_name(
168                         $this->questiondisplay->options->questions[$sub]->qtype).$storemess);
170                 $mform->addElement('static', 'sub_'.$sub.'_questiontext',
171                         get_string('questiondefinition', 'qtype_multianswer'));
173                 if (isset ($this->questiondisplay->options->questions[$sub]->questiontext)) {
174                     $mform->setDefault('sub_'.$sub.'_questiontext',
175                             $this->questiondisplay->options->questions[$sub]->questiontext['text']);
176                 }
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);
183                 if ($this->questiondisplay->options->questions[$sub]->qtype == 'shortanswer') {
184                     $mform->addElement('static', 'sub_'.$sub.'_usecase',
185                             get_string('casesensitive', 'qtype_shortanswer'));
186                 }
188                 if ($this->questiondisplay->options->questions[$sub]->qtype == 'multichoice') {
189                     $mform->addElement('static', 'sub_'.$sub.'_layout',
190                             get_string('layout', 'qtype_multianswer'));
191                 }
193                 foreach ($this->questiondisplay->options->questions[$sub]->answer as $key => $ans) {
194                     $mform->addElement('static', 'sub_'.$sub.'_answer['.$key.']',
195                             get_string('answer', 'question'));
197                     if ($this->questiondisplay->options->questions[$sub]->qtype == 'numerical' &&
198                             $key == 0) {
199                         $mform->addElement('static', 'sub_'.$sub.'_tolerance['.$key.']',
200                                 get_string('acceptederror', 'qtype_numerical'));
201                     }
203                     $mform->addElement('static', 'sub_'.$sub.'_fraction['.$key.']',
204                             get_string('grade'));
206                     $mform->addElement('static', 'sub_'.$sub.'_feedback['.$key.']',
207                             get_string('feedback', 'question'));
208                 }
209             }
211             $this->negative_diff = $countsavedsubquestions - $countsubquestions;
212             if (($this->negative_diff > 0) ||$this->qtype_change ||
213                     ($this->used_in_quiz && $this->negative_diff != 0)) {
214                 $mform->addElement('header', 'additemhdr',
215                         get_string('warningquestionmodified', 'qtype_multianswer'));
216             }
217             if ($this->negative_diff > 0) {
218                 $mform->addElement('static', 'alert1', "<strong>".
219                         get_string('questiondeleted', 'qtype_multianswer')."</strong>",
220                         get_string('questionsless', 'qtype_multianswer', $this->negative_diff));
221             }
222             if ($this->qtype_change) {
223                 $mform->addElement('static', 'alert1', "<strong>".
224                         get_string('questiontypechanged', 'qtype_multianswer')."</strong>",
225                         get_string('questiontypechangedcomment', 'qtype_multianswer'));
226             }
227         }
228         if ($this->used_in_quiz) {
229             if ($this->negative_diff < 0) {
230                 $diff = $countsubquestions - $countsavedsubquestions;
231                 $mform->addElement('static', 'alert1', "<strong>".
232                         get_string('questionsadded', 'qtype_multianswer')."</strong>",
233                         "<strong>".get_string('questionsmore', 'qtype_multianswer', $diff).
234                         "</strong>");
235             }
236             $a = new stdClass();
237             $a->nb_of_quiz = $this->nb_of_quiz;
238             $a->nb_of_attempts = $this->nb_of_attempts;
239             $mform->addElement('header', 'additemhdr2',
240                     get_string('questionusedinquiz', 'qtype_multianswer', $a));
241             $mform->addElement('static', 'alertas',
242                     get_string('youshouldnot', 'qtype_multianswer'));
243         }
244         if (($this->negative_diff > 0 || $this->used_in_quiz &&
245                 ($this->negative_diff > 0 || $this->negative_diff < 0 || $this->qtype_change)) &&
246                         $this->reload) {
247             $mform->addElement('header', 'additemhdr',
248                     get_string('questionsaveasedited', 'qtype_multianswer'));
249             $mform->addElement('checkbox', 'confirm', '',
250                     get_string('confirmquestionsaveasedited', 'qtype_multianswer'));
251             $mform->setDefault('confirm', 0);
252         } else {
253             $mform->addElement('hidden', 'confirm', 0);
254             $mform->setType('confirm', PARAM_BOOL);
255         }
257         $this->add_interactive_settings(true, true);
258     }
261     public function set_data($question) {
262         global $DB;
263         $default_values = array();
264         if (isset($question->id) and $question->id and $question->qtype &&
265                 $question->questiontext) {
267             foreach ($question->options->questions as $key => $wrapped) {
268                 if (!empty($wrapped)) {
269                     // The old way of restoring the definitions is kept to gradually
270                     // update all multianswer questions.
271                     if (empty($wrapped->questiontext)) {
272                         $parsableanswerdef = '{' . $wrapped->defaultmark . ':';
273                         switch ($wrapped->qtype) {
274                             case 'multichoice':
275                                 $parsableanswerdef .= 'MULTICHOICE:';
276                                 break;
277                             case 'shortanswer':
278                                 $parsableanswerdef .= 'SHORTANSWER:';
279                                 break;
280                             case 'numerical':
281                                 $parsableanswerdef .= 'NUMERICAL:';
282                                 break;
283                             default:
284                                 print_error('unknownquestiontype', 'question', '',
285                                         $wrapped->qtype);
286                         }
287                         $separator = '';
288                         foreach ($wrapped->options->answers as $subanswer) {
289                             $parsableanswerdef .= $separator
290                                 . '%' . round(100*$subanswer->fraction) . '%';
291                             if (is_array($subanswer->answer)) {
292                                 $parsableanswerdef .= $subanswer->answer['text'];
293                             } else {
294                                 $parsableanswerdef .= $subanswer->answer;
295                             }
296                             if (!empty($wrapped->options->tolerance)) {
297                                 // Special for numerical answers.
298                                 $parsableanswerdef .= ":{$wrapped->options->tolerance}";
299                                 // We only want tolerance for the first alternative, it will
300                                 // be applied to all of the alternatives.
301                                 unset($wrapped->options->tolerance);
302                             }
303                             if ($subanswer->feedback) {
304                                 $parsableanswerdef .= "#$subanswer->feedback";
305                             }
306                             $separator = '~';
307                         }
308                         $parsableanswerdef .= '}';
309                         // Fix the questiontext fields of old questions.
310                         $DB->set_field('question', 'questiontext', $parsableanswerdef,
311                                 array('id' => $wrapped->id));
312                     } else {
313                         $parsableanswerdef = str_replace('&#', '&\#', $wrapped->questiontext);
314                     }
315                     $question->questiontext = str_replace("{#$key}", $parsableanswerdef,
316                             $question->questiontext);
317                 }
318             }
319         }
321         // Set default to $questiondisplay questions elements.
322         if ($this->reload) {
323             if (isset($this->questiondisplay->options->questions)) {
324                 $subquestions = fullclone($this->questiondisplay->options->questions);
325                 if (count($subquestions)) {
326                     $sub = 1;
327                     foreach ($subquestions as $subquestion) {
328                         $prefix = 'sub_'.$sub.'_';
330                         // Validate parameters.
331                         $answercount = 0;
332                         $maxgrade = false;
333                         $maxfraction = -1;
334                         if ($subquestion->qtype == 'shortanswer') {
335                             switch ($subquestion->usecase) {
336                                 case '1':
337                                     $default_values[$prefix.'usecase'] =
338                                             get_string('caseyes', 'qtype_shortanswer');
339                                     break;
340                                 case '0':
341                                 default :
342                                     $default_values[$prefix.'usecase'] =
343                                             get_string('caseno', 'qtype_shortanswer');
344                             }
345                         }
347                         if ($subquestion->qtype == 'multichoice') {
348                             $default_values[$prefix.'layout'] = $subquestion->layout;
349                             switch ($subquestion->layout) {
350                                 case '0':
351                                     $default_values[$prefix.'layout'] =
352                                             get_string('layoutselectinline', 'qtype_multianswer');
353                                     break;
354                                 case '1':
355                                     $default_values[$prefix.'layout'] =
356                                             get_string('layoutvertical', 'qtype_multianswer');
357                                     break;
358                                 case '2':
359                                     $default_values[$prefix.'layout'] =
360                                             get_string('layouthorizontal', 'qtype_multianswer');
361                                     break;
362                                 default:
363                                     $default_values[$prefix.'layout'] =
364                                             get_string('layoutundefined', 'qtype_multianswer');
365                             }
366                         }
367                         foreach ($subquestion->answer as $key => $answer) {
368                             if ($subquestion->qtype == 'numerical' && $key == 0) {
369                                 $default_values[$prefix.'tolerance['.$key.']'] =
370                                         $subquestion->tolerance[0];
371                             }
372                             if (is_array($answer)) {
373                                 $answer = $answer['text'];
374                             }
375                             $trimmedanswer = trim($answer);
376                             if ($trimmedanswer !== '') {
377                                 $answercount++;
378                                 if ($subquestion->qtype == 'numerical' &&
379                                         !($this->is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
380                                     $this->_form->setElementError($prefix.'answer['.$key.']',
381                                             get_string('answermustbenumberorstar',
382                                                     'qtype_numerical'));
383                                 }
384                                 if ($subquestion->fraction[$key] == 1) {
385                                     $maxgrade = true;
386                                 }
387                                 if ($subquestion->fraction[$key] > $maxfraction) {
388                                     $maxfraction = $subquestion->fraction[$key];
389                                 }
390                             }
392                             $default_values[$prefix.'answer['.$key.']'] =
393                                     htmlspecialchars($answer);
394                         }
395                         if ($answercount == 0) {
396                             if ($subquestion->qtype == 'multichoice') {
397                                 $this->_form->setElementError($prefix.'answer[0]',
398                                         get_string('notenoughanswers', 'qtype_multichoice', 2));
399                             } else {
400                                 $this->_form->setElementError($prefix.'answer[0]',
401                                         get_string('notenoughanswers', 'question', 1));
402                             }
403                         }
404                         if ($maxgrade == false) {
405                             $this->_form->setElementError($prefix.'fraction[0]',
406                                     get_string('fractionsnomax', 'question'));
407                         }
408                         foreach ($subquestion->feedback as $key => $answer) {
410                             $default_values[$prefix.'feedback['.$key.']'] =
411                                     htmlspecialchars ($answer['text']);
412                         }
413                         foreach ($subquestion->fraction as $key => $answer) {
414                             $default_values[$prefix.'fraction['.$key.']'] = $answer;
415                         }
417                         $sub++;
418                     }
419                 }
420             }
421         }
422         $default_values['alertas']= "<strong>".get_string('questioninquiz', 'qtype_multianswer').
423                 "</strong>";
425         if ($default_values != "") {
426             $question = (object)((array)$question + $default_values);
427         }
428         $question = $this->data_preprocessing_hints($question, true, true);
429         parent::set_data($question);
430     }
432     /**
433      * Validate that a string is a nubmer formatted correctly for the current locale.
434      * @param string $x a string
435      * @return bool whether $x is a number that the numerical question type can interpret.
436      */
437     protected function is_valid_number($x) {
438         if (is_null($this->ap)) {
439             $this->ap = new qtype_numerical_answer_processor(array());
440         }
442         list($value, $unit) = $this->ap->apply_units($x);
444         return !is_null($value) && !$unit;
445     }
448     public function validation($data, $files) {
449         $errors = parent::validation($data, $files);
451         $questiondisplay = qtype_multianswer_extract_question($data['questiontext']);
453         if (isset($questiondisplay->options->questions)) {
454             $subquestions = fullclone($questiondisplay->options->questions);
455             if (count($subquestions)) {
456                 $sub = 1;
457                 foreach ($subquestions as $subquestion) {
458                     $prefix = 'sub_'.$sub.'_';
459                     $answercount = 0;
460                     $maxgrade = false;
461                     $maxfraction = -1;
462                     if (isset($this->savedquestiondisplay->options->questions[$sub]->qtype) &&
463                             $this->savedquestiondisplay->options->questions[$sub]->qtype !=
464                                     $questiondisplay->options->questions[$sub]->qtype) {
465                         $storemess = " STORED QTYPE ".question_bank::get_qtype_name(
466                                 $this->savedquestiondisplay->options->questions[$sub]->qtype);
467                     }
468                     foreach ($subquestion->answer as $key => $answer) {
469                         if (is_array($answer)) {
470                             $answer = $answer['text'];
471                         }
472                         $trimmedanswer = trim($answer);
473                         if ($trimmedanswer !== '') {
474                             $answercount++;
475                             if ($subquestion->qtype == 'numerical' &&
476                                     !($this->is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
477                                 $errors[$prefix.'answer['.$key.']'] =
478                                         get_string('answermustbenumberorstar', 'qtype_numerical');
479                             }
480                             if ($subquestion->fraction[$key] == 1) {
481                                 $maxgrade = true;
482                             }
483                             if ($subquestion->fraction[$key] > $maxfraction) {
484                                 $maxfraction = $subquestion->fraction[$key];
485                             }
486                         }
487                     }
488                     if ($answercount == 0) {
489                         if ($subquestion->qtype == 'multichoice') {
490                             $errors[$prefix.'answer[0]'] =
491                                     get_string('notenoughanswers', 'qtype_multichoice', 2);
492                         } else {
493                             $errors[$prefix.'answer[0]'] =
494                                     get_string('notenoughanswers', 'question', 1);
495                         }
496                     }
497                     if ($maxgrade == false) {
498                         $errors[$prefix.'fraction[0]'] =
499                                 get_string('fractionsnomax', 'question');
500                     }
501                     $sub++;
502                 }
503             } else {
504                 $errors['questiontext'] = get_string('questionsmissing', 'qtype_multianswer');
505             }
506         }
508         if (($this->negative_diff > 0 || $this->used_in_quiz &&
509                 ($this->negative_diff > 0 || $this->negative_diff < 0 ||
510                         $this->qtype_change)) && !$this->confirm) {
511             $errors['confirm'] =
512                     get_string('confirmsave', 'qtype_multianswer', $this->negative_diff);
513         }
515         return $errors;
516     }
518     public function qtype() {
519         return 'multianswer';
520     }