weekly release 3.1dev
[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     /** @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;
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);
68         $this->usedinquiz = false;
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         }
83         parent::__construct($submiturl, $question, $category, $contexts, $formeditable);
84     }
86     protected function definition_inner($mform) {
87         $mform->addElement('hidden', 'reload', 1);
88         $mform->setType('reload', PARAM_INT);
90         // Remove meaningless defaultmark field.
91         $mform->removeElement('defaultmark');
92         $this->confirm = optional_param('confirm', false, PARAM_BOOL);
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->qtypechange = 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                 }
165                             $mform->addElement('header', 'subhdr'.$sub, get_string('questionno', 'question',
166                        '{#'.$sub.'}').'&nbsp;'.question_bank::get_qtype_name(
167                         $this->questiondisplay->options->questions[$sub]->qtype).$storemess);
169                 $mform->addElement('static', 'sub_'.$sub.'_questiontext',
170                         get_string('questiondefinition', 'qtype_multianswer'));
172                 if (isset ($this->questiondisplay->options->questions[$sub]->questiontext)) {
173                     $mform->setDefault('sub_'.$sub.'_questiontext',
174                             $this->questiondisplay->options->questions[$sub]->questiontext['text']);
175                 }
177                 $mform->addElement('static', 'sub_'.$sub.'_defaultmark',
178                         get_string('defaultmark', 'question'));
179                 $mform->setDefault('sub_'.$sub.'_defaultmark',
180                         $this->questiondisplay->options->questions[$sub]->defaultmark);
182                 if ($this->questiondisplay->options->questions[$sub]->qtype == 'shortanswer') {
183                     $mform->addElement('static', 'sub_'.$sub.'_usecase',
184                             get_string('casesensitive', 'qtype_shortanswer'));
185                 }
187                 if ($this->questiondisplay->options->questions[$sub]->qtype == 'multichoice') {
188                     $mform->addElement('static', 'sub_'.$sub.'_layout',
189                             get_string('layout', 'qtype_multianswer'));
190                     $mform->addElement('static', 'sub_'.$sub.'_shuffleanswers',
191                             get_string('shuffleanswers', 'qtype_multichoice'));
192                 }
194                 foreach ($this->questiondisplay->options->questions[$sub]->answer as $key => $ans) {
195                     $mform->addElement('static', 'sub_'.$sub.'_answer['.$key.']',
196                             get_string('answer', 'question'));
198                     if ($this->questiondisplay->options->questions[$sub]->qtype == 'numerical' &&
199                             $key == 0) {
200                         $mform->addElement('static', 'sub_'.$sub.'_tolerance['.$key.']',
201                                 get_string('acceptederror', 'qtype_numerical'));
202                     }
204                     $mform->addElement('static', 'sub_'.$sub.'_fraction['.$key.']',
205                             get_string('grade'));
207                     $mform->addElement('static', 'sub_'.$sub.'_feedback['.$key.']',
208                             get_string('feedback', 'question'));
209                 }
210             }
212             $this->negativediff = $countsavedsubquestions - $countsubquestions;
213             if (($this->negativediff > 0) ||$this->qtypechange ||
214                     ($this->usedinquiz && $this->negativediff != 0)) {
215                 $mform->addElement('header', 'additemhdr',
216                         get_string('warningquestionmodified', 'qtype_multianswer'));
217             }
218             if ($this->negativediff > 0) {
219                 $mform->addElement('static', 'alert1', "<strong>".
220                         get_string('questiondeleted', 'qtype_multianswer')."</strong>",
221                         get_string('questionsless', 'qtype_multianswer', $this->negativediff));
222             }
223             if ($this->qtypechange) {
224                 $mform->addElement('static', 'alert1', "<strong>".
225                         get_string('questiontypechanged', 'qtype_multianswer')."</strong>",
226                         get_string('questiontypechangedcomment', 'qtype_multianswer'));
227             }
228         }
229         if ($this->usedinquiz) {
230             if ($this->negativediff < 0) {
231                 $diff = $countsubquestions - $countsavedsubquestions;
232                 $mform->addElement('static', 'alert1', "<strong>".
233                         get_string('questionsadded', 'qtype_multianswer')."</strong>",
234                         "<strong>".get_string('questionsmore', 'qtype_multianswer', $diff).
235                         "</strong>");
236             }
237             $a = new stdClass();
238             $a->nbofquiz = $this->nbofquiz;
239             $a->nbofattempts = $this->nbofattempts;
240             $mform->addElement('header', 'additemhdr2',
241                     get_string('questionusedinquiz', 'qtype_multianswer', $a));
242             $mform->addElement('static', 'alertas',
243                     get_string('youshouldnot', 'qtype_multianswer'));
244         }
245         if (($this->negativediff > 0 || $this->usedinquiz &&
246                 ($this->negativediff > 0 || $this->negativediff < 0 || $this->qtypechange)) &&
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'));
252             $mform->setDefault('confirm', 0);
253         } else {
254             $mform->addElement('hidden', 'confirm', 0);
255             $mform->setType('confirm', PARAM_BOOL);
256         }
258         $this->add_interactive_settings(true, true);
259     }
262     public function set_data($question) {
263         global $DB;
264         $defaultvalues = array();
265         if (isset($question->id) and $question->id and $question->qtype &&
266                 $question->questiontext) {
268             foreach ($question->options->questions as $key => $wrapped) {
269                 if (!empty($wrapped)) {
270                     // The old way of restoring the definitions is kept to gradually
271                     // update all multianswer questions.
272                     if (empty($wrapped->questiontext)) {
273                         $parsableanswerdef = '{' . $wrapped->defaultmark . ':';
274                         switch ($wrapped->qtype) {
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);
287                         }
288                         $separator = '';
289                         foreach ($wrapped->options->answers as $subanswer) {
290                             $parsableanswerdef .= $separator
291                                 . '%' . round(100 * $subanswer->fraction) . '%';
292                             if (is_array($subanswer->answer)) {
293                                 $parsableanswerdef .= $subanswer->answer['text'];
294                             } else {
295                                 $parsableanswerdef .= $subanswer->answer;
296                             }
297                             if (!empty($wrapped->options->tolerance)) {
298                                 // Special for numerical answers.
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) {
305                                 $parsableanswerdef .= "#{$subanswer->feedback}";
306                             }
307                             $separator = '~';
308                         }
309                         $parsableanswerdef .= '}';
310                         // Fix the questiontext fields of old questions.
311                         $DB->set_field('question', 'questiontext', $parsableanswerdef,
312                                 array('id' => $wrapped->id));
313                     } else {
314                         $parsableanswerdef = str_replace('&#', '&\#', $wrapped->questiontext);
315                     }
316                     $question->questiontext = str_replace("{#$key}", $parsableanswerdef,
317                             $question->questiontext);
318                 }
319             }
320         }
322         // Set default to $questiondisplay questions elements.
323         if ($this->reload) {
324             if (isset($this->questiondisplay->options->questions)) {
325                 $subquestions = fullclone($this->questiondisplay->options->questions);
326                 if (count($subquestions)) {
327                     $sub = 1;
328                     foreach ($subquestions as $subquestion) {
329                         $prefix = 'sub_'.$sub.'_';
331                         // Validate parameters.
332                         $answercount = 0;
333                         $maxgrade = false;
334                         $maxfraction = -1;
335                         if ($subquestion->qtype == 'shortanswer') {
336                             switch ($subquestion->usecase) {
337                                 case '1':
338                                     $defaultvalues[$prefix.'usecase'] =
339                                             get_string('caseyes', 'qtype_shortanswer');
340                                     break;
341                                 case '0':
342                                 default :
343                                     $defaultvalues[$prefix.'usecase'] =
344                                             get_string('caseno', 'qtype_shortanswer');
345                             }
346                         }
348                         if ($subquestion->qtype == 'multichoice') {
349                             $defaultvalues[$prefix.'layout'] = $subquestion->layout;
350                             switch ($subquestion->layout) {
351                                 case '0':
352                                     $defaultvalues[$prefix.'layout'] =
353                                             get_string('layoutselectinline', 'qtype_multianswer');
354                                     break;
355                                 case '1':
356                                     $defaultvalues[$prefix.'layout'] =
357                                             get_string('layoutvertical', 'qtype_multianswer');
358                                     break;
359                                 case '2':
360                                     $defaultvalues[$prefix.'layout'] =
361                                             get_string('layouthorizontal', 'qtype_multianswer');
362                                     break;
363                                 default:
364                                     $defaultvalues[$prefix.'layout'] =
365                                             get_string('layoutundefined', 'qtype_multianswer');
366                             }
367                             if ($subquestion->shuffleanswers ) {
368                                 $defaultvalues[$prefix.'shuffleanswers'] = get_string('yes', 'moodle');
369                             } else {
370                                 $defaultvalues[$prefix.'shuffleanswers'] = get_string('no', 'moodle');
371                             }
372                         }
373                         foreach ($subquestion->answer as $key => $answer) {
374                             if ($subquestion->qtype == 'numerical' && $key == 0) {
375                                 $defaultvalues[$prefix.'tolerance['.$key.']'] =
376                                         $subquestion->tolerance[0];
377                             }
378                             if (is_array($answer)) {
379                                 $answer = $answer['text'];
380                             }
381                             $trimmedanswer = trim($answer);
382                             if ($trimmedanswer !== '') {
383                                 $answercount++;
384                                 if ($subquestion->qtype == 'numerical' &&
385                                         !($this->is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
386                                     $this->_form->setElementError($prefix.'answer['.$key.']',
387                                             get_string('answermustbenumberorstar',
388                                                     'qtype_numerical'));
389                                 }
390                                 if ($subquestion->fraction[$key] == 1) {
391                                     $maxgrade = true;
392                                 }
393                                 if ($subquestion->fraction[$key] > $maxfraction) {
394                                     $maxfraction = $subquestion->fraction[$key];
395                                 }
396                             }
398                             $defaultvalues[$prefix.'answer['.$key.']'] =
399                                     htmlspecialchars($answer);
400                         }
401                         if ($answercount == 0) {
402                             if ($subquestion->qtype == 'multichoice') {
403                                 $this->_form->setElementError($prefix.'answer[0]',
404                                         get_string('notenoughanswers', 'qtype_multichoice', 2));
405                             } else {
406                                 $this->_form->setElementError($prefix.'answer[0]',
407                                         get_string('notenoughanswers', 'question', 1));
408                             }
409                         }
410                         if ($maxgrade == false) {
411                             $this->_form->setElementError($prefix.'fraction[0]',
412                                     get_string('fractionsnomax', 'question'));
413                         }
414                         foreach ($subquestion->feedback as $key => $answer) {
416                             $defaultvalues[$prefix.'feedback['.$key.']'] =
417                                     htmlspecialchars ($answer['text']);
418                         }
419                         foreach ($subquestion->fraction as $key => $answer) {
420                             $defaultvalues[$prefix.'fraction['.$key.']'] = $answer;
421                         }
423                         $sub++;
424                     }
425                 }
426             }
427         }
428         $defaultvalues['alertas'] = "<strong>".get_string('questioninquiz', 'qtype_multianswer').
429                 "</strong>";
431         if ($defaultvalues != "") {
432             $question = (object)((array)$question + $defaultvalues);
433         }
434         $question = $this->data_preprocessing_hints($question, true, true);
435         parent::set_data($question);
436     }
438     /**
439      * Validate that a string is a nubmer formatted correctly for the current locale.
440      * @param string $x a string
441      * @return bool whether $x is a number that the numerical question type can interpret.
442      */
443     protected function is_valid_number($x) {
444         if (is_null($this->ap)) {
445             $this->ap = new qtype_numerical_answer_processor(array());
446         }
448         list($value, $unit) = $this->ap->apply_units($x);
450         return !is_null($value) && !$unit;
451     }
454     public function validation($data, $files) {
455         $errors = parent::validation($data, $files);
457         $questiondisplay = qtype_multianswer_extract_question($data['questiontext']);
459         if (isset($questiondisplay->options->questions)) {
460             $subquestions = fullclone($questiondisplay->options->questions);
461             if (count($subquestions)) {
462                 $sub = 1;
463                 foreach ($subquestions as $subquestion) {
464                     $prefix = 'sub_'.$sub.'_';
465                     $answercount = 0;
466                     $maxgrade = false;
467                     $maxfraction = -1;
469                     foreach ($subquestion->answer as $key => $answer) {
470                         if (is_array($answer)) {
471                             $answer = $answer['text'];
472                         }
473                         $trimmedanswer = trim($answer);
474                         if ($trimmedanswer !== '') {
475                             $answercount++;
476                             if ($subquestion->qtype == 'numerical' &&
477                                     !($this->is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
478                                 $errors[$prefix.'answer['.$key.']'] =
479                                         get_string('answermustbenumberorstar', 'qtype_numerical');
480                             }
481                             if ($subquestion->fraction[$key] == 1) {
482                                 $maxgrade = true;
483                             }
484                             if ($subquestion->fraction[$key] > $maxfraction) {
485                                 $maxfraction = $subquestion->fraction[$key];
486                             }
487                         }
488                     }
489                     if ($answercount == 0) {
490                         if ($subquestion->qtype == 'multichoice') {
491                             $errors[$prefix.'answer[0]'] =
492                                     get_string('notenoughanswers', 'qtype_multichoice', 2);
493                         } else {
494                             $errors[$prefix.'answer[0]'] =
495                                     get_string('notenoughanswers', 'question', 1);
496                         }
497                     }
498                     if ($maxgrade == false) {
499                         $errors[$prefix.'fraction[0]'] =
500                                 get_string('fractionsnomax', 'question');
501                     }
502                     $sub++;
503                 }
504             } else {
505                 $errors['questiontext'] = get_string('questionsmissing', 'qtype_multianswer');
506             }
507         }
509         if (($this->negativediff > 0 || $this->usedinquiz &&
510                 ($this->negativediff > 0 || $this->negativediff < 0 ||
511                         $this->qtypechange)) && !$this->confirm) {
512             $errors['confirm'] =
513                     get_string('confirmsave', 'qtype_multianswer', $this->negativediff);
514         }
516         return $errors;
517     }
519     public function qtype() {
520         return 'multianswer';
521     }