Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
namespace mod_questionnaire\question;
18
use mod_questionnaire\edit_question_form;
19
use mod_questionnaire\responsetype\response\response;
20
use \questionnaire;
21
 
22
defined('MOODLE_INTERNAL') || die();
23
use \html_writer;
24
 
25
/**
26
 * This file contains the parent class for questionnaire question types.
27
 *
28
 * @author Mike Churchward
29
 * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org)
30
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
31
 * @package mod_questionnaire
32
 */
33
 // Constants.
34
define('QUESCHOOSE', 0);
35
define('QUESYESNO', 1);
36
define('QUESTEXT', 2);
37
define('QUESESSAY', 3);
38
define('QUESRADIO', 4);
39
define('QUESCHECK', 5);
40
define('QUESDROP', 6);
41
define('QUESRATE', 8);
42
define('QUESDATE', 9);
43
define('QUESNUMERIC', 10);
44
define('QUESSLIDER', 11);
45
define('QUESPAGEBREAK', 99);
46
define('QUESSECTIONTEXT', 100);
47
 
48
global $idcounter, $CFG;
49
$idcounter = 0;
50
 
51
require_once($CFG->dirroot.'/mod/questionnaire/locallib.php');
52
 
53
 
54
/**
55
 * Class for describing a question
56
 *
57
 * @author Mike Churchward
58
 * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org)
59
 * @package mod_questionnaire
60
 */
61
abstract class question {
62
 
63
    // Class Properties.
64
    /** @var int $id The database id of this question. */
65
    public $id = 0;
66
 
67
    /** @var int $surveyid The database id of the survey this question belongs to. */
68
    public $surveyid = 0;
69
 
70
    /** @var string $name The name of this question. */
71
    public $name = '';
72
 
73
    /** @var string $type The name of the question type. */
74
    public $type = '';
75
 
76
    /** @var array $choices Array holding any choices for this question. */
77
    public $choices = [];
78
 
79
    /** @var array $dependencies Array holding any dependencies for this question. */
80
    public $dependencies = [];
81
 
82
    /** @var string $responsetable The table name for responses. */
83
    public $responsetable = '';
84
 
85
    /** @var int $length The length field. */
86
    public $length = 0;
87
 
88
    /** @var int $precise The precision field. */
89
    public $precise = 0;
90
 
91
    /** @var int $position Position in the questionnaire */
92
    public $position = 0;
93
 
94
    /** @var string $content The question's content. */
95
    public $content = '';
96
 
97
    /** @var string $allchoices The list of all question's choices. */
98
    public $allchoices = '';
99
 
100
    /** @var boolean $required The required flag. */
101
    public $required = 'n';
102
 
103
    /** @var boolean $deleted The deleted flag. */
104
    public $deleted = 'n';
105
 
106
    /** @var mixed $extradata Any custom data for the question type. */
107
    public $extradata = '';
108
 
109
    /** @var array $qtypenames List of all question names. */
110
    private static $qtypenames = [
111
        QUESYESNO => 'yesno',
112
        QUESTEXT => 'text',
113
        QUESESSAY => 'essay',
114
        QUESRADIO => 'radio',
115
        QUESCHECK => 'check',
116
        QUESDROP => 'drop',
117
        QUESRATE => 'rate',
118
        QUESDATE => 'date',
119
        QUESNUMERIC => 'numerical',
120
        QUESPAGEBREAK => 'pagebreak',
121
        QUESSECTIONTEXT => 'sectiontext',
122
        QUESSLIDER => 'slider',
123
    ];
124
 
125
    /** @var array $notifications Array of extra messages for display purposes. */
126
    private $notifications = [];
127
 
128
    // Class Methods.
129
 
130
    /**
131
     * The class constructor
132
     * @param int $id
133
     * @param \stdClass $question
134
     * @param \context $context
135
     * @param array $params
136
     */
137
    public function __construct($id = 0, $question = null, $context = null, $params = []) {
138
        global $DB;
139
        static $qtypes = null;
140
 
141
        if ($qtypes === null) {
142
            $qtypes = $DB->get_records('questionnaire_question_type', [], 'typeid',
143
                                       'typeid, type, has_choices, response_table') ?? [];
144
        }
145
 
146
        if ($id) {
147
            $question = $DB->get_record('questionnaire_question', ['id' => $id]);
148
        }
149
 
150
        if (is_object($question)) {
151
            $this->id = $question->id;
152
            $this->surveyid = $question->surveyid;
153
            $this->name = $question->name;
154
            $this->length = $question->length;
155
            $this->precise = $question->precise;
156
            $this->position = $question->position;
157
            $this->content = $question->content;
158
            $this->required = $question->required;
159
            $this->deleted = $question->deleted;
160
            $this->extradata = $question->extradata;
161
 
162
            $this->type_id = $question->type_id;
163
            $this->type = $qtypes[$this->type_id]->type;
164
            $this->responsetable = $qtypes[$this->type_id]->response_table;
165
 
166
            if (!empty($question->choices)) {
167
                $this->choices = $question->choices;
168
            } else if ($qtypes[$this->type_id]->has_choices == 'y') {
169
                $this->get_choices();
170
            }
171
            // Added for dependencies.
172
            $this->get_dependencies();
173
        }
174
        $this->context = $context;
175
 
176
        foreach ($params as $property => $value) {
177
            $this->$property = $value;
178
        }
179
 
180
        if ($respclass = $this->responseclass()) {
181
            $this->responsetype = new $respclass($this);
182
        }
183
    }
184
 
185
    /**
186
     * Short name for this question type - no spaces, etc..
187
     * @return string
188
     */
189
    abstract public function helpname();
190
 
191
    /**
192
     * Build a question from data.
193
     * @param int $qtype
194
     * @param int|array $qdata
195
     * @param \stdClass $context
196
     * @return mixed
197
     */
198
    public static function question_builder($qtype, $qdata = null, $context = null) {
199
        $qclassname = '\\mod_questionnaire\\question\\'.self::qtypename($qtype);
200
        $qid = 0;
201
        if (!empty($qdata) && is_array($qdata)) {
202
            $qdata = (object)$qdata;
203
        } else if (!empty($qdata) && is_int($qdata)) {
204
            $qid = $qdata;
205
        }
206
        return new $qclassname($qid, $qdata, $context, ['type_id' => $qtype]);
207
    }
208
 
209
    /**
210
     * Return the different question type names.
211
     * @param int $qtype
212
     * @return string
213
     */
214
    public static function qtypename($qtype) {
215
        if (array_key_exists($qtype, self::$qtypenames)) {
216
            return self::$qtypenames[$qtype];
217
        } else {
218
            return('');
219
        }
220
    }
221
 
222
    /**
223
     * Return all of the different question type names.
224
     * @return array
225
     */
226
    public static function qtypenames() {
227
        return self::$qtypenames;
228
    }
229
 
230
    /**
231
     * Override and return true if the question has choices.
232
     * @return bool
233
     */
234
    public function has_choices() {
235
        return false;
236
    }
237
 
238
    /**
239
     * Load any choices into the object.
240
     * @throws \dml_exception
241
     */
242
    private function get_choices() {
243
        global $DB;
244
 
245
        if ($choices = $DB->get_records('questionnaire_quest_choice', ['question_id' => $this->id], 'id ASC')) {
246
            foreach ($choices as $choice) {
247
                $this->choices[$choice->id] = \mod_questionnaire\question\choice::create_from_data($choice);
248
            }
249
        } else {
250
            $this->choices = [];
251
        }
252
    }
253
 
254
    /**
255
     * Return true if this question has been marked as required.
256
     * @return bool
257
     */
258
    public function required() {
259
        return ($this->required == 'y');
260
    }
261
 
262
    /**
263
     * Return true if the question has defined dependencies.
264
     * @return bool
265
     */
266
    public function has_dependencies() {
267
        return !empty($this->dependencies);
268
    }
269
 
270
    /**
271
     * Override this and return true if the question type allows dependent questions.
272
     * @return bool
273
     */
274
    public function allows_dependents() {
275
        return false;
276
    }
277
 
278
    /**
279
     * Load any dependencies.
280
     */
281
    private function get_dependencies() {
282
        global $DB;
283
 
284
        $this->dependencies = [];
285
        if ($dependencies = $DB->get_records('questionnaire_dependency',
286
            ['questionid' => $this->id , 'surveyid' => $this->surveyid], 'id ASC')) {
287
            foreach ($dependencies as $dependency) {
288
                $this->dependencies[$dependency->id] = new \stdClass();
289
                $this->dependencies[$dependency->id]->dependquestionid = $dependency->dependquestionid;
290
                $this->dependencies[$dependency->id]->dependchoiceid = $dependency->dependchoiceid;
291
                $this->dependencies[$dependency->id]->dependlogic = $dependency->dependlogic;
292
                $this->dependencies[$dependency->id]->dependandor = $dependency->dependandor;
293
            }
294
        }
295
    }
296
 
297
    /**
298
     * Returns an array of dependency options for the question as an array of id value / display value pairs. Override in specific
299
     * question types that support this differently.
300
     * @return array An array of valid pair options.
301
     */
302
    protected function get_dependency_options() {
303
        $options = [];
304
        if ($this->allows_dependents() && $this->has_choices()) {
305
            foreach ($this->choices as $key => $choice) {
306
                $contents = questionnaire_choice_values($choice->content);
307
                if (!empty($contents->modname)) {
308
                    $choice->content = $contents->modname;
309
                } else if (!empty($contents->title)) { // Must be an image; use its title for the dropdown list.
310
                    $choice->content = format_string($contents->title);
311
                } else {
312
                    $choice->content = format_string($contents->text);
313
                }
314
                $options[$this->id . ',' . $key] = $this->name . '->' . $choice->content;
315
            }
316
        }
317
        return $options;
318
    }
319
 
320
    /**
321
     * Return true if all dependencies or this question have been fulfilled, or there aren't any.
322
     * @param int $rid The response ID to check.
323
     * @param array $questions An array containing all possible parent question objects.
324
     * @return bool
325
     */
326
    public function dependency_fulfilled($rid, $questions) {
327
        if (!$this->has_dependencies()) {
328
            $fulfilled = true;
329
        } else {
330
            foreach ($this->dependencies as $dependency) {
331
                $choicematches = $questions[$dependency->dependquestionid]->response_has_choice($rid, $dependency->dependchoiceid);
332
 
333
                // Note: dependencies are sorted, first all and-dependencies, then or-dependencies.
334
                if ($dependency->dependandor == 'and') {
335
                    $dependencyandfulfilled = false;
336
                    // This answer given.
337
                    if (($dependency->dependlogic == 1) && $choicematches) {
338
                        $dependencyandfulfilled = true;
339
                    }
340
 
341
                    // This answer NOT given.
342
                    if (($dependency->dependlogic == 0) && !$choicematches) {
343
                        $dependencyandfulfilled = true;
344
                    }
345
 
346
                    // Something mandatory not fulfilled? Stop looking and continue to next question.
347
                    if ($dependencyandfulfilled == false) {
348
                        break;
349
                    }
350
 
351
                    // In case we have no or-dependencies.
352
                    $dependencyorfulfilled = true;
353
                }
354
 
355
                // Note: dependencies are sorted, first all and-dependencies, then or-dependencies.
356
                if ($dependency->dependandor == 'or') {
357
                    $dependencyorfulfilled = false;
358
                    // To reach this point, the and-dependencies have all been fultilled or do not exist, so set them ok.
359
                    $dependencyandfulfilled = true;
360
                    // This answer given.
361
                    if (($dependency->dependlogic == 1) && $choicematches) {
362
                        $dependencyorfulfilled = true;
363
                    }
364
 
365
                    // This answer NOT given.
366
                    if (($dependency->dependlogic == 0) && !$choicematches) {
367
                        $dependencyorfulfilled = true;
368
                    }
369
 
370
                    // Something fulfilled? A single match is sufficient so continue to next question.
371
                    if ($dependencyorfulfilled == true) {
372
                        break;
373
                    }
374
                }
375
 
376
            }
377
            $fulfilled = ($dependencyandfulfilled && $dependencyorfulfilled);
378
        }
379
        return $fulfilled;
380
    }
381
 
382
    /**
383
     * Return the responsetype table for this question.
384
     * @return string
385
     */
386
    public function response_table() {
387
        return $this->responsetype->response_table();
388
    }
389
 
390
    /**
391
     * Return true if the specified response for this question contains the specified choice.
392
     * @param int $rid
393
     * @param int $choiceid
394
     * @return bool
395
     */
396
    public function response_has_choice($rid, $choiceid) {
397
        global $DB;
398
        $choiceval = $this->responsetype->transform_choiceid($choiceid);
399
        return $DB->record_exists($this->response_table(),
400
            ['response_id' => $rid, 'question_id' => $this->id, 'choice_id' => $choiceval]);
401
    }
402
 
403
    /**
404
     * Insert response data method.
405
     * @param \stdClass $responsedata All of the responsedata.
406
     * @return bool
407
     */
408
    public function insert_response($responsedata) {
409
        if (isset($this->responsetype) && is_object($this->responsetype) &&
410
            is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) {
411
            return $this->responsetype->insert_response($responsedata);
412
        } else {
413
            return false;
414
        }
415
    }
416
 
417
    /**
418
     * Get results data method.
419
     * @param array|bool $rids
420
     * @return array|false
421
     */
422
    public function get_results($rids = false) {
423
        if (isset ($this->responsetype) && is_object($this->responsetype) &&
424
            is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) {
425
            return $this->responsetype->get_results($rids);
426
        } else {
427
            return false;
428
        }
429
    }
430
 
431
    /**
432
     * Display results method.
433
     * @param bool $rids
434
     * @param string $sort
435
     * @param bool $anonymous
436
     * @return false|string
437
     */
438
    public function display_results($rids=false, $sort='', $anonymous=false) {
439
        if (isset ($this->responsetype) && is_object($this->responsetype) &&
440
            is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) {
441
            return $this->responsetype->display_results($rids, $sort, $anonymous);
442
        } else {
443
            return false;
444
        }
445
    }
446
 
447
    /**
448
     * Add a notification.
449
     * @param string $message
450
     */
451
    public function add_notification($message) {
452
        $this->notifications[] = $message;
453
    }
454
 
455
    /**
456
     * Get any notifications.
457
     * @return array | boolean The notifications array or false.
458
     */
459
    public function get_notifications() {
460
        if (empty($this->notifications)) {
461
            return false;
462
        } else {
463
            return $this->notifications;
464
        }
465
    }
466
 
467
    /**
468
     * Each question type must define its response class.
469
     * @return object The response object based off of questionnaire_response_base.
470
     */
471
    abstract protected function responseclass();
472
 
473
    /**
474
     * True if question type allows responses.
475
     * @return bool
476
     */
477
    public function supports_responses() {
478
        return !empty($this->responseclass());
479
    }
480
 
481
    /**
482
     * True if question type supports feedback options. False by default.
483
     * @return bool
484
     */
485
    public function supports_feedback() {
486
        return false;
487
    }
488
 
489
    /**
490
     * True if question type supports feedback scores and weights. Same as supports_feedback() by default.
491
     * @return bool
492
     */
493
    public function supports_feedback_scores() {
494
        return $this->supports_feedback();
495
    }
496
 
497
    /**
498
     * True if the question supports feedback and has valid settings for feedback. Override if the default logic is not enough.
499
     * @return bool
500
     */
501
    public function valid_feedback() {
502
        if ($this->supports_feedback() && $this->has_choices() && $this->required() && !empty($this->name)) {
503
            foreach ($this->choices as $choice) {
504
                if ($choice->value != null) {
505
                    return true;
506
                }
507
            }
508
        }
509
        return false;
510
    }
511
 
512
    /**
513
     * Provide the feedback scores for all requested response id's. This should be provided only by questions that provide feedback.
514
     * @param array $rids
515
     * @return array|bool
516
     */
517
    public function get_feedback_scores(array $rids) {
518
        if ($this->valid_feedback() && isset($this->responsetype) && is_object($this->responsetype) &&
519
            is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) {
520
            return $this->responsetype->get_feedback_scores($rids);
521
        } else {
522
            return false;
523
        }
524
    }
525
 
526
    /**
527
     * Get the maximum score possible for feedback if appropriate. Override if default behaviour is not correct.
528
     * @return int|bool
529
     */
530
    public function get_feedback_maxscore() {
531
        if ($this->valid_feedback()) {
532
            $maxscore = 0;
533
            foreach ($this->choices as $choice) {
534
                if (isset($choice->value) && ($choice->value != null)) {
535
                    if ($choice->value > $maxscore) {
536
                        $maxscore = $choice->value;
537
                    }
538
                }
539
            }
540
        } else {
541
            $maxscore = false;
542
        }
543
        return $maxscore;
544
    }
545
 
546
    /**
547
     * Check question's form data for complete response.
548
     * @param \stdClass $responsedata The data entered into the response.
549
     * @return bool
550
     */
551
    public function response_complete($responsedata) {
552
        if (is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) {
553
            // If $responsedata is a response object, look through the answers.
554
            if (isset($responsedata->answers[$this->id]) && !empty($responsedata->answers[$this->id])) {
555
                $answer = $responsedata->answers[$this->id][0];
556
                if (!empty($answer->choiceid) && isset($this->choices[$answer->choiceid]) &&
557
                    $this->choices[$answer->choiceid]->is_other_choice()) {
558
                    $answered = !empty($answer->value);
559
                } else {
560
                    $answered = (!empty($answer->choiceid) || !empty($answer->value));
561
                }
562
            } else {
563
                $answered = false;
564
            }
565
        } else {
566
            // If $responsedata is webform data, check that its not empty.
567
            $answered = isset($responsedata->{'q'.$this->id}) && ($responsedata->{'q'.$this->id} != '');
568
        }
569
        return !($this->required() && ($this->deleted == 'n') && !$answered);
570
    }
571
 
572
    /**
573
     * Check question's form data for valid response. Override this if type has specific format requirements.
574
     * @param \stdClass $responsedata The data entered into the response.
575
     * @return bool
576
     */
577
    public function response_valid($responsedata) {
578
        return true;
579
    }
580
 
581
    /**
582
     * Update data record from object or optional question data.
583
     * @param \stdClass $questionrecord An object with all updated question record data.
584
     * @param bool $updatechoices True if choices should also be updated.
585
     */
586
    public function update($questionrecord = null, $updatechoices = true) {
587
        global $DB;
588
 
589
        if ($questionrecord === null) {
590
            $questionrecord = new \stdClass();
591
            $questionrecord->id = $this->id;
592
            $questionrecord->surveyid = $this->surveyid;
593
            $questionrecord->name = $this->name;
594
            $questionrecord->type_id = $this->type_id;
595
            $questionrecord->result_id = $this->result_id;
596
            $questionrecord->length = $this->length;
597
            $questionrecord->precise = $this->precise;
598
            $questionrecord->position = $this->position;
599
            $questionrecord->content = $this->content;
600
            $questionrecord->required = $this->required;
601
            $questionrecord->deleted = $this->deleted;
602
            $questionrecord->extradata = $this->extradata;
603
            $questionrecord->dependquestion = $this->dependquestion;
604
            $questionrecord->dependchoice = $this->dependchoice;
605
        } else {
606
            // Make sure the "id" field is this question's.
607
            if (isset($this->qid) && ($this->qid > 0)) {
608
                $questionrecord->id = $this->qid;
609
            } else {
610
                $questionrecord->id = $this->id;
611
            }
612
        }
613
        $DB->update_record('questionnaire_question', $questionrecord);
614
 
615
        if ($updatechoices && $this->has_choices()) {
616
            $this->update_choices();
617
        }
618
    }
619
 
620
    /**
621
     * Add the question to the database from supplied arguments.
622
     * @param \stdClass $questionrecord The required data for adding the question.
623
     * @param array $choicerecords An array of choice records with 'content' and 'value' properties.
624
     * @param boolean $calcposition Whether or not to calculate the next available position in the survey.
625
     */
626
    public function add($questionrecord, array $choicerecords = null, $calcposition = true) {
627
        global $DB;
628
 
629
        // Create new question.
630
        if ($calcposition) {
631
            // Set the position to the end.
632
            $sql = 'SELECT MAX(position) as maxpos '.
633
                   'FROM {questionnaire_question} '.
634
                   'WHERE surveyid = ? AND deleted = ?';
635
            $params = ['surveyid' => $questionrecord->surveyid, 'deleted' => 'n'];
636
            if ($record = $DB->get_record_sql($sql, $params)) {
637
                $questionrecord->position = $record->maxpos + 1;
638
            } else {
639
                $questionrecord->position = 1;
640
            }
641
        }
642
 
643
        // Make sure we add all necessary data.
644
        if (!isset($questionrecord->type_id) || empty($questionrecord->type_id)) {
645
            $questionrecord->type_id = $this->type_id;
646
        }
647
 
648
        $this->qid = $DB->insert_record('questionnaire_question', $questionrecord);
649
 
650
        if ($this->has_choices() && !empty($choicerecords)) {
651
            foreach ($choicerecords as $choicerecord) {
652
                $choicerecord->question_id = $this->qid;
653
                $this->add_choice($choicerecord);
654
            }
655
        }
656
    }
657
 
658
    /**
659
     * Update all choices.
660
     * @return bool
661
     */
662
    public function update_choices() {
663
        $retvalue = true;
664
        if ($this->has_choices() && isset($this->choices)) {
665
            // Need to fix this messed-up qid/id issue.
666
            if (isset($this->qid) && ($this->qid > 0)) {
667
                $qid = $this->qid;
668
            } else {
669
                $qid = $this->id;
670
            }
671
            foreach ($this->choices as $key => $choice) {
672
                $choicerecord = new \stdClass();
673
                $choicerecord->id = $key;
674
                $choicerecord->question_id = $qid;
675
                $choicerecord->content = $choice->content;
676
                $choicerecord->value = $choice->value;
677
                $retvalue &= $this->update_choice($choicerecord);
678
            }
679
        }
680
        return $retvalue;
681
    }
682
 
683
    /**
684
     * Update the choice with the choicerecord.
685
     * @param \stdClass $choicerecord
686
     * @return bool
687
     */
688
    public function update_choice($choicerecord) {
689
        global $DB;
690
        return $DB->update_record('questionnaire_quest_choice', $choicerecord);
691
    }
692
 
693
    /**
694
     * Add a new choice to the database.
695
     * @param \stdClass $choicerecord
696
     * @return bool
697
     */
698
    public function add_choice($choicerecord) {
699
        global $DB;
700
        $retvalue = true;
701
        if ($cid = $DB->insert_record('questionnaire_quest_choice', $choicerecord)) {
702
            $this->choices[$cid] = new \stdClass();
703
            $this->choices[$cid]->content = $choicerecord->content;
704
            $this->choices[$cid]->value = isset($choicerecord->value) ? $choicerecord->value : null;
705
        } else {
706
            $retvalue = false;
707
        }
708
        return $retvalue;
709
    }
710
 
711
    /**
712
     * Delete the choice from the question object and the database.
713
     * @param int|\stdClass $choice Either the integer id of the choice, or the choice record.
714
     */
715
    public function delete_choice($choice) {
716
        $retvalue = true;
717
        if (is_int($choice)) {
718
            $cid = $choice;
719
        } else {
720
            $cid = $choice->id;
721
        }
722
        if (\mod_questionnaire\question\choice::delete_from_db_by_id($cid)) {
723
            unset($this->choices[$cid]);
724
        } else {
725
            $retvalue = false;
726
        }
727
        return $retvalue;
728
    }
729
 
730
    /**
731
     * Insert extradata field into db. This will be stored as a string. If a question needs a different format, override this.
732
     * @param string $extradata
733
     * @return bool
734
     */
735
    public function insert_extradata($extradata) {
736
        global $DB;
737
        return $DB->set_field('questionnaire_question', 'extradata', $extradata, ['id' => $this->id]);
738
    }
739
 
740
    /**
741
     * Update the dependency record.
742
     * @param \stdClass $dependencyrecord
743
     * @return bool
744
     */
745
    public function update_dependency($dependencyrecord) {
746
        global $DB;
747
        return $DB->update_record('questionnaire_dependency', $dependencyrecord);
748
    }
749
 
750
    /**
751
     * Add a dependency record.
752
     * @param \stdClass $dependencyrecord
753
     * @return bool
754
     */
755
    public function add_dependency($dependencyrecord) {
756
        global $DB;
757
 
758
        $retvalue = true;
759
        if ($did = $DB->insert_record('questionnaire_dependency', $dependencyrecord)) {
760
            $this->dependencies[$did] = new \stdClass();
761
            $this->dependencies[$did]->dependquestionid = $dependencyrecord->dependquestionid;
762
            $this->dependencies[$did]->dependchoiceid = $dependencyrecord->dependchoiceid;
763
            $this->dependencies[$did]->dependlogic = $dependencyrecord->dependlogic;
764
            $this->dependencies[$did]->dependandor = $dependencyrecord->dependandor;
765
        } else {
766
            $retvalue = false;
767
        }
768
        return $retvalue;
769
    }
770
 
771
    /**
772
     * Delete the dependency from the question object and the database.
773
     * @param int|\stdClass $dependency Either the integer id of the dependency, or the dependency record.
774
     */
775
    public function delete_dependency($dependency) {
776
        global $DB;
777
 
778
        $retvalue = true;
779
        if (is_int($dependency)) {
780
            $did = $dependency;
781
        } else {
782
            $did = $dependency->id;
783
        }
784
        if ($DB->delete_records('questionnaire_dependency', ['id' => $did])) {
785
            unset($this->dependencies[$did]);
786
        } else {
787
            $retvalue = false;
788
        }
789
        return $retvalue;
790
    }
791
 
792
    /**
793
     * Set the question required field in the object and database.
794
     * @param bool $required Whether question should be required or not.
795
     */
796
    public function set_required($required) {
797
        global $DB;
798
        $rval = $required ? 'y' : 'n';
799
        // Need to fix this messed-up qid/id issue.
800
        if (isset($this->qid) && ($this->qid > 0)) {
801
            $qid = $this->qid;
802
        } else {
803
            $qid = $this->id;
804
        }
805
        $this->required = $rval;
806
        return $DB->set_field('questionnaire_question', 'required', $rval, ['id' => $qid]);
807
    }
808
 
809
    /**
810
     * Question specific display method.
811
     * @param \stdClass $formdata
812
     * @param array $descendantsdata
813
     * @param bool $blankquestionnaire
814
     *
815
     */
816
    abstract protected function question_survey_display($formdata, $descendantsdata, $blankquestionnaire);
817
 
818
    /**
819
     * Question specific response display method.
820
     * @param \stdClass $data
821
     *
822
     */
823
    abstract protected function response_survey_display($data);
824
 
825
    /**
826
     * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this.
827
     * @return bool|string
828
     */
829
    public function question_template() {
830
        return false;
831
    }
832
 
833
    /**
834
     * Override and return a form template if provided. Output of response_survey_display is iterpreted based on this.
835
     * @return bool|string
836
     */
837
    public function response_template() {
838
        return false;
839
    }
840
 
841
    /**
842
     * Override and return a form template if provided. Output of results_output is iterpreted based on this.
843
     * @param bool $pdf
844
     * @return bool|string
845
     */
846
    public function results_template($pdf = false) {
847
        if (isset ($this->responsetype) && is_object($this->responsetype) &&
848
            is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) {
849
            return $this->responsetype->results_template($pdf);
850
        } else {
851
            return false;
852
        }
853
    }
854
 
855
    /**
856
     * Get the output for question renderers / templates.
857
     * @param \mod_questionnaire\responsetype\response\response $response
858
     * @param boolean $blankquestionnaire
859
     * @param array $dependants Array of all questions/choices depending on this question.
860
     * @param int $qnum
861
     * @return \stdClass
862
     */
863
    public function question_output($response, $blankquestionnaire, $dependants=[], $qnum='') {
864
        $pagetags = $this->questionstart_survey_display($qnum, $response);
865
        $pagetags->qformelement = $this->question_survey_display($response, $dependants, $blankquestionnaire);
866
        return $pagetags;
867
    }
868
 
869
    /**
870
     * Get the output for question renderers / templates.
871
     * @param \mod_questionnaire\responsetype\response\response $response
872
     * @param string $qnum
873
     * @return \stdClass
874
     */
875
    public function response_output($response, $qnum='') {
876
        $pagetags = $this->questionstart_survey_display($qnum, $response);
877
        $pagetags->qformelement = $this->response_survey_display($response);
878
        return $pagetags;
879
    }
880
 
881
    /**
882
     * Get the output for the start of the questions in a survey.
883
     * @param int $qnum
884
     * @param \mod_questionnaire\responsetype\response\response $response
885
     * @return \stdClass
886
     */
887
    public function questionstart_survey_display($qnum, $response=null) {
888
        global $OUTPUT, $SESSION, $questionnaire, $PAGE;
889
 
890
        $pagetags = new \stdClass();
891
        $currenttab = $SESSION->questionnaire->current_tab;
892
        $pagetype = $PAGE->pagetype;
893
        $skippedclass = '';
894
        // If no questions autonumbering.
895
        $nonumbering = false;
896
        if (!$questionnaire->questions_autonumbered()) {
897
            $qnum = '';
898
            $nonumbering = true;
899
        }
900
 
901
        // For now, check what the response type is until we've got it all refactored.
902
        if ($response instanceof \mod_questionnaire\responsetype\response\response) {
903
            $skippedquestion = !isset($response->answers[$this->id]);
904
        } else {
905
            $skippedquestion = !empty($response) && !isset($response->{'q'.$this->id});
906
        }
907
 
908
        // If we are on report page and this questionnaire has dependquestions and this question was skipped.
909
        if (($pagetype == 'mod-questionnaire-myreport' || $pagetype == 'mod-questionnaire-report') &&
910
            ($nonumbering == false) && !empty($this->dependencies) && $skippedquestion) {
911
            $skippedclass = ' unselected';
912
            $qnum = '<span class="'.$skippedclass.'">('.$qnum.')</span>';
913
        }
914
        // In preview mode, hide children questions that have not been answered.
915
        // In report mode, If questionnaire is set to no numbering,
916
        // also hide answers to questions that have not been answered.
917
        $displayclass = 'qn-container';
918
        if ($pagetype == 'mod-questionnaire-preview' || ($nonumbering &&
919
            ($currenttab == 'mybyresponse' || $currenttab == 'individualresp'))) {
920
            // This needs to be done to ensure all dependency data is loaded.
921
            // TODO - Perhaps this should be a function called by the questionnaire after it loads all questions?
922
            $questionnaire->load_parents($this);
923
            // Want this to come from the renderer, meaning we need $questionnaire.
924
            $pagetags->dependencylist = $questionnaire->renderer->get_dependency_html($this->id, $this->dependencies);
925
        }
926
 
927
        $pagetags->fieldset = (object)['id' => $this->id, 'class' => $displayclass];
928
 
929
        // Do not display the info box for the label question type.
930
        if ($this->type_id != QUESSECTIONTEXT) {
931
            if (!$nonumbering) {
932
                $pagetags->qnum = $qnum;
933
            }
934
            $required = '';
935
            if ($this->required()) {
936
                $required = html_writer::start_tag('div', ['class' => 'accesshide']);
937
                $required .= get_string('required', 'questionnaire');
938
                $required .= html_writer::end_tag('div');
939
                $required .= html_writer::empty_tag('img', ['class' => 'req', 'title' => get_string('required', 'questionnaire'),
940
                    'alt' => get_string('required', 'questionnaire'), 'src' => $OUTPUT->image_url('req')]);
941
            }
942
            $pagetags->required = $required; // Need to replace this with better renderer / template?
943
        }
944
        // If question text is "empty", i.e. 2 non-breaking spaces were inserted, empty it.
945
        if ($this->content == '<p>  </p>') {
946
            $this->content = '';
947
        }
948
        $pagetags->skippedclass = $skippedclass;
949
        if ($this->type_id == QUESNUMERIC || $this->type_id == QUESTEXT) {
950
            $pagetags->label = (object)['for' => self::qtypename($this->type_id) . $this->id];
951
        } else if ($this->type_id == QUESDROP) {
952
            $pagetags->label = (object)['for' => self::qtypename($this->type_id) . $this->name];
953
        } else if ($this->type_id == QUESESSAY) {
954
            $pagetags->label = (object)['for' => 'edit-q' . $this->id];
955
        }
956
        $options = ['noclean' => true, 'para' => false, 'filter' => true, 'context' => $this->context, 'overflowdiv' => true];
957
        $content = format_text(file_rewrite_pluginfile_urls($this->content, 'pluginfile.php',
958
            $this->context->id, 'mod_questionnaire', 'question', $this->id), FORMAT_HTML, $options);
959
        $pagetags->qcontent = $content;
960
 
961
        return $pagetags;
962
    }
963
 
964
    // This section contains functions for editing the specific question types.
965
    // There are required methods that must be implemented, and helper functions that can be used.
966
 
967
    // Required functions that can be overridden by the question type.
968
 
969
    /**
970
     * Override this, or any of the internal methods, to provide specific form data for editing the question type.
971
     * The structure of the elements here is the default layout for the question form.
972
     * @param edit_question_form $form The main moodleform object.
973
     * @param questionnaire $questionnaire The questionnaire being edited.
974
     * @return bool
975
     */
976
    public function edit_form(edit_question_form $form, questionnaire $questionnaire) {
977
        $mform =& $form->_form;
978
        $this->form_header($mform);
979
        $this->form_name($mform);
980
        $this->form_required($mform);
981
        $this->form_length($mform);
982
        $this->form_precise($mform);
983
        $this->form_question_text($mform, ($form->_customdata['modcontext'] ?? ''));
984
 
985
        if ($this->has_choices()) {
986
            // This is used only by the question editing form.
987
            $this->allchoices = $this->form_choices($mform);
988
        }
989
 
990
        $this->form_extradata($mform);
991
 
992
        // Added for advanced dependencies, parameter $editformobject is needed to use repeat_elements.
993
        if ($questionnaire->navigate > 0) {
994
            $this->form_dependencies($form, $questionnaire->questions);
995
        }
996
 
997
        // Exclude the save/cancel buttons from any collapsing sections.
998
        $mform->closeHeaderBefore('buttonar');
999
 
1000
        // Hidden fields.
1001
        $mform->addElement('hidden', 'id', 0);
1002
        $mform->setType('id', PARAM_INT);
1003
        $mform->addElement('hidden', 'qid', 0);
1004
        $mform->setType('qid', PARAM_INT);
1005
        $mform->addElement('hidden', 'sid', 0);
1006
        $mform->setType('sid', PARAM_INT);
1007
        $mform->addElement('hidden', 'type_id', $this->type_id);
1008
        $mform->setType('type_id', PARAM_INT);
1009
        $mform->addElement('hidden', 'action', 'question');
1010
        $mform->setType('action', PARAM_ALPHA);
1011
 
1012
        // Buttons.
1013
        $buttonarray[] = &$mform->createElement('submit', 'submitbutton', get_string('savechanges'));
1014
        if (isset($this->qid)) {
1015
            $buttonarray[] = &$mform->createElement('submit', 'makecopy', get_string('saveasnew', 'questionnaire'));
1016
        }
1017
        $buttonarray[] = &$mform->createElement('cancel');
1018
        $mform->addGroup($buttonarray, 'buttonar', '', [' '], false);
1019
 
1020
        return true;
1021
    }
1022
 
1023
    /**
1024
     * Add the form header.
1025
     * @param \MoodleQuickForm $mform
1026
     * @param string $helpname
1027
     */
1028
    protected function form_header(\MoodleQuickForm $mform, $helpname = '') {
1029
        // Display different messages for new question creation and existing question modification.
1030
        if (isset($this->qid) && !empty($this->qid)) {
1031
            $header = get_string('editquestion', 'questionnaire', questionnaire_get_type($this->type_id));
1032
        } else {
1033
            $header = get_string('addnewquestion', 'questionnaire', questionnaire_get_type($this->type_id));
1034
        }
1035
        if (empty($helpname)) {
1036
            $helpname = $this->helpname();
1037
        }
1038
 
1039
        $mform->addElement('header', 'questionhdredit', $header);
1040
        $mform->addHelpButton('questionhdredit', $helpname, 'questionnaire');
1041
    }
1042
 
1043
    /**
1044
     * Add the form name field.
1045
     * @param \MoodleQuickForm $mform
1046
     * @return \MoodleQuickForm
1047
     */
1048
    protected function form_name(\MoodleQuickForm $mform) {
1049
        $mform->addElement('text', 'name', get_string('optionalname', 'questionnaire'),
1050
                        ['size' => '30', 'maxlength' => '30']);
1051
        $mform->setType('name', PARAM_TEXT);
1052
        $mform->addHelpButton('name', 'optionalname', 'questionnaire');
1053
        return $mform;
1054
    }
1055
 
1056
    /**
1057
     * Add the form required field.
1058
     * @param \MoodleQuickForm $mform
1059
     * @return \MoodleQuickForm
1060
     */
1061
    protected function form_required(\MoodleQuickForm $mform) {
1062
        $reqgroup = [];
1063
        $reqgroup[] =& $mform->createElement('radio', 'required', '', get_string('yes'), 'y');
1064
        $reqgroup[] =& $mform->createElement('radio', 'required', '', get_string('no'), 'n');
1065
        $mform->addGroup($reqgroup, 'reqgroup', get_string('required', 'questionnaire'), ' ', false);
1066
        $mform->addHelpButton('reqgroup', 'required', 'questionnaire');
1067
        return $mform;
1068
    }
1069
 
1070
    /**
1071
     * Return the length form element.
1072
     * @param \MoodleQuickForm $mform
1073
     * @param string $helpname
1074
     */
1075
    protected function form_length(\MoodleQuickForm $mform, $helpname = '') {
1076
        self::form_length_text($mform, $helpname);
1077
    }
1078
 
1079
    /**
1080
     * Return the precision form element.
1081
     * @param \MoodleQuickForm $mform
1082
     * @param string $helpname
1083
     */
1084
    protected function form_precise(\MoodleQuickForm $mform, $helpname = '') {
1085
        self::form_precise_text($mform, $helpname);
1086
    }
1087
 
1088
    /**
1089
     * Determine form dependencies.
1090
     * @param \MoodleQuickForm $form The moodle form to add elements to.
1091
     * @param array $questions
1092
     * @return bool
1093
     */
1094
    protected function form_dependencies($form, $questions) {
1095
        // Create a new area for multiple dependencies.
1096
        $mform = $form->_form;
1097
        $position = ($this->position !== 0) ? $this->position : count($questions) + 1;
1098
        $dependencies = [];
1099
        $dependencies[''][0] = get_string('choosedots');
1100
        foreach ($questions as $question) {
1101
            if (($question->position < $position) && !empty($question->name) &&
1102
                !empty($dependopts = $question->get_dependency_options())) {
1103
                $dependencies[$question->name] = $dependopts;
1104
            }
1105
        }
1106
 
1107
        $children = [];
1108
        if (isset($this->qid)) {
1109
            // Use also for the delete dialogue later.
1110
            foreach ($questions as $questionlistitem) {
1111
                if ($questionlistitem->has_dependencies()) {
1112
                    foreach ($questionlistitem->dependencies as $key => $outerdependencies) {
1113
                        if ($outerdependencies->dependquestionid == $this->qid) {
1114
                            $children[$key] = $outerdependencies;
1115
                        }
1116
                    }
1117
                }
1118
            }
1119
        }
1120
 
1121
        if (count($dependencies) > 1) {
1122
            $mform->addElement('header', 'dependencies_hdr', get_string('dependencies', 'questionnaire'));
1123
            $mform->setExpanded('dependencies_hdr');
1124
            $mform->closeHeaderBefore('qst_and_choices_hdr');
1125
 
1126
            $dependenciescountand = 0;
1127
            $dependenciescountor = 0;
1128
 
1129
            foreach ($this->dependencies as $dependency) {
1130
                if ($dependency->dependandor == "and") {
1131
                    $dependenciescountand++;
1132
                } else if ($dependency->dependandor == "or") {
1133
                    $dependenciescountor++;
1134
                }
1135
            }
1136
 
1137
            /* I decided to allow changing dependencies of parent questions, because forcing the editor to remove dependencies
1138
             * bottom up, starting at the lowest child question is a pain for large questionnaires.
1139
             * So the following "if" becomes the default and the else-branch is completely commented.
1140
             * TODO Since the best way to get the list of child questions is currently to click on delete (and choose not to
1141
             * delete), one might consider to list the child questions in addition here.
1142
             */
1143
 
1144
            // Area for "must"-criteria.
1145
            $mform->addElement('static', 'mandatory', '',
1146
                '<div class="dimmed_text">' . get_string('mandatory', 'questionnaire') . '</div>');
1147
            $selectand = $mform->createElement('select', 'dependlogic_and', get_string('condition', 'questionnaire'),
1148
                [get_string('answernotgiven', 'questionnaire'), get_string('answergiven', 'questionnaire')]);
1149
            $selectand->setSelected('1');
1150
            $groupitemsand = [];
1151
            $groupitemsand[] =& $mform->createElement('selectgroups', 'dependquestions_and',
1152
                get_string('parent', 'questionnaire'), $dependencies);
1153
            $groupitemsand[] =& $selectand;
1154
            $groupand = $mform->createElement('group', 'selectdependencies_and', get_string('dependquestion', 'questionnaire'),
1155
                $groupitemsand, ' ', false);
1156
            $form->repeat_elements([$groupand], $dependenciescountand + 1, [],
1157
                'numdependencies_and', 'adddependencies_and', 2, null, true);
1158
 
1159
            // Area for "can"-criteria.
1160
            $mform->addElement('static', 'optional', '',
1161
                '<div class="dimmed_text">' . get_string('optional', 'questionnaire') . '</div>');
1162
            $selector = $mform->createElement('select', 'dependlogic_or', get_string('condition', 'questionnaire'),
1163
                [get_string('answernotgiven', 'questionnaire'), get_string('answergiven', 'questionnaire')]);
1164
            $selector->setSelected('1');
1165
            $groupitemsor = [];
1166
            $groupitemsor[] =& $mform->createElement('selectgroups', 'dependquestions_or',
1167
                get_string('parent', 'questionnaire'), $dependencies);
1168
            $groupitemsor[] =& $selector;
1169
            $groupor = $mform->createElement('group', 'selectdependencies_or', get_string('dependquestion', 'questionnaire'),
1170
                $groupitemsor, ' ', false);
1171
            $form->repeat_elements([$groupor], $dependenciescountor + 1, [], 'numdependencies_or',
1172
                'adddependencies_or', 2, null, true);
1173
        }
1174
        return true;
1175
    }
1176
 
1177
    /**
1178
     * Return the question text element.
1179
     * @param \MoodleQuickForm $mform
1180
     * @param string $context
1181
     * @return \MoodleQuickForm
1182
     */
1183
    protected function form_question_text(\MoodleQuickForm $mform, $context) {
1184
        $editoroptions = ['maxfiles' => EDITOR_UNLIMITED_FILES, 'trusttext' => true, 'context' => $context];
1185
        $mform->addElement('editor', 'content', get_string('text', 'questionnaire'), null, $editoroptions);
1186
        $mform->setType('content', PARAM_RAW);
1187
        $mform->addRule('content', null, 'required', null, 'client');
1188
        return $mform;
1189
    }
1190
 
1191
    /**
1192
     * Add the choices to the form.
1193
     * @param \MoodleQuickForm $mform
1194
     * @return string
1195
     */
1196
    protected function form_choices(\MoodleQuickForm $mform) {
1197
        if ($this->has_choices()) {
1198
            $numchoices = count($this->choices);
1199
            $allchoices = '';
1200
            foreach ($this->choices as $choice) {
1201
                if (!empty($allchoices)) {
1202
                    $allchoices .= "\n";
1203
                }
1204
                $allchoices .= $choice->content;
1205
            }
1206
 
1207
            $helpname = $this->helpname();
1208
 
1209
            $mform->addElement('html', '<div class="qoptcontainer">');
1210
            $options = ['wrap' => 'virtual', 'class' => 'qopts'];
1211
            $mform->addElement('textarea', 'allchoices', get_string('possibleanswers', 'questionnaire'), $options);
1212
            $mform->setType('allchoices', PARAM_RAW);
1213
            $mform->addRule('allchoices', null, 'required', null, 'client');
1214
            $mform->addHelpButton('allchoices', $helpname, 'questionnaire');
1215
            $mform->addElement('html', '</div>');
1216
            $mform->addElement('hidden', 'num_choices', $numchoices);
1217
            $mform->setType('num_choices', PARAM_INT);
1218
        }
1219
        return $allchoices;
1220
    }
1221
 
1222
    /**
1223
     * Override if the question uses the extradata field.
1224
     * @param \MoodleQuickForm $mform
1225
     * @param string $helpname
1226
     * @return \MoodleQuickForm
1227
     */
1228
    protected function form_extradata(\MoodleQuickForm $mform, $helpname = '') {
1229
        $mform->addElement('hidden', 'extradata');
1230
        $mform->setType('extradata', PARAM_INT);
1231
        return $mform;
1232
    }
1233
 
1234
    // Helper functions for commonly used editing functions.
1235
 
1236
    /**
1237
     * Add the length element as hidden.
1238
     * @param \MoodleQuickForm $mform
1239
     * @param int $value
1240
     * @return \MoodleQuickForm
1241
     */
1242
    public static function form_length_hidden(\MoodleQuickForm $mform, $value = 0) {
1243
        $mform->addElement('hidden', 'length', $value);
1244
        $mform->setType('length', PARAM_INT);
1245
        return $mform;
1246
    }
1247
 
1248
    /**
1249
     * Add the length element as text.
1250
     * @param \MoodleQuickForm $mform
1251
     * @param string $helpname
1252
     * @param int $value
1253
     * @return \MoodleQuickForm
1254
     */
1255
    public static function form_length_text(\MoodleQuickForm $mform, $helpname = '', $value = 0) {
1256
        $mform->addElement('text', 'length', get_string($helpname, 'questionnaire'), ['size' => '1'], $value);
1257
        $mform->setType('length', PARAM_INT);
1258
        if (!empty($helpname)) {
1259
            $mform->addHelpButton('length', $helpname, 'questionnaire');
1260
        }
1261
        return $mform;
1262
    }
1263
 
1264
    /**
1265
     * Add the precise element as hidden.
1266
     * @param \MoodleQuickForm $mform
1267
     * @param int $value
1268
     * @return \MoodleQuickForm
1269
     */
1270
    public static function form_precise_hidden(\MoodleQuickForm $mform, $value = 0) {
1271
        $mform->addElement('hidden', 'precise', $value);
1272
        $mform->setType('precise', PARAM_INT);
1273
        return $mform;
1274
    }
1275
 
1276
    /**
1277
     * Add the precise element as text.
1278
     * @param \MoodleQuickForm $mform
1279
     * @param string $helpname
1280
     * @param int $value
1281
     * @return \MoodleQuickForm
1282
     * @throws \coding_exception
1283
     */
1284
    public static function form_precise_text(\MoodleQuickForm $mform, $helpname = '', $value = 0) {
1285
        $mform->addElement('text', 'precise', get_string($helpname, 'questionnaire'), ['size' => '1']);
1286
        $mform->setType('precise', PARAM_INT);
1287
        if (!empty($helpname)) {
1288
            $mform->addHelpButton('precise', $helpname, 'questionnaire');
1289
        }
1290
        return $mform;
1291
    }
1292
 
1293
    /**
1294
     * Create and update question data from the forms.
1295
     * @param \stdClass $formdata
1296
     * @param questionnaire $questionnaire
1297
     */
1298
    public function form_update($formdata, $questionnaire) {
1299
        global $DB;
1300
 
1301
        $this->form_preprocess_data($formdata);
1302
        if (!empty($formdata->qid)) {
1303
 
1304
            // Update existing question.
1305
            // Handle any attachments in the content.
1306
            $formdata->itemid = $formdata->content['itemid'];
1307
            $formdata->format = $formdata->content['format'];
1308
            $formdata->content = $formdata->content['text'];
1309
            $formdata->content = file_save_draft_area_files($formdata->itemid, $questionnaire->context->id, 'mod_questionnaire',
1310
                'question', $formdata->qid, ['subdirs' => true], $formdata->content);
1311
 
1312
            $fields = ['name', 'type_id', 'length', 'precise', 'required', 'content', 'extradata'];
1313
            $questionrecord = new \stdClass();
1314
            $questionrecord->id = $formdata->qid;
1315
            foreach ($fields as $f) {
1316
                if (isset($formdata->$f)) {
1317
                    $questionrecord->$f = trim($formdata->$f);
1318
                }
1319
            }
1320
 
1321
            $this->update($questionrecord, false);
1322
 
1323
            if ($questionnaire->has_dependencies()) {
1324
                questionnaire_check_page_breaks($questionnaire);
1325
            }
1326
        } else {
1327
            // Create new question:
1328
            // Need to update any image content after the question is created, so create then update the content.
1329
            $formdata->surveyid = $formdata->sid;
1330
            $fields = ['surveyid', 'name', 'type_id', 'length', 'precise', 'required', 'position', 'extradata'];
1331
            $questionrecord = new \stdClass();
1332
            foreach ($fields as $f) {
1333
                if (isset($formdata->$f)) {
1334
                    $questionrecord->$f = trim($formdata->$f);
1335
                }
1336
            }
1337
            $questionrecord->content = '';
1338
 
1339
            $this->add($questionrecord);
1340
 
1341
            // Handle any attachments in the content.
1342
            $formdata->itemid = $formdata->content['itemid'];
1343
            $formdata->format = $formdata->content['format'];
1344
            $formdata->content = $formdata->content['text'];
1345
            $content = file_save_draft_area_files($formdata->itemid, $questionnaire->context->id, 'mod_questionnaire',
1346
                'question', $this->qid, ['subdirs' => true], $formdata->content);
1347
            $DB->set_field('questionnaire_question', 'content', $content, ['id' => $this->qid]);
1348
        }
1349
 
1350
        if ($this->has_choices()) {
1351
            // Now handle any choice updates.
1352
            $cidx = 0;
1353
            if (isset($this->choices) && !isset($formdata->makecopy)) {
1354
                $oldcount = count($this->choices);
1355
                $echoice = reset($this->choices);
1356
                $ekey = key($this->choices);
1357
            } else {
1358
                $oldcount = 0;
1359
            }
1360
 
1361
            $newchoices = explode("\n", $formdata->allchoices);
1362
            $nidx = 0;
1363
            $newcount = count($newchoices);
1364
 
1365
            while (($nidx < $newcount) && ($cidx < $oldcount)) {
1366
                if ($newchoices[$nidx] != $echoice->content) {
1367
                    $choicerecord = new \stdClass();
1368
                    $choicerecord->id = $ekey;
1369
                    $choicerecord->question_id = $this->qid;
1370
                    $choicerecord->content = trim($newchoices[$nidx]);
1371
                    $r = preg_match_all("/^(\d{1,2})(=.*)$/", $newchoices[$nidx], $matches);
1372
                    // This choice has been attributed a "score value" OR this is a rate question type.
1373
                    if ($r) {
1374
                        $newscore = $matches[1][0];
1375
                        $choicerecord->value = $newscore;
1376
                    } else {     // No score value for this choice.
1377
                        $choicerecord->value = null;
1378
                    }
1379
                    $this->update_choice($choicerecord);
1380
                }
1381
                $nidx++;
1382
                $echoice = next($this->choices);
1383
                $ekey = key($this->choices);
1384
                $cidx++;
1385
            }
1386
 
1387
            while ($nidx < $newcount) {
1388
                // New choices.
1389
                $choicerecord = new \stdClass();
1390
                $choicerecord->question_id = $this->qid;
1391
                $choicerecord->content = trim($newchoices[$nidx]);
1392
                $r = preg_match_all("/^(\d{1,2})(=.*)$/", $choicerecord->content, $matches);
1393
                // This choice has been attributed a "score value" OR this is a rate question type.
1394
                if ($r) {
1395
                    $choicerecord->value = $matches[1][0];
1396
                }
1397
                $this->add_choice($choicerecord);
1398
                $nidx++;
1399
            }
1400
 
1401
            while ($cidx < $oldcount) {
1402
                end($this->choices);
1403
                $ekey = key($this->choices);
1404
                $this->delete_choice($ekey);
1405
                $cidx++;
1406
            }
1407
        }
1408
 
1409
        // Now handle the dependencies the same way as choices.
1410
        // Shouldn't the MOODLE-API provide this case of insert/update/delete?.
1411
        // First handle dependendies updates.
1412
        if (!isset($formdata->fixed_deps)) {
1413
            if ($this->has_dependencies() && !isset($formdata->makecopy)) {
1414
                $oldcount = count($this->dependencies);
1415
                $edependency = reset($this->dependencies);
1416
                $ekey = key($this->dependencies);
1417
            } else {
1418
                $oldcount = 0;
1419
            }
1420
 
1421
            $cidx = 0;
1422
            $nidx = 0;
1423
 
1424
            // All 3 arrays in this object have the same length.
1425
            if (isset($formdata->dependquestion)) {
1426
                $newcount = count($formdata->dependquestion);
1427
            } else {
1428
                $newcount = 0;
1429
            }
1430
            while (($nidx < $newcount) && ($cidx < $oldcount)) {
1431
                if ($formdata->dependquestion[$nidx] != $edependency->dependquestionid ||
1432
                    $formdata->dependchoice[$nidx] != $edependency->dependchoiceid ||
1433
                    $formdata->dependlogic_cleaned[$nidx] != $edependency->dependlogic ||
1434
                    $formdata->dependandor[$nidx] != $edependency->dependandor) {
1435
 
1436
                    $dependencyrecord = new \stdClass();
1437
                    $dependencyrecord->id = $ekey;
1438
                    $dependencyrecord->questionid = $this->qid;
1439
                    $dependencyrecord->surveyid = $this->surveyid;
1440
                    $dependencyrecord->dependquestionid = $formdata->dependquestion[$nidx];
1441
                    $dependencyrecord->dependchoiceid = $formdata->dependchoice[$nidx];
1442
                    $dependencyrecord->dependlogic = $formdata->dependlogic_cleaned[$nidx];
1443
                    $dependencyrecord->dependandor = $formdata->dependandor[$nidx];
1444
 
1445
                    $this->update_dependency($dependencyrecord);
1446
                }
1447
                $nidx++;
1448
                $edependency = next($this->dependencies);
1449
                $ekey = key($this->dependencies);
1450
                $cidx++;
1451
            }
1452
 
1453
            while ($nidx < $newcount) {
1454
                // New dependencies.
1455
                $dependencyrecord = new \stdClass();
1456
                $dependencyrecord->questionid = $this->qid;
1457
                $dependencyrecord->surveyid = $formdata->sid;
1458
                $dependencyrecord->dependquestionid = $formdata->dependquestion[$nidx];
1459
                $dependencyrecord->dependchoiceid = $formdata->dependchoice[$nidx];
1460
                $dependencyrecord->dependlogic = $formdata->dependlogic_cleaned[$nidx];
1461
                $dependencyrecord->dependandor = $formdata->dependandor[$nidx];
1462
 
1463
                $this->add_dependency($dependencyrecord);
1464
                $nidx++;
1465
            }
1466
 
1467
            while ($cidx < $oldcount) {
1468
                end($this->dependencies);
1469
                $ekey = key($this->dependencies);
1470
                $this->delete_dependency($ekey);
1471
                $cidx++;
1472
            }
1473
        }
1474
    }
1475
 
1476
    /**
1477
     * Any preprocessing of general data.
1478
     * @param \stdClass $formdata
1479
     * @return bool
1480
     */
1481
    protected function form_preprocess_data($formdata) {
1482
        if ($this->has_choices()) {
1483
            // Eliminate trailing blank lines.
1484
            $formdata->allchoices = preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $formdata->allchoices);
1485
            // Trim to eliminate potential trailing carriage return.
1486
            $formdata->allchoices = trim($formdata->allchoices);
1487
            $this->form_preprocess_choicedata($formdata);
1488
        }
1489
 
1490
        // Dependencies logic does not (yet) need preprocessing, might change with more complex conditions.
1491
        // Check, if entries exist and whether they are not only 0 (form elements created but no value selected).
1492
        if (isset($formdata->dependquestions_and) &&
1493
            !(count(array_keys($formdata->dependquestions_and, 0, true)) == count($formdata->dependquestions_and))) {
1494
            for ($i = 0; $i < count($formdata->dependquestions_and); $i++) {
1495
                $dependency = explode(",", $formdata->dependquestions_and[$i]);
1496
 
1497
                if ($dependency[0] != 0) {
1498
                    $formdata->dependquestion[] = $dependency[0];
1499
                    $formdata->dependchoice[] = $dependency[1];
1500
                    $formdata->dependlogic_cleaned[] = $formdata->dependlogic_and[$i];
1501
                    $formdata->dependandor[] = "and";
1502
                }
1503
            }
1504
        }
1505
 
1506
        if (isset($formdata->dependquestions_or) &&
1507
            !(count(array_keys($formdata->dependquestions_or, 0, true)) == count($formdata->dependquestions_or))) {
1508
            for ($i = 0; $i < count($formdata->dependquestions_or); $i++) {
1509
                $dependency = explode(",", $formdata->dependquestions_or[$i]);
1510
 
1511
                if ($dependency[0] != 0) {
1512
                    $formdata->dependquestion[] = $dependency[0];
1513
                    $formdata->dependchoice[] = $dependency[1];
1514
                    $formdata->dependlogic_cleaned[] = $formdata->dependlogic_or[$i];
1515
                    $formdata->dependandor[] = "or";
1516
                }
1517
            }
1518
        }
1519
        return true;
1520
    }
1521
 
1522
    /**
1523
     * Override this function for question specific choice preprocessing.
1524
     * @param \stdClass $formdata
1525
     * @return false
1526
     */
1527
    protected function form_preprocess_choicedata($formdata) {
1528
        if (empty($formdata->allchoices)) {
1529
            error (get_string('enterpossibleanswers', 'questionnaire'));
1530
        }
1531
        return false;
1532
    }
1533
 
1534
    /**
1535
     * True if question provides mobile support.
1536
     * @return bool
1537
     */
1538
    public function supports_mobile() {
1539
        return false;
1540
    }
1541
 
1542
    /**
1543
     * Override and return false if not supporting mobile app.
1544
     * @param int $qnum
1545
     * @param bool $autonum
1546
     * @return \stdClass
1547
     */
1548
    public function mobile_question_display($qnum, $autonum = false) {
1549
        $options = ['noclean' => true, 'para' => false, 'filter' => true,
1550
            'context' => $this->context, 'overflowdiv' => true];
1551
        $mobiledata = (object)[
1552
            'id' => $this->id,
1553
            'name' => $this->name,
1554
            'type_id' => $this->type_id,
1555
            'length' => $this->length,
1556
            'content' => format_text(file_rewrite_pluginfile_urls($this->content, 'pluginfile.php', $this->context->id,
1557
                'mod_questionnaire', 'question', $this->id), FORMAT_HTML, $options),
1558
            'content_stripped' => strip_tags($this->content),
1559
            'required' => ($this->required == 'y') ? 1 : 0,
1560
            'deleted' => $this->deleted,
1561
            'response_table' => $this->responsetable,
1562
            'fieldkey' => $this->mobile_fieldkey(),
1563
            'precise' => $this->precise,
1564
            'qnum' => $qnum,
1565
            'errormessage' => get_string('required') . ': ' . $this->name
1566
        ];
1567
        $mobiledata->choices = $this->mobile_question_choices_display();
1568
 
1569
        if ($this->mobile_question_extradata_display()) {
1570
            $mobiledata->extradata = json_decode($this->extradata);
1571
        }
1572
        if ($autonum) {
1573
            $mobiledata->content = $qnum . '. ' . $mobiledata->content;
1574
            $mobiledata->content_stripped = $qnum . '. ' . $mobiledata->content_stripped;
1575
        }
1576
        $mobiledata->responses = '';
1577
        return $mobiledata;
1578
    }
1579
 
1580
    /**
1581
     * Override and return false if not supporting mobile app.
1582
     * @return array
1583
     */
1584
    public function mobile_question_choices_display() {
1585
        $choices = [];
1586
        $cnum = 0;
1587
        if ($this->has_choices()) {
1588
            foreach ($this->choices as $choice) {
1589
                $choices[$cnum] = clone($choice);
1590
                $contents = questionnaire_choice_values($choice->content);
1591
                $choices[$cnum]->content = format_text($contents->text, FORMAT_HTML, ['noclean' => true]).$contents->image;
1592
                $cnum++;
1593
            }
1594
        }
1595
        return $choices;
1596
    }
1597
 
1598
    /**
1599
     * Return a field key to be used by the mobile app.
1600
     * @param int $choiceid
1601
     * @return string
1602
     */
1603
    public function mobile_fieldkey($choiceid = 0) {
1604
        $choicefield = '';
1605
        if ($choiceid !== 0) {
1606
            $choicefield = '_' . $choiceid;
1607
        }
1608
        return 'response_' . $this->type_id . '_' . $this->id . $choicefield;
1609
    }
1610
 
1611
    /**
1612
     * Return the mobile response data.
1613
     * @param response $response
1614
     * @return array
1615
     */
1616
    public function get_mobile_response_data($response) {
1617
        $resultdata = [];
1618
        if (isset($response->answers[$this->id][0])) {
1619
            $resultdata[$this->mobile_fieldkey()] = $response->answers[$this->id][0]->value;
1620
        } else {
1621
            $resultdata[$this->mobile_fieldkey()] = false;
1622
        }
1623
 
1624
        return $resultdata;
1625
    }
1626
 
1627
    /**
1628
     * True if question need extradata for mobile app.
1629
     *
1630
     * @return bool
1631
     */
1632
    public function mobile_question_extradata_display() {
1633
        return false;
1634
    }
1635
 
1636
    /**
1637
     * Return the otherdata to be used by the mobile app.
1638
     *
1639
     * @return array
1640
     */
1641
    public function mobile_otherdata() {
1642
        return [];
1643
    }
1644
}