MDL-43670 quiz: improve code comments a little
[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             // TODO MDL-43779 should not have quiz-specific code here.
67             $this->savedquestiondisplay = fullclone($question);
68             $this->nb_of_quiz = $DB->count_records('quiz_question_instances', array('questionid' => $question->id));
69             $this->used_in_quiz = $this->nb_of_quiz > 0;
70             $this->nb_of_attempts = $DB->count_records_sql("
71                     SELECT count(1)
72                       FROM {quiz_question_instances} qqi
73                       JOIN {quiz_attempts} quiza ON quiza.quiz = qqi.quizid
74                      WHERE qqi.questionid = ?
75                        AND quiza.preview = 0", array($question->id));
76         }
78         parent::__construct($submiturl, $question, $category, $contexts, $formeditable);
79     }
81     protected function definition_inner($mform) {
82         $mform->addElement('hidden', 'reload', 1);
83         $mform->setType('reload', PARAM_INT);
85         // Remove meaningless defaultmark field.
86         $mform->removeElement('defaultmark');
87         $this->confirm = optional_param('confirm', false, PARAM_BOOL);
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));
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         }
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         }
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++) {
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                 }
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->qtype_change = true;
155                     $storemess = ' ' . html_writer::tag('span', get_string(
156                             'storedqtype', 'qtype_multianswer', question_bank::get_qtype_name(
157                                     $this->savedquestiondisplay->options->questions[$sub]->qtype)),
158                             array('class' => 'error'));
159                 }
161                 $mform->addElement('header', 'subhdr'.$sub, get_string('questionno', 'question',
162                        '{#'.$sub.'}').'&nbsp;'.question_bank::get_qtype_name(
163                         $this->questiondisplay->options->questions[$sub]->qtype).$storemess);
165                 $mform->addElement('static', 'sub_'.$sub.'_questiontext',
166                         get_string('questiondefinition', 'qtype_multianswer'));
168                 if (isset ($this->questiondisplay->options->questions[$sub]->questiontext)) {
169                     $mform->setDefault('sub_'.$sub.'_questiontext',
170                             $this->questiondisplay->options->questions[$sub]->questiontext['text']);
171                 }
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);
178                 if ($this->questiondisplay->options->questions[$sub]->qtype == 'shortanswer') {
179                     $mform->addElement('static', 'sub_'.$sub.'_usecase',
180                             get_string('casesensitive', 'qtype_shortanswer'));
181                 }
183                 if ($this->questiondisplay->options->questions[$sub]->qtype == 'multichoice') {
184                     $mform->addElement('static', 'sub_'.$sub.'_layout',
185                             get_string('layout', 'qtype_multianswer'));
186                 }
188                 foreach ($this->questiondisplay->options->questions[$sub]->answer as $key => $ans) {
189                     $mform->addElement('static', 'sub_'.$sub.'_answer['.$key.']',
190                             get_string('answer', 'question'));
192                     if ($this->questiondisplay->options->questions[$sub]->qtype == 'numerical' &&
193                             $key == 0) {
194                         $mform->addElement('static', 'sub_'.$sub.'_tolerance['.$key.']',
195                                 get_string('acceptederror', 'qtype_numerical'));
196                     }
198                     $mform->addElement('static', 'sub_'.$sub.'_fraction['.$key.']',
199                             get_string('grade'));
201                     $mform->addElement('static', 'sub_'.$sub.'_feedback['.$key.']',
202                             get_string('feedback', 'question'));
203                 }
204             }
206             $this->negative_diff = $countsavedsubquestions - $countsubquestions;
207             if (($this->negative_diff > 0) ||$this->qtype_change ||
208                     ($this->used_in_quiz && $this->negative_diff != 0)) {
209                 $mform->addElement('header', 'additemhdr',
210                         get_string('warningquestionmodified', 'qtype_multianswer'));
211             }
212             if ($this->negative_diff > 0) {
213                 $mform->addElement('static', 'alert1', "<strong>".
214                         get_string('questiondeleted', 'qtype_multianswer')."</strong>",
215                         get_string('questionsless', 'qtype_multianswer', $this->negative_diff));
216             }
217             if ($this->qtype_change) {
218                 $mform->addElement('static', 'alert1', "<strong>".
219                         get_string('questiontypechanged', 'qtype_multianswer')."</strong>",
220                         get_string('questiontypechangedcomment', 'qtype_multianswer'));
221             }
222         }
223         if ($this->used_in_quiz) {
224             if ($this->negative_diff < 0) {
225                 $diff = $countsubquestions - $countsavedsubquestions;
226                 $mform->addElement('static', 'alert1', "<strong>".
227                         get_string('questionsadded', 'qtype_multianswer')."</strong>",
228                         "<strong>".get_string('questionsmore', 'qtype_multianswer', $diff).
229                         "</strong>");
230             }
231             $a = new stdClass();
232             $a->nb_of_quiz = $this->nb_of_quiz;
233             $a->nb_of_attempts = $this->nb_of_attempts;
234             $mform->addElement('header', 'additemhdr2',
235                     get_string('questionusedinquiz', 'qtype_multianswer', $a));
236             $mform->addElement('static', 'alertas',
237                     get_string('youshouldnot', 'qtype_multianswer'));
238         }
239         if (($this->negative_diff > 0 || $this->used_in_quiz &&
240                 ($this->negative_diff > 0 || $this->negative_diff < 0 || $this->qtype_change)) &&
241                         $this->reload) {
242             $mform->addElement('header', 'additemhdr',
243                     get_string('questionsaveasedited', 'qtype_multianswer'));
244             $mform->addElement('checkbox', 'confirm', '',
245                     get_string('confirmquestionsaveasedited', 'qtype_multianswer'));
246             $mform->setDefault('confirm', 0);
247         } else {
248             $mform->addElement('hidden', 'confirm', 0);
249             $mform->setType('confirm', PARAM_BOOL);
250         }
252         $this->add_interactive_settings(true, true);
253     }
256     public function set_data($question) {
257         global $DB;
258         $default_values = array();
259         if (isset($question->id) and $question->id and $question->qtype &&
260                 $question->questiontext) {
262             foreach ($question->options->questions as $key => $wrapped) {
263                 if (!empty($wrapped)) {
264                     // The old way of restoring the definitions is kept to gradually
265                     // update all multianswer questions.
266                     if (empty($wrapped->questiontext)) {
267                         $parsableanswerdef = '{' . $wrapped->defaultmark . ':';
268                         switch ($wrapped->qtype) {
269                             case 'multichoice':
270                                 $parsableanswerdef .= 'MULTICHOICE:';
271                                 break;
272                             case 'shortanswer':
273                                 $parsableanswerdef .= 'SHORTANSWER:';
274                                 break;
275                             case 'numerical':
276                                 $parsableanswerdef .= 'NUMERICAL:';
277                                 break;
278                             default:
279                                 print_error('unknownquestiontype', 'question', '',
280                                         $wrapped->qtype);
281                         }
282                         $separator = '';
283                         foreach ($wrapped->options->answers as $subanswer) {
284                             $parsableanswerdef .= $separator
285                                 . '%' . round(100*$subanswer->fraction) . '%';
286                             if (is_array($subanswer->answer)) {
287                                 $parsableanswerdef .= $subanswer->answer['text'];
288                             } else {
289                                 $parsableanswerdef .= $subanswer->answer;
290                             }
291                             if (!empty($wrapped->options->tolerance)) {
292                                 // Special for numerical answers.
293                                 $parsableanswerdef .= ":{$wrapped->options->tolerance}";
294                                 // We only want tolerance for the first alternative, it will
295                                 // be applied to all of the alternatives.
296                                 unset($wrapped->options->tolerance);
297                             }
298                             if ($subanswer->feedback) {
299                                 $parsableanswerdef .= "#$subanswer->feedback";
300                             }
301                             $separator = '~';
302                         }
303                         $parsableanswerdef .= '}';
304                         // Fix the questiontext fields of old questions.
305                         $DB->set_field('question', 'questiontext', $parsableanswerdef,
306                                 array('id' => $wrapped->id));
307                     } else {
308                         $parsableanswerdef = str_replace('&#', '&\#', $wrapped->questiontext);
309                     }
310                     $question->questiontext = str_replace("{#$key}", $parsableanswerdef,
311                             $question->questiontext);
312                 }
313             }
314         }
316         // Set default to $questiondisplay questions elements.
317         if ($this->reload) {
318             if (isset($this->questiondisplay->options->questions)) {
319                 $subquestions = fullclone($this->questiondisplay->options->questions);
320                 if (count($subquestions)) {
321                     $sub = 1;
322                     foreach ($subquestions as $subquestion) {
323                         $prefix = 'sub_'.$sub.'_';
325                         // Validate parameters.
326                         $answercount = 0;
327                         $maxgrade = false;
328                         $maxfraction = -1;
329                         if ($subquestion->qtype == 'shortanswer') {
330                             switch ($subquestion->usecase) {
331                                 case '1':
332                                     $default_values[$prefix.'usecase'] =
333                                             get_string('caseyes', 'qtype_shortanswer');
334                                     break;
335                                 case '0':
336                                 default :
337                                     $default_values[$prefix.'usecase'] =
338                                             get_string('caseno', 'qtype_shortanswer');
339                             }
340                         }
342                         if ($subquestion->qtype == 'multichoice') {
343                             $default_values[$prefix.'layout'] = $subquestion->layout;
344                             switch ($subquestion->layout) {
345                                 case '0':
346                                     $default_values[$prefix.'layout'] =
347                                             get_string('layoutselectinline', 'qtype_multianswer');
348                                     break;
349                                 case '1':
350                                     $default_values[$prefix.'layout'] =
351                                             get_string('layoutvertical', 'qtype_multianswer');
352                                     break;
353                                 case '2':
354                                     $default_values[$prefix.'layout'] =
355                                             get_string('layouthorizontal', 'qtype_multianswer');
356                                     break;
357                                 default:
358                                     $default_values[$prefix.'layout'] =
359                                             get_string('layoutundefined', 'qtype_multianswer');
360                             }
361                         }
362                         foreach ($subquestion->answer as $key => $answer) {
363                             if ($subquestion->qtype == 'numerical' && $key == 0) {
364                                 $default_values[$prefix.'tolerance['.$key.']'] =
365                                         $subquestion->tolerance[0];
366                             }
367                             if (is_array($answer)) {
368                                 $answer = $answer['text'];
369                             }
370                             $trimmedanswer = trim($answer);
371                             if ($trimmedanswer !== '') {
372                                 $answercount++;
373                                 if ($subquestion->qtype == 'numerical' &&
374                                         !($this->is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
375                                     $this->_form->setElementError($prefix.'answer['.$key.']',
376                                             get_string('answermustbenumberorstar',
377                                                     'qtype_numerical'));
378                                 }
379                                 if ($subquestion->fraction[$key] == 1) {
380                                     $maxgrade = true;
381                                 }
382                                 if ($subquestion->fraction[$key] > $maxfraction) {
383                                     $maxfraction = $subquestion->fraction[$key];
384                                 }
385                             }
387                             $default_values[$prefix.'answer['.$key.']'] =
388                                     htmlspecialchars($answer);
389                         }
390                         if ($answercount == 0) {
391                             if ($subquestion->qtype == 'multichoice') {
392                                 $this->_form->setElementError($prefix.'answer[0]',
393                                         get_string('notenoughanswers', 'qtype_multichoice', 2));
394                             } else {
395                                 $this->_form->setElementError($prefix.'answer[0]',
396                                         get_string('notenoughanswers', 'question', 1));
397                             }
398                         }
399                         if ($maxgrade == false) {
400                             $this->_form->setElementError($prefix.'fraction[0]',
401                                     get_string('fractionsnomax', 'question'));
402                         }
403                         foreach ($subquestion->feedback as $key => $answer) {
405                             $default_values[$prefix.'feedback['.$key.']'] =
406                                     htmlspecialchars ($answer['text']);
407                         }
408                         foreach ($subquestion->fraction as $key => $answer) {
409                             $default_values[$prefix.'fraction['.$key.']'] = $answer;
410                         }
412                         $sub++;
413                     }
414                 }
415             }
416         }
417         $default_values['alertas']= "<strong>".get_string('questioninquiz', 'qtype_multianswer').
418                 "</strong>";
420         if ($default_values != "") {
421             $question = (object)((array)$question + $default_values);
422         }
423         $question = $this->data_preprocessing_hints($question, true, true);
424         parent::set_data($question);
425     }
427     /**
428      * Validate that a string is a nubmer formatted correctly for the current locale.
429      * @param string $x a string
430      * @return bool whether $x is a number that the numerical question type can interpret.
431      */
432     protected function is_valid_number($x) {
433         if (is_null($this->ap)) {
434             $this->ap = new qtype_numerical_answer_processor(array());
435         }
437         list($value, $unit) = $this->ap->apply_units($x);
439         return !is_null($value) && !$unit;
440     }
443     public function validation($data, $files) {
444         $errors = parent::validation($data, $files);
446         $questiondisplay = qtype_multianswer_extract_question($data['questiontext']);
448         if (isset($questiondisplay->options->questions)) {
449             $subquestions = fullclone($questiondisplay->options->questions);
450             if (count($subquestions)) {
451                 $sub = 1;
452                 foreach ($subquestions as $subquestion) {
453                     $prefix = 'sub_'.$sub.'_';
454                     $answercount = 0;
455                     $maxgrade = false;
456                     $maxfraction = -1;
457                     if (isset($this->savedquestiondisplay->options->questions[$sub]->qtype) &&
458                             $this->savedquestiondisplay->options->questions[$sub]->qtype !=
459                                     $questiondisplay->options->questions[$sub]->qtype) {
460                         $storemess = " STORED QTYPE ".question_bank::get_qtype_name(
461                                 $this->savedquestiondisplay->options->questions[$sub]->qtype);
462                     }
463                     foreach ($subquestion->answer as $key => $answer) {
464                         if (is_array($answer)) {
465                             $answer = $answer['text'];
466                         }
467                         $trimmedanswer = trim($answer);
468                         if ($trimmedanswer !== '') {
469                             $answercount++;
470                             if ($subquestion->qtype == 'numerical' &&
471                                     !($this->is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
472                                 $errors[$prefix.'answer['.$key.']'] =
473                                         get_string('answermustbenumberorstar', 'qtype_numerical');
474                             }
475                             if ($subquestion->fraction[$key] == 1) {
476                                 $maxgrade = true;
477                             }
478                             if ($subquestion->fraction[$key] > $maxfraction) {
479                                 $maxfraction = $subquestion->fraction[$key];
480                             }
481                         }
482                     }
483                     if ($answercount == 0) {
484                         if ($subquestion->qtype == 'multichoice') {
485                             $errors[$prefix.'answer[0]'] =
486                                     get_string('notenoughanswers', 'qtype_multichoice', 2);
487                         } else {
488                             $errors[$prefix.'answer[0]'] =
489                                     get_string('notenoughanswers', 'question', 1);
490                         }
491                     }
492                     if ($maxgrade == false) {
493                         $errors[$prefix.'fraction[0]'] =
494                                 get_string('fractionsnomax', 'question');
495                     }
496                     $sub++;
497                 }
498             } else {
499                 $errors['questiontext'] = get_string('questionsmissing', 'qtype_multianswer');
500             }
501         }
503         if (($this->negative_diff > 0 || $this->used_in_quiz &&
504                 ($this->negative_diff > 0 || $this->negative_diff < 0 ||
505                         $this->qtype_change)) && !$this->confirm) {
506             $errors['confirm'] =
507                     get_string('confirmsave', 'qtype_multianswer', $this->negative_diff);
508         }
510         return $errors;
511     }
513     public function qtype() {
514         return 'multianswer';
515     }