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
use mod_questionnaire\feedback\section;
18
 
19
defined('MOODLE_INTERNAL') || die();
20
 
21
require_once($CFG->dirroot.'/mod/questionnaire/locallib.php');
22
 
23
/**
24
 * Provided the main API functions for questionnaire.
25
 *
26
 * @package mod_questionnaire
27
 * @copyright  2016 Mike Churchward (mike.churchward@poetgroup.org)
28
 * @author     Mike Churchward
29
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30
 */
31
class questionnaire {
32
 
33
    // Class Properties.
34
 
35
    /**
36
     * @var \mod_questionnaire\question\question[] $quesitons
37
     */
38
    public $questions = [];
39
 
40
    /**
41
     * The survey record.
42
     * @var object $survey
43
     */
44
     // Todo var $survey; TODO.
45
 
46
    /**
47
     * @var $renderer Contains the page renderer when loaded, or false if not.
48
     */
49
    public $renderer = false;
50
 
51
    /**
52
     * @var $page Contains the renderable, templatable page when loaded, or false if not.
53
     */
54
    public $page = false;
55
 
56
    // Class Methods.
57
 
58
    /**
59
     * The constructor.
60
     * @param stdClass $course
61
     * @param stdClass $cm
62
     * @param int $id
63
     * @param null|stdClass $questionnaire
64
     * @param bool $addquestions
65
     * @throws dml_exception
66
     */
67
    public function __construct(&$course, &$cm, $id = 0, $questionnaire = null, $addquestions = true) {
68
        global $DB;
69
 
70
        if ($id) {
71
            $questionnaire = $DB->get_record('questionnaire', array('id' => $id));
72
        }
73
 
74
        if (is_object($questionnaire)) {
75
            $properties = get_object_vars($questionnaire);
76
            foreach ($properties as $property => $value) {
77
                $this->$property = $value;
78
            }
79
        }
80
 
81
        if (!empty($this->sid)) {
82
            $this->add_survey($this->sid);
83
        }
84
 
85
        $this->course = $course;
86
        $this->cm = $cm;
87
        // When we are creating a brand new questionnaire, we will not yet have a context.
88
        if (!empty($cm) && !empty($this->id)) {
89
            $this->context = context_module::instance($cm->id);
90
        } else {
91
            $this->context = null;
92
        }
93
 
94
        if ($addquestions && !empty($this->sid)) {
95
            $this->add_questions($this->sid);
96
        }
97
 
98
        // Load the capabilities for this user and questionnaire, if not creating a new one.
99
        if (!empty($this->cm->id)) {
100
            $this->capabilities = questionnaire_load_capabilities($this->cm->id);
101
        }
102
 
103
        // Don't automatically add responses.
104
        $this->responses = [];
105
    }
106
 
107
    /**
108
     * Adding a survey record to the object.
109
     * @param int $sid
110
     * @param null $survey
111
     */
112
    public function add_survey($sid = 0, $survey = null) {
113
        global $DB;
114
 
115
        if ($sid) {
116
            $this->survey = $DB->get_record('questionnaire_survey', array('id' => $sid));
117
        } else if (is_object($survey)) {
118
            $this->survey = clone($survey);
119
        }
120
    }
121
 
122
    /**
123
     * Adding questions to the object.
124
     * @param bool $sid
125
     */
126
    public function add_questions($sid = false) {
127
        global $DB;
128
 
129
        if ($sid === false) {
130
            $sid = $this->sid;
131
        }
132
 
133
        if (!isset($this->questions)) {
134
            $this->questions = [];
135
            $this->questionsbysec = [];
136
        }
137
 
138
        $select = 'surveyid = ? AND deleted = ?';
139
        $params = [$sid, 'n'];
140
        if ($records = $DB->get_records_select('questionnaire_question', $select, $params, 'position')) {
141
            $sec = 1;
142
            $isbreak = false;
143
            foreach ($records as $record) {
144
 
145
                $this->questions[$record->id] = \mod_questionnaire\question\question::question_builder($record->type_id,
146
                    $record, $this->context);
147
 
148
                if ($record->type_id != QUESPAGEBREAK) {
149
                    $this->questionsbysec[$sec][] = $record->id;
150
                    $isbreak = false;
151
                } else {
152
                    // Sanity check: no section break allowed as first position, no 2 consecutive section breaks.
153
                    if ($record->position != 1 && $isbreak == false) {
154
                        $sec++;
155
                        $isbreak = true;
156
                    }
157
                }
158
            }
159
        }
160
    }
161
 
162
    /**
163
     * Load all response information for this user.
164
     *
165
     * @param int $userid
166
     */
167
    public function add_user_responses($userid = null) {
168
        global $USER, $DB;
169
 
170
        // Empty questionnaires cannot have responses.
171
        if (empty($this->id)) {
172
            return;
173
        }
174
 
175
        if ($userid === null) {
176
            $userid = $USER->id;
177
        }
178
 
179
        $responses = $this->get_responses($userid);
180
        foreach ($responses as $response) {
181
            $this->responses[$response->id] = mod_questionnaire\responsetype\response\response::create_from_data($response);
182
        }
183
    }
184
 
185
    /**
186
     * Load the specified response information.
187
     *
188
     * @param int $responseid
189
     */
190
    public function add_response(int $responseid) {
191
        global $DB;
192
 
193
        // Empty questionnaires cannot have responses.
194
        if (empty($this->id)) {
195
            return;
196
        }
197
 
198
        $response = $DB->get_record('questionnaire_response', ['id' => $responseid]);
199
        $this->responses[$response->id] = mod_questionnaire\responsetype\response\response::create_from_data($response);
200
    }
201
 
202
    /**
203
     * Load the response information from a submitted web form.
204
     *
205
     * @param stdClass $formdata
206
     */
207
    public function add_response_from_formdata(stdClass $formdata) {
208
        $this->responses[0] = mod_questionnaire\responsetype\response\response::response_from_webform($formdata, $this->questions);
209
    }
210
 
211
    /**
212
     * Return a response object from a submitted mobile app form.
213
     *
214
     * @param stdClass $appdata
215
     * @param int $sec
216
     * @return bool|\mod_questionnaire\responsetype\response\response
217
     */
218
    public function build_response_from_appdata(stdClass $appdata, $sec=0) {
219
        $questions = [];
220
        if ($sec == 0) {
221
            $questions = $this->questions;
222
        } else {
223
            foreach ($this->questionsbysec[$sec] as $questionid) {
224
                $questions[$questionid] = $this->questions[$questionid];
225
            }
226
        }
227
        return mod_questionnaire\responsetype\response\response::response_from_appdata($this->id, 0, $appdata, $questions);
228
    }
229
 
230
    /**
231
     * Add the renderer to the questionnaire object.
232
     * @param plugin_renderer_base $renderer The module renderer, extended from core renderer.
233
     */
234
    public function add_renderer(plugin_renderer_base $renderer) {
235
        $this->renderer = $renderer;
236
    }
237
 
238
    /**
239
     * Add the templatable page to the questionnaire object.
240
     * @param templatable $page The page to render, implementing core classes.
241
     */
242
    public function add_page($page) {
243
        $this->page = $page;
244
    }
245
 
246
    /**
247
     * Return true if questions should be automatically numbered.
248
     * @return bool
249
     */
250
    public function questions_autonumbered() {
251
        // Value of 1 if questions should be numbered. Value of 3 if both questions and pages should be numbered.
252
        return (!empty($this->autonum) && (($this->autonum == 1) || ($this->autonum == 3)));
253
    }
254
 
255
    /**
256
     * Return true if pages should be automatically numbered.
257
     * @return bool
258
     */
259
    public function pages_autonumbered() {
260
        // Value of 2 if pages should be numbered. Value of 3 if both questions and pages should be numbered.
261
        return (!empty($this->autonum) && (($this->autonum == 2) || ($this->autonum == 3)));
262
    }
263
 
264
    /**
265
     * The main module view function.
266
     */
267
    public function view() {
268
        global $CFG, $USER, $PAGE;
269
 
270
        $PAGE->set_title(format_string($this->name));
271
        $PAGE->set_heading(format_string($this->course->fullname));
272
 
273
        // Initialise the JavaScript.
274
        $PAGE->requires->js_init_call('M.mod_questionnaire.init_attempt_form', null, false, questionnaire_get_js_module());
275
 
276
        $message = $this->user_access_messages($USER->id, true);
277
        if ($message !== false) {
278
            $this->page->add_to_page('notifications', $message);
279
        } else {
280
            // Handle the main questionnaire completion page.
281
            $quser = $USER->id;
282
 
283
            $msg = $this->print_survey($quser, $USER->id);
284
 
285
            // If Questionnaire was submitted with all required fields completed ($msg is empty),
286
            // then record the submittal.
287
            $viewform = data_submitted($CFG->wwwroot."/mod/questionnaire/complete.php");
288
            if ($viewform && confirm_sesskey() && isset($viewform->submit) && isset($viewform->submittype) &&
289
                ($viewform->submittype == "Submit Survey") && empty($msg)) {
290
                if (!empty($viewform->rid)) {
291
                    $viewform->rid = (int)$viewform->rid;
292
                }
293
                if (!empty($viewform->sec)) {
294
                    $viewform->sec = (int)$viewform->sec;
295
                }
296
                $this->response_delete($viewform->rid, $viewform->sec);
297
                $this->rid = $this->response_insert($viewform, $quser);
298
                $this->response_commit($this->rid);
299
 
300
                $this->update_grades($quser);
301
 
302
                // Update completion state.
303
                $completion = new completion_info($this->course);
304
                if ($completion->is_enabled($this->cm) && $this->completionsubmit) {
305
                    $completion->update_state($this->cm, COMPLETION_COMPLETE);
306
                }
307
 
308
                // Log this submitted response. Note this removes the anonymity in the logged event.
309
                $context = context_module::instance($this->cm->id);
310
                $anonymous = $this->respondenttype == 'anonymous';
311
                $params = array(
312
                    'context' => $context,
313
                    'courseid' => $this->course->id,
314
                    'relateduserid' => $USER->id,
315
                    'anonymous' => $anonymous,
316
                    'other' => array('questionnaireid' => $this->id)
317
                );
318
                $event = \mod_questionnaire\event\attempt_submitted::create($params);
319
                $event->trigger();
320
 
321
                $this->submission_notify($this->rid);
322
                $this->response_goto_thankyou();
323
            }
324
        }
325
    }
326
 
327
    /**
328
     * Delete the specified response, and insert a new one.
329
     * @param int $rid
330
     * @param int $sec
331
     * @param int $quser
332
     * @return bool|int
333
     */
334
    public function delete_insert_response($rid, $sec, $quser) {
335
        $this->response_delete($rid, $sec);
336
        $this->rid = $this->response_insert((object)['sec' => $sec, 'rid' => $rid], $quser);
337
        return $this->rid;
338
    }
339
 
340
    /**
341
     * Commit the response.
342
     * @param int $rid
343
     * @param int $quser
344
     */
345
    public function commit_submission_response($rid, $quser) {
346
        $this->response_commit($rid);
347
        // If it was a previous save, rid is in the form...
348
        if (!empty($rid) && is_numeric($rid)) {
349
            $rid = $rid;
350
            // Otherwise its in this object.
351
        } else {
352
            $rid = $this->rid;
353
        }
354
 
355
        $this->update_grades($quser);
356
 
357
        // Update completion state.
358
        $completion = new \completion_info($this->course);
359
        if ($completion->is_enabled($this->cm) && $this->completionsubmit) {
360
            $completion->update_state($this->cm, COMPLETION_COMPLETE);
361
        }
362
        // Log this submitted response.
363
        $context = \context_module::instance($this->cm->id);
364
        $anonymous = $this->respondenttype == 'anonymous';
365
        $params = [
366
            'context' => $context,
367
            'courseid' => $this->course->id,
368
            'relateduserid' => $quser,
369
            'anonymous' => $anonymous,
370
            'other' => array('questionnaireid' => $this->id)
371
        ];
372
        $event = \mod_questionnaire\event\attempt_submitted::create($params);
373
        $event->trigger();
374
    }
375
 
376
    /**
377
     * Update the grade for this questionnaire and user.
378
     *
379
     * @param int $userid
380
     */
381
    private function update_grades($userid) {
382
        if ($this->grade != 0) {
383
            $questionnaire = new \stdClass();
384
            $questionnaire->id = $this->id;
385
            $questionnaire->name = $this->name;
386
            $questionnaire->grade = $this->grade;
387
            $questionnaire->cmidnumber = $this->cm->idnumber;
388
            $questionnaire->courseid = $this->course->id;
389
            questionnaire_update_grades($questionnaire, $userid);
390
        }
391
    }
392
 
393
    /**
394
     * Function to view an entire responses data.
395
     * @param int $rid
396
     * @param string $referer
397
     * @param string $resps
398
     * @param bool $compare
399
     * @param bool $isgroupmember
400
     * @param bool $allresponses
401
     * @param int $currentgroupid
402
     * @param string $outputtarget
403
     */
404
    public function view_response($rid, $referer= '', $resps = '', $compare = false, $isgroupmember = false, $allresponses = false,
405
                                  $currentgroupid = 0, $outputtarget = 'html') {
406
        $this->print_survey_start('', 1, 1, 0, $rid, false, $outputtarget);
407
 
408
        $i = 0;
409
        $this->add_response($rid);
410
        if ($referer != 'print') {
411
            $feedbackmessages = $this->response_analysis($rid, $resps, $compare, $isgroupmember, $allresponses, $currentgroupid);
412
 
413
            if ($feedbackmessages) {
414
                $msgout = '';
415
                foreach ($feedbackmessages as $msg) {
416
                    $msgout .= $msg;
417
                }
418
                $this->page->add_to_page('feedbackmessages', $msgout);
419
            }
420
 
421
            if ($this->survey->feedbacknotes) {
422
                $text = file_rewrite_pluginfile_urls($this->survey->feedbacknotes, 'pluginfile.php',
423
                    $this->context->id, 'mod_questionnaire', 'feedbacknotes', $this->survey->id);
424
                $this->page->add_to_page('feedbacknotes', $this->renderer->box(format_text($text, FORMAT_HTML)));
425
            }
426
        }
427
        $pdf = ($outputtarget == 'pdf') ? true : false;
428
        foreach ($this->questions as $question) {
429
            if ($question->type_id < QUESPAGEBREAK) {
430
                $i++;
431
            }
432
            if ($question->type_id != QUESPAGEBREAK) {
433
                $this->page->add_to_page('responses',
434
                    $this->renderer->response_output($question, $this->responses[$rid], $i, $pdf));
435
            }
436
        }
437
    }
438
 
439
    /**
440
     * Function to view all loaded responses.
441
     */
442
    public function view_all_responses() {
443
        $this->print_survey_start('', 1, 1, 0);
444
 
445
        // If a student's responses have been deleted by teacher while student was viewing the report,
446
        // then responses may have become empty, hence this test is necessary.
447
 
448
        if (!empty($this->responses)) {
449
            $this->page->add_to_page('responses', $this->renderer->all_response_output($this->responses, $this->questions));
450
        } else {
451
            $this->page->add_to_page('responses', $this->renderer->all_response_output(get_string('noresponses', 'questionnaire')));
452
        }
453
 
454
        $this->print_survey_end(1, 1);
455
    }
456
 
457
    // Access Methods.
458
 
459
    /**
460
     * True if the questionnaire is active.
461
     * @return bool
462
     */
463
    public function is_active() {
464
        return (!empty($this->survey));
465
    }
466
 
467
    /**
468
     * True if the questionnaire is open.
469
     * @return bool
470
     */
471
    public function is_open() {
472
        return ($this->opendate > 0) ? ($this->opendate < time()) : true;
473
    }
474
 
475
    /**
476
     * True if the questionnaire is closed.
477
     * @return bool
478
     */
479
    public function is_closed() {
480
        return ($this->closedate > 0) ? ($this->closedate < time()) : false;
481
    }
482
 
483
    /**
484
     * True if the specified user can complete this questionnaire.
485
     * @param int $userid
486
     * @return bool
487
     */
488
    public function user_can_take($userid) {
489
 
490
        if (!$this->is_active() || !$this->user_is_eligible($userid)) {
491
            return false;
492
        } else if ($this->qtype == QUESTIONNAIREUNLIMITED) {
493
            return true;
494
        } else if ($userid > 0) {
495
            return $this->user_time_for_new_attempt($userid);
496
        } else {
497
            return false;
498
        }
499
    }
500
 
501
    /**
502
     * True if the specified user is eligible to complete this questionnaire.
503
     * @param int $userid
504
     * @return bool
505
     */
506
    public function user_is_eligible($userid) {
507
        return ($this->capabilities->view && $this->capabilities->submit);
508
    }
509
 
510
    /**
511
     * Return any message if the user cannot complete this questionnaire, explaining why.
512
     * @param int $userid
513
     * @param bool $asnotification Return as a rendered notification.
514
     * @return bool|string
515
     */
516
    public function user_access_messages($userid = 0, $asnotification = false) {
517
        global $USER;
518
 
519
        if ($userid == 0) {
520
            $userid = $USER->id;
521
        }
522
        $message = false;
523
 
524
        if (!$this->is_active()) {
525
            if ($this->capabilities->manage) {
526
                $msg = 'removenotinuse';
527
            } else {
528
                $msg = 'notavail';
529
            }
530
            $message = get_string($msg, 'questionnaire');
531
 
532
        } else if ($this->survey->realm == 'template') {
533
            $message = get_string('templatenotviewable', 'questionnaire');
534
 
535
        } else if (!$this->is_open()) {
536
            $message = get_string('notopen', 'questionnaire', userdate($this->opendate));
537
 
538
        } else if ($this->is_closed()) {
539
            $message = get_string('closed', 'questionnaire', userdate($this->closedate));
540
 
541
        } else if (!$this->user_is_eligible($userid)) {
542
            $message = get_string('noteligible', 'questionnaire');
543
 
544
        } else if (!$this->user_can_take($userid)) {
545
            switch ($this->qtype) {
546
                case QUESTIONNAIREDAILY:
547
                    $msgstring = ' ' . get_string('today', 'questionnaire');
548
                    break;
549
                case QUESTIONNAIREWEEKLY:
550
                    $msgstring = ' ' . get_string('thisweek', 'questionnaire');
551
                    break;
552
                case QUESTIONNAIREMONTHLY:
553
                    $msgstring = ' ' . get_string('thismonth', 'questionnaire');
554
                    break;
555
                default:
556
                    $msgstring = '';
557
                    break;
558
            }
559
            $message = get_string("alreadyfilled", "questionnaire", $msgstring);
560
        }
561
 
562
        if (($message !== false) && $asnotification) {
563
            $message = $this->renderer->notification($message, \core\output\notification::NOTIFY_ERROR);
564
        }
565
 
566
        return $message;
567
    }
568
 
569
    /**
570
     * True if the specified user has a saved response for this questionnaire.
571
     * @param int $userid
572
     * @return bool
573
     */
574
    public function user_has_saved_response($userid) {
575
        global $DB;
576
 
577
        return $DB->record_exists('questionnaire_response',
578
            ['questionnaireid' => $this->id, 'userid' => $userid, 'complete' => 'n']);
579
    }
580
 
581
    /**
582
     * True if the specified user can complete this questionnaire at this time.
583
     * @param int $userid
584
     * @return bool
585
     */
586
    public function user_time_for_new_attempt($userid) {
587
        global $DB;
588
 
589
        $params = ['questionnaireid' => $this->id, 'userid' => $userid, 'complete' => 'y'];
590
        if (!($attempts = $DB->get_records('questionnaire_response', $params, 'submitted DESC'))) {
591
            return true;
592
        }
593
 
594
        $attempt = reset($attempts);
595
        $timenow = time();
596
 
597
        switch ($this->qtype) {
598
 
599
            case QUESTIONNAIREUNLIMITED:
600
                $cantake = true;
601
                break;
602
 
603
            case QUESTIONNAIREONCE:
604
                $cantake = false;
605
                break;
606
 
607
            case QUESTIONNAIREDAILY:
608
                $attemptyear = date('Y', $attempt->submitted);
609
                $currentyear = date('Y', $timenow);
610
                $attemptdayofyear = date('z', $attempt->submitted);
611
                $currentdayofyear = date('z', $timenow);
612
                $cantake = (($attemptyear < $currentyear) ||
613
                    (($attemptyear == $currentyear) && ($attemptdayofyear < $currentdayofyear)));
614
                break;
615
 
616
            case QUESTIONNAIREWEEKLY:
617
                $attemptyear = date('Y', $attempt->submitted);
618
                $currentyear = date('Y', $timenow);
619
                $attemptweekofyear = date('W', $attempt->submitted);
620
                $currentweekofyear = date('W', $timenow);
621
                $cantake = (($attemptyear < $currentyear) ||
622
                    (($attemptyear == $currentyear) && ($attemptweekofyear < $currentweekofyear)));
623
                break;
624
 
625
            case QUESTIONNAIREMONTHLY:
626
                $attemptyear = date('Y', $attempt->submitted);
627
                $currentyear = date('Y', $timenow);
628
                $attemptmonthofyear = date('n', $attempt->submitted);
629
                $currentmonthofyear = date('n', $timenow);
630
                $cantake = (($attemptyear < $currentyear) ||
631
                    (($attemptyear == $currentyear) && ($attemptmonthofyear < $currentmonthofyear)));
632
                break;
633
 
634
            default:
635
                $cantake = false;
636
                break;
637
        }
638
 
639
        return $cantake;
640
    }
641
 
642
    /**
643
     * True if the accessing course contains the actual questionnaire, as opposed to an instance of a public questionnaire.
644
     * @return bool
645
     */
646
    public function is_survey_owner() {
647
        return (!empty($this->survey->courseid) && ($this->course->id == $this->survey->courseid));
648
    }
649
 
650
    /**
651
     * True if the user can view the specified response.
652
     * @param int $rid
653
     * @return bool|void
654
     */
655
    public function can_view_response($rid) {
656
        global $USER, $DB;
657
 
658
        if (!empty($rid)) {
659
            $response = $DB->get_record('questionnaire_response', array('id' => $rid));
660
 
661
            // If the response was not found, can't view it.
662
            if (empty($response)) {
663
                return false;
664
            }
665
 
666
            // If the response belongs to a different survey than this one, can't view it.
667
            if ($response->questionnaireid != $this->id) {
668
                return false;
669
            }
670
 
671
            // If you can view all responses always, then you can view it.
672
            if ($this->capabilities->readallresponseanytime) {
673
                return true;
674
            }
675
 
676
            // If you are allowed to view this response for another user.
677
            // If resp_view is set to QUESTIONNAIRE_STUDENTVIEWRESPONSES_NEVER, then this will always be false.
678
            if ($this->capabilities->readallresponses &&
679
                ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_ALWAYS ||
680
                 ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENCLOSED && $this->is_closed()) ||
681
                 ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENANSWERED  && !$this->user_can_take($USER->id)))) {
682
                return true;
683
            }
684
 
685
            // If you can read your own response.
686
            if (($response->userid == $USER->id) && $this->capabilities->readownresponses &&
687
                ($this->count_submissions($USER->id) > 0)) {
688
                return true;
689
            }
690
 
691
        } else {
692
            // If you can view all responses always, then you can view it.
693
            if ($this->capabilities->readallresponseanytime) {
694
                return true;
695
            }
696
 
697
            // If you are allowed to view this response for another user.
698
            // If resp_view is set to QUESTIONNAIRE_STUDENTVIEWRESPONSES_NEVER, then this will always be false.
699
            if ($this->capabilities->readallresponses &&
700
                ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_ALWAYS ||
701
                 ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENCLOSED && $this->is_closed()) ||
702
                 ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENANSWERED  && !$this->user_can_take($USER->id)))) {
703
                return true;
704
            }
705
 
706
            // If you can read your own response.
707
            if ($this->capabilities->readownresponses && ($this->count_submissions($USER->id) > 0)) {
708
                return true;
709
            }
710
        }
711
    }
712
 
713
    /**
714
     * True if the user can view the responses to this questionnaire, and there are valid responses.
715
     * @param null|int $usernumresp
716
     * @return bool
717
     */
718
    public function can_view_all_responses($usernumresp = null) {
719
        global $USER, $SESSION;
720
 
721
        $owner = $this->is_survey_owner();
722
        $numresp = $this->count_submissions();
723
        if ($usernumresp === null) {
724
            $usernumresp = $this->count_submissions($USER->id);
725
        }
726
 
727
        // Number of Responses in currently selected group (or all participants etc.).
728
        if (isset($SESSION->questionnaire->numselectedresps)) {
729
            $numselectedresps = $SESSION->questionnaire->numselectedresps;
730
        } else {
731
            $numselectedresps = $numresp;
732
        }
733
 
734
        // If questionnaire is set to separate groups, prevent user who is not member of any group
735
        // to view All responses.
736
        $canviewgroups = true;
737
        $canviewallgroups = has_capability('moodle/site:accessallgroups', $this->context);
738
        $groupmode = groups_get_activity_groupmode($this->cm, $this->course);
739
        if ($groupmode == 1) {
740
            $canviewgroups = groups_has_membership($this->cm, $USER->id);
741
        }
742
 
743
        $grouplogic = $canviewgroups || $canviewallgroups;
744
        $respslogic = ($numresp > 0) && ($numselectedresps > 0);
745
        return $this->can_view_all_responses_anytime($grouplogic, $respslogic) ||
746
            $this->can_view_all_responses_with_restrictions($usernumresp, $grouplogic, $respslogic);
747
    }
748
 
749
    /**
750
     * True if the user can view all of the responses to this questionnaire any time, and there are valid responses.
751
     * @param bool $grouplogic
752
     * @param bool $respslogic
753
     * @return bool
754
     */
755
    public function can_view_all_responses_anytime($grouplogic = true, $respslogic = true) {
756
        // Can view if you are a valid group user, this is the owning course, and there are responses, and you have no
757
        // response view restrictions.
758
        return $grouplogic && $respslogic && $this->is_survey_owner() && $this->capabilities->readallresponseanytime;
759
    }
760
 
761
    /**
762
     * True if the user can view all of the responses to this questionnaire any time, and there are valid responses.
763
     * @param null|int $usernumresp
764
     * @param bool $grouplogic
765
     * @param bool $respslogic
766
     * @return bool
767
     */
768
    public function can_view_all_responses_with_restrictions($usernumresp, $grouplogic = true, $respslogic = true) {
769
        // Can view if you are a valid group user, this is the owning course, and there are responses, and you can view
770
        // subject to viewing settings..
771
        return $grouplogic && $respslogic && $this->is_survey_owner() &&
772
            ($this->capabilities->readallresponses &&
773
                ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_ALWAYS ||
774
                    ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENCLOSED && $this->is_closed()) ||
775
                    ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENANSWERED && $usernumresp)));
776
 
777
    }
778
 
779
    /**
780
     * Return the number of submissions for this questionnaire.
781
     * @param bool $userid
782
     * @param int $groupid
783
     * @return int
784
     */
785
    public function count_submissions($userid=false, $groupid=0) {
786
        global $DB;
787
 
788
        $params = [];
789
        $groupsql = '';
790
        $groupcnd = '';
791
        if ($groupid != 0) {
792
            $groupsql = 'INNER JOIN {groups_members} gm ON r.userid = gm.userid ';
793
            $groupcnd = ' AND gm.groupid = :groupid ';
794
            $params['groupid'] = $groupid;
795
        }
796
 
797
        // Since submission can be across questionnaires in the case of public questionnaires, need to check the realm.
798
        // Public questionnaires can have responses to multiple questionnaire instances.
799
        if ($this->survey_is_public_master()) {
800
            $sql = 'SELECT COUNT(r.id) ' .
801
                'FROM {questionnaire_response} r ' .
802
                'INNER JOIN {questionnaire} q ON r.questionnaireid = q.id ' .
803
                'INNER JOIN {questionnaire_survey} s ON q.sid = s.id ' .
804
                $groupsql .
805
                'WHERE s.id = :surveyid AND r.complete = :status' . $groupcnd;
806
            $params['surveyid'] = $this->sid;
807
            $params['status'] = 'y';
808
        } else {
809
            $sql = 'SELECT COUNT(r.id) ' .
810
                'FROM {questionnaire_response} r ' .
811
                $groupsql .
812
                'WHERE r.questionnaireid = :questionnaireid AND r.complete = :status' . $groupcnd;
813
            $params['questionnaireid'] = $this->id;
814
            $params['status'] = 'y';
815
        }
816
        if ($userid) {
817
            $sql .= ' AND r.userid = :userid';
818
            $params['userid'] = $userid;
819
        }
820
        return $DB->count_records_sql($sql, $params);
821
    }
822
 
823
    /**
824
     * Get the requested responses for this questionnaire.
825
     *
826
     * @param int|bool $userid
827
     * @param int $groupid
828
     * @return array
829
     */
830
    public function get_responses($userid=false, $groupid=0) {
831
        global $DB;
832
 
833
        $params = [];
834
        $groupsql = '';
835
        $groupcnd = '';
836
        if ($groupid != 0) {
837
            $groupsql = 'INNER JOIN {groups_members} gm ON r.userid = gm.userid ';
838
            $groupcnd = ' AND gm.groupid = :groupid ';
839
            $params['groupid'] = $groupid;
840
        }
841
 
842
        // Since submission can be across questionnaires in the case of public questionnaires, need to check the realm.
843
        // Public questionnaires can have responses to multiple questionnaire instances.
844
        if ($this->survey_is_public_master()) {
845
            $sql = 'SELECT r.* ' .
846
                'FROM {questionnaire_response} r ' .
847
                'INNER JOIN {questionnaire} q ON r.questionnaireid = q.id ' .
848
                'INNER JOIN {questionnaire_survey} s ON q.sid = s.id ' .
849
                $groupsql .
850
                'WHERE s.id = :surveyid AND r.complete = :status' . $groupcnd;
851
            $params['surveyid'] = $this->sid;
852
            $params['status'] = 'y';
853
        } else {
854
            $sql = 'SELECT r.* ' .
855
                'FROM {questionnaire_response} r ' .
856
                $groupsql .
857
                'WHERE r.questionnaireid = :questionnaireid AND r.complete = :status' . $groupcnd;
858
            $params['questionnaireid'] = $this->id;
859
            $params['status'] = 'y';
860
        }
861
        if ($userid) {
862
            $sql .= ' AND r.userid = :userid';
863
            $params['userid'] = $userid;
864
        }
865
 
866
        $sql .= ' ORDER BY r.id';
867
        return $DB->get_records_sql($sql, $params) ?? [];
868
    }
869
 
870
    /**
871
     * True if any of the questions are required.
872
     * @param int $section
873
     * @return bool
874
     */
875
    private function has_required($section = 0) {
876
        if (empty($this->questions)) {
877
            return false;
878
        } else if ($section <= 0) {
879
            foreach ($this->questions as $question) {
880
                if ($question->required()) {
881
                    return true;
882
                }
883
            }
884
        } else {
885
            foreach ($this->questionsbysec[$section] as $questionid) {
886
                if ($this->questions[$questionid]->required()) {
887
                    return true;
888
                }
889
            }
890
        }
891
        return false;
892
    }
893
 
894
    /**
895
     * Check if current questionnaire has dependencies set and any question has dependencies.
896
     *
897
     * @return boolean Whether dependencies are set or not.
898
     */
899
    public function has_dependencies() {
900
        $hasdependencies = false;
901
        if (($this->navigate > 0) && isset($this->questions) && !empty($this->questions)) {
902
            foreach ($this->questions as $question) {
903
                if ($question->has_dependencies()) {
904
                    $hasdependencies = true;
905
                    break;
906
                }
907
            }
908
        }
909
        return $hasdependencies;
910
    }
911
 
912
    /**
913
     * Get a list of all dependent questions.
914
     * @param int $questionid
915
     * @return array
916
     */
917
    public function get_all_dependants($questionid) {
918
        $directids = $this->get_dependants($questionid);
919
        $directs = [];
920
        $indirects = [];
921
        foreach ($directids as $directid) {
922
            $this->load_parents($this->questions[$directid]);
923
            $indirectids = $this->get_dependants($directid);
924
            foreach ($this->questions[$directid]->dependencies as $dep) {
925
                if ($dep->dependquestionid == $questionid) {
926
                    $directs[$directid][] = $dep;
927
                }
928
            }
929
            foreach ($indirectids as $indirectid) {
930
                $this->load_parents($this->questions[$indirectid]);
931
                foreach ($this->questions[$indirectid]->dependencies as $dep) {
932
                    if ($dep->dependquestionid != $questionid) {
933
                        $indirects[$indirectid][] = $dep;
934
                    }
935
                }
936
            }
937
        }
938
        $alldependants = new stdClass();
939
        $alldependants->directs = $directs;
940
        $alldependants->indirects = $indirects;
941
        return($alldependants);
942
    }
943
 
944
    /**
945
     * Get a list of all dependent questions.
946
     * @param int $questionid
947
     * @return array
948
     */
949
    public function get_dependants($questionid) {
950
        $qu = [];
951
        // Create an array which shows for every question the child-IDs.
952
        foreach ($this->questions as $question) {
953
            if ($question->has_dependencies()) {
954
                foreach ($question->dependencies as $dependency) {
955
                    if (($dependency->dependquestionid == $questionid) && !in_array($question->id, $qu)) {
956
                        $qu[] = $question->id;
957
                    }
958
                }
959
            }
960
        }
961
        return($qu);
962
    }
963
 
964
    /**
965
     * Function to sort descendants array in get_dependants function.
966
     * @param mixed $a
967
     * @param mixed $b
968
     * @return int
969
     */
970
    private static function cmp($a, $b) {
971
        if ($a == $b) {
972
            return 0;
973
        } else if ($a < $b) {
974
            return -1;
975
        } else {
976
            return 1;
977
        }
978
    }
979
 
980
    /**
981
     * Get all descendants and choices for questions with descendants.
982
     * @return array
983
     */
984
    public function get_dependants_and_choices() {
985
        $questions = array_reverse($this->questions, true);
986
        $parents = [];
987
        foreach ($questions as $question) {
988
            foreach ($question->dependencies as $dependency) {
989
                $child = new stdClass();
990
                $child->choiceid = $dependency->dependchoiceid;
991
                $child->logic = $dependency->dependlogic;
992
                $child->andor = $dependency->dependandor;
993
                $parents[$dependency->dependquestionid][$question->id][] = $child;
994
            }
995
        }
996
        return($parents);
997
    }
998
 
999
    /**
1000
     * Load needed parent question information into the dependencies structure for the requested question.
1001
     * @param \mod_questionnaire\question\question $question
1002
     * @return bool
1003
     */
1004
    public function load_parents($question) {
1005
        foreach ($question->dependencies as $did => $dependency) {
1006
            $dependquestion = $this->questions[$dependency->dependquestionid];
1007
            $qdependchoice = '';
1008
            switch ($dependquestion->type_id) {
1009
                case QUESRADIO:
1010
                case QUESDROP:
1011
                case QUESCHECK:
1012
                    $qdependchoice = $dependency->dependchoiceid;
1013
                    $dependchoice = $dependquestion->choices[$dependency->dependchoiceid]->content;
1014
 
1015
                    $contents = questionnaire_choice_values($dependchoice);
1016
                    if ($contents->modname) {
1017
                        $dependchoice = $contents->modname;
1018
                    }
1019
                    break;
1020
                case QUESYESNO:
1021
                    switch ($dependency->dependchoiceid) {
1022
                        case 0:
1023
                            $dependchoice = get_string('yes');
1024
                            $qdependchoice = 'y';
1025
                            break;
1026
                        case 1:
1027
                            $dependchoice = get_string('no');
1028
                            $qdependchoice = 'n';
1029
                            break;
1030
                    }
1031
                    break;
1032
            }
1033
            // Qdependquestion, parenttype and qdependchoice fields to be used in preview mode.
1034
            $question->dependencies[$did]->qdependquestion = 'q'.$dependquestion->id;
1035
            $question->dependencies[$did]->qdependchoice = $qdependchoice;
1036
            $question->dependencies[$did]->parenttype = $dependquestion->type_id;
1037
            // Other fields to be used in Questions edit mode.
1038
            $question->dependencies[$did]->position = $question->position;
1039
            $question->dependencies[$did]->name = $question->name;
1040
            $question->dependencies[$did]->content = $question->content;
1041
            $question->dependencies[$did]->parentposition = $dependquestion->position;
1042
            $question->dependencies[$did]->parent = $dependquestion->name.'->'.$dependchoice;
1043
        }
1044
        return true;
1045
    }
1046
 
1047
    /**
1048
     * Determine the next valid page and return it. Return false if no valid next page.
1049
     * @param int $secnum
1050
     * @param int $rid
1051
     * @return int | bool
1052
     */
1053
    public function next_page($secnum, $rid) {
1054
        $secnum++;
1055
        $numsections = isset($this->questionsbysec) ? count($this->questionsbysec) : 0;
1056
        if ($this->has_dependencies()) {
1057
            while (!$this->eligible_questions_on_page($secnum, $rid)) {
1058
                $secnum++;
1059
                // We have reached the end of questionnaire on a page without any question left.
1060
                if ($secnum > $numsections) {
1061
                    $secnum = false;
1062
                    break;
1063
                }
1064
            }
1065
        }
1066
        return $secnum;
1067
    }
1068
 
1069
    /**
1070
     * Determine the previous valid page and return it. Return false if no valid previous page.
1071
     * @param int $secnum
1072
     * @param int $rid
1073
     * @return int | bool
1074
     */
1075
    public function prev_page($secnum, $rid) {
1076
        $secnum--;
1077
        if ($this->has_dependencies()) {
1078
            while (($secnum > 0) && !$this->eligible_questions_on_page($secnum, $rid)) {
1079
                $secnum--;
1080
            }
1081
        }
1082
        if ($secnum === 0) {
1083
            $secnum = false;
1084
        }
1085
        return $secnum;
1086
    }
1087
 
1088
    /**
1089
     * Return the correct action to a next page request.
1090
     * @param mod_questionnaire\responsetype\response\response $response
1091
     * @param int $userid
1092
     * @return bool|int|string
1093
     */
1094
    public function next_page_action($response, $userid) {
1095
        $msg = $this->response_check_format($response->sec, $response);
1096
        if (empty($msg)) {
1097
            $response->rid = $this->existing_response_action($response, $userid);
1098
            return $this->next_page($response->sec, $response->rid);
1099
        } else {
1100
            return $msg;
1101
        }
1102
    }
1103
 
1104
    /**
1105
     * Return the correct action to a previous page request.
1106
     * @param mod_questionnaire\responsetype\response\response $response
1107
     * @param int $userid
1108
     * @return bool|int
1109
     */
1110
    public function previous_page_action($response, $userid) {
1111
        $response->rid = $this->existing_response_action($response, $userid);
1112
        return $this->prev_page($response->sec, $response->rid);
1113
    }
1114
 
1115
    /**
1116
     * Handle updating an existing response.
1117
     * @param mod_questionnaire\responsetype\response\response $response
1118
     * @param int $userid
1119
     * @return bool|int
1120
     */
1121
    public function existing_response_action($response, $userid) {
1122
        $this->response_delete($response->rid, $response->sec);
1123
        return $this->response_insert($response, $userid);
1124
    }
1125
 
1126
    /**
1127
     * Are there any eligible questions to be displayed on the specified page/section.
1128
     * @param int $secnum The section number to check.
1129
     * @param int $rid The current response id.
1130
     * @return boolean
1131
     */
1132
    public function eligible_questions_on_page($secnum, $rid) {
1133
        $questionstodisplay = false;
1134
 
1135
        foreach ($this->questionsbysec[$secnum] as $questionid) {
1136
            if ($this->questions[$questionid]->dependency_fulfilled($rid, $this->questions)) {
1137
                $questionstodisplay = true;
1138
                break;
1139
            }
1140
        }
1141
        return $questionstodisplay;
1142
    }
1143
 
1144
    // Display Methods.
1145
 
1146
    /**
1147
     * The main display method for the survey. Adds HTML to the templates.
1148
     * @param int $quser
1149
     * @param bool $userid
1150
     * @return string|void
1151
     */
1152
    public function print_survey($quser, $userid=false) {
1153
        global $SESSION, $CFG;
1154
 
1155
        if (!($formdata = data_submitted()) || !confirm_sesskey()) {
1156
            $formdata = new stdClass();
1157
        }
1158
 
1159
        $formdata->rid = $this->get_latest_responseid($quser);
1160
        // If student saved a "resume" questionnaire OR left a questionnaire unfinished
1161
        // and there are more pages than one find the page of the last answered question.
1162
        if (($formdata->rid != 0) && (empty($formdata->sec) || intval($formdata->sec) < 1)) {
1163
            $formdata->sec = $this->response_select_max_sec($formdata->rid);
1164
        }
1165
        if (empty($formdata->sec)) {
1166
            $formdata->sec = 1;
1167
        } else {
1168
            $formdata->sec = (intval($formdata->sec) > 0) ? intval($formdata->sec) : 1;
1169
        }
1170
 
1171
        $numsections = isset($this->questionsbysec) ? count($this->questionsbysec) : 0;    // Indexed by section.
1172
        $msg = '';
1173
        $action = $CFG->wwwroot.'/mod/questionnaire/complete.php?id='.$this->cm->id;
1174
 
1175
        // TODO - Need to rework this. Too much crossover with ->view method.
1176
 
1177
        // Skip logic :: if this is page 1, it cannot be the end page with no questions on it!
1178
        if ($formdata->sec == 1) {
1179
            $SESSION->questionnaire->end = false;
1180
        }
1181
 
1182
        if (!empty($formdata->submit)) {
1183
            // Skip logic: we have reached the last page without any questions on it.
1184
            if (isset($SESSION->questionnaire->end) && $SESSION->questionnaire->end == true) {
1185
                return;
1186
            }
1187
 
1188
            $msg = $this->response_check_format($formdata->sec, $formdata);
1189
            if (empty($msg)) {
1190
                return;
1191
            }
1192
            $formdata->rid = $this->existing_response_action($formdata, $userid);
1193
        }
1194
 
1195
        if (!empty($formdata->resume) && ($this->resume)) {
1196
            $this->response_delete($formdata->rid, $formdata->sec);
1197
            $formdata->rid = $this->response_insert($formdata, $quser, true);
1198
            $this->response_goto_saved($action);
1199
            return;
1200
        }
1201
 
1202
        // Save each section 's $formdata somewhere in case user returns to that page when navigating the questionnaire.
1203
        if (!empty($formdata->next)) {
1204
            $msg = $this->response_check_format($formdata->sec, $formdata);
1205
            if ($msg) {
1206
                $formdata->next = '';
1207
                $formdata->rid = $this->existing_response_action($formdata, $userid);
1208
            } else {
1209
                $nextsec = $this->next_page_action($formdata, $userid);
1210
                if ($nextsec === false) {
1211
                    $SESSION->questionnaire->end = true; // End of questionnaire reached on a no questions page.
1212
                    $formdata->sec = $numsections + 1;
1213
                } else {
1214
                    $formdata->sec = $nextsec;
1215
                }
1216
            }
1217
        }
1218
 
1219
        if (!empty($formdata->prev)) {
1220
            // If skip logic and this is last page reached with no questions,
1221
            // unlock questionnaire->end to allow navigate back to previous page.
1222
            if (isset($SESSION->questionnaire->end) && ($SESSION->questionnaire->end == true)) {
1223
                $SESSION->questionnaire->end = false;
1224
                $formdata->sec--;
1225
            }
1226
 
1227
            // Prevent navigation to previous page if wrong format in answered questions).
1228
            $msg = $this->response_check_format($formdata->sec, $formdata, false, true);
1229
            if ($msg) {
1230
                $formdata->prev = '';
1231
                $formdata->rid = $this->existing_response_action($formdata, $userid);
1232
            } else {
1233
                $prevsec = $this->previous_page_action($formdata, $userid);
1234
                if ($prevsec === false) {
1235
                    $formdata->sec = 0;
1236
                } else {
1237
                    $formdata->sec = $prevsec;
1238
                }
1239
            }
1240
        }
1241
 
1242
        if (!empty($formdata->rid)) {
1243
            $this->add_response($formdata->rid);
1244
        }
1245
 
1246
        $formdatareferer = !empty($formdata->referer) ? htmlspecialchars($formdata->referer) : '';
1247
        $formdatarid = isset($formdata->rid) ? $formdata->rid : '0';
1248
        $this->page->add_to_page('formstart', $this->renderer->complete_formstart($action, ['referer' => $formdatareferer,
1249
            'a' => $this->id, 'sid' => $this->survey->id, 'rid' => $formdatarid, 'sec' => $formdata->sec, 'sesskey' => sesskey()]));
1250
        if (isset($this->questions) && $numsections) { // Sanity check.
1251
            $this->survey_render($formdata, $formdata->sec, $msg);
1252
            $controlbuttons = [];
1253
            if ($formdata->sec > 1) {
1254
                $controlbuttons['prev'] = ['type' => 'submit', 'class' => 'btn btn-secondary control-button-prev',
1255
                    'value' => '<< '.get_string('previouspage', 'questionnaire')];
1256
            }
1257
            if ($this->resume) {
1258
                $controlbuttons['resume'] = ['type' => 'submit', 'class' => 'btn btn-secondary control-button-save',
1259
                    'value' => get_string('save_and_exit', 'questionnaire')];
1260
            }
1261
 
1262
            // Add a 'hidden' variable for the mod's 'view.php', and use a language variable for the submit button.
1263
 
1264
            if ($formdata->sec == $numsections) {
1265
                $controlbuttons['submittype'] = ['type' => 'hidden', 'value' => 'Submit Survey'];
1266
                $controlbuttons['submit'] = ['type' => 'submit', 'class' => 'btn btn-primary control-button-submit',
1267
                    'value' => get_string('submitsurvey', 'questionnaire')];
1268
            } else {
1269
                $controlbuttons['next'] = ['type' => 'submit', 'class' => 'btn btn-secondary control-button-next',
1270
                    'value' => get_string('nextpage', 'questionnaire').' >>'];
1271
            }
1272
            $this->page->add_to_page('controlbuttons', $this->renderer->complete_controlbuttons($controlbuttons));
1273
        } else {
1274
            $this->page->add_to_page('controlbuttons',
1275
                $this->renderer->complete_controlbuttons(get_string('noneinuse', 'questionnaire')));
1276
        }
1277
        $this->page->add_to_page('formend', $this->renderer->complete_formend());
1278
 
1279
        return $msg;
1280
    }
1281
 
1282
    /**
1283
     * Print the entire survey page.
1284
     * @param stdClass $formdata
1285
     * @param int $section
1286
     * @param string $message
1287
     * @return bool|void
1288
     */
1289
    private function survey_render(&$formdata, $section = 1, $message = '') {
1290
 
1291
        $this->usehtmleditor = null;
1292
 
1293
        if (empty($section)) {
1294
            $section = 1;
1295
        }
1296
        $numsections = isset($this->questionsbysec) ? count($this->questionsbysec) : 0;
1297
        if ($section > $numsections) {
1298
            $formdata->sec = $numsections;
1299
            $this->page->add_to_page('notifications',
1300
                $this->renderer->notification(get_string('finished', 'questionnaire'), \core\output\notification::NOTIFY_WARNING));
1301
            return(false);  // Invalid section.
1302
        }
1303
 
1304
        // Check to see if there are required questions.
1305
        $hasrequired = $this->has_required($section);
1306
 
1307
        // Find out what question number we are on $i New fix for question numbering.
1308
        $i = 0;
1309
        if ($section > 1) {
1310
            for ($j = 2; $j <= $section; $j++) {
1311
                foreach ($this->questionsbysec[$j - 1] as $questionid) {
1312
                    if ($this->questions[$questionid]->type_id < QUESPAGEBREAK) {
1313
                        $i++;
1314
                    }
1315
                }
1316
            }
1317
        }
1318
 
1319
        $this->print_survey_start($message, $section, $numsections, $hasrequired, '', 1);
1320
        // Only show progress bar on questionnaires with more than one page.
1321
        if ($this->progressbar && isset($this->questionsbysec) && count($this->questionsbysec) > 1) {
1322
            $this->page->add_to_page('progressbar',
1323
                    $this->renderer->render_progress_bar($section, $this->questionsbysec));
1324
        }
1325
        foreach ($this->questionsbysec[$section] as $questionid) {
1326
            if ($this->questions[$questionid]->type_id != QUESSECTIONTEXT) {
1327
                $i++;
1328
            }
1329
            // Need questionnaire id to get the questionnaire object in sectiontext (Label) question class.
1330
            $formdata->questionnaire_id = $this->id;
1331
            if (isset($formdata->rid) && !empty($formdata->rid)) {
1332
                $this->add_response($formdata->rid);
1333
            } else {
1334
                $this->add_response_from_formdata($formdata);
1335
            }
1336
            $this->page->add_to_page('questions',
1337
                $this->renderer->question_output($this->questions[$questionid],
1338
                    (isset($this->responses[$formdata->rid]) ? $this->responses[$formdata->rid] : []),
1339
                    $i, $this->usehtmleditor, []));
1340
        }
1341
 
1342
        $this->print_survey_end($section, $numsections);
1343
 
1344
        return;
1345
    }
1346
 
1347
    /**
1348
     * Print the start of the survey page.
1349
     * @param string $message
1350
     * @param int $section
1351
     * @param int $numsections
1352
     * @param bool $hasrequired
1353
     * @param string $rid
1354
     * @param bool $blankquestionnaire
1355
     * @param string $outputtarget
1356
     */
1357
    private function print_survey_start($message, $section, $numsections, $hasrequired, $rid='', $blankquestionnaire=false,
1358
                                        $outputtarget = 'html') {
1359
        global $CFG, $DB;
1360
        require_once($CFG->libdir.'/filelib.php');
1361
 
1362
        $userid = '';
1363
        $resp = '';
1364
        $groupname = '';
1365
        $currentgroupid = 0;
1366
        $timesubmitted = '';
1367
        // Available group modes (0 = no groups; 1 = separate groups; 2 = visible groups).
1368
        if ($rid) {
1369
            $courseid = $this->course->id;
1370
            if ($resp = $DB->get_record('questionnaire_response', array('id' => $rid)) ) {
1371
                if ($this->respondenttype == 'fullname') {
1372
                    $userid = $resp->userid;
1373
                    // Display name of group(s) that student belongs to... if questionnaire is set to Groups separate or visible.
1374
                    if (groups_get_activity_groupmode($this->cm, $this->course)) {
1375
                        if ($groups = groups_get_all_groups($courseid, $resp->userid)) {
1376
                            if (count($groups) == 1) {
1377
                                $group = current($groups);
1378
                                $currentgroupid = $group->id;
1379
                                $groupname = ' ('.get_string('group').': '.$group->name.')';
1380
                            } else {
1381
                                $groupname = ' ('.get_string('groups').': ';
1382
                                foreach ($groups as $group) {
1383
                                    $groupname .= $group->name.', ';
1384
                                }
1385
                                $groupname = substr($groupname, 0, strlen($groupname) - 2).')';
1386
                            }
1387
                        } else {
1388
                            $groupname = ' ('.get_string('groupnonmembers').')';
1389
                        }
1390
                    }
1391
 
1392
                    $params = array(
1393
                        'objectid' => $this->survey->id,
1394
                        'context' => $this->context,
1395
                        'courseid' => $this->course->id,
1396
                        'relateduserid' => $userid,
1397
                        'other' => array('action' => 'vresp', 'currentgroupid' => $currentgroupid, 'rid' => $rid)
1398
                    );
1399
                    $event = \mod_questionnaire\event\response_viewed::create($params);
1400
                    $event->trigger();
1401
                }
1402
            }
1403
        }
1404
        $ruser = '';
1405
        if ($resp && !$blankquestionnaire) {
1406
            if ($userid) {
1407
                if ($user = $DB->get_record('user', array('id' => $userid))) {
1408
                    $ruser = fullname($user);
1409
                }
1410
            }
1411
            if ($this->respondenttype == 'anonymous') {
1412
                $ruser = '- '.get_string('anonymous', 'questionnaire').' -';
1413
            } else {
1414
                // JR DEV comment following line out if you do NOT want time submitted displayed in Anonymous surveys.
1415
                if ($resp->submitted) {
1416
                    $timesubmitted = '&nbsp;'.get_string('submitted', 'questionnaire').'&nbsp;'.userdate($resp->submitted);
1417
                }
1418
            }
1419
        }
1420
        if ($ruser) {
1421
            $respinfo = '';
1422
            if ($outputtarget == 'html') {
1423
                // Disable the pdf function for now, until it looks a lot better.
1424
                if (false) {
1425
                    $linkname = get_string('downloadpdf', 'mod_questionnaire');
1426
                    $link = new moodle_url('/mod/questionnaire/report.php',
1427
                        [
1428
                            'action' => 'vresp',
1429
                            'instance' => $this->id,
1430
                            'target' => 'pdf',
1431
                            'individualresponse' => 1,
1432
                            'rid' => $rid
1433
                        ]
1434
                    );
1435
                    $downpdficon = new pix_icon('b/pdfdown', $linkname, 'mod_questionnaire');
1436
                    $respinfo .= $this->renderer->action_link($link, null, null, null, $downpdficon);
1437
                }
1438
 
1439
                $linkname = get_string('print', 'mod_questionnaire');
1440
                $link = new \moodle_url('/mod/questionnaire/report.php',
1441
                    ['action' => 'vresp', 'instance' => $this->id, 'target' => 'print', 'individualresponse' => 1, 'rid' => $rid]);
1442
                $htmlicon = new pix_icon('t/print', $linkname);
1443
                $options = ['menubar' => true, 'location' => false, 'scrollbars' => true, 'resizable' => true,
1444
                    'height' => 600, 'width' => 800, 'title' => $linkname];
1445
                $name = 'popup';
1446
                $action = new popup_action('click', $link, $name, $options);
1447
                $respinfo .= $this->renderer->action_link($link, null, $action, ['title' => $linkname], $htmlicon) . '&nbsp;';
1448
            }
1449
            $respinfo .= get_string('respondent', 'questionnaire').': <strong>'.$ruser.'</strong>';
1450
            if ($this->survey_is_public()) {
1451
                // For a public questionnaire, look for the course that used it.
1452
                $coursename = '';
1453
                $sql = 'SELECT q.id, q.course, c.fullname ' .
1454
                       'FROM {questionnaire_response} qr ' .
1455
                       'INNER JOIN {questionnaire} q ON qr.questionnaireid = q.id ' .
1456
                       'INNER JOIN {course} c ON q.course = c.id ' .
1457
                       'WHERE qr.id = ? AND qr.complete = ? ';
1458
                if ($record = $DB->get_record_sql($sql, [$rid, 'y'])) {
1459
                    $coursename = $record->fullname;
1460
                }
1461
                $respinfo .= ' '.get_string('course'). ': '.$coursename;
1462
            }
1463
            $respinfo .= $groupname;
1464
            $respinfo .= $timesubmitted;
1465
            $this->page->add_to_page('respondentinfo', $this->renderer->respondent_info($respinfo));
1466
        }
1467
 
1468
        // We don't want to display the print icon in the print popup window itself!
1469
        if ($this->capabilities->printblank && $blankquestionnaire && $section == 1) {
1470
            // Open print friendly as popup window.
1471
            $linkname = '&nbsp;'.get_string('printblank', 'questionnaire');
1472
            $title = get_string('printblanktooltip', 'questionnaire');
1473
            $url = '/mod/questionnaire/print.php?qid='.$this->id.'&amp;rid=0&amp;'.'courseid='.$this->course->id.'&amp;sec=1';
1474
            $options = array('menubar' => true, 'location' => false, 'scrollbars' => true, 'resizable' => true,
1475
                'height' => 600, 'width' => 800, 'title' => $title);
1476
            $name = 'popup';
1477
            $link = new moodle_url($url);
1478
            $action = new popup_action('click', $link, $name, $options);
1479
            $class = "floatprinticon";
1480
            $this->page->add_to_page('printblank',
1481
                $this->renderer->action_link($link, $linkname, $action, array('class' => $class, 'title' => $title),
1482
                    new pix_icon('t/print', $title)));
1483
        }
1484
        if ($section == 1) {
1485
            if (!empty($this->survey->title)) {
1486
                $this->survey->title = format_string($this->survey->title);
1487
                $this->page->add_to_page('title', $this->survey->title);
1488
            }
1489
            if (!empty($this->survey->subtitle)) {
1490
                $this->survey->subtitle = format_string($this->survey->subtitle);
1491
                $this->page->add_to_page('subtitle', $this->survey->subtitle);
1492
            }
1493
            if ($this->survey->info) {
1494
                $infotext = file_rewrite_pluginfile_urls($this->survey->info, 'pluginfile.php',
1495
                    $this->context->id, 'mod_questionnaire', 'info', $this->survey->id);
1496
                $this->page->add_to_page('addinfo', $infotext);
1497
            }
1498
        }
1499
 
1500
        if ($message) {
1501
            $this->page->add_to_page('message', $this->renderer->notification($message, \core\output\notification::NOTIFY_ERROR));
1502
        }
1503
    }
1504
 
1505
    /**
1506
     * Print the end of the survey page.
1507
     * @param int $section
1508
     * @param int $numsections
1509
     */
1510
    private function print_survey_end($section, $numsections) {
1511
        // If no pages autonumbering.
1512
        if (!$this->pages_autonumbered()) {
1513
            return;
1514
        }
1515
        if ($numsections > 1) {
1516
            $a = new stdClass();
1517
            $a->page = $section;
1518
            $a->totpages = $numsections;
1519
            $this->page->add_to_page('pageinfo',
1520
                $this->renderer->container(get_string('pageof', 'questionnaire', $a).'&nbsp;&nbsp;', 'surveyPage'));
1521
        }
1522
    }
1523
 
1524
    /**
1525
     * Display a survey suitable for printing.
1526
     * @param int $courseid
1527
     * @param string $message
1528
     * @param string $referer
1529
     * @param int $rid
1530
     * @param bool $blankquestionnaire If we are printing a blank questionnaire.
1531
     * @return false|void
1532
     */
1533
    public function survey_print_render($courseid, $message = '', $referer='', $rid=0, $blankquestionnaire=false) {
1534
        global $DB, $CFG;
1535
 
1536
        if (! $course = $DB->get_record("course", array("id" => $courseid))) {
1537
            throw new \moodle_exception('incorrectcourseid', 'mod_questionnaire');
1538
        }
1539
 
1540
        $this->course = $course;
1541
 
1542
        if (!empty($rid)) {
1543
            // If we're viewing a response, use this method.
1544
            $this->view_response($rid, $referer);
1545
            return;
1546
        }
1547
 
1548
        if (empty($section)) {
1549
            $section = 1;
1550
        }
1551
 
1552
        if (isset($this->questionsbysec)) {
1553
            $numsections = count($this->questionsbysec);
1554
        } else {
1555
            $numsections = 0;
1556
        }
1557
 
1558
        if ($section > $numsections) {
1559
            return(false);  // Invalid section.
1560
        }
1561
 
1562
        $hasrequired = $this->has_required();
1563
 
1564
        // Find out what question number we are on $i.
1565
        $i = 1;
1566
        for ($j = 2; $j <= $section; $j++) {
1567
            $i += count($this->questionsbysec[$j - 1]);
1568
        }
1569
 
1570
        $action = $CFG->wwwroot.'/mod/questionnaire/preview.php?id='.$this->cm->id;
1571
        $this->page->add_to_page('formstart',
1572
            $this->renderer->complete_formstart($action));
1573
        // Print all sections.
1574
        $formdata = new stdClass();
1575
        $errors = 1;
1576
        if (data_submitted()) {
1577
            $formdata = data_submitted();
1578
            $formdata->rid = $formdata->rid ?? 0;
1579
            $this->add_response_from_formdata($formdata);
1580
            $pageerror = '';
1581
            $s = 1;
1582
            $errors = 0;
1583
            foreach ($this->questionsbysec as $section) {
1584
                $errormessage = $this->response_check_format($s, $formdata);
1585
                if ($errormessage) {
1586
                    if ($numsections > 1) {
1587
                        $pageerror = get_string('page', 'questionnaire').' '.$s.' : ';
1588
                    }
1589
                    $this->page->add_to_page('notifications',
1590
                        $this->renderer->notification($pageerror.$errormessage, \core\output\notification::NOTIFY_ERROR));
1591
                    $errors++;
1592
                }
1593
                $s ++;
1594
            }
1595
        }
1596
 
1597
        $this->print_survey_start($message, 1, 1, $hasrequired, '');
1598
 
1599
        if (($referer == 'preview') && $this->has_dependencies()) {
1600
            $allqdependants = $this->get_dependants_and_choices();
1601
        } else {
1602
            $allqdependants = [];
1603
        }
1604
        if ($errors == 0) {
1605
            $this->page->add_to_page('message',
1606
                $this->renderer->notification(get_string('submitpreviewcorrect', 'questionnaire'),
1607
                    \core\output\notification::NOTIFY_SUCCESS));
1608
        }
1609
 
1610
        $page = 1;
1611
        foreach ($this->questionsbysec as $section) {
1612
            $output = '';
1613
            if ($numsections > 1) {
1614
                $output .= $this->renderer->print_preview_pagenumber(get_string('page', 'questionnaire').' '.$page);
1615
                $page++;
1616
            }
1617
            foreach ($section as $questionid) {
1618
                if ($this->questions[$questionid]->type_id == QUESSECTIONTEXT) {
1619
                    $i--;
1620
                }
1621
                if (isset($allqdependants[$questionid])) {
1622
                    $dependants = $allqdependants[$questionid];
1623
                } else {
1624
                    $dependants = [];
1625
                }
1626
                $output .= $this->renderer->question_output($this->questions[$questionid], $this->responses[0] ?? [],
1627
                    $i++, null, $dependants);
1628
                $this->page->add_to_page('questions', $output);
1629
                $output = '';
1630
            }
1631
        }
1632
        // End of questions.
1633
        if ($referer == 'preview' && !$blankquestionnaire) {
1634
            $url = $CFG->wwwroot.'/mod/questionnaire/preview.php?id='.$this->cm->id;
1635
            $this->page->add_to_page('formend',
1636
                $this->renderer->print_preview_formend($url, get_string('submitpreview', 'questionnaire'), get_string('reset')));
1637
        }
1638
        return;
1639
    }
1640
 
1641
    /**
1642
     * Update an existing survey.
1643
     * @param stdClass $sdata
1644
     * @return bool|int
1645
     */
1646
    public function survey_update($sdata) {
1647
        global $DB;
1648
 
1649
        $errstr = ''; // TODO: notused!
1650
 
1651
        // New survey.
1652
        if (empty($this->survey->id)) {
1653
            // Create a new survey in the database.
1654
            $fields = array('name', 'realm', 'title', 'subtitle', 'email', 'theme', 'thanks_page', 'thank_head',
1655
                'thank_body', 'feedbacknotes', 'info', 'feedbacksections', 'feedbackscores', 'chart_type');
1656
            // Theme field deprecated.
1657
            $record = new stdClass();
1658
            $record->id = 0;
1659
            $record->courseid = $sdata->courseid;
1660
            foreach ($fields as $f) {
1661
                if (isset($sdata->$f)) {
1662
                    $record->$f = $sdata->$f;
1663
                }
1664
            }
1665
 
1666
            $this->survey = new stdClass();
1667
            $this->survey->id = $DB->insert_record('questionnaire_survey', $record);
1668
            $this->add_survey($this->survey->id);
1669
 
1670
            if (!$this->survey->id) {
1671
                $errstr = get_string('errnewname', 'questionnaire') .' [ :  ]'; // TODO: notused!
1672
                return(false);
1673
            }
1674
        } else {
1675
            if (empty($sdata->name) || empty($sdata->title) || empty($sdata->realm)) {
1676
                return(false);
1677
            }
1678
            if (!isset($sdata->chart_type)) {
1679
                $sdata->chart_type = '';
1680
            }
1681
 
1682
            $fields = array('name', 'realm', 'title', 'subtitle', 'email', 'theme', 'thanks_page',
1683
                'thank_head', 'thank_body', 'feedbacknotes', 'info', 'feedbacksections', 'feedbackscores', 'chart_type');
1684
            $name = $DB->get_field('questionnaire_survey', 'name', array('id' => $this->survey->id));
1685
 
1686
            // Trying to change survey name.
1687
            if (trim($name) != trim(stripslashes($sdata->name))) {  // Var $sdata will already have slashes added to it.
1688
                $count = $DB->count_records('questionnaire_survey', array('name' => $sdata->name));
1689
                if ($count != 0) {
1690
                    $errstr = get_string('errnewname', 'questionnaire');  // TODO: notused!
1691
                    return(false);
1692
                }
1693
            }
1694
 
1695
            // UPDATE the row in the DB with current values.
1696
            $surveyrecord = new stdClass();
1697
            $surveyrecord->id = $this->survey->id;
1698
            foreach ($fields as $f) {
1699
                if (isset($sdata->{$f})) {
1700
                    $surveyrecord->$f = trim($sdata->{$f});
1701
                }
1702
            }
1703
 
1704
            $result = $DB->update_record('questionnaire_survey', $surveyrecord);
1705
            if (!$result) {
1706
                $errstr = get_string('warning', 'questionnaire').' [ :  ]';  // TODO: notused!
1707
                return(false);
1708
            }
1709
        }
1710
 
1711
        return($this->survey->id);
1712
    }
1713
 
1714
    /**
1715
     * Creates an editable copy of a survey.
1716
     * @param int $owner
1717
     * @return bool|int
1718
     */
1719
    public function survey_copy($owner) {
1720
        global $DB;
1721
 
1722
        // Clear the sid, clear the creation date, change the name, and clear the status.
1723
        $survey = clone($this->survey);
1724
 
1725
        unset($survey->id);
1726
        $survey->courseid = $owner;
1727
        // Make sure that the survey name is not larger than the field size (CONTRIB-2999). Leave room for extra chars.
1728
        $survey->name = core_text::substr($survey->name, 0, (64 - 10));
1729
 
1730
        $survey->name .= '_copy';
1731
        $survey->status = 0;
1732
 
1733
        // Check for 'name' conflict, and resolve.
1734
        $i = 0;
1735
        $name = $survey->name;
1736
        while ($DB->count_records('questionnaire_survey', array('name' => $name)) > 0) {
1737
            $name = $survey->name.(++$i);
1738
        }
1739
        if ($i) {
1740
            $survey->name .= $i;
1741
        }
1742
 
1743
        // Create new survey.
1744
        if (!($newsid = $DB->insert_record('questionnaire_survey', $survey))) {
1745
            return(false);
1746
        }
1747
 
1748
        // Make copies of all the questions.
1749
        $pos = 1;
1750
        // Skip logic: some changes needed here for dependencies down below.
1751
        $qidarray = array();
1752
        $cidarray = array();
1753
        foreach ($this->questions as $question) {
1754
            // Fix some fields first.
1755
            $oldid = $question->id;
1756
            unset($question->id);
1757
            $question->surveyid = $newsid;
1758
            $question->position = $pos++;
1759
 
1760
            // Copy question to new survey.
1761
            if (!($newqid = $DB->insert_record('questionnaire_question', $question))) {
1762
                return(false);
1763
            }
1764
            $qidarray[$oldid] = $newqid;
1765
            foreach ($question->choices as $key => $choice) {
1766
                $oldcid = $key;
1767
                unset($choice->id);
1768
                $choice->question_id = $newqid;
1769
                if (!$newcid = $DB->insert_record('questionnaire_quest_choice', $choice)) {
1770
                    return(false);
1771
                }
1772
                $cidarray[$oldcid] = $newcid;
1773
            }
1774
        }
1775
 
1776
        // Replicate all dependency data.
1777
        if ($dependquestions = $DB->get_records('questionnaire_dependency', ['surveyid' => $this->survey->id], 'questionid')) {
1778
            foreach ($dependquestions as $dquestion) {
1779
                $record = new stdClass();
1780
                $record->questionid = $qidarray[$dquestion->questionid];
1781
                $record->surveyid = $newsid;
1782
                $record->dependquestionid = $qidarray[$dquestion->dependquestionid];
1783
                // The response may not use choice id's (example boolean). If not, just copy the value.
1784
                $responsetype = $this->questions[$dquestion->dependquestionid]->responsetype;
1785
                if ($responsetype->transform_choiceid($dquestion->dependchoiceid) == $dquestion->dependchoiceid) {
1786
                    $record->dependchoiceid = $cidarray[$dquestion->dependchoiceid];
1787
                } else {
1788
                    $record->dependchoiceid = $dquestion->dependchoiceid;
1789
                }
1790
                $record->dependlogic = $dquestion->dependlogic;
1791
                $record->dependandor = $dquestion->dependandor;
1792
                $DB->insert_record('questionnaire_dependency', $record);
1793
            }
1794
        }
1795
 
1796
        // Replicate any feedback data.
1797
        // TODO: Need to handle image attachments (same for other copies above).
1798
        if ($fbsections = $DB->get_records('questionnaire_fb_sections', ['surveyid' => $this->survey->id], 'id')) {
1799
            foreach ($fbsections as $fbsid => $fbsection) {
1800
                $fbsection->surveyid = $newsid;
1801
                $scorecalculation = section::decode_scorecalculation($fbsection->scorecalculation);
1802
                $newscorecalculation = [];
1803
                foreach ($scorecalculation as $qid => $val) {
1804
                    $newscorecalculation[$qidarray[$qid]] = $val;
1805
                }
1806
                $fbsection->scorecalculation = serialize($newscorecalculation);
1807
                unset($fbsection->id);
1808
                $newfbsid = $DB->insert_record('questionnaire_fb_sections', $fbsection);
1809
                if ($feedbackrecs = $DB->get_records('questionnaire_feedback', ['sectionid' => $fbsid], 'id')) {
1810
                    foreach ($feedbackrecs as $feedbackrec) {
1811
                        $feedbackrec->sectionid = $newfbsid;
1812
                        unset($feedbackrec->id);
1813
                        $DB->insert_record('questionnaire_feedback', $feedbackrec);
1814
                    }
1815
                }
1816
            }
1817
        }
1818
 
1819
        return($newsid);
1820
    }
1821
 
1822
    // RESPONSE LIBRARY.
1823
 
1824
    /**
1825
     * Check that all questions have been answered in a suitable way.
1826
     * @param int $section
1827
     * @param stdClass $formdata
1828
     * @param bool $checkmissing
1829
     * @param bool $checkwrongformat
1830
     * @return string
1831
     */
1832
    private function response_check_format($section, $formdata, $checkmissing = true, $checkwrongformat = true) {
1833
        $missing = 0;
1834
        $strmissing = '';     // Missing questions.
1835
        $wrongformat = 0;
1836
        $strwrongformat = ''; // Wrongly formatted questions (Numeric, 5:Check Boxes, Date).
1837
        $i = 1;
1838
        for ($j = 2; $j <= $section; $j++) {
1839
            // ADDED A SIMPLE LOOP FOR MAKING SURE PAGE BREAKS (type 99) AND LABELS (type 100) ARE NOT ALLOWED.
1840
            foreach ($this->questionsbysec[$j - 1] as $questionid) {
1841
                $tid = $this->questions[$questionid]->type_id;
1842
                if ($tid < QUESPAGEBREAK) {
1843
                    $i++;
1844
                }
1845
            }
1846
        }
1847
        $qnum = $i - 1;
1848
 
1849
        if (key_exists($section, $this->questionsbysec)) {
1850
            foreach ($this->questionsbysec[$section] as $questionid) {
1851
                $tid = $this->questions[$questionid]->type_id;
1852
                if ($tid != QUESSECTIONTEXT) {
1853
                    $qnum++;
1854
                }
1855
                if (!$this->questions[$questionid]->response_complete($formdata)) {
1856
                    $missing++;
1857
                    $strnum = get_string('num', 'questionnaire') . $qnum . '. ';
1858
                    $strmissing .= $strnum;
1859
                    // Pop-up   notification at the point of the error.
1860
                    $strnoti = get_string('missingquestion', 'questionnaire') . $strnum;
1861
                    $this->questions[$questionid]->add_notification($strnoti);
1862
                }
1863
                if (!$this->questions[$questionid]->response_valid($formdata)) {
1864
                    $wrongformat++;
1865
                    $strwrongformat .= get_string('num', 'questionnaire') . $qnum . '. ';
1866
                }
1867
            }
1868
        }
1869
        $message = '';
1870
        $nonumbering = false;
1871
        // If no questions autonumbering do not display missing question(s) number(s).
1872
        if (!$this->questions_autonumbered()) {
1873
            $nonumbering = true;
1874
        }
1875
        if ($checkmissing && $missing) {
1876
            if ($nonumbering) {
1877
                $strmissing = '';
1878
            }
1879
            if ($missing == 1) {
1880
                $message = get_string('missingquestion', 'questionnaire').$strmissing;
1881
            } else {
1882
                $message = get_string('missingquestions', 'questionnaire').$strmissing;
1883
            }
1884
            if ($wrongformat) {
1885
                $message .= '<br />';
1886
            }
1887
        }
1888
        if ($checkwrongformat && $wrongformat) {
1889
            if ($nonumbering) {
1890
                $message .= get_string('wronganswers', 'questionnaire');
1891
            } else {
1892
                if ($wrongformat == 1) {
1893
                    $message .= get_string('wrongformat', 'questionnaire').$strwrongformat;
1894
                } else {
1895
                    $message .= get_string('wrongformats', 'questionnaire').$strwrongformat;
1896
                }
1897
            }
1898
        }
1899
        return ($message);
1900
    }
1901
 
1902
    /**
1903
     * Delete the spcified response.
1904
     * @param int $rid
1905
     * @param null|int $sec
1906
     */
1907
    private function response_delete($rid, $sec = null) {
1908
        global $DB;
1909
 
1910
        if (empty($rid)) {
1911
            return;
1912
        }
1913
 
1914
        if ($sec != null) {
1915
            if ($sec < 1) {
1916
                return;
1917
            }
1918
 
1919
            // Skip logic.
1920
            $numsections = isset($this->questionsbysec) ? count($this->questionsbysec) : 0;
1921
            $sec = min($numsections , $sec);
1922
 
1923
            /* get question_id's in this section */
1924
            $qids = array();
1925
            foreach ($this->questionsbysec[$sec] as $questionid) {
1926
                $qids[] = $questionid;
1927
            }
1928
            if (empty($qids)) {
1929
                return;
1930
            } else {
1931
                list($qsql, $params) = $DB->get_in_or_equal($qids);
1932
                $qsql = ' AND question_id ' . $qsql;
1933
            }
1934
 
1935
        } else {
1936
            /* delete all */
1937
            $qsql = '';
1938
            $params = array();
1939
        }
1940
 
1941
        /* delete values */
1942
        $select = 'response_id = \'' . $rid . '\' ' . $qsql;
1943
        foreach (array('response_bool', 'resp_single', 'resp_multiple', 'response_rank', 'response_text',
1944
                     'response_other', 'response_date') as $tbl) {
1945
            $DB->delete_records_select('questionnaire_'.$tbl, $select, $params);
1946
        }
1947
    }
1948
 
1949
    /**
1950
     * Commit the specified response.
1951
     * @param int $rid
1952
     * @return bool
1953
     */
1954
    private function response_commit($rid) {
1955
        global $DB;
1956
 
1957
        $record = new stdClass();
1958
        $record->id = $rid;
1959
        $record->complete = 'y';
1960
        $record->submitted = time();
1961
 
1962
        if ($this->grade < 0) {
1963
            $record->grade = 1;  // Don't know what to do if its a scale...
1964
        } else {
1965
            $record->grade = $this->grade;
1966
        }
1967
        return $DB->update_record('questionnaire_response', $record);
1968
    }
1969
 
1970
    /**
1971
     * Get the latest response id for the user, or verify that the given response id is valid.
1972
     * @param int $userid
1973
     * @return int
1974
     */
1975
    public function get_latest_responseid($userid) {
1976
        global $DB;
1977
 
1978
        // Find latest in progress rid.
1979
        $params = ['questionnaireid' => $this->id, 'userid' => $userid, 'complete' => 'n'];
1980
        if ($records = $DB->get_records('questionnaire_response', $params, 'submitted DESC', 'id,questionnaireid', 0, 1)) {
1981
            $rec = reset($records);
1982
            return $rec->id;
1983
        } else {
1984
            return 0;
1985
        }
1986
    }
1987
 
1988
    /**
1989
     * Returns the number of the section in which questions have been answered in a response.
1990
     * @param int $rid
1991
     * @return int
1992
     */
1993
    private function response_select_max_sec($rid) {
1994
        global $DB;
1995
 
1996
        $pos = $this->response_select_max_pos($rid);
1997
        $select = 'surveyid = ? AND type_id = ? AND position < ? AND deleted = ?';
1998
        $params = [$this->sid, QUESPAGEBREAK, $pos, 'n'];
1999
        $max = $DB->count_records_select('questionnaire_question', $select, $params) + 1;
2000
 
2001
        return $max;
2002
    }
2003
 
2004
    /**
2005
     * Returns the position of the last answered question in a response.
2006
     * @param int $rid
2007
     * @return int
2008
     */
2009
    private function response_select_max_pos($rid) {
2010
        global $DB;
2011
 
2012
        $max = 0;
2013
 
2014
        foreach (array('response_bool', 'resp_single', 'resp_multiple', 'response_rank', 'response_text',
2015
                     'response_other', 'response_date') as $tbl) {
2016
            $sql = 'SELECT MAX(q.position) as num FROM {questionnaire_'.$tbl.'} a, {questionnaire_question} q '.
2017
                'WHERE a.response_id = ? AND '.
2018
                'q.id = a.question_id AND '.
2019
                'q.surveyid = ? AND '.
2020
                'q.deleted = \'n\'';
2021
            if ($record = $DB->get_record_sql($sql, array($rid, $this->sid))) {
2022
                $newmax = (int)$record->num;
2023
                if ($newmax > $max) {
2024
                    $max = $newmax;
2025
                }
2026
            }
2027
        }
2028
        return $max;
2029
    }
2030
 
2031
    /**
2032
     * Handle all submission notification actions.
2033
     * @param int $rid The id of the response record.
2034
     * @return boolean Operation success.
2035
     *
2036
     */
2037
    private function submission_notify($rid) {
2038
        global $DB;
2039
 
2040
        $success = true;
2041
 
2042
        if (isset($this->survey)) {
2043
            if (isset($this->survey->email)) {
2044
                $email = $this->survey->email;
2045
            } else {
2046
                $email = $DB->get_field('questionnaire_survey', 'email', ['id' => $this->survey->id]);
2047
            }
2048
        } else {
2049
            $email = '';
2050
        }
2051
 
2052
        if (!empty($email)) {
2053
            $success = $this->response_send_email($rid, $email);
2054
        }
2055
 
2056
        if (!empty($this->notifications)) {
2057
            // Handle notification of submissions.
2058
            $success = $this->send_submission_notifications($rid) && $success;
2059
        }
2060
 
2061
        return $success;
2062
    }
2063
 
2064
    /**
2065
     * Send submission notifications to users with "submissionnotification" capability.
2066
     * @param int $rid The id of the response record.
2067
     * @return boolean Operation success.
2068
     *
2069
     */
2070
    private function send_submission_notifications($rid) {
2071
        global $CFG, $USER;
2072
 
2073
        $this->add_response($rid);
2074
        $message = '';
2075
 
2076
        if ($this->notifications == 2) {
2077
            $message .= $this->get_full_submission_for_notifications($rid);
2078
        }
2079
 
2080
        $success = true;
2081
        if ($notifyusers = $this->get_notifiable_users($USER->id)) {
2082
            $info = new stdClass();
2083
            // Need to handle user differently for anonymous surveys.
2084
            if ($this->respondenttype != 'anonymous') {
2085
                $info->userfrom = $USER;
2086
                $info->username = fullname($info->userfrom, true);
2087
                $info->profileurl = $CFG->wwwroot.'/user/view.php?id='.$info->userfrom->id.'&course='.$this->course->id;
2088
                $langstringtext = 'submissionnotificationtextuser';
2089
                $langstringhtml = 'submissionnotificationhtmluser';
2090
            } else {
2091
                $info->userfrom = \core_user::get_noreply_user();
2092
                $info->username = '';
2093
                $info->profileurl = '';
2094
                $langstringtext = 'submissionnotificationtextanon';
2095
                $langstringhtml = 'submissionnotificationhtmlanon';
2096
            }
2097
            $info->name = format_string($this->name);
2098
            $info->submissionurl = $CFG->wwwroot.'/mod/questionnaire/report.php?action=vresp&sid='.$this->survey->id.
2099
                '&rid='.$rid.'&instance='.$this->id;
2100
            $info->coursename = $this->course->fullname;
2101
 
2102
            $info->postsubject = get_string('submissionnotificationsubject', 'questionnaire');
2103
            $info->posttext = get_string($langstringtext, 'questionnaire', $info);
2104
            $info->posthtml = '<p>' . get_string($langstringhtml, 'questionnaire', $info) . '</p>';
2105
            if (!empty($message)) {
2106
                $info->posttext .= html_to_text($message);
2107
                $info->posthtml .= $message;
2108
            }
2109
 
2110
            foreach ($notifyusers as $notifyuser) {
2111
                $info->userto = $notifyuser;
2112
                $this->send_message($info, 'notification');
2113
            }
2114
        }
2115
 
2116
        return $success;
2117
    }
2118
 
2119
    /**
2120
     * Message someone about something.
2121
     *
2122
     * @param object $info The information for the message.
2123
     * @param string $eventtype
2124
     * @return void
2125
     */
2126
    private function send_message($info, $eventtype) {
2127
        $eventdata = new \core\message\message();
2128
        $eventdata->courseid = $this->course->id;
2129
        $eventdata->modulename = 'questionnaire';
2130
        $eventdata->userfrom = $info->userfrom;
2131
        $eventdata->userto = $info->userto;
2132
        $eventdata->subject = $info->postsubject;
2133
        $eventdata->fullmessage = $info->posttext;
2134
        $eventdata->fullmessageformat = FORMAT_PLAIN;
2135
        $eventdata->fullmessagehtml = $info->posthtml;
2136
        $eventdata->smallmessage = $info->postsubject;
2137
 
2138
        $eventdata->name = $eventtype;
2139
        $eventdata->component = 'mod_questionnaire';
2140
        $eventdata->notification = 1;
2141
        $eventdata->contexturl = $info->submissionurl;
2142
        $eventdata->contexturlname = $info->name;
2143
 
2144
        message_send($eventdata);
2145
    }
2146
 
2147
    /**
2148
     * Returns a list of users that should receive notification about given submission.
2149
     *
2150
     * @param int $userid The submission to grade
2151
     * @return array
2152
     */
2153
    public function get_notifiable_users($userid) {
2154
        // Potential users should be active users only.
2155
        $potentialusers = get_enrolled_users($this->context, 'mod/questionnaire:submissionnotification',
2156
            null, 'u.*', null, null, null, true);
2157
 
2158
        $notifiableusers = [];
2159
        if (groups_get_activity_groupmode($this->cm) == SEPARATEGROUPS) {
2160
            if ($groups = groups_get_all_groups($this->course->id, $userid, $this->cm->groupingid)) {
2161
                foreach ($groups as $group) {
2162
                    foreach ($potentialusers as $potentialuser) {
2163
                        if ($potentialuser->id == $userid) {
2164
                            // Do not send self.
2165
                            continue;
2166
                        }
2167
                        if (groups_is_member($group->id, $potentialuser->id)) {
2168
                            $notifiableusers[$potentialuser->id] = $potentialuser;
2169
                        }
2170
                    }
2171
                }
2172
            } else {
2173
                // User not in group, try to find graders without group.
2174
                foreach ($potentialusers as $potentialuser) {
2175
                    if ($potentialuser->id == $userid) {
2176
                        // Do not send self.
2177
                        continue;
2178
                    }
2179
                    if (!groups_has_membership($this->cm, $potentialuser->id)) {
2180
                        $notifiableusers[$potentialuser->id] = $potentialuser;
2181
                    }
2182
                }
2183
            }
2184
        } else {
2185
            foreach ($potentialusers as $potentialuser) {
2186
                if ($potentialuser->id == $userid) {
2187
                    // Do not send self.
2188
                    continue;
2189
                }
2190
                $notifiableusers[$potentialuser->id] = $potentialuser;
2191
            }
2192
        }
2193
        return $notifiableusers;
2194
    }
2195
 
2196
    /**
2197
     * Return a formatted string containing all the questions and answers for a specific submission.
2198
     * @param int $rid
2199
     * @return string
2200
     */
2201
    private function get_full_submission_for_notifications($rid) {
2202
        $responses = $this->get_full_submission_for_export($rid);
2203
        $message = '';
2204
        foreach ($responses as $response) {
2205
            $message .= html_to_text($response->questionname) . "<br />\n";
2206
            $message .= get_string('question') . ': ' . html_to_text($response->questiontext) . "<br />\n";
2207
            $message .= get_string('answers', 'questionnaire') . ":<br />\n";
2208
            foreach ($response->answers as $answer) {
2209
                $message .= html_to_text($answer) . "<br />\n";
2210
            }
2211
            $message .= "<br />\n";
2212
        }
2213
 
2214
        return $message;
2215
    }
2216
 
2217
    /**
2218
     * Construct the response data for a given response and return a structured export.
2219
     * @param int $rid
2220
     * @return string
2221
     * @throws coding_exception
2222
     */
2223
    public function get_structured_response($rid) {
2224
        $this->add_response($rid);
2225
        return $this->get_full_submission_for_export($rid);
2226
    }
2227
 
2228
    /**
2229
     * Return a JSON structure containing all the questions and answers for a specific submission.
2230
     * @param int $rid
2231
     * @return array
2232
     */
2233
    private function get_full_submission_for_export($rid) {
2234
        if (!isset($this->responses[$rid])) {
2235
            $this->add_response($rid);
2236
        }
2237
 
2238
        $exportstructure = [];
2239
        foreach ($this->questions as $question) {
2240
            $rqid = 'q' . $question->id;
2241
            $response = new stdClass();
2242
            $response->questionname = $question->position . '. ' . $question->name;
2243
            $response->questiontext = $question->content;
2244
            $response->answers = [];
2245
            if ($question->type_id == 8) {
2246
                $choices = [];
2247
                $cids = [];
2248
                foreach ($question->choices as $cid => $choice) {
2249
                    if (!empty($choice->value) && (strpos($choice->content, '=') !== false)) {
2250
                        $choices[$choice->value] = substr($choice->content, (strpos($choice->content, '=') + 1));
2251
                    } else {
2252
                        $cids[$rqid . '_' . $cid] = $choice->content;
2253
                    }
2254
                }
2255
                if (isset($this->responses[$rid]->answers[$question->id])) {
2256
                    foreach ($cids as $rqid => $choice) {
2257
                        $cid = substr($rqid, (strpos($rqid, '_') + 1));
2258
                        if (isset($this->responses[$rid]->answers[$question->id][$cid])) {
2259
                            if (isset($question->choices[$cid]) &&
2260
                                isset($choices[$this->responses[$rid]->answers[$question->id][$cid]->value])) {
2261
                                $rating = $choices[$this->responses[$rid]->answers[$question->id][$cid]->value];
2262
                            } else {
2263
                                $rating = $this->responses[$rid]->answers[$question->id][$cid]->value;
2264
                            }
2265
                            $response->answers[] = $question->choices[$cid]->content . ' = ' . $rating;
2266
                        }
2267
                    }
2268
                }
2269
            } else if ($question->has_choices()) {
2270
                $answertext = '';
2271
                if (isset($this->responses[$rid]->answers[$question->id])) {
2272
                    $i = 0;
2273
                    foreach ($this->responses[$rid]->answers[$question->id] as $answer) {
2274
                        if ($i > 0) {
2275
                            $answertext .= '; ';
2276
                        }
2277
                        if ($question->choices[$answer->choiceid]->is_other_choice()) {
2278
                            $answertext .= $answer->value;
2279
                        } else {
2280
                            $answertext .= $question->choices[$answer->choiceid]->content;
2281
                        }
2282
                        $i++;
2283
                    }
2284
                }
2285
                $response->answers[] = $answertext;
2286
 
2287
            } else if (isset($this->responses[$rid]->answers[$question->id])) {
2288
                $response->answers[] = $this->responses[$rid]->answers[$question->id][0]->value;
2289
            }
2290
            $exportstructure[] = $response;
2291
        }
2292
 
2293
        return $exportstructure;
2294
    }
2295
 
2296
    /**
2297
     * Format the submission answers for legacy email delivery.
2298
     * @param array $answers The array of response answers.
2299
     * @return array The formatted set of answers as plain text and HTML.
2300
     */
2301
    private function get_formatted_answers_for_emails($answers) {
2302
        global $USER;
2303
 
2304
        // Line endings for html and plaintext emails.
2305
        $endhtml = "\r\n<br />";
2306
        $endplaintext = "\r\n";
2307
 
2308
        reset($answers);
2309
 
2310
        $formatted = array('plaintext' => '', 'html' => '');
2311
        for ($i = 0; $i < count($answers[0]); $i++) {
2312
            $sep = ' : ';
2313
 
2314
            switch($i) {
2315
                case 1:
2316
                    $sep = ' ';
2317
                    break;
2318
                case 4:
2319
                    $formatted['plaintext'] .= get_string('user').' ';
2320
                    $formatted['html'] .= get_string('user').' ';
2321
                    break;
2322
                case 6:
2323
                    if ($this->respondenttype != 'anonymous') {
2324
                        $formatted['html'] .= get_string('email').$sep.$USER->email. $endhtml;
2325
                        $formatted['plaintext'] .= get_string('email'). $sep. $USER->email. $endplaintext;
2326
                    }
2327
            }
2328
            $formatted['html'] .= $answers[0][$i].$sep.$answers[1][$i]. $endhtml;
2329
            $formatted['plaintext'] .= $answers[0][$i].$sep.$answers[1][$i]. $endplaintext;
2330
        }
2331
 
2332
        return $formatted;
2333
    }
2334
 
2335
    /**
2336
     * Send the full response submission to the defined email addresses.
2337
     * @param int $rid The id of the response record.
2338
     * @param string $email The comma separated list of emails to send to.
2339
     * @return bool
2340
     */
2341
    private function response_send_email($rid, $email) {
2342
        global $CFG;
2343
 
2344
        $submission = $this->generate_csv(0, $rid, '', null, 1);
2345
        if (!empty($submission)) {
2346
            $answers = $this->get_formatted_answers_for_emails($submission);
2347
        } else {
2348
            $answers = ['html' => '', 'plaintext' => ''];
2349
        }
2350
 
2351
        $name = s($this->name);
2352
        if (empty($email)) {
2353
            return(false);
2354
        }
2355
 
2356
        // Line endings for html and plaintext emails.
2357
        $endhtml = "\r\n<br>";
2358
        $endplaintext = "\r\n";
2359
 
2360
        $subject = get_string('surveyresponse', 'questionnaire') .": $name [$rid]";
2361
        $url = $CFG->wwwroot.'/mod/questionnaire/report.php?action=vresp&amp;sid='.$this->survey->id.
2362
            '&amp;rid='.$rid.'&amp;instance='.$this->id;
2363
 
2364
        // Html and plaintext body.
2365
        $bodyhtml = '<a href="'.$url.'">'.$url.'</a>'.$endhtml;
2366
        $bodyplaintext = $url.$endplaintext;
2367
        $bodyhtml .= get_string('surveyresponse', 'questionnaire') .' "'.$name.'"'.$endhtml;
2368
        $bodyplaintext .= get_string('surveyresponse', 'questionnaire') .' "'.$name.'"'.$endplaintext;
2369
 
2370
        $bodyhtml .= $answers['html'];
2371
        $bodyplaintext .= $answers['plaintext'];
2372
 
2373
        // Use plaintext version for altbody.
2374
        $altbody = "\n$bodyplaintext\n";
2375
 
2376
        $return = true;
2377
        $mailaddresses = preg_split('/,|;/', $email);
2378
        foreach ($mailaddresses as $email) {
2379
            $userto = new stdClass();
2380
            $userto->email = trim($email);
2381
            $userto->mailformat = 1;
2382
            // Dummy userid to keep email_to_user happy in moodle 2.6.
2383
            $userto->id = -10;
2384
            $userfrom = $CFG->noreplyaddress;
2385
            if (email_to_user($userto, $userfrom, $subject, $altbody, $bodyhtml)) {
2386
                $return = $return && true;
2387
            } else {
2388
                $return = false;
2389
            }
2390
        }
2391
        return $return;
2392
    }
2393
 
2394
    /**
2395
     * Insert the provided response.
2396
     * @param object $responsedata An object containing all data for the response.
2397
     * @param int $userid
2398
     * @param bool $resume
2399
     * @return bool|int
2400
     */
2401
    public function response_insert($responsedata, $userid, $resume=false) {
2402
        global $DB;
2403
 
2404
        $record = new stdClass();
2405
        $record->submitted = time();
2406
 
2407
        if (empty($responsedata->rid)) {
2408
            // Create a uniqe id for this response.
2409
            $record->questionnaireid = $this->id;
2410
            $record->userid = $userid;
2411
            $responsedata->rid = $DB->insert_record('questionnaire_response', $record);
2412
            $responsedata->id = $responsedata->rid;
2413
        } else {
2414
            $record->id = $responsedata->rid;
2415
            $DB->update_record('questionnaire_response', $record);
2416
        }
2417
        if ($resume) {
2418
            // Log this saved response.
2419
            // Needed for the event logging.
2420
            $context = context_module::instance($this->cm->id);
2421
            $anonymous = $this->respondenttype == 'anonymous';
2422
            $params = array(
2423
                'context' => $context,
2424
                'courseid' => $this->course->id,
2425
                'relateduserid' => $userid,
2426
                'anonymous' => $anonymous,
2427
                'other' => array('questionnaireid' => $this->id)
2428
            );
2429
            $event = \mod_questionnaire\event\attempt_saved::create($params);
2430
            $event->trigger();
2431
        }
2432
 
2433
        if (!isset($responsedata->sec)) {
2434
            $responsedata->sec = 1;
2435
        }
2436
        if (!empty($this->questionsbysec[$responsedata->sec])) {
2437
            foreach ($this->questionsbysec[$responsedata->sec] as $questionid) {
2438
                $this->questions[$questionid]->insert_response($responsedata);
2439
            }
2440
        }
2441
        return($responsedata->rid);
2442
    }
2443
 
2444
    /**
2445
     * Get the answers for the all response types.
2446
     * @param int $rid
2447
     * @return array
2448
     */
2449
    private function response_select($rid) {
2450
        // Response_bool (yes/no).
2451
        $values = \mod_questionnaire\responsetype\boolean::response_select($rid);
2452
 
2453
        // Response_single (radio button or dropdown).
2454
        $values += \mod_questionnaire\responsetype\single::response_select($rid);
2455
 
2456
        // Response_multiple.
2457
        $values += \mod_questionnaire\responsetype\multiple::response_select($rid);
2458
 
2459
        // Response_rank.
2460
        $values += \mod_questionnaire\responsetype\rank::response_select($rid);
2461
 
2462
        // Response_text.
2463
        $values += \mod_questionnaire\responsetype\text::response_select($rid);
2464
 
2465
        // Response_date.
2466
        $values += \mod_questionnaire\responsetype\date::response_select($rid);
2467
 
2468
        return($values);
2469
    }
2470
 
2471
    /**
2472
     * Redirect to the appropriate finish page.
2473
     */
2474
    private function response_goto_thankyou() {
2475
        global $CFG, $USER, $DB;
2476
 
2477
        $select = 'id = '.$this->survey->id;
2478
        $fields = 'thanks_page, thank_head, thank_body';
2479
        if ($result = $DB->get_record_select('questionnaire_survey', $select, null, $fields)) {
2480
            $thankurl = $result->thanks_page;
2481
            $thankhead = $result->thank_head;
2482
            $thankbody = $result->thank_body;
2483
        } else {
2484
            $thankurl = '';
2485
            $thankhead = '';
2486
            $thankbody = '';
2487
        }
2488
        if (!empty($thankurl)) {
2489
            if (!headers_sent()) {
2490
                header("Location: $thankurl");
2491
                exit;
2492
            }
2493
            echo '
2494
                <script language="JavaScript" type="text/javascript">
2495
                <!--
2496
                window.location="'.$thankurl.'"
2497
                //-->
2498
                </script>
2499
                <noscript>
2500
                <h2 class="thankhead">Thank You for completing this survey.</h2>
2501
                <blockquote class="thankbody">Please click
2502
                <a href="'.$thankurl.'">here</a> to continue.</blockquote>
2503
                </noscript>
2504
            ';
2505
            exit;
2506
        }
2507
        if (empty($thankhead)) {
2508
            $thankhead = get_string('thank_head', 'questionnaire');
2509
        }
2510
        if ($this->progressbar && isset($this->questionsbysec) && count($this->questionsbysec) > 1) {
2511
            // Show 100% full progress bar on completion.
2512
            $this->page->add_to_page('progressbar',
2513
                    $this->renderer->render_progress_bar(count($this->questionsbysec) + 1, $this->questionsbysec));
2514
        }
2515
        $this->page->add_to_page('title', $thankhead);
2516
        $this->page->add_to_page('addinfo',
2517
            format_text(file_rewrite_pluginfile_urls($thankbody, 'pluginfile.php',
2518
                $this->context->id, 'mod_questionnaire', 'thankbody', $this->survey->id), FORMAT_HTML, ['noclean' => true]));
2519
        // Default set currentgroup to view all participants.
2520
        // TODO why not set to current respondent's groupid (if any)?
2521
        $currentgroupid = 0;
2522
        $currentgroupid = groups_get_activity_group($this->cm);
2523
        if (!groups_is_member($currentgroupid, $USER->id)) {
2524
            $currentgroupid = 0;
2525
        }
2526
        if ($this->capabilities->readownresponses) {
2527
            $url = new moodle_url('myreport.php', ['id' => $this->cm->id, 'instance' => $this->cm->instance, 'user' => $USER->id,
2528
                'byresponse' => 0, 'action' => 'vresp']);
2529
            $this->page->add_to_page('continue', $this->renderer->single_button($url, get_string('continue')));
2530
        } else {
2531
            $url = new moodle_url('/course/view.php', ['id' => $this->course->id]);
2532
            $this->page->add_to_page('continue', $this->renderer->single_button($url, get_string('continue')));
2533
        }
2534
        return;
2535
    }
2536
 
2537
    /**
2538
     * Redirect to the provided url.
2539
     * @param string $url
2540
     */
2541
    private function response_goto_saved($url) {
2542
        global $CFG, $USER;
2543
        $resumesurvey = get_string('resumesurvey', 'questionnaire');
2544
        $savedprogress = get_string('savedprogress', 'questionnaire', '<strong>'.$resumesurvey.'</strong>');
2545
 
2546
        $this->page->add_to_page('notifications',
2547
            $this->renderer->notification($savedprogress, \core\output\notification::NOTIFY_SUCCESS));
2548
        $this->page->add_to_page('respondentinfo',
2549
            $this->renderer->homelink($CFG->wwwroot.'/course/view.php?id='.$this->course->id,
2550
                get_string("backto", "moodle", $this->course->fullname)));
2551
 
2552
        if ($this->resume) {
2553
            $message = $this->user_access_messages($USER->id, true);
2554
            if ($message === false) {
2555
                if ($this->user_can_take($USER->id)) {
2556
                    if ($this->questions) { // Sanity check.
2557
                        if ($this->user_has_saved_response($USER->id)) {
2558
                            $this->page->add_to_page('respondentinfo',
2559
                                $this->renderer->homelink($CFG->wwwroot . '/mod/questionnaire/complete.php?' .
2560
                                    'id=' . $this->cm->id . '&resume=1', $resumesurvey));
2561
                        }
2562
                    }
2563
                }
2564
            }
2565
        }
2566
        return;
2567
    }
2568
 
2569
    // Survey Results Methods.
2570
 
2571
    /**
2572
     * Add the navigation to the responses page.
2573
     * @param int $currrid
2574
     * @param int $currentgroupid
2575
     * @param stdClass $cm
2576
     * @param bool $byresponse
2577
     */
2578
    public function survey_results_navbar_alpha($currrid, $currentgroupid, $cm, $byresponse) {
2579
        global $CFG, $DB;
2580
 
2581
        // Is this questionnaire set to fullname or anonymous?
2582
        $isfullname = $this->respondenttype != 'anonymous';
2583
        if ($isfullname) {
2584
            $responses = $this->get_responses(false, $currentgroupid);
2585
        } else {
2586
            $responses = $this->get_responses();
2587
        }
2588
        if (!$responses) {
2589
            return;
2590
        }
2591
        $total = count($responses);
2592
        if ($total === 0) {
2593
            return;
2594
        }
2595
        $rids = array();
2596
        if ($isfullname) {
2597
            $ridssub = array();
2598
            $ridsuserfullname = array();
2599
            $ridsuserid = array();
2600
        }
2601
        $i = 0;
2602
        $currpos = -1;
2603
        foreach ($responses as $response) {
2604
            array_push($rids, $response->id);
2605
            if ($isfullname) {
2606
                $user = $DB->get_record('user', array('id' => $response->userid));
2607
                array_push($ridssub, $response->submitted);
2608
                array_push($ridsuserfullname, fullname($user));
2609
                array_push($ridsuserid, $response->userid);
2610
            }
2611
            if ($response->id == $currrid) {
2612
                $currpos = $i;
2613
            }
2614
            $i++;
2615
        }
2616
 
2617
        $url = $CFG->wwwroot.'/mod/questionnaire/report.php?action=vresp&group='.$currentgroupid.'&individualresponse=1';
2618
        if (!$byresponse) {     // Display navbar.
2619
            // Build navbar.
2620
            $navbar = new \stdClass();
2621
            $prevrid = ($currpos > 0) ? $rids[$currpos - 1] : null;
2622
            $nextrid = ($currpos < $total - 1) ? $rids[$currpos + 1] : null;
2623
            $firstrid = $rids[0];
2624
            $lastrid = $rids[$total - 1];
2625
            if ($prevrid != null) {
2626
                $pos = $currpos - 1;
2627
                $title = '';
2628
                $firstuserfullname = '';
2629
                $navbar->firstrespondent = ['url' => ($url.'&rid='.$firstrid)];
2630
                $navbar->previous = ['url' => ($url.'&rid='.$prevrid)];
2631
                if ($isfullname) {
2632
                    $responsedate = userdate($ridssub[$pos]);
2633
                    $title = $ridsuserfullname[$pos];
2634
                    // Only add date if more than one response by a student.
2635
                    if ($ridsuserid[$pos] == $ridsuserid[$currpos]) {
2636
                        $title .= ' | '.$responsedate;
2637
                    }
2638
                    $firstuserfullname = $ridsuserfullname[0];
2639
                }
2640
                $navbar->firstrespondent['title'] = $firstuserfullname;
2641
                $navbar->previous['title'] = $title;
2642
            }
2643
            $navbar->respnumber = ['currpos' => ($currpos + 1), 'total' => $total];
2644
            if ($nextrid != null) {
2645
                $pos = $currpos + 1;
2646
                $responsedate = '';
2647
                $title = '';
2648
                $lastuserfullname = '';
2649
                $navbar->lastrespondent = ['url' => ($url.'&rid='.$lastrid)];
2650
                $navbar->next = ['url' => ($url.'&rid='.$nextrid)];
2651
                if ($isfullname) {
2652
                    $responsedate = userdate($ridssub[$pos]);
2653
                    $title = $ridsuserfullname[$pos];
2654
                    // Only add date if more than one response by a student.
2655
                    if ($ridsuserid[$pos] == $ridsuserid[$currpos]) {
2656
                        $title .= ' | '.$responsedate;
2657
                    }
2658
                    $lastuserfullname = $ridsuserfullname[$total - 1];
2659
                }
2660
                $navbar->lastrespondent['title'] = $lastuserfullname;
2661
                $navbar->next['title'] = $title;
2662
            }
2663
            $url = $CFG->wwwroot.'/mod/questionnaire/report.php?action=vresp&byresponse=1&group='.$currentgroupid;
2664
            // Display navbar.
2665
            $navbar->listlink = $url;
2666
 
2667
            // Display a "print this response" icon here in prevision of total removal of tabs in version 2.6.
2668
            $linkname = '&nbsp;'.get_string('print', 'questionnaire');
2669
            $url = '/mod/questionnaire/print.php?qid='.$this->id.'&rid='.$currrid.
2670
                '&courseid='.$this->course->id.'&sec=1';
2671
            $title = get_string('printtooltip', 'questionnaire');
2672
            $options = array('menubar' => true, 'location' => false, 'scrollbars' => true,
2673
                'resizable' => true, 'height' => 600, 'width' => 800);
2674
            $name = 'popup';
2675
            $link = new moodle_url($url);
2676
            $action = new popup_action('click', $link, $name, $options);
2677
            $actionlink = $this->renderer->action_link($link, $linkname, $action, ['title' => $title],
2678
                new pix_icon('t/print', $title));
2679
            $navbar->printaction = $actionlink;
2680
            $this->page->add_to_page('navigationbar', $this->renderer->navigationbar($navbar));
2681
 
2682
        } else { // Display respondents list.
2683
            $resparr = [];
2684
            for ($i = 0; $i < $total; $i++) {
2685
                if ($isfullname) {
2686
                    $responsedate = userdate($ridssub[$i]);
2687
                    $resparr[] = '<a title = "'.$responsedate.'" href="'.$url.'&amp;rid='.
2688
                        $rids[$i].'&amp;individualresponse=1" >'.$ridsuserfullname[$i].'</a> ';
2689
                } else {
2690
                    $responsedate = '';
2691
                    $resparr[] = '<a title = "'.$responsedate.'" href="'.$url.'&amp;rid='.
2692
                        $rids[$i].'&amp;individualresponse=1" >'.
2693
                        get_string('response', 'questionnaire').($i + 1).'</a> ';
2694
                }
2695
            }
2696
            // Table formatting from http://wikkawiki.org/PageAndCategoryDivisionInACategory.
2697
            $total = count($resparr);
2698
            $entries = count($resparr);
2699
            // Default max 3 columns, max 25 lines per column.
2700
            // TODO make this setting customizable.
2701
            $maxlines = 20;
2702
            $maxcols = 3;
2703
            if ($entries >= $maxlines) {
2704
                $colnumber = min (intval($entries / $maxlines), $maxcols);
2705
            } else {
2706
                $colnumber = 1;
2707
            }
2708
            $lines = 0;
2709
            $a = 0;
2710
            // How many lines with an entry in every column do we have?
2711
            while ($entries / $colnumber > 1) {
2712
                $lines++;
2713
                $entries = $entries - $colnumber;
2714
            }
2715
            // Prepare output.
2716
            $respcols = new stdClass();
2717
            for ($i = 0; $i < $colnumber; $i++) {
2718
                $colname = 'respondentscolumn'.$i;
2719
                $respcols->$colname = (object)['respondentlink' => []];
2720
                for ($j = 0; $j < $lines; $j++) {
2721
                    $respcols->{$colname}->respondentlink[] = $resparr[$a];
2722
                    $a++;
2723
                }
2724
                // The rest of the entries (less than the number of cols).
2725
                if ($entries) {
2726
                    $respcols->{$colname}->respondentlink[] = $resparr[$a];
2727
                    $entries--;
2728
                    $a++;
2729
                }
2730
            }
2731
 
2732
            $this->page->add_to_page('responses', $this->renderer->responselist($respcols));
2733
        }
2734
    }
2735
 
2736
    /**
2737
     * Display responses for current user (your responses).
2738
     * @param int $currrid
2739
     * @param int $userid
2740
     * @param int $instance
2741
     * @param array $resps
2742
     * @param string $reporttype
2743
     * @param string $sid
2744
     */
2745
    public function survey_results_navbar_student($currrid, $userid, $instance, $resps, $reporttype='myreport', $sid='') {
2746
        global $DB;
2747
        $stranonymous = get_string('anonymous', 'questionnaire');
2748
 
2749
        $total = count($resps);
2750
        $rids = array();
2751
        $ridssub = array();
2752
        $ridsusers = array();
2753
        $i = 0;
2754
        $currpos = -1;
2755
        $title = '';
2756
        foreach ($resps as $response) {
2757
            array_push($rids, $response->id);
2758
            array_push($ridssub, $response->submitted);
2759
            $ruser = '';
2760
            if ($reporttype == 'report') {
2761
                if ($this->respondenttype != 'anonymous') {
2762
                    if ($user = $DB->get_record('user', ['id' => $response->userid])) {
2763
                        $ruser = ' | ' .fullname($user);
2764
                    }
2765
                } else {
2766
                    $ruser = ' | ' . $stranonymous;
2767
                }
2768
            }
2769
            array_push($ridsusers, $ruser);
2770
            if ($response->id == $currrid) {
2771
                $currpos = $i;
2772
            }
2773
            $i++;
2774
        }
2775
        $prevrid = ($currpos > 0) ? $rids[$currpos - 1] : null;
2776
        $nextrid = ($currpos < $total - 1) ? $rids[$currpos + 1] : null;
2777
 
2778
        if ($reporttype == 'myreport') {
2779
            $url = 'myreport.php?instance='.$instance.'&user='.$userid.'&action=vresp&byresponse=1&individualresponse=1';
2780
        } else {
2781
            $url = 'report.php?instance='.$instance.'&user='.$userid.'&action=vresp&byresponse=1&individualresponse=1&sid='.$sid;
2782
        }
2783
        $navbar = new \stdClass();
2784
        $displaypos = 1;
2785
        if ($prevrid != null) {
2786
            $title = userdate($ridssub[$currpos - 1].$ridsusers[$currpos - 1]);
2787
            $navbar->previous = ['url' => ($url.'&rid='.$prevrid), 'title' => $title];
2788
        }
2789
        for ($i = 0; $i < $currpos; $i++) {
2790
            $title = userdate($ridssub[$i]).$ridsusers[$i];
2791
            $navbar->prevrespnumbers[] = ['url' => ($url.'&rid='.$rids[$i]), 'title' => $title, 'respnumber' => $displaypos];
2792
            $displaypos++;
2793
        }
2794
        $navbar->currrespnumber = $displaypos;
2795
        for (++$i; $i < $total; $i++) {
2796
            $displaypos++;
2797
            $title = userdate($ridssub[$i]).$ridsusers[$i];
2798
            $navbar->nextrespnumbers[] = ['url' => ($url.'&rid='.$rids[$i]), 'title' => $title, 'respnumber' => $displaypos];
2799
        }
2800
        if ($nextrid != null) {
2801
            $title = userdate($ridssub[$currpos + 1]).$ridsusers[$currpos + 1];
2802
            $navbar->next = ['url' => ($url.'&rid='.$nextrid), 'title' => $title];
2803
        }
2804
        $this->page->add_to_page('navigationbar', $this->renderer->usernavigationbar($navbar));
2805
        $this->page->add_to_page('bottomnavigationbar', $this->renderer->usernavigationbar($navbar));
2806
    }
2807
 
2808
    /**
2809
     * Builds HTML for the results for the survey. If a question id and choice id(s) are given, then the results are only calculated
2810
     * for respodants who chose from the choice ids for the given question id. Returns empty string on success, else returns an
2811
     * error string.
2812
     * @param string $rid
2813
     * @param bool $uid
2814
     * @param bool $pdf
2815
     * @param string $currentgroupid
2816
     * @param string $sort
2817
     * @return string|void
2818
     */
2819
    public function survey_results($rid = '', $uid=false, $pdf = false, $currentgroupid='', $sort='') {
2820
        global $SESSION, $DB;
2821
 
2822
        $SESSION->questionnaire->noresponses = false;
2823
 
2824
        // Build associative array holding whether each question
2825
        // type has answer choices or not and the table the answers are in
2826
        // TO DO - FIX BELOW TO USE STANDARD FUNCTIONS.
2827
        $haschoices = array();
2828
        $responsetable = array();
2829
        if (!($types = $DB->get_records('questionnaire_question_type', array(), 'typeid', 'typeid, has_choices, response_table'))) {
2830
            $errmsg = sprintf('%s [ %s: question_type ]',
2831
                get_string('errortable', 'questionnaire'), 'Table');
2832
            return($errmsg);
2833
        }
2834
        foreach ($types as $type) {
2835
            $haschoices[$type->typeid] = $type->has_choices; // TODO is that variable actually used?
2836
            $responsetable[$type->typeid] = $type->response_table;
2837
        }
2838
 
2839
        // Load survey title (and other globals).
2840
        if (empty($this->survey)) {
2841
            $errmsg = get_string('erroropening', 'questionnaire') ." [ ID:{$this->sid} R:";
2842
            return($errmsg);
2843
        }
2844
 
2845
        if (empty($this->questions)) {
2846
            $errmsg = get_string('erroropening', 'questionnaire') .' '. 'No questions found.';
2847
            return($errmsg);
2848
        }
2849
 
2850
        // Find total number of survey responses and relevant response ID's.
2851
        if (!empty($rid)) {
2852
            $rids = $rid;
2853
            if (is_array($rids)) {
2854
                $navbar = false;
2855
            } else {
2856
                $navbar = true;
2857
            }
2858
            $numresps = 1;
2859
        } else {
2860
            $navbar = false;
2861
            if ($uid !== false) { // One participant only.
2862
                $rows = $this->get_responses($uid);
2863
                // All participants or all members of a group.
2864
            } else if ($currentgroupid == 0) {
2865
                $rows = $this->get_responses();
2866
            } else { // Members of a specific group.
2867
                $rows = $this->get_responses(false, $currentgroupid);
2868
            }
2869
            if (!$rows) {
2870
                $this->page->add_to_page('respondentinfo',
2871
                    $this->renderer->notification(get_string('noresponses', 'questionnaire'),
2872
                        \core\output\notification::NOTIFY_ERROR));
2873
                $SESSION->questionnaire->noresponses = true;
2874
                return;
2875
            }
2876
            $numresps = count($rows);
2877
            $this->page->add_to_page('respondentinfo',
2878
                ' '.get_string('responses', 'questionnaire').': <strong>'.$numresps.'</strong>');
2879
            if (empty($rows)) {
2880
                $errmsg = get_string('erroropening', 'questionnaire') .' '. get_string('noresponsedata', 'questionnaire');
2881
                return($errmsg);
2882
            }
2883
 
2884
            $rids = array();
2885
            foreach ($rows as $row) {
2886
                array_push($rids, $row->id);
2887
            }
2888
        }
2889
 
2890
        if ($navbar) {
2891
            // Show response navigation bar.
2892
            $this->survey_results_navbar($rid);
2893
        }
2894
 
2895
        $this->page->add_to_page('title', format_string($this->survey->title));
2896
        if ($this->survey->subtitle) {
2897
            $this->page->add_to_page('subtitle', format_string($this->survey->subtitle));
2898
        }
2899
        if ($this->survey->info) {
2900
            $infotext = file_rewrite_pluginfile_urls($this->survey->info, 'pluginfile.php',
2901
                $this->context->id, 'mod_questionnaire', 'info', $this->survey->id);
2902
            $this->page->add_to_page('addinfo', format_text($infotext, FORMAT_HTML, ['noclean' => true]));
2903
        }
2904
 
2905
        $qnum = 0;
2906
 
2907
        $anonymous = $this->respondenttype == 'anonymous';
2908
 
2909
        foreach ($this->questions as $question) {
2910
            if ($question->type_id == QUESPAGEBREAK) {
2911
                continue;
2912
            }
2913
            if ($question->type_id != QUESSECTIONTEXT) {
2914
                $qnum++;
2915
            }
2916
            if (!$pdf) {
2917
                $this->page->add_to_page('responses', $this->renderer->container_start('qn-container'));
2918
                $this->page->add_to_page('responses', $this->renderer->container_start('qn-info'));
2919
                if ($question->type_id != QUESSECTIONTEXT) {
2920
                    $this->page->add_to_page('responses', $this->renderer->heading($qnum, 2, 'qn-number'));
2921
                }
2922
                $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-info.
2923
                $this->page->add_to_page('responses', $this->renderer->container_start('qn-content'));
2924
            }
2925
            // If question text is "empty", i.e. 2 non-breaking spaces were inserted, do not display any question text.
2926
            if ($question->content == '<p>  </p>') {
2927
                $question->content = '';
2928
            }
2929
            if ($pdf) {
2930
                $response = new stdClass();
2931
                if ($question->type_id != QUESSECTIONTEXT) {
2932
                    $response->qnum = $qnum;
2933
                }
2934
                $response->qcontent = format_text(file_rewrite_pluginfile_urls($question->content, 'pluginfile.php',
2935
                    $question->context->id, 'mod_questionnaire', 'question', $question->id),
2936
                    FORMAT_HTML, ['noclean' => true]);
2937
                $response->results = $this->renderer->results_output($question, $rids, $sort, $anonymous, $pdf);
2938
                $this->page->add_to_page('responses', $response);
2939
            } else {
2940
                $this->page->add_to_page('responses',
2941
                    $this->renderer->container(format_text(file_rewrite_pluginfile_urls($question->content, 'pluginfile.php',
2942
                        $question->context->id, 'mod_questionnaire', 'question', $question->id),
2943
                        FORMAT_HTML, ['noclean' => true]), 'qn-question'));
2944
                $this->page->add_to_page('responses', $this->renderer->results_output($question, $rids, $sort, $anonymous));
2945
                $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-content.
2946
                $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-container.
2947
            }
2948
        }
2949
 
2950
        return;
2951
    }
2952
 
2953
    /**
2954
     * Get unique list of question types used in the current survey.
2955
     * author: Guy Thomas
2956
     * @param bool $uniquebytable
2957
     * @return array
2958
     */
2959
    protected function get_survey_questiontypes($uniquebytable = false) {
2960
 
2961
        $uniquetypes = [];
2962
        $uniquetables = [];
2963
 
2964
        foreach ($this->questions as $question) {
2965
            $type = $question->type_id;
2966
            $responsetable = $question->responsetable;
2967
            // Build SQL for this question type if not already done.
2968
            if (!$uniquebytable || !in_array($responsetable, $uniquetables)) {
2969
                if (!in_array($type, $uniquetypes)) {
2970
                    $uniquetypes[] = $type;
2971
                }
2972
                if (!in_array($responsetable, $uniquetables)) {
2973
                    $uniquetables[] = $responsetable;
2974
                }
2975
            }
2976
        }
2977
 
2978
        return $uniquetypes;
2979
    }
2980
 
2981
    /**
2982
     * Return array of all types considered to be choices.
2983
     *
2984
     * @return array
2985
     */
2986
    protected function choice_types() {
2987
        return [QUESRADIO, QUESDROP, QUESCHECK, QUESRATE];
2988
    }
2989
 
2990
    /**
2991
     * Return all the fields to be used for users in questionnaire sql.
2992
     * author: Guy Thomas
2993
     * @return array|string
2994
     */
2995
    protected function user_fields() {
2996
        if (class_exists('\core_user\fields')) {
2997
            $userfieldsarr = \core_user\fields::get_name_fields();
2998
        } else {
2999
            $userfieldsarr = get_all_user_name_fields();
3000
        }
3001
        $userfieldsarr = array_merge($userfieldsarr, ['username', 'department', 'institution']);
3002
        return $userfieldsarr;
3003
    }
3004
 
3005
    /**
3006
     * Get all survey responses in one go.
3007
     * author: Guy Thomas
3008
     * @param string $rid
3009
     * @param string $userid
3010
     * @param bool $groupid
3011
     * @param int $showincompletes
3012
     * @return array
3013
     */
3014
    protected function get_survey_all_responses($rid = '', $userid = '', $groupid = false, $showincompletes = 0) {
3015
        global $DB;
3016
        $uniquetypes = $this->get_survey_questiontypes(true);
3017
        $allresponsessql = "";
3018
        $allresponsesparams = [];
3019
 
3020
        // If a questionnaire is "public", and this is the master course, need to get responses from all instances.
3021
        if ($this->survey_is_public_master()) {
3022
            $qids = array_keys($DB->get_records('questionnaire', ['sid' => $this->sid], 'id') ?? []);
3023
        } else {
3024
            $qids = $this->id;
3025
        }
3026
 
3027
        foreach ($uniquetypes as $type) {
3028
            $question = \mod_questionnaire\question\question::question_builder($type);
3029
            if (!isset($question->responsetype)) {
3030
                continue;
3031
            }
3032
            $allresponsessql .= $allresponsessql == '' ? '' : ' UNION ALL ';
3033
            list ($sql, $params) = $question->responsetype->get_bulk_sql($qids, $rid, $userid, $groupid, $showincompletes);
3034
            $allresponsesparams = array_merge($allresponsesparams, $params);
3035
            $allresponsessql .= $sql;
3036
        }
3037
 
3038
        $allresponsessql .= " ORDER BY usrid, id";
3039
        $allresponses = $DB->get_recordset_sql($allresponsessql, $allresponsesparams);
3040
        return $allresponses ?? [];
3041
    }
3042
 
3043
    /**
3044
     * Return true if the survey is a 'public' one.
3045
     *
3046
     * @return boolean
3047
     */
3048
    public function survey_is_public() {
3049
        return is_object($this->survey) && ($this->survey->realm == 'public');
3050
    }
3051
 
3052
    /**
3053
     * Return true if the survey is a 'public' one and this is the master instance.
3054
     *
3055
     * @return boolean
3056
     */
3057
    public function survey_is_public_master() {
3058
        return $this->survey_is_public() && ($this->course->id == $this->survey->courseid);
3059
    }
3060
 
3061
    /**
3062
     * Process individual row for csv output
3063
     * @param array $row
3064
     * @param stdClass $resprow resultset row
3065
     * @param int $currentgroupid
3066
     * @param array $questionsbyposition
3067
     * @param int $nbinfocols
3068
     * @param int $numrespcols
3069
     * @param int $showincompletes
3070
     * @return array
3071
     */
3072
    protected function process_csv_row(array &$row,
3073
                                       stdClass $resprow,
3074
                                       $currentgroupid,
3075
                                       array &$questionsbyposition,
3076
                                       $nbinfocols,
3077
                                       $numrespcols, $showincompletes = 0) {
3078
        global $DB;
3079
 
3080
        static $config = null;
3081
        // If using an anonymous response, map users to unique user numbers so that number of unique anonymous users can be seen.
3082
        static $anonumap = [];
3083
 
3084
        if ($config === null) {
3085
            $config = get_config('questionnaire', 'downloadoptions');
3086
        }
3087
        $options = empty($config) ? array() : explode(',', $config);
3088
        if ($showincompletes == 1) {
3089
            $options[] = 'complete';
3090
        }
3091
 
3092
        $positioned = [];
3093
        $user = new stdClass();
3094
        foreach ($this->user_fields() as $userfield) {
3095
            $user->$userfield = $resprow->$userfield;
3096
        }
3097
        $user->id = $resprow->userid;
3098
        $isanonymous = ($this->respondenttype == 'anonymous');
3099
 
3100
        // Moodle:
3101
        // Get the course name that this questionnaire belongs to.
3102
        if (!$this->survey_is_public()) {
3103
            $courseid = $this->course->id;
3104
            $coursename = $this->course->fullname;
3105
        } else {
3106
            // For a public questionnaire, look for the course that used it.
3107
            $sql = 'SELECT q.id, q.course, c.fullname ' .
3108
                   'FROM {questionnaire_response} qr ' .
3109
                   'INNER JOIN {questionnaire} q ON qr.questionnaireid = q.id ' .
3110
                   'INNER JOIN {course} c ON q.course = c.id ' .
3111
                   'WHERE qr.id = ? AND qr.complete = ? ';
3112
            if ($record = $DB->get_record_sql($sql, [$resprow->rid, 'y'])) {
3113
                $courseid = $record->course;
3114
                $coursename = $record->fullname;
3115
            } else {
3116
                $courseid = $this->course->id;
3117
                $coursename = $this->course->fullname;
3118
            }
3119
        }
3120
 
3121
        // Moodle:
3122
        // Determine if the user is a member of a group in this course or not.
3123
        // TODO - review for performance.
3124
        $groupname = '';
3125
        if (groups_get_activity_groupmode($this->cm, $this->course)) {
3126
            if ($currentgroupid > 0) {
3127
                $groupname = groups_get_group_name($currentgroupid);
3128
            } else {
3129
                if ($user->id) {
3130
                    if ($groups = groups_get_all_groups($courseid, $user->id)) {
3131
                        foreach ($groups as $group) {
3132
                            $groupname .= $group->name.', ';
3133
                        }
3134
                        $groupname = substr($groupname, 0, strlen($groupname) - 2);
3135
                    } else {
3136
                        $groupname = ' ('.get_string('groupnonmembers').')';
3137
                    }
3138
                }
3139
            }
3140
        }
3141
 
3142
        if ($isanonymous) {
3143
            if (!isset($anonumap[$user->id])) {
3144
                $anonumap[$user->id] = count($anonumap) + 1;
3145
            }
3146
            $fullname = get_string('anonymous', 'questionnaire') . $anonumap[$user->id];
3147
            $username = '';
3148
            $uid = '';
3149
        } else {
3150
            $uid = $user->id;
3151
            $fullname = fullname($user);
3152
            $username = $user->username;
3153
        }
3154
 
3155
        if (in_array('response', $options)) {
3156
            array_push($positioned, $resprow->rid);
3157
        }
3158
        if (in_array('submitted', $options)) {
3159
            // For better compabitility & readability with Excel.
3160
            $submitted = date(get_string('strfdateformatcsv', 'questionnaire'), $resprow->submitted);
3161
            array_push($positioned, $submitted);
3162
        }
3163
        if (in_array('institution', $options)) {
3164
            array_push($positioned, $user->institution);
3165
        }
3166
        if (in_array('department', $options)) {
3167
            array_push($positioned, $user->department);
3168
        }
3169
        if (in_array('course', $options)) {
3170
            array_push($positioned, $coursename);
3171
        }
3172
        if (in_array('group', $options)) {
3173
            array_push($positioned, $groupname);
3174
        }
3175
        if (in_array('id', $options)) {
3176
            array_push($positioned, $uid);
3177
        }
3178
        if (in_array('fullname', $options)) {
3179
            array_push($positioned, $fullname);
3180
        }
3181
        if (in_array('username', $options)) {
3182
            array_push($positioned, $username);
3183
        }
3184
        if (in_array('complete', $options)) {
3185
            array_push($positioned, $resprow->complete);
3186
        }
3187
 
3188
        for ($c = $nbinfocols; $c < $numrespcols; $c++) {
3189
            if (isset($row[$c])) {
3190
                $positioned[] = $row[$c];
3191
            } else if (isset($questionsbyposition[$c])) {
3192
                $question = $questionsbyposition[$c];
3193
                $qtype = intval($question->type_id);
3194
                if ($qtype === QUESCHECK) {
3195
                    $positioned[] = '0';
3196
                } else {
3197
                    $positioned[] = null;
3198
                }
3199
            } else {
3200
                $positioned[] = null;
3201
            }
3202
        }
3203
        return $positioned;
3204
    }
3205
 
3206
    /**
3207
     * Exports the results of a survey to an array.
3208
     * @param int $currentgroupid
3209
     * @param string $rid
3210
     * @param string $userid
3211
     * @param int $choicecodes
3212
     * @param int $choicetext
3213
     * @param int $showincompletes
3214
     * @param int $rankaverages
3215
     * @return array
3216
     */
3217
    public function generate_csv($currentgroupid, $rid='', $userid='', $choicecodes=1, $choicetext=0, $showincompletes=0,
3218
                                 $rankaverages=0) {
3219
        global $DB;
3220
 
3221
        raise_memory_limit('1G');
3222
 
3223
        $output = array();
3224
        $stringother = get_string('other', 'questionnaire');
3225
 
3226
        $config = get_config('questionnaire', 'downloadoptions');
3227
        $options = empty($config) ? array() : explode(',', $config);
3228
        if ($showincompletes == 1) {
3229
            $options[] = 'complete';
3230
        }
3231
        $columns = array();
3232
        $types = array();
3233
        foreach ($options as $option) {
3234
            if (in_array($option, array('response', 'submitted', 'id'))) {
3235
                $columns[] = get_string($option, 'questionnaire');
3236
                $types[] = 0;
3237
            } else {
3238
                $columns[] = get_string($option);
3239
                $types[] = 1;
3240
            }
3241
        }
3242
        $nbinfocols = count($columns);
3243
 
3244
        $idtocsvmap = array(
3245
            '0',    // 0: unused
3246
            '0',    // 1: bool -> boolean
3247
            '1',    // 2: text -> string
3248
            '1',    // 3: essay -> string
3249
            '0',    // 4: radio -> string
3250
            '0',    // 5: check -> string
3251
            '0',    // 6: dropdn -> string
3252
            '0',    // 7: rating -> number
3253
            '0',    // 8: rate -> number
3254
            '1',    // 9: date -> string
3255
            '0',    // 10: numeric -> number.
3256
            '0',    // 11: slider -> number.
3257
        );
3258
 
3259
        if (!$survey = $DB->get_record('questionnaire_survey', array('id' => $this->survey->id))) {
3260
            throw new \moodle_exception('surveynotexists', 'mod_questionnaire');
3261
        }
3262
 
3263
        // Get all responses for this survey in one go.
3264
        $allresponsesrs = $this->get_survey_all_responses($rid, $userid, $currentgroupid, $showincompletes);
3265
 
3266
        // Do we have any questions of type RADIO, DROP, CHECKBOX OR RATE? If so lets get all their choices in one go.
3267
        $choicetypes = $this->choice_types();
3268
 
3269
        // Get unique list of question types used in this survey.
3270
        $uniquetypes = $this->get_survey_questiontypes();
3271
 
3272
        if (count(array_intersect($choicetypes, $uniquetypes)) > 0) {
3273
            $choiceparams = [$this->survey->id];
3274
            $choicesql = "
3275
                SELECT DISTINCT c.id as cid, q.id as qid, q.precise AS precise, q.name, c.content
3276
                  FROM {questionnaire_question} q
3277
                  JOIN {questionnaire_quest_choice} c ON question_id = q.id
3278
                 WHERE q.surveyid = ? ORDER BY cid ASC
3279
            ";
3280
            $choicerecords = $DB->get_records_sql($choicesql, $choiceparams);
3281
            $choicesbyqid = [];
3282
            if (!empty($choicerecords)) {
3283
                // Hash the options by question id.
3284
                foreach ($choicerecords as $choicerecord) {
3285
                    if (!isset($choicesbyqid[$choicerecord->qid])) {
3286
                        // New question id detected, intialise empty array to store choices.
3287
                        $choicesbyqid[$choicerecord->qid] = [];
3288
                    }
3289
                    $choicesbyqid[$choicerecord->qid][$choicerecord->cid] = $choicerecord;
3290
                }
3291
            }
3292
        }
3293
 
3294
        $num = 1;
3295
 
3296
        $questionidcols = [];
3297
 
3298
        foreach ($this->questions as $question) {
3299
            // Skip questions that aren't response capable.
3300
            if (!isset($question->responsetype)) {
3301
                continue;
3302
            }
3303
            // Establish the table's field names.
3304
            $qid = $question->id;
3305
            $qpos = $question->position;
3306
            $col = $question->name;
3307
            $type = $question->type_id;
3308
            if (in_array($type, $choicetypes)) {
3309
                /* single or multiple or rate */
3310
                if (!isset($choicesbyqid[$qid])) {
3311
                    throw new coding_exception('Choice question has no choices!', 'question id '.$qid.' of type '.$type);
3312
                }
3313
                $choices = $choicesbyqid[$qid];
3314
 
3315
                switch ($type) {
3316
 
3317
                    case QUESRADIO: // Single.
3318
                    case QUESDROP:
3319
                        $columns[][$qpos] = $col;
3320
                        $questionidcols[][$qpos] = $qid;
3321
                        array_push($types, $idtocsvmap[$type]);
3322
                        $thisnum = 1;
3323
                        foreach ($choices as $choice) {
3324
                            $content = $choice->content;
3325
                            // If "Other" add a column for the actual "other" text entered.
3326
                            if (\mod_questionnaire\question\choice::content_is_other_choice($content)) {
3327
                                $col = $choice->name.'_'.$stringother;
3328
                                $columns[][$qpos] = $col;
3329
                                $questionidcols[][$qpos] = null;
3330
                                array_push($types, '0');
3331
                            }
3332
                        }
3333
                        break;
3334
 
3335
                    case QUESCHECK: // Multiple.
3336
                        $thisnum = 1;
3337
                        foreach ($choices as $choice) {
3338
                            $content = $choice->content;
3339
                            $modality = '';
3340
                            $contents = questionnaire_choice_values($content);
3341
                            if ($contents->modname) {
3342
                                $modality = $contents->modname;
3343
                            } else if ($contents->title) {
3344
                                $modality = $contents->title;
3345
                            } else {
3346
                                $modality = strip_tags($contents->text);
3347
                            }
3348
                            $col = $choice->name.'->'.$modality;
3349
                            $columns[][$qpos] = $col;
3350
                            $questionidcols[][$qpos] = $qid.'_'.$choice->cid;
3351
                            array_push($types, '0');
3352
                            // If "Other" add a column for the "other" checkbox.
3353
                            // Then add a column for the actual "other" text entered.
3354
                            if (\mod_questionnaire\question\choice::content_is_other_choice($content)) {
3355
                                $content = $stringother;
3356
                                $col = $choice->name.'->['.$content.']';
3357
                                $columns[][$qpos] = $col;
3358
                                $questionidcols[][$qpos] = null;
3359
                                array_push($types, '0');
3360
                            }
3361
                        }
3362
                        break;
3363
 
3364
                    case QUESRATE: // Rate.
3365
                        foreach ($choices as $choice) {
3366
                            $nameddegrees = 0;
3367
                            $modality = '';
3368
                            $content = $choice->content;
3369
                            $osgood = false;
3370
                            if (\mod_questionnaire\question\rate::type_is_osgood_rate_scale($choice->precise)) {
3371
                                $osgood = true;
3372
                            }
3373
                            if (preg_match("/^[0-9]{1,3}=/", $content, $ndd)) {
3374
                                $nameddegrees++;
3375
                            } else {
3376
                                if ($osgood) {
3377
                                    list($contentleft, $contentright) = array_merge(preg_split('/[|]/', $content), array(' '));
3378
                                    $contents = questionnaire_choice_values($contentleft);
3379
                                    if ($contents->title) {
3380
                                        $contentleft = $contents->title;
3381
                                    }
3382
                                    $contents = questionnaire_choice_values($contentright);
3383
                                    if ($contents->title) {
3384
                                        $contentright = $contents->title;
3385
                                    }
3386
                                    $modality = strip_tags($contentleft.'|'.$contentright);
3387
                                    $modality = preg_replace("/[\r\n\t]/", ' ', $modality);
3388
                                } else {
3389
                                    $contents = questionnaire_choice_values($content);
3390
                                    if ($contents->modname) {
3391
                                        $modality = $contents->modname;
3392
                                    } else if ($contents->title) {
3393
                                        $modality = $contents->title;
3394
                                    } else {
3395
                                        $modality = strip_tags($contents->text);
3396
                                        $modality = preg_replace("/[\r\n\t]/", ' ', $modality);
3397
                                    }
3398
                                }
3399
                                $col = $choice->name.'->'.$modality;
3400
                                $columns[][$qpos] = $col;
3401
                                $questionidcols[][$qpos] = $qid.'_'.$choice->cid;
3402
                                array_push($types, $idtocsvmap[$type]);
3403
                            }
3404
                        }
3405
                        break;
3406
                }
3407
            } else {
3408
                $columns[][$qpos] = $col;
3409
                $questionidcols[][$qpos] = $qid;
3410
                array_push($types, $idtocsvmap[$type]);
3411
            }
3412
            $num++;
3413
        }
3414
 
3415
        array_push($output, $columns);
3416
        $numrespcols = count($output[0]); // Number of columns used for storing question responses.
3417
 
3418
        // Flatten questionidcols.
3419
        $tmparr = [];
3420
        for ($c = 0; $c < $nbinfocols; $c++) {
3421
            $tmparr[] = null; // Pad with non question columns.
3422
        }
3423
        foreach ($questionidcols as $i => $positions) {
3424
            foreach ($positions as $position => $qid) {
3425
                $tmparr[] = $qid;
3426
            }
3427
        }
3428
        $questionidcols = $tmparr;
3429
 
3430
        // Create array of question positions hashed by question / question + choiceid.
3431
        // And array of questions hashed by position.
3432
        $questionpositions = [];
3433
        $questionsbyposition = [];
3434
        $p = 0;
3435
        foreach ($questionidcols as $qid) {
3436
            if ($qid === null) {
3437
                // This is just padding, skip.
3438
                $p++;
3439
                continue;
3440
            }
3441
            $questionpositions[$qid] = $p;
3442
            if (strpos($qid, '_') !== false) {
3443
                $tmparr = explode ('_', $qid);
3444
                $questionid = $tmparr[0];
3445
            } else {
3446
                $questionid = $qid;
3447
            }
3448
            $questionsbyposition[$p] = $this->questions[$questionid];
3449
            $p++;
3450
        }
3451
 
3452
        $formatoptions = new stdClass();
3453
        $formatoptions->filter = false;  // To prevent any filtering in CSV output.
3454
 
3455
        if ($rankaverages) {
3456
            $averages = [];
3457
            $rids = [];
3458
            $allresponsesrs2 = $this->get_survey_all_responses($rid, $userid, $currentgroupid);
3459
            foreach ($allresponsesrs2 as $responserow) {
3460
                if (!isset($rids[$responserow->rid])) {
3461
                    $rids[$responserow->rid] = $responserow->rid;
3462
                }
3463
            }
3464
        }
3465
 
3466
        // Get textual versions of responses, add them to output at the correct col position.
3467
        $prevresprow = false; // Previous response row.
3468
        $row = [];
3469
        if ($rankaverages) {
3470
            $averagerow = [];
3471
        }
3472
        foreach ($allresponsesrs as $responserow) {
3473
            $rid = $responserow->rid;
3474
            $qid = $responserow->question_id;
3475
 
3476
            // It's possible for a response to exist for a deleted question. Ignore these.
3477
            if (!isset($this->questions[$qid])) {
3478
                continue;
3479
            }
3480
 
3481
            $question = $this->questions[$qid];
3482
            $qtype = intval($question->type_id);
3483
            if ($rankaverages) {
3484
                if ($qtype === QUESRATE) {
3485
                    if (empty($averages[$qid])) {
3486
                        $results = $this->questions[$qid]->responsetype->get_results($rids);
3487
                        foreach ($results as $qresult) {
3488
                            $averages[$qid][$qresult->id] = $qresult->average;
3489
                        }
3490
                    }
3491
                }
3492
            }
3493
            $questionobj = $this->questions[$qid];
3494
 
3495
            if ($prevresprow !== false && $prevresprow->rid !== $rid) {
3496
                $output[] = $this->process_csv_row($row, $prevresprow, $currentgroupid, $questionsbyposition,
3497
                    $nbinfocols, $numrespcols, $showincompletes);
3498
                $row = [];
3499
            }
3500
 
3501
            if ($qtype === QUESRATE || $qtype === QUESCHECK) {
3502
                $key = $qid.'_'.$responserow->choice_id;
3503
                $position = $questionpositions[$key];
3504
                if ($qtype === QUESRATE) {
3505
                    $choicetxt = $responserow->rankvalue;
3506
                    if ($rankaverages) {
3507
                        $averagerow[$position] = $averages[$qid][$responserow->choice_id];
3508
                    }
3509
                } else {
3510
                    $content = $choicesbyqid[$qid][$responserow->choice_id]->content;
3511
                    if (\mod_questionnaire\question\choice::content_is_other_choice($content)) {
3512
                        // If this is an "other" column, put the text entered in the next position.
3513
                        $row[$position + 1] = $responserow->response;
3514
                        $choicetxt = empty($responserow->choice_id) ? '0' : '1';
3515
                    } else if (!empty($responserow->choice_id)) {
3516
                        $choicetxt = '1';
3517
                    } else {
3518
                        $choicetxt = '0';
3519
                    }
3520
                }
3521
                $responsetxt = $choicetxt;
3522
                $row[$position] = $responsetxt;
3523
            } else {
3524
                $position = $questionpositions[$qid];
3525
                if ($questionobj->has_choices()) {
3526
                    // This is choice type question, so process as so.
3527
                    $c = 0;
3528
                    if (in_array(intval($question->type_id), $choicetypes)) {
3529
                        $choices = $choicesbyqid[$qid];
3530
                        // Get position of choice.
3531
                        foreach ($choices as $choice) {
3532
                            $c++;
3533
                            if ($responserow->choice_id === $choice->cid) {
3534
                                break;
3535
                            }
3536
                        }
3537
                    }
3538
 
3539
                    $content = $choicesbyqid[$qid][$responserow->choice_id]->content;
3540
                    if (\mod_questionnaire\question\choice::content_is_other_choice($content)) {
3541
                        // If this has an "other" text, use it.
3542
                        $responsetxt = \mod_questionnaire\question\choice::content_other_choice_display($content);
3543
                        $responsetxt1 = $responserow->response;
3544
                    } else if (($choicecodes == 1) && ($choicetext == 1)) {
3545
                        $responsetxt = $c.' : '.$content;
3546
                    } else if ($choicecodes == 1) {
3547
                        $responsetxt = $c;
3548
                    } else {
3549
                        $responsetxt = $content;
3550
                    }
3551
                } else if (intval($qtype) === QUESYESNO) {
3552
                    // At this point, the boolean responses are returned as characters in the "response"
3553
                    // field instead of "choice_id" for csv exports (CONTRIB-6436).
3554
                    $responsetxt = $responserow->response === 'y' ? "1" : "0";
3555
                } else {
3556
                    // Strip potential html tags from modality name.
3557
                    $responsetxt = $responserow->response;
3558
                    if (!empty($responsetxt)) {
3559
                        $responsetxt = $responserow->response;
3560
                        $responsetxt = strip_tags($responsetxt);
3561
                        $responsetxt = preg_replace("/[\r\n\t]/", ' ', $responsetxt);
3562
                    }
3563
                }
3564
                $row[$position] = $responsetxt;
3565
                // Check for "other" text and set it to the next position if present.
3566
                if (!empty($responsetxt1)) {
3567
                    $responsetxt1 = preg_replace("/[\r\n\t]/", ' ', $responsetxt1);
3568
                    $row[$position + 1] = $responsetxt1;
3569
                    unset($responsetxt1);
3570
                }
3571
            }
3572
 
3573
            $prevresprow = $responserow;
3574
        }
3575
 
3576
        if ($prevresprow !== false) {
3577
            // Add final row to output. May not exist if no response data was ever present.
3578
            $output[] = $this->process_csv_row($row, $prevresprow, $currentgroupid, $questionsbyposition,
3579
                $nbinfocols, $numrespcols, $showincompletes);
3580
        }
3581
 
3582
        // Add averages row if appropriate.
3583
        if ($rankaverages) {
3584
            $summaryrow = [];
3585
            $summaryrow[0] = get_string('averagesrow', 'questionnaire');
3586
            $i = 1;
3587
            for ($i = 1; $i < $nbinfocols; $i++) {
3588
                $summaryrow[$i] = '';
3589
            }
3590
            $pos = 0;
3591
            for ($i = $nbinfocols; $i < $numrespcols; $i++) {
3592
                $summaryrow[$i] = isset($averagerow[$i]) ? $averagerow[$i] : '';
3593
            }
3594
            $output[] = $summaryrow;
3595
        }
3596
 
3597
        // Change table headers to incorporate actual question numbers.
3598
        $numquestion = 0;
3599
        $oldkey = 0;
3600
 
3601
        for ($i = $nbinfocols; $i < $numrespcols; $i++) {
3602
            $sep = '';
3603
            $thisoutput = current($output[0][$i]);
3604
            $thiskey = key($output[0][$i]);
3605
            // Case of unnamed rate single possible answer (full stop char is used for support).
3606
            if (strstr($thisoutput, '->.')) {
3607
                $thisoutput = str_replace('->.', '', $thisoutput);
3608
            }
3609
            // If variable is not named no separator needed between Question number and potential sub-variables.
3610
            if ($thisoutput == '' || strstr($thisoutput, '->.') || substr($thisoutput, 0, 2) == '->'
3611
                || substr($thisoutput, 0, 1) == '_') {
3612
                $sep = '';
3613
            } else {
3614
                $sep = '_';
3615
            }
3616
            if ($thiskey > $oldkey) {
3617
                $oldkey = $thiskey;
3618
                $numquestion++;
3619
            }
3620
            // Abbreviated modality name in multiple or rate questions (COLORS->blue=the color of the sky...).
3621
            $pos = strpos($thisoutput, '=');
3622
            if ($pos) {
3623
                $thisoutput = substr($thisoutput, 0, $pos);
3624
            }
3625
            $out = 'Q'.sprintf("%02d", $numquestion).$sep.$thisoutput;
3626
            $output[0][$i] = $out;
3627
        }
3628
        return $output;
3629
    }
3630
 
3631
    /**
3632
     * Function to move a question to a new position.
3633
     * Adapted from feedback plugin.
3634
     *
3635
     * @param int $moveqid The id of the question to be moved.
3636
     * @param int $movetopos The position to move question to.
3637
     *
3638
     */
3639
    public function move_question($moveqid, $movetopos) {
3640
        global $DB;
3641
 
3642
        $questions = $this->questions;
3643
        $movequestion = $this->questions[$moveqid];
3644
 
3645
        if (is_array($questions)) {
3646
            $index = 1;
3647
            foreach ($questions as $question) {
3648
                if ($index == $movetopos) {
3649
                    $index++;
3650
                }
3651
                if ($question->id == $movequestion->id) {
3652
                    $movequestion->position = $movetopos;
3653
                    $DB->update_record("questionnaire_question", $movequestion);
3654
                    continue;
3655
                }
3656
                $question->position = $index;
3657
                $DB->update_record("questionnaire_question", $question);
3658
                $index++;
3659
            }
3660
            return true;
3661
        }
3662
        return false;
3663
    }
3664
 
3665
    /**
3666
     * Render the response analysis page.
3667
     * @param int $rid
3668
     * @param array $resps
3669
     * @param bool $compare
3670
     * @param bool $isgroupmember
3671
     * @param bool $allresponses
3672
     * @param int $currentgroupid
3673
     * @param array $filteredsections
3674
     * @return array|string
3675
     */
3676
    public function response_analysis($rid, $resps, $compare, $isgroupmember, $allresponses, $currentgroupid,
3677
                                      $filteredsections = null) {
3678
        global $DB, $CFG;
3679
        require_once($CFG->libdir.'/tablelib.php');
3680
        require_once($CFG->dirroot.'/mod/questionnaire/drawchart.php');
3681
 
3682
        // Find if there are any feedbacks in this questionnaire.
3683
        $sql = "SELECT * FROM {questionnaire_fb_sections} WHERE surveyid = ? AND section IS NOT NULL";
3684
        if (!$fbsections = $DB->get_records_sql($sql, [$this->survey->id])) {
3685
            return '';
3686
        }
3687
 
3688
        $action = optional_param('action', 'vall', PARAM_ALPHA);
3689
 
3690
        $resp = $DB->get_record('questionnaire_response', ['id' => $rid]);
3691
        if (!empty($resp)) {
3692
            $userid = $resp->userid;
3693
            $user = $DB->get_record('user', ['id' => $userid]);
3694
            if (!empty($user)) {
3695
                if ($this->respondenttype == 'anonymous') {
3696
                    $ruser = '- ' . get_string('anonymous', 'questionnaire') . ' -';
3697
                } else {
3698
                    $ruser = fullname($user);
3699
                }
3700
            }
3701
        }
3702
        // Available group modes (0 = no groups; 1 = separate groups; 2 = visible groups).
3703
        $groupmode = groups_get_activity_groupmode($this->cm, $this->course);
3704
        $groupname = get_string('allparticipants');
3705
        if ($groupmode > 0) {
3706
            if ($currentgroupid > 0) {
3707
                $groupname = groups_get_group_name($currentgroupid);
3708
            } else {
3709
                $groupname = get_string('allparticipants');
3710
            }
3711
        }
3712
        if ($this->survey->feedbackscores) {
3713
            $table = new html_table();
3714
            $table->size = [null, null];
3715
            $table->align = ['left', 'right', 'right'];
3716
            $table->head = [];
3717
            $table->wrap = [];
3718
            if ($compare) {
3719
                $table->head = [get_string('feedbacksection', 'questionnaire'), $ruser, $groupname];
3720
            } else {
3721
                $table->head = [get_string('feedbacksection', 'questionnaire'), $groupname];
3722
            }
3723
        }
3724
 
3725
        $fbsectionsnb = array_keys($fbsections);
3726
        $numsections = count($fbsections);
3727
 
3728
        // Get all response ids for all respondents.
3729
        $rids = array();
3730
        foreach ($resps as $key => $resp) {
3731
            $rids[] = $key;
3732
        }
3733
        $nbparticipants = count($rids);
3734
        $responsescores = [];
3735
 
3736
        // Calculate max score per question in questionnaire.
3737
        $qmax = [];
3738
        $maxtotalscore = 0;
3739
        foreach ($this->questions as $question) {
3740
            $qid = $question->id;
3741
            if ($question->valid_feedback()) {
3742
                $qmax[$qid] = $question->get_feedback_maxscore();
3743
                $maxtotalscore += $qmax[$qid];
3744
                // Get all the feedback scores for this question.
3745
                $responsescores[$qid] = $question->get_feedback_scores($rids);
3746
            }
3747
        }
3748
        // Just in case no values have been entered in the various questions possible answers field.
3749
        if ($maxtotalscore === 0) {
3750
            return '';
3751
        }
3752
        $feedbackmessages = [];
3753
 
3754
        // Get individual scores for each question in this responses set.
3755
        $qscore = [];
3756
        $allqscore = [];
3757
 
3758
        if (!$allresponses && $groupmode != 0) {
3759
            $nbparticipants = max(1, $nbparticipants - !$isgroupmember);
3760
        }
3761
        foreach ($responsescores as $qid => $responsescore) {
3762
            if (!empty($responsescore)) {
3763
                foreach ($responsescore as $rrid => $response) {
3764
                    // If this is current user's response OR if current user is viewing another group's results.
3765
                    if ($rrid == $rid || $allresponses) {
3766
                        if (!isset($qscore[$qid])) {
3767
                            $qscore[$qid] = 0;
3768
                        }
3769
                        $qscore[$qid] = $response->score;
3770
                    }
3771
                    // Course score.
3772
                    if (!isset($allqscore[$qid])) {
3773
                        $allqscore[$qid] = 0;
3774
                    }
3775
                    // Only add current score if conditions below are met.
3776
                    if ($groupmode == 0 || $isgroupmember || (!$isgroupmember && $rrid != $rid) || $allresponses) {
3777
                        $allqscore[$qid] += $response->score;
3778
                    }
3779
                }
3780
            }
3781
        }
3782
        $totalscore = array_sum($qscore);
3783
        $scorepercent = round($totalscore / $maxtotalscore * 100);
3784
        $oppositescorepercent = 100 - $scorepercent;
3785
        $alltotalscore = array_sum($allqscore);
3786
        $allscorepercent = round($alltotalscore / $nbparticipants / $maxtotalscore * 100);
3787
 
3788
        // No need to go further if feedback is global, i.e. only relying on total score.
3789
        if ($this->survey->feedbacksections == 1) {
3790
            $sectionid = $fbsectionsnb[0];
3791
            $sectionlabel = $fbsections[$sectionid]->sectionlabel;
3792
 
3793
            $sectionheading = $fbsections[$sectionid]->sectionheading;
3794
            $labels = array();
3795
            if ($feedbacks = $DB->get_records('questionnaire_feedback', ['sectionid' => $sectionid])) {
3796
                foreach ($feedbacks as $feedback) {
3797
                    if ($feedback->feedbacklabel != '') {
3798
                        $labels[] = $feedback->feedbacklabel;
3799
                    }
3800
                }
3801
            }
3802
            $feedback = $DB->get_record_select('questionnaire_feedback',
3803
                'sectionid = ? AND minscore <= ? AND ? < maxscore', [$sectionid, $scorepercent, $scorepercent]);
3804
 
3805
            // To eliminate all potential % chars in heading text (might interfere with the sprintf function).
3806
            $sectionheading = str_replace('%', '', $sectionheading);
3807
            // Replace section heading placeholders with their actual value (if any).
3808
            $original = array('$scorepercent', '$oppositescorepercent');
3809
            $result = array('%s%%', '%s%%');
3810
            $sectionheading = str_replace($original, $result, $sectionheading);
3811
            $sectionheading = sprintf($sectionheading , $scorepercent, $oppositescorepercent);
3812
            $sectionheading = file_rewrite_pluginfile_urls($sectionheading, 'pluginfile.php',
3813
                $this->context->id, 'mod_questionnaire', 'sectionheading', $sectionid);
3814
            $feedbackmessages[] = $this->renderer->box_start();
3815
            $feedbackmessages[] = format_text($sectionheading, FORMAT_HTML, ['noclean' => true]);
3816
            $feedbackmessages[] = $this->renderer->box_end();
3817
 
3818
            if (!empty($feedback->feedbacktext)) {
3819
                // Clean the text, ready for display.
3820
                $formatoptions = new stdClass();
3821
                $formatoptions->noclean = true;
3822
                $feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php',
3823
                    $this->context->id, 'mod_questionnaire', 'feedback', $feedback->id);
3824
                $feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions);
3825
                $feedbackmessages[] = $this->renderer->box_start();
3826
                $feedbackmessages[] = $feedbacktext;
3827
                $feedbackmessages[] = $this->renderer->box_end();
3828
            }
3829
            $score = array($scorepercent, 100 - $scorepercent);
3830
            $allscore = null;
3831
            if ($compare  || $allresponses) {
3832
                $allscore = array($allscorepercent, 100 - $allscorepercent);
3833
            }
3834
            $usergraph = get_config('questionnaire', 'usergraph');
3835
            if ($usergraph && $this->survey->chart_type) {
3836
                $this->page->add_to_page('feedbackcharts',
3837
                    draw_chart ($feedbacktype = 'global', $labels, $groupname,
3838
                        $allresponses, $this->survey->chart_type, $score, $allscore, $sectionlabel));
3839
            }
3840
            // Display class or group score. Pending chart library decision to display?
3841
            // Find out if this feedback sectionlabel has a pipe separator.
3842
            $lb = explode("|", $sectionlabel);
3843
            $oppositescore = '';
3844
            $oppositeallscore = '';
3845
            if (count($lb) > 1) {
3846
                $sectionlabel = $lb[0].' | '.$lb[1];
3847
                $oppositescore = ' | '.$score[1].'%';
3848
                $oppositeallscore = ' | '.$allscore[1].'%';
3849
            }
3850
            if ($this->survey->feedbackscores) {
3851
                $table = $table ?? new html_table();
3852
                if ($compare) {
3853
                    $table->data[] = array($sectionlabel, $score[0].'%'.$oppositescore, $allscore[0].'%'.$oppositeallscore);
3854
                } else {
3855
                    $table->data[] = array($sectionlabel, $allscore[0].'%'.$oppositeallscore);
3856
                }
3857
 
3858
                $this->page->add_to_page('feedbackscores', html_writer::table($table));
3859
            }
3860
 
3861
            return $feedbackmessages;
3862
        }
3863
 
3864
        // Now process scores for more than one section.
3865
 
3866
        // Initialize scores and maxscores to 0.
3867
        $score = array();
3868
        $allscore = array();
3869
        $maxscore = array();
3870
        $scorepercent = array();
3871
        $allscorepercent = array();
3872
        $oppositescorepercent = array();
3873
        $alloppositescorepercent = array();
3874
        $chartlabels = array();
3875
        // Sections where all questions are unseen because of the $advdependencies.
3876
        $nanscores = array();
3877
 
3878
        for ($i = 1; $i <= $numsections; $i++) {
3879
            $score[$i] = 0;
3880
            $allscore[$i] = 0;
3881
            $maxscore[$i] = 0;
3882
            $scorepercent[$i] = 0;
3883
        }
3884
 
3885
        for ($section = 1; $section <= $numsections; $section++) {
3886
            // Get feedback messages only for this sections.
3887
            if (($filteredsections != null) && !in_array($section, $filteredsections)) {
3888
                continue;
3889
            }
3890
            foreach ($fbsections as $key => $fbsection) {
3891
                if ($fbsection->section == $section) {
3892
                    $feedbacksectionid = $key;
3893
                    $scorecalculation = section::decode_scorecalculation($fbsection->scorecalculation);
3894
                    if (empty($scorecalculation) && !is_array($scorecalculation)) {
3895
                        $scorecalculation = [];
3896
                    }
3897
                    $sectionheading = $fbsection->sectionheading;
3898
                    $imageid = $fbsection->id;
3899
                    $chartlabels[$section] = $fbsection->sectionlabel;
3900
                }
3901
            }
3902
            foreach ($scorecalculation as $qid => $key) {
3903
                // Just in case a question pertaining to a section has been deleted or made not required
3904
                // after being included in scorecalculation.
3905
                if (isset($qscore[$qid])) {
3906
                    $key = ($key == 0) ? 1 : $key;
3907
                    $score[$section] += round($qscore[$qid] * $key);
3908
                    $maxscore[$section] += round($qmax[$qid] * $key);
3909
                    if ($compare  || $allresponses) {
3910
                        $allscore[$section] += round($allqscore[$qid] * $key);
3911
                    }
3912
                }
3913
            }
3914
 
3915
            if ($maxscore[$section] == 0) {
3916
                array_push($nanscores, $section);
3917
            }
3918
 
3919
            $scorepercent[$section] = ($maxscore[$section] > 0) ? (round($score[$section] / $maxscore[$section] * 100)) : 0;
3920
            $oppositescorepercent[$section] = 100 - $scorepercent[$section];
3921
 
3922
            if (($compare || $allresponses) && $nbparticipants != 0) {
3923
                $allscorepercent[$section] = ($maxscore[$section] > 0) ? (round(($allscore[$section] / $nbparticipants) /
3924
                    $maxscore[$section] * 100)) : 0;
3925
                $alloppositescorepercent[$section] = 100 - $allscorepercent[$section];
3926
            }
3927
 
3928
            if (!$allresponses) {
3929
                if (is_nan($scorepercent[$section])) {
3930
                    // Info: all questions of $section are unseen
3931
                    // -> $scorepercent[$section] = round($score[$section] / $maxscore[$section] * 100) == NAN
3932
                    // -> $maxscore[$section] == 0 -> division by zero
3933
                    // $DB->get_record_select(...) fails, don't show feedbackmessage.
3934
                    continue;
3935
                }
3936
                // To eliminate all potential % chars in heading text (might interfere with the sprintf function).
3937
                $sectionheading = str_replace('%', '', $sectionheading);
3938
 
3939
                // Replace section heading placeholders with their actual value (if any).
3940
                $original = array('$scorepercent', '$oppositescorepercent');
3941
                $result = array("$scorepercent[$section]%", "$oppositescorepercent[$section]%");
3942
                $sectionheading = str_replace($original, $result, $sectionheading);
3943
                $formatoptions = new stdClass();
3944
                $formatoptions->noclean = true;
3945
                $sectionheading = file_rewrite_pluginfile_urls($sectionheading, 'pluginfile.php',
3946
                    $this->context->id, 'mod_questionnaire', 'sectionheading', $imageid);
3947
                $sectionheading = format_text($sectionheading, 1, $formatoptions);
3948
                $feedbackmessages[] = $this->renderer->box_start('reportQuestionTitle');
3949
                $feedbackmessages[] = format_text($sectionheading, FORMAT_HTML, $formatoptions);
3950
                $feedback = $DB->get_record_select('questionnaire_feedback',
3951
                    'sectionid = ? AND minscore <= ? AND ? < maxscore',
3952
                    array($feedbacksectionid, $scorepercent[$section], $scorepercent[$section]),
3953
                    'id,feedbacktext,feedbacktextformat');
3954
                $feedbackmessages[] = $this->renderer->box_end();
3955
                if (!empty($feedback->feedbacktext)) {
3956
                    // Clean the text, ready for display.
3957
                    $formatoptions = new stdClass();
3958
                    $formatoptions->noclean = true;
3959
                    $feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php',
3960
                        $this->context->id, 'mod_questionnaire', 'feedback', $feedback->id);
3961
                    $feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions);
3962
                    $feedbackmessages[] = $this->renderer->box_start('feedbacktext');
3963
                    $feedbackmessages[] = $feedbacktext;
3964
                    $feedbackmessages[] = $this->renderer->box_end();
3965
                }
3966
            }
3967
        }
3968
 
3969
        // Display class or group score.
3970
        switch ($action) {
3971
            case 'vallasort':
3972
                asort($allscore);
3973
                break;
3974
            case 'vallarsort':
3975
                arsort($allscore);
3976
                break;
3977
            default:
3978
        }
3979
 
3980
        if ($this->survey->feedbackscores) {
3981
            foreach ($allscore as $key => $sc) {
3982
                if (isset($chartlabels[$key])) {
3983
                    $lb = explode("|", $chartlabels[$key]);
3984
                    $oppositescore = '';
3985
                    $oppositeallscore = '';
3986
                    if (count($lb) > 1) {
3987
                        $sectionlabel = $lb[0] . ' | ' . $lb[1];
3988
                        $oppositescore = ' | ' . $oppositescorepercent[$key] . '%';
3989
                        $oppositeallscore = ' | ' . $alloppositescorepercent[$key] . '%';
3990
                    } else {
3991
                        $sectionlabel = $chartlabels[$key];
3992
                    }
3993
                    // If all questions of $section are unseen then don't show feedbackscores for this section.
3994
                    if ($compare && !is_nan($scorepercent[$key])) {
3995
                        $table = $table ?? new html_table();
3996
                        $table->data[] = array($sectionlabel, $scorepercent[$key] . '%' . $oppositescore,
3997
                            $allscorepercent[$key] . '%' . $oppositeallscore);
3998
                    } else if (isset($allscorepercent[$key]) && !is_nan($allscorepercent[$key])) {
3999
                        $table = $table ?? new html_table();
4000
                        $table->data[] = array($sectionlabel, $allscorepercent[$key] . '%' . $oppositeallscore);
4001
                    }
4002
                }
4003
            }
4004
        }
4005
        $usergraph = get_config('questionnaire', 'usergraph');
4006
 
4007
        // Don't show feedbackcharts for sections in $nanscores -> remove sections from array.
4008
        foreach ($nanscores as $val) {
4009
            unset($chartlabels[$val]);
4010
            unset($scorepercent[$val]);
4011
            unset($allscorepercent[$val]);
4012
        }
4013
 
4014
        if ($usergraph && $this->survey->chart_type) {
4015
            $this->page->add_to_page(
4016
                'feedbackcharts',
4017
                draw_chart(
4018
                    'sections',
4019
                    array_values($chartlabels),
4020
                    $groupname,
4021
                    $allresponses,
4022
                    $this->survey->chart_type,
4023
                    array_values($scorepercent),
4024
                    array_values($allscorepercent),
4025
                    $sectionlabel
4026
                )
4027
            );
4028
        }
4029
        if ($this->survey->feedbackscores) {
4030
            $this->page->add_to_page('feedbackscores', html_writer::table($table));
4031
        }
4032
 
4033
        return $feedbackmessages;
4034
    }
4035
 
4036
    // Mobile support area.
4037
 
4038
    /**
4039
     * Save the data from the mobile app.
4040
     * @param int $userid
4041
     * @param int $sec
4042
     * @param bool $completed
4043
     * @param int $rid
4044
     * @param bool $submit
4045
     * @param string $action
4046
     * @param array $responses
4047
     * @return array
4048
     */
4049
    public function save_mobile_data($userid, $sec, $completed, $rid, $submit, $action, array $responses) {
4050
        global $DB, $CFG; // Do not delete "$CFG".
4051
 
4052
        $ret = [];
4053
        $response = $this->build_response_from_appdata((object)$responses, $sec);
4054
        $response->sec = $sec;
4055
        $response->rid = $rid;
4056
        $response->id = $rid;
4057
 
4058
        if ($action == 'nextpage') {
4059
            $result = $this->next_page_action($response, $userid);
4060
            if (is_string($result)) {
4061
                $ret['warnings'] = $result;
4062
            } else {
4063
                $ret['nextpagenum'] = $result;
4064
            }
4065
        } else if ($action == 'previouspage') {
4066
            $ret['nextpagenum'] = $this->previous_page_action($response, $userid);
4067
        } else if (!$completed) {
4068
            // If reviewing a completed questionnaire, don't insert a response.
4069
            $msg = $this->response_check_format($response->sec, $response);
4070
            if (empty($msg)) {
4071
                $rid = $this->response_insert($response, $userid);
4072
            } else {
4073
                $ret['warnings'] = $msg;
4074
                $ret['response'] = $response;
4075
            }
4076
        }
4077
 
4078
        if ($submit && (!isset($ret['warnings']) || empty($ret['warnings']))) {
4079
            $this->commit_submission_response($rid, $userid);
4080
        }
4081
        return $ret;
4082
    }
4083
 
4084
    /**
4085
     * Get all of the areas that can have files.
4086
     * @return array
4087
     * @throws dml_exception
4088
     */
4089
    public function get_all_file_areas() {
4090
        global $DB;
4091
 
4092
        $areas = [];
4093
        $areas['info'] = $this->sid;
4094
        $areas['thankbody'] = $this->sid;
4095
 
4096
        // Add question areas.
4097
        if (empty($this->questions)) {
4098
            $this->add_questions();
4099
        }
4100
        $areas['question'] = [];
4101
        foreach ($this->questions as $question) {
4102
            $areas['question'][] = $question->id;
4103
        }
4104
 
4105
        // Add feedback areas.
4106
        $areas['feedbacknotes'] = $this->sid;
4107
        $fbsections = $DB->get_records('questionnaire_fb_sections', ['surveyid' => $this->sid]);
4108
        if (!empty($fbsections)) {
4109
            $areas['sectionheading'] = [];
4110
            foreach ($fbsections as $section) {
4111
                $areas['sectionheading'][] = $section->id;
4112
                $feedbacks = $DB->get_records('questionnaire_feedback', ['sectionid' => $section->id]);
4113
                if (!empty($feedbacks)) {
4114
                    $areas['feedback'] = [];
4115
                    foreach ($feedbacks as $feedback) {
4116
                        $areas['feedback'][] = $feedback->id;
4117
                    }
4118
                }
4119
            }
4120
        }
4121
 
4122
        return $areas;
4123
    }
4124
}