1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Defines the editing form for the calculated question data set items.
19 *
20 * @package    qtype
21 * @subpackage calculated
22 * @copyright  2007 Jamie Pratt me@jamiep.org
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26
27defined('MOODLE_INTERNAL') || die();
28
29require_once($CFG->dirroot . '/question/type/edit_question_form.php');
30
31
32/**
33 * Calculated question data set items editing form definition.
34 *
35 * @copyright  2007 Jamie Pratt me@jamiep.org
36 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
38class question_dataset_dependent_items_form extends question_wizard_form {
39    /**
40     * Question object with options and answers already loaded by get_question_options
41     * Be careful how you use this it is needed sometimes to set up the structure of the
42     * form in definition_inner but data is always loaded into the form with set_defaults.
43     *
44     * @var object
45     */
46    public $question;
47    /**
48     * Reference to question type object
49     *
50     * @var question_dataset_dependent_questiontype
51     */
52    public $qtypeobj;
53
54    /** @var stdClass the question category. */
55    protected $category;
56
57    /** @var context the context of the question category. */
58    protected $categorycontext;
59
60    public $datasetdefs;
61
62    public $maxnumber = -1;
63
64    public $regenerate;
65
66    public $noofitems;
67
68    public $outsidelimit = false;
69
70    public $commentanswers = array();
71
72    /**
73     * Add question-type specific form fields.
74     *
75     * @param MoodleQuickForm $mform the form being built.
76     */
77    public function __construct($submiturl, $question, $regenerate) {
78        global $SESSION, $CFG, $DB;
79        $this->regenerate = $regenerate;
80        $this->question = $question;
81        $this->qtypeobj = question_bank::get_qtype($this->question->qtype);
82        // Validate the question category.
83        if (!$category = $DB->get_record('question_categories',
84                array('id' => $question->category))) {
85            print_error('categorydoesnotexist', 'question', $returnurl);
86        }
87        $this->category = $category;
88        $this->categorycontext = context::instance_by_id($category->contextid);
89        // Get the dataset defintions for this question.
90        if (empty($question->id)) {
91            $this->datasetdefs = $this->qtypeobj->get_dataset_definitions(
92                    $question->id, $SESSION->calculated->definitionform->dataset);
93        } else {
94            if (empty($question->options)) {
95                $this->get_question_options($question);
96            }
97            $this->datasetdefs = $this->qtypeobj->get_dataset_definitions(
98                    $question->id, array());
99        }
100
101        foreach ($this->datasetdefs as $datasetdef) {
102            // Get maxnumber.
103            if ($this->maxnumber == -1 || $datasetdef->itemcount < $this->maxnumber) {
104                $this->maxnumber = $datasetdef->itemcount;
105            }
106        }
107        foreach ($this->datasetdefs as $defid => $datasetdef) {
108            if (isset($datasetdef->id)) {
109                $this->datasetdefs[$defid]->items =
110                        $this->qtypeobj->get_database_dataset_items($datasetdef->id);
111            }
112        }
113        parent::__construct($submiturl);
114    }
115
116    protected function definition() {
117        global $PAGE;
118
119        $labelsharedwildcard = get_string("sharedwildcard", "qtype_calculated");
120        $mform = $this->_form;
121        $mform->setDisableShortforms();
122
123        $strquestionlabel = $this->qtypeobj->comment_header($this->question);
124        if ($this->maxnumber != -1 ) {
125            $this->noofitems = $this->maxnumber;
126        } else {
127            $this->noofitems = 0;
128        }
129        $label = get_string("sharedwildcards", "qtype_calculated");
130
131        $html2 = $this->qtypeobj->print_dataset_definitions_category_shared(
132                $this->question, $this->datasetdefs);
133        $mform->addElement('static', 'listcategory', $label, $html2);
134        // ...----------------------------------------------------------------------.
135        $mform->addElement('submit', 'updatedatasets',
136                get_string('updatedatasetparam', 'qtype_calculated'));
137        $mform->registerNoSubmitButton('updatedatasets');
138        $mform->addElement('header', 'additemhdr',
139                get_string('itemtoadd', 'qtype_calculated'));
140        $idx = 1;
141        $data = array();
142        $j = (($this->noofitems) * count($this->datasetdefs))+1;
143        foreach ($this->datasetdefs as $defkey => $datasetdef) {
144            if ($datasetdef->category |= 0 ) {
145                $name = get_string('sharedwildcard', 'qtype_calculated', $datasetdef->name);
146            } else {
147                $name = get_string('wildcard', 'qtype_calculated', $datasetdef->name);
148            }
149            $mform->addElement('float', "number[{$j}]", $name);
150            $this->qtypeobj->custom_generator_tools_part($mform, $idx, $j);
151            $idx++;
152            $mform->addElement('hidden', "definition[{$j}]");
153            $mform->setType("definition[{$j}]", PARAM_RAW);
154            $mform->addElement('hidden', "itemid[{$j}]");
155            $mform->setType("itemid[{$j}]", PARAM_RAW);
156            $mform->addElement('static', "divider[{$j}]", '', '<hr />');
157            $mform->setType("divider[{$j}]", PARAM_RAW);
158            $j++;
159        }
160
161        $mform->addElement('header', 'updateanswershdr',
162                get_string('answerstoleranceparam', 'qtype_calculated'));
163        $mform->addElement('submit', 'updateanswers',
164                get_string('updatetolerancesparam', 'qtype_calculated'));
165        $mform->setAdvanced('updateanswers', true);
166        $mform->registerNoSubmitButton('updateanswers');
167
168        $answers = fullclone($this->question->options->answers);
169        $key1 =1;
170        foreach ($answers as $key => $answer) {
171            $ans = shorten_text($answer->answer, 17, true);
172            if ($ans === '*') {
173                $mform->addElement('static',
174                        'answercomment[' . ($this->noofitems+$key1) . ']', $ans);
175                $mform->addElement('hidden', 'tolerance['.$key.']', '');
176                $mform->setType('tolerance['.$key.']', PARAM_FLOAT); // No need to worry about localisation, as the value of this field will not be shown to users anymore.
177                $mform->setAdvanced('tolerance['.$key.']', true);
178                $mform->addElement('hidden', 'tolerancetype['.$key.']', '');
179                $mform->setType('tolerancetype['.$key.']', PARAM_RAW);
180                $mform->setAdvanced('tolerancetype['.$key.']', true);
181                $mform->addElement('hidden', 'correctanswerlength['.$key.']', '');
182                $mform->setType('correctanswerlength['.$key.']', PARAM_RAW);
183                $mform->setAdvanced('correctanswerlength['.$key.']', true);
184                $mform->addElement('hidden', 'correctanswerformat['.$key.']', '');
185                $mform->setType('correctanswerformat['.$key.']', PARAM_RAW);
186                $mform->setAdvanced('correctanswerformat['.$key.']', true);
187            } else if ( $ans !== '' ) {
188                $mform->addElement('static', 'answercomment[' . ($this->noofitems+$key1) . ']',
189                        $ans);
190                $mform->addElement('float', 'tolerance['.$key.']',
191                        get_string('tolerance', 'qtype_calculated'));
192                $mform->setAdvanced('tolerance['.$key.']', true);
193                $mform->addElement('select', 'tolerancetype['.$key.']',
194                        get_string('tolerancetype', 'qtype_numerical'),
195                        $this->qtypeobj->tolerance_types());
196                $mform->setAdvanced('tolerancetype['.$key.']', true);
197
198                $mform->addElement('select', 'correctanswerlength['.$key.']',
199                        get_string('correctanswershows', 'qtype_calculated'), range(0, 9));
200                $mform->setAdvanced('correctanswerlength['.$key.']', true);
201
202                $answerlengthformats = array(
203                    '1' => get_string('decimalformat', 'qtype_numerical'),
204                    '2' => get_string('significantfiguresformat', 'qtype_calculated')
205                );
206                $mform->addElement('select', 'correctanswerformat['.$key.']',
207                        get_string('correctanswershowsformat', 'qtype_calculated'),
208                        $answerlengthformats);
209                $mform->setAdvanced('correctanswerformat['.$key.']', true);
210                $mform->addElement('static', 'dividertolerance', '', '<hr />');
211                $mform->setAdvanced('dividertolerance', true);
212            }
213            $key1++;
214        }
215
216        $addremoveoptions = array();
217        $addremoveoptions['1']='1';
218        for ($i=10; $i<=100; $i+=10) {
219             $addremoveoptions["{$i}"] = "{$i}";
220        }
221        $showoptions = Array();
222        $showoptions['1']='1';
223        $showoptions['2']='2';
224        $showoptions['5']='5';
225        for ($i=10; $i<=100; $i+=10) {
226             $showoptions["{$i}"] = "{$i}";
227        }
228        $mform->addElement('header', 'addhdr', get_string('add', 'moodle'));
229        $mform->closeHeaderBefore('addhdr');
230
231        if ($this->qtypeobj->supports_dataset_item_generation()) {
232            $radiogrp = array();
233            $radiogrp[] =& $mform->createElement('radio', 'nextpageparam[forceregeneration]',
234                    null, get_string('reuseifpossible', 'qtype_calculated'), 0);
235            $radiogrp[] =& $mform->createElement('radio', 'nextpageparam[forceregeneration]',
236                    null, get_string('forceregenerationshared', 'qtype_calculated'), 1);
237            $radiogrp[] =& $mform->createElement('radio', 'nextpageparam[forceregeneration]',
238                    null, get_string('forceregenerationall', 'qtype_calculated'), 2);
239            $mform->addGroup($radiogrp, 'forceregenerationgrp',
240                    get_string('nextitemtoadd', 'qtype_calculated'), "<br/>", false);
241        }
242
243        $mform->addElement('submit', 'getnextbutton', get_string('getnextnow', 'qtype_calculated'));
244        $mform->addElement('static', "dividera", '', '<hr />');
245        $addgrp = array();
246        $addgrp[] =& $mform->createElement('submit', 'addbutton', get_string('add', 'moodle'));
247        $addgrp[] =& $mform->createElement('select', "selectadd",
248                get_string('additem', 'qtype_calculated'), $addremoveoptions);
249        $addgrp[] = & $mform->createElement('static', "stat", "Items",
250                get_string('newsetwildcardvalues', 'qtype_calculatedsimple'));
251        $mform->addGroup($addgrp, 'addgrp', get_string('additem', 'qtype_calculated'), ' ', false);
252        $mform->addElement('static', "divideradd", '', '');
253        if ($this->noofitems > 0) {
254            $mform->addElement('header', 'deleteitemhdr', get_string('delete', 'moodle'));
255            $deletegrp = array();
256            $deletegrp[] = $mform->createElement('submit', 'deletebutton',
257                    get_string('delete', 'moodle'));
258            $deletegrp[] = $mform->createElement('select', 'selectdelete',
259                    get_string('deleteitem', 'qtype_calculated')."1", $addremoveoptions);
260            $deletegrp[] = $mform->createElement('static', "stat", "Items",
261                    get_string('setwildcardvalues', 'qtype_calculatedsimple'));
262            $mform->addGroup($deletegrp, 'deletegrp', '', '   ', false);
263        } else {
264            $mform->addElement('static', 'warning', '', '<span class="error">' .
265                    get_string('youmustaddatleastoneitem', 'qtype_calculated').'</span>');
266        }
267
268        $addgrp1 = array();
269        $addgrp1[] = $mform->createElement('submit', 'showbutton',
270                get_string('showitems', 'qtype_calculated'));
271        $addgrp1[] = $mform->createElement('select', "selectshow", '' , $showoptions);
272        $addgrp1[] = $mform->createElement('static', "stat", '',
273                get_string('setwildcardvalues', 'qtype_calculated'));
274        $mform->addGroup($addgrp1, 'addgrp1', '', '   ', false);
275        $mform->registerNoSubmitButton('showbutton');
276        $mform->closeHeaderBefore('addgrp1');
277        // ...----------------------------------------------------------------------.
278        $j = $this->noofitems * count($this->datasetdefs);
279        $k = optional_param('selectshow', 1, PARAM_INT);
280        for ($i = $this->noofitems; $i >= 1; $i--) {
281            if ($k > 0) {
282                $mform->addElement('header', 'setnoheader' . $i, "<b>" .
283                        get_string('setno', 'qtype_calculated', $i)."</b>&nbsp;&nbsp;");
284            }
285            foreach ($this->datasetdefs as $defkey => $datasetdef) {
286                if ($k > 0) {
287                    if ($datasetdef->category == 0 ) {
288                        $mform->addElement('float', "number[{$j}]",
289                                get_string('wildcard', 'qtype_calculated', $datasetdef->name));
290                    } else {
291                        $mform->addElement('float', "number[{$j}]", get_string(
292                                'sharedwildcard', 'qtype_calculated', $datasetdef->name));
293                    }
294
295                } else {
296                    $mform->addElement('hidden', "number[{$j}]" , '');
297                    $mform->setType("number[{$j}]", PARAM_LOCALISEDFLOAT); // Localisation handling has to be done manually.
298                }
299                $mform->addElement('hidden', "itemid[{$j}]");
300                $mform->setType("itemid[{$j}]", PARAM_INT);
301
302                $mform->addElement('hidden', "definition[{$j}]");
303                $mform->setType("definition[{$j}]", PARAM_NOTAGS);
304                $data[$datasetdef->name] =$datasetdef->items[$i]->value;
305
306                $j--;
307            }
308            if ('' != $strquestionlabel && ($k > 0 )) {
309                // ... $this->outsidelimit || !empty($this->numbererrors ).
310                $repeated[] = $mform->addElement('static', "answercomment[{$i}]", $strquestionlabel);
311                // Decode equations in question text.
312                $qtext = $this->qtypeobj->substitute_variables(
313                        $this->question->questiontext, $data);
314                $textequations = $this->qtypeobj->find_formulas($qtext);
315                if ($textequations != '' && count($textequations) > 0 ) {
316                    $mform->addElement('static', "divider1[{$j}]", '',
317                            'Formulas {=..} in question text');
318                    foreach ($textequations as $key => $equation) {
319                        if ($formulaerrors = qtype_calculated_find_formula_errors($equation)) {
320                            $str = $formulaerrors;
321                        } else {
322                            eval('$str = '.$equation.';');
323                        }
324                        $equation = shorten_text($equation, 17, true);
325                        $mform->addElement('static', "textequation", "{={$equation}}", "=".$str);
326                    }
327                }
328
329            }
330            $k--;
331
332        }
333        $mform->addElement('static', 'outsidelimit', '', '');
334
335        // Submit buttons.
336        if ($this->noofitems > 0) {
337            $buttonarray = array();
338            $buttonarray[] = $mform->createElement(
339                    'submit', 'savechanges', get_string('savechanges'));
340
341            $previewlink = $PAGE->get_renderer('core_question')->question_preview_link(
342                        $this->question->id, $this->categorycontext, true);
343            $buttonarray[] = $mform->createElement('static', 'previewlink', '', $previewlink);
344
345            $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
346            $mform->closeHeaderBefore('buttonar');
347        }
348
349        $this->add_hidden_fields();
350
351        $mform->addElement('hidden', 'category');
352        $mform->setType('category', PARAM_SEQUENCE);
353
354        $mform->addElement('hidden', 'wizard', 'datasetitems');
355        $mform->setType('wizard', PARAM_ALPHA);
356    }
357
358    public function set_data($question) {
359        $formdata = array();
360        $fromform = new stdClass();
361        if (isset($question->options)) {
362            $answers = $question->options->answers;
363            if (count($answers)) {
364                if (optional_param('updateanswers', false, PARAM_BOOL) ||
365                        optional_param('updatedatasets', false, PARAM_BOOL)) {
366                    foreach ($answers as $key => $answer) {
367                        $fromform->tolerance[$key]= $this->_form->getElementValue(
368                                'tolerance['.$key.']');
369                        $answer->tolerance = $fromform->tolerance[$key];
370                        $fromform->tolerancetype[$key]= $this->_form->getElementValue(
371                                'tolerancetype['.$key.']');
372                        if (is_array($fromform->tolerancetype[$key])) {
373                            $fromform->tolerancetype[$key]= $fromform->tolerancetype[$key][0];
374                        }
375                        $answer->tolerancetype = $fromform->tolerancetype[$key];
376                        $fromform->correctanswerlength[$key]= $this->_form->getElementValue(
377                                'correctanswerlength['.$key.']');
378                        if (is_array($fromform->correctanswerlength[$key])) {
379                            $fromform->correctanswerlength[$key] =
380                                    $fromform->correctanswerlength[$key][0];
381                        }
382                        $answer->correctanswerlength = $fromform->correctanswerlength[$key];
383                        $fromform->correctanswerformat[$key] = $this->_form->getElementValue(
384                                'correctanswerformat['.$key.']');
385                        if (is_array($fromform->correctanswerformat[$key])) {
386                            $fromform->correctanswerformat[$key] =
387                                    $fromform->correctanswerformat[$key][0];
388                        }
389                        $answer->correctanswerformat = $fromform->correctanswerformat[$key];
390                    }
391                    $this->qtypeobj->save_question_calculated($question, $fromform);
392
393                } else {
394                    foreach ($answers as $key => $answer) {
395                        $formdata['tolerance['.$key.']'] = $answer->tolerance;
396                        $formdata['tolerancetype['.$key.']'] = $answer->tolerancetype;
397                        $formdata['correctanswerlength['.$key.']'] = $answer->correctanswerlength;
398                        $formdata['correctanswerformat['.$key.']'] = $answer->correctanswerformat;
399                    }
400                }
401            }
402        }
403        // Fill out all data sets and also the fields for the next item to add.
404        $j = $this->noofitems * count($this->datasetdefs);
405        for ($itemnumber = $this->noofitems; $itemnumber >= 1; $itemnumber--) {
406            $data = array();
407            foreach ($this->datasetdefs as $defid => $datasetdef) {
408                if (isset($datasetdef->items[$itemnumber])) {
409                    $value = $datasetdef->items[$itemnumber]->value;
410                    if ($this->_form->getElementType("number[{$j}]") == 'hidden') {
411                        // Some of the number elements are from the float type and some are from the hidden type.
412                        // We need to manually handle localised floats for hidden elements.
413                        $value = format_float($value, -1);
414                    }
415                    $formdata["number[{$j}]"] = $value;
416                    $formdata["definition[{$j}]"] = $defid;
417                    $formdata["itemid[{$j}]"] = $datasetdef->items[$itemnumber]->id;
418                    $data[$datasetdef->name] = $datasetdef->items[$itemnumber]->value;
419                }
420                $j--;
421            }
422            $comment = $this->qtypeobj->comment_on_datasetitems($this->qtypeobj, $question->id,
423                    $question->questiontext, $answers, $data, $itemnumber);
424            if ($comment->outsidelimit) {
425                $this->outsidelimit=$comment->outsidelimit;
426            }
427            $totalcomment='';
428            foreach ($question->options->answers as $key => $answer) {
429                $totalcomment .= $comment->stranswers[$key].'<br/>';
430            }
431            $formdata['answercomment['.$itemnumber.']'] = $totalcomment;
432        }
433
434        $formdata['nextpageparam[forceregeneration]'] = $this->regenerate;
435        $formdata['selectdelete'] = '1';
436        $formdata['selectadd'] = '1';
437        $j = $this->noofitems * count($this->datasetdefs)+1;
438        $data = array(); // Data for comment_on_datasetitems later.
439        // Dataset generation defaults.
440        if ($this->qtypeobj->supports_dataset_item_generation()) {
441            $itemnumber = $this->noofitems+1;
442            foreach ($this->datasetdefs as $defid => $datasetdef) {
443                if (!optional_param('updatedatasets', false, PARAM_BOOL) &&
444                        !optional_param('updateanswers', false, PARAM_BOOL)) {
445                    $value = $this->qtypeobj->generate_dataset_item($datasetdef->options);
446                } else {
447                    $value = $this->_form->getElementValue("number[{$j}]");
448                }
449                if ($this->_form->getElementType("number[{$j}]") == 'hidden') {
450                    // Some of the number elements are from the float type and some are from the hidden type.
451                    // We need to manually handle localised floats for hidden elements.
452                    $value = format_float($value, -1);
453                }
454                $formdata["number[{$j}]"] = $value;
455                $formdata["definition[{$j}]"] = $defid;
456                $formdata["itemid[{$j}]"] = isset($datasetdef->items[$itemnumber]) ?
457                        $datasetdef->items[$itemnumber]->id : 0;
458                $data[$datasetdef->name] = $formdata["number[{$j}]"];
459                $j++;
460            }
461        }
462
463        // Existing records override generated data depending on radio element.
464        $j = $this->noofitems * count($this->datasetdefs) + 1;
465        if (!$this->regenerate && !optional_param('updatedatasets', false, PARAM_BOOL) &&
466                !optional_param('updateanswers', false, PARAM_BOOL)) {
467            $itemnumber = $this->noofitems + 1;
468            foreach ($this->datasetdefs as $defid => $datasetdef) {
469                if (isset($datasetdef->items[$itemnumber])) {
470                    $formdata["number[{$j}]"] = $datasetdef->items[$itemnumber]->value;
471                    $formdata["definition[{$j}]"] = $defid;
472                    $formdata["itemid[{$j}]"] = $datasetdef->items[$itemnumber]->id;
473                    $data[$datasetdef->name] = $datasetdef->items[$itemnumber]->value;
474                }
475                $j++;
476            }
477        }
478
479        $comment = $this->qtypeobj->comment_on_datasetitems($this->qtypeobj, $question->id,
480                $question->questiontext, $answers, $data, ($this->noofitems + 1));
481        if (isset($comment->outsidelimit) && $comment->outsidelimit) {
482            $this->outsidelimit=$comment->outsidelimit;
483        }
484        $key1 = 1;
485        foreach ($question->options->answers as $key => $answer) {
486            $formdata['answercomment['.($this->noofitems+$key1).']'] = $comment->stranswers[$key];
487            $key1++;
488        }
489
490        if ($this->outsidelimit) {
491            $formdata['outsidelimit']= '<span class="error">' .
492                    get_string('oneanswertrueansweroutsidelimits', 'qtype_calculated') . '</span>';
493        }
494        $formdata = $this->qtypeobj->custom_generator_set_data($this->datasetdefs, $formdata);
495
496        parent::set_data((object)($formdata + (array)$question));
497    }
498
499    public function validation($data, $files) {
500        $errors = array();
501        if (isset($data['savechanges']) && ($this->noofitems==0) ) {
502            $errors['warning'] = get_string('warning', 'mnet');
503        }
504        if ($this->outsidelimit) {
505            $errors['outsidelimits'] =
506                    get_string('oneanswertrueansweroutsidelimits', 'qtype_calculated');
507        }
508        $numbers = $data['number'];
509        foreach ($numbers as $key => $number) {
510            if (! is_numeric($number)) {
511                if (stristr($number, ',')) {
512                    $errors['number['.$key.']'] = get_string('nocommaallowed', 'qtype_calculated');
513                } else {
514                    $errors['number['.$key.']'] = get_string('notvalidnumber', 'qtype_calculated');
515                }
516            } else if (stristr($number, 'x')) {
517                $a = new stdClass();
518                $a->name = '';
519                $a->value = $number;
520                $errors['number['.$key.']'] = get_string('hexanotallowed', 'qtype_calculated', $a);
521            } else if (is_nan($number)) {
522                $errors['number['.$key.']'] = get_string('notvalidnumber', 'qtype_calculated');
523            }
524        }
525        return $errors;
526    }
527}
528