Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace mod_questionnaire\responsetype;
18
 
19
use Composer\Package\Package;
20
use mod_questionnaire\db\bulk_sql_config;
21
 
22
/**
23
 * Class for rank responses.
24
 *
25
 * @author Mike Churchward
26
 * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org)
27
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
28
 * @package mod_questionnaire
29
 */
30
class rank extends responsetype {
31
    /**
32
     * Provide the necessary response data table name. Should probably always be used with late static binding 'static::' form
33
     * rather than 'self::' form to allow for class extending.
34
     *
35
     * @return string response table name.
36
     */
37
    public static function response_table() {
38
        return 'questionnaire_response_rank';
39
    }
40
 
41
    /**
42
     * Provide an array of answer objects from web form data for the question.
43
     *
44
     * @param \stdClass $responsedata All of the responsedata as an object.
45
     * @param \mod_questionnaire\question\question $question
46
     * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects.
47
     * @throws \coding_exception
48
     */
49
    public static function answers_from_webform($responsedata, $question) {
50
        $answers = [];
51
        foreach ($question->choices as $cid => $choice) {
52
            $other = isset($responsedata->{'q' . $question->id . '_' . $cid}) ?
53
                $responsedata->{'q' . $question->id . '_' . $cid} : null;
54
            // Choice not set or not answered.
55
            if (!isset($other) || $other == '') {
56
                continue;
57
            }
58
            if ($other == get_string('notapplicable', 'questionnaire')) {
59
                $rank = -1;
60
            } else {
61
                $rank = intval($other);
62
            }
63
            $record = new \stdClass();
64
            $record->responseid = $responsedata->rid;
65
            $record->questionid = $question->id;
66
            $record->choiceid = $cid;
67
            $record->value = $rank;
68
            $answers[$cid] = answer\answer::create_from_data($record);
69
        }
70
        return $answers;
71
    }
72
 
73
    /**
74
     * Provide an array of answer objects from mobile data for the question.
75
     *
76
     * @param \stdClass $responsedata All of the responsedata as an object.
77
     * @param \mod_questionnaire\question\question $question
78
     * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects.
79
     */
80
    public static function answers_from_appdata($responsedata, $question) {
81
        $answers = [];
82
        if (isset($responsedata->{'q'.$question->id}) && !empty($responsedata->{'q'.$question->id})) {
83
            foreach ($responsedata->{'q' . $question->id} as $choiceid => $choicevalue) {
84
                if (isset($question->choices[$choiceid])) {
85
                    $record = new \stdClass();
86
                    $record->responseid = $responsedata->rid;
87
                    $record->questionid = $question->id;
88
                    $record->choiceid = $choiceid;
89
                    if (!empty($question->nameddegrees)) {
90
                        // If using named degrees, the app returns the label string. Find the value.
91
                        $nameddegreevalue = array_search($choicevalue, $question->nameddegrees);
92
                        if ($nameddegreevalue !== false) {
93
                            $choicevalue = $nameddegreevalue;
94
                        }
95
                    }
96
                    $record->value = $choicevalue;
97
                    $answers[] = answer\answer::create_from_data($record);
98
                }
99
            }
100
        }
101
        return $answers;
102
    }
103
 
104
    /**
105
     * Insert a provided response to the question.
106
     *
107
     * @param object $responsedata All of the responsedata as an object.
108
     * @return int|bool - on error the subtype should call set_error and return false.
109
     */
110
    public function insert_response($responsedata) {
111
        global $DB;
112
 
113
        if (!$responsedata instanceof \mod_questionnaire\responsetype\response\response) {
114
            $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this->question]);
115
        } else {
116
            $response = $responsedata;
117
        }
118
 
119
        $resid = false;
120
 
121
        if (isset($response->answers[$this->question->id])) {
122
            foreach ($response->answers[$this->question->id] as $answer) {
123
                // Record the choice selection.
124
                $record = new \stdClass();
125
                $record->response_id = $response->id;
126
                $record->question_id = $this->question->id;
127
                $record->choice_id = $answer->choiceid;
128
                $record->rankvalue = $answer->value;
129
                $resid = $DB->insert_record(static::response_table(), $record);
130
            }
131
        }
132
        return $resid;
133
    }
134
 
135
    /**
136
     * @param bool $rids
137
     * @param bool $anonymous
138
     * @return array
139
     *
140
     * TODO - This works differently than all other get_results methods. This needs to be refactored.
141
     */
142
    public function get_results($rids=false, $anonymous=false) {
143
        global $DB;
144
 
145
        $rsql = '';
146
        if (!empty($rids)) {
147
            list($rsql, $params) = $DB->get_in_or_equal($rids);
148
            $rsql = ' AND response_id ' . $rsql;
149
        }
150
 
151
        $select = 'question_id=' . $this->question->id . ' AND content NOT LIKE \'!other%\' ORDER BY id ASC';
152
        if ($rows = $DB->get_records_select('questionnaire_quest_choice', $select)) {
153
            foreach ($rows as $row) {
154
                $this->counts[$row->content] = new \stdClass();
155
                $nbna = $DB->count_records(static::response_table(), array('question_id' => $this->question->id,
156
                                'choice_id' => $row->id, 'rankvalue' => '-1'));
157
                $this->counts[$row->content]->nbna = $nbna;
158
            }
159
        }
160
 
161
        // For nameddegrees, need an array by degree value of positions (zero indexed).
162
        $rankvalue = [];
163
        if (!empty($this->question->nameddegrees)) {
164
            $rankvalue = array_flip(array_keys($this->question->nameddegrees));
165
        }
166
 
167
        $isrestricted = ($this->question->length < count($this->question->choices)) && $this->question->no_duplicate_choices();
168
        // Usual case.
169
        if (!$isrestricted) {
170
            if (!empty ($rankvalue)) {
171
                $sql = "SELECT r.id, c.content, r.rankvalue, c.id AS choiceid
172
                FROM {questionnaire_quest_choice} c, {".static::response_table()."} r
173
                WHERE r.choice_id = c.id
174
                AND c.question_id = " . $this->question->id . "
175
                AND r.rankvalue >= 0{$rsql}
176
                ORDER BY choiceid";
177
                $results = $DB->get_records_sql($sql, $params);
178
                $value = [];
179
                foreach ($results as $result) {
180
                    if (isset($rankvalue[$result->rankvalue])) {
181
                        if (isset ($value[$result->choiceid])) {
182
                            $value[$result->choiceid] += $rankvalue[$result->rankvalue] + 1;
183
                        } else {
184
                            $value[$result->choiceid] = $rankvalue[$result->rankvalue] + 1;
185
                        }
186
                    }
187
                }
188
            }
189
 
190
            $sql = "SELECT c.id, c.content, a.average, a.num
191
                    FROM {questionnaire_quest_choice} c
192
                    INNER JOIN
193
                         (SELECT c2.id, AVG(a2.rankvalue) AS average, COUNT(a2.response_id) AS num
194
                          FROM {questionnaire_quest_choice} c2, {".static::response_table()."} a2
195
                          WHERE c2.question_id = ? AND a2.question_id = ? AND a2.choice_id = c2.id AND a2.rankvalue >= 0{$rsql}
196
                          GROUP BY c2.id) a ON a.id = c.id
197
                          order by c.id";
198
            $results = $DB->get_records_sql($sql, array_merge(array($this->question->id, $this->question->id), $params));
199
            if (!empty ($rankvalue)) {
200
                foreach ($results as $key => $result) {
201
                    if (isset($value[$key])) {
202
                        $result->averagevalue = $value[$key] / $result->num;
203
                    }
204
                }
205
            }
206
            // Reindex by 'content'. Can't do this from the query as it won't work with MS-SQL.
207
            foreach ($results as $key => $result) {
208
                $results[$result->content] = $result;
209
                unset($results[$key]);
210
            }
211
            return $results;
212
            // Case where scaleitems is less than possible choices.
213
        } else {
214
            $sql = "SELECT c.id, c.content, a.sum, a.num
215
                    FROM {questionnaire_quest_choice} c
216
                    INNER JOIN
217
                         (SELECT c2.id, SUM(a2.rankvalue) AS sum, COUNT(a2.response_id) AS num
218
                          FROM {questionnaire_quest_choice} c2, {".static::response_table()."} a2
219
                          WHERE c2.question_id = ? AND a2.question_id = ? AND a2.choice_id = c2.id AND a2.rankvalue >= 0{$rsql}
220
                          GROUP BY c2.id) a ON a.id = c.id";
221
            $results = $DB->get_records_sql($sql, array_merge(array($this->question->id, $this->question->id), $params));
222
            // Formula to calculate the best ranking order.
223
            $nbresponses = count($rids);
224
            foreach ($results as $key => $result) {
225
                $result->average = ($result->sum + ($nbresponses - $result->num) * ($this->length + 1)) / $nbresponses;
226
                $results[$result->content] = $result;
227
                unset($results[$key]);
228
            }
229
            return $results;
230
        }
231
    }
232
 
233
    /**
234
     * Provide the feedback scores for all requested response id's. This should be provided only by questions that provide feedback.
235
     * @param array $rids
236
     * @return array | boolean
237
     */
238
    public function get_feedback_scores(array $rids) {
239
        global $DB;
240
 
241
        $rsql = '';
242
        $params = [$this->question->id];
243
        if (!empty($rids)) {
244
            list($rsql, $rparams) = $DB->get_in_or_equal($rids);
245
            $params = array_merge($params, $rparams);
246
            $rsql = ' AND response_id ' . $rsql;
247
        }
248
        $params[] = 'y';
249
 
250
        $sql = 'SELECT r.id, r.response_id as rid, r.question_id AS qid, r.choice_id AS cid, r.rankvalue ' .
251
            'FROM {'.$this->response_table().'} r ' .
252
            'INNER JOIN {questionnaire_quest_choice} c ON r.choice_id = c.id ' .
253
            'WHERE r.question_id= ? ' . $rsql . ' ' .
254
            'ORDER BY rid,cid ASC';
255
        $responses = $DB->get_recordset_sql($sql, $params);
256
 
257
        $rid = 0;
258
        $feedbackscores = [];
259
        foreach ($responses as $response) {
260
            if ($rid != $response->rid) {
261
                $rid = $response->rid;
262
                $feedbackscores[$rid] = new \stdClass();
263
                $feedbackscores[$rid]->rid = $rid;
264
                $feedbackscores[$rid]->score = 0;
265
            }
266
            // Only count scores that are currently defined (in case old responses are using older data).
267
            $feedbackscores[$rid]->score += isset($this->question->nameddegrees[$response->rankvalue]) ? $response->rankvalue : 0;
268
        }
269
 
270
        return (!empty($feedbackscores) ? $feedbackscores : false);
271
    }
272
 
273
    /**
274
     * Provide a template for results screen if defined.
275
     * @param bool $pdf
276
     * @return mixed The template string or false/
277
     */
278
    public function results_template($pdf = false) {
279
        if ($pdf) {
280
            return 'mod_questionnaire/resultspdf_rate';
281
        } else {
282
            return 'mod_questionnaire/results_rate';
283
        }
284
    }
285
 
286
    /**
287
     * Provide the result information for the specified result records.
288
     *
289
     * @param int|array $rids - A single response id, or array.
290
     * @param string $sort - Optional display sort.
291
     * @param boolean $anonymous - Whether or not responses are anonymous.
292
     * @return string - Display output.
293
     */
294
    public function display_results($rids=false, $sort='', $anonymous=false) {
295
        $output = '';
296
 
297
        if (is_array($rids)) {
298
            $prtotal = 1;
299
        } else if (is_int($rids)) {
300
            $prtotal = 0;
301
        }
302
 
303
        if ($rows = $this->get_results($rids, $sort, $anonymous)) {
304
            $stravgvalue = ''; // For printing table heading.
305
            foreach ($this->counts as $key => $value) {
306
                $ccontent = $key;
307
                $avgvalue = '';
308
                if (array_key_exists($ccontent, $rows)) {
309
                    $avg = $rows[$ccontent]->average;
310
                    $this->counts[$ccontent]->num = $rows[$ccontent]->num;
311
                    if (isset($rows[$ccontent]->averagevalue)) {
312
                        $avgvalue = $rows[$ccontent]->averagevalue;
313
                        $osgood = false;
314
                        if ($this->question->osgood_rate_scale()) { // Osgood's semantic differential.
315
                            $osgood = true;
316
                        }
317
                        if ($stravgvalue == '' && !$osgood) {
318
                            $stravgvalue = ' ('.get_string('andaveragevalues', 'questionnaire').')';
319
                        }
320
                    } else {
321
                        $avgvalue = null;
322
                    }
323
                } else {
324
                    $avg = 0;
325
                }
326
                $this->counts[$ccontent]->avg = $avg;
327
                $this->counts[$ccontent]->avgvalue = $avgvalue;
328
            }
329
            $output1 = $this->mkresavg($sort, $stravgvalue);
330
            $output2 = $this->mkrescount($rids, $rows, $sort);
331
            $output = (object)array_merge((array)$output1, (array)$output2);
332
        } else {
333
            $output = (object)['noresponses' => true];
334
        }
335
        return $output;
336
    }
337
 
338
    /**
339
     * Return an array of answers by question/choice for the given response. Must be implemented by the subclass.
340
     *
341
     * @param int $rid The response id.
342
     * @return array
343
     */
344
    public static function response_select($rid) {
345
        global $DB;
346
 
347
        $values = [];
348
        $sql = 'SELECT a.id as aid, q.id AS qid, q.precise AS precise, c.id AS cid, q.content, c.content as ccontent,
349
                                a.rankvalue as arank '.
350
            'FROM {'.static::response_table().'} a, {questionnaire_question} q, {questionnaire_quest_choice} c '.
351
            'WHERE a.response_id= ? AND a.question_id=q.id AND a.choice_id=c.id '.
352
            'ORDER BY aid, a.question_id, c.id';
353
        $records = $DB->get_records_sql($sql, [$rid]);
354
        foreach ($records as $row) {
355
            // Next two are 'qid' and 'cid', each with numeric and hash keys.
356
            $osgood = false;
357
            if (\mod_questionnaire\question\rate::type_is_osgood_rate_scale($row->precise)) {
358
                $osgood = true;
359
            }
360
            $qid = $row->qid.'_'.$row->cid;
361
            unset($row->aid); // Get rid of the answer id.
362
            unset($row->qid);
363
            unset($row->cid);
364
            unset($row->precise);
365
            $row = (array)$row;
366
            $newrow = [];
367
            foreach ($row as $key => $val) {
368
                if ($key != 'content') { // No need to keep question text - ony keep choice text and rank.
369
                    if ($key == 'ccontent') {
370
                        if ($osgood) {
371
                            list($contentleft, $contentright) = array_merge(preg_split('/[|]/', $val), [' ']);
372
                            $contents = questionnaire_choice_values($contentleft);
373
                            if ($contents->title) {
374
                                $contentleft = $contents->title;
375
                            }
376
                            $contents = questionnaire_choice_values($contentright);
377
                            if ($contents->title) {
378
                                $contentright = $contents->title;
379
                            }
380
                            $val = strip_tags($contentleft.'|'.$contentright);
381
                            $val = preg_replace("/[\r\n\t]/", ' ', $val);
382
                        } else {
383
                            $contents = questionnaire_choice_values($val);
384
                            if ($contents->modname) {
385
                                $val = $contents->modname;
386
                            } else if ($contents->title) {
387
                                $val = $contents->title;
388
                            } else if ($contents->text) {
389
                                $val = strip_tags($contents->text);
390
                                $val = preg_replace("/[\r\n\t]/", ' ', $val);
391
                            }
392
                        }
393
                    }
394
                    $newrow[] = $val;
395
                }
396
            }
397
            $values[$qid] = $newrow;
398
        }
399
 
400
        return $values;
401
    }
402
 
403
    /**
404
     * Return an array of answer objects by question for the given response id.
405
     * THIS SHOULD REPLACE response_select.
406
     *
407
     * @param int $rid The response id.
408
     * @return array array answer
409
     * @throws \dml_exception
410
     */
411
    public static function response_answers_by_question($rid) {
412
        global $DB;
413
 
414
        $answers = [];
415
        $sql = 'SELECT id, response_id as responseid, question_id as questionid, choice_id as choiceid, rankvalue as value ' .
416
            'FROM {' . static::response_table() .'} ' .
417
            'WHERE response_id = ? ';
418
        $records = $DB->get_records_sql($sql, [$rid]);
419
        foreach ($records as $record) {
420
            $answers[$record->questionid][$record->choiceid] = answer\answer::create_from_data($record);
421
        }
422
 
423
        return $answers;
424
    }
425
 
426
    /**
427
     * Configure bulk sql
428
     * @return bulk_sql_config
429
     */
430
    protected function bulk_sql_config() {
431
        return new bulk_sql_config(static::response_table(), 'qrr', true, false, true);
432
    }
433
 
434
    /**
435
     * Return a structure for averages.
436
     * @param string $sort
437
     * @param string $stravgvalue
438
     * @return \stdClass
439
     */
440
    private function mkresavg($sort, $stravgvalue='') {
441
        global $CFG;
442
 
443
        $stravgrank = get_string('averagerank', 'questionnaire');
444
        $osgood = false;
445
        if ($this->question->precise == 3) { // Osgood's semantic differential.
446
            $osgood = true;
447
            $stravgrank = get_string('averageposition', 'questionnaire');
448
        }
449
        $stravg = '<div style="text-align:right">'.$stravgrank.$stravgvalue.'</div>';
450
 
451
        $isna = $this->question->precise == 1;
452
        $isnahead = '';
453
        $nbchoices = count($this->counts);
454
        $isrestricted = ($this->question->length < $nbchoices) && $this->question->precise == 2;
455
 
456
        if ($isna) {
457
            $isnahead = get_string('notapplicable', 'questionnaire');
458
        }
459
        $pagetags = new \stdClass();
460
        $pagetags->averages = new \stdClass();
461
 
462
        if ($isna) {
463
            $header1 = new \stdClass();
464
            $header1->text = '';
465
            $header1->align = '';
466
            $header2 = new \stdClass();
467
            $header2->text = $stravg;
468
            $header2->align = '';
469
            $header3 = new \stdClass();
470
            $header3->text = '&dArr;';
471
            $header3->align = 'center';
472
            $header4 = new \stdClass();
473
            $header4->text = $isnahead;
474
            $header4->align = 'right';
475
        } else {
476
            if ($osgood) {
477
                $stravg = '<div style="text-align:center">'.$stravgrank.'</div>';
478
                $header1 = new \stdClass();
479
                $header1->text = '';
480
                $header1->align = '';
481
                $header2 = new \stdClass();
482
                $header2->text = $stravg;
483
                $header2->align = '';
484
                $header3 = new \stdClass();
485
                $header3->text = '';
486
                $header3->align = 'center';
487
            } else {
488
                $header1 = new \stdClass();
489
                $header1->text = '';
490
                $header1->align = '';
491
                $header2 = new \stdClass();
492
                $header2->text = $stravg;
493
                $header2->align = '';
494
                $header3 = new \stdClass();
495
                $header3->text = '&dArr;';
496
                $header3->align = 'center';
497
            }
498
        }
499
        // PDF columns are based on a 11.69in x 8.27in page. Margins are 15mm each side, or 1.1811 in total.
500
        $pdfwidth = 11.69 - 1.1811;
501
        if ($isna) {
502
            $header1->width = '55%';
503
            $header2->width = '35%';
504
            $header3->width = '5%';
505
            $header4->width = '5%';
506
            $header1->pdfwidth = $pdfwidth * .55;
507
            $header2->pdfwidth = $pdfwidth * .35;
508
            $header3->pdfwidth = $pdfwidth * .05;
509
            $header4->pdfwidth = $pdfwidth * .05;
510
        } else if ($osgood) {
511
            $header1->width = '25%';
512
            $header2->width = '50%';
513
            $header3->width = '25%';
514
            $header1->pdfwidth = $pdfwidth * .25;
515
            $header2->pdfwidth = $pdfwidth * .5;
516
            $header3->pdfwidth = $pdfwidth * .25;
517
        } else {
518
            $header1->width = '60%';
519
            $header2->width = '35%';
520
            $header3->width = '5%';
521
            $header1->pdfwidth = $pdfwidth * .6;
522
            $header2->pdfwidth = $pdfwidth * .35;
523
            $header3->pdfwidth = $pdfwidth * .05;
524
        }
525
        $pagetags->averages->headers = [$header1, $header2, $header3];
526
        if (isset($header4)) {
527
            $pagetags->averages->headers[] = $header4;
528
        }
529
 
530
        $imageurl = $CFG->wwwroot.'/mod/questionnaire/images/hbar.gif';
531
        $spacerimage = $CFG->wwwroot . '/mod/questionnaire/images/hbartransp.gif';
532
        $llength = $this->question->length;
533
        if (!$llength) {
534
            $llength = 5;
535
        }
536
        // Add an extra column to accomodate lower ranks in this case.
537
        $llength += $isrestricted;
538
        $width = 100 / $llength;
539
        $n = array();
540
        $nameddegrees = 0;
541
        foreach ($this->question->nameddegrees as $degree) {
542
            // To take into account languages filter.
543
            $content = (format_text($degree, FORMAT_HTML, ['noclean' => true]));
544
            $n[$nameddegrees] = $degree;
545
            $nameddegrees++;
546
        }
547
        for ($j = 0; $j < $this->question->length; $j++) {
548
            if (isset($n[$j])) {
549
                $str = $n[$j];
550
            } else {
551
                $str = $j + 1;
552
            }
553
        }
554
        $rankcols = [];
555
        $pdfwidth = $header2->pdfwidth / (100 / $width);
556
        for ($i = 0; $i <= $llength - 1; $i++) {
557
            if ($isrestricted && $i == $llength - 1) {
558
                $str = "...";
559
                $rankcols[] = (object)['width' => $width . '%', 'text' => '...', 'pdfwidth' => $pdfwidth];
560
            } else if (isset($n[$i])) {
561
                $str = $n[$i];
562
                $rankcols[] = (object)['width' => $width . '%', 'text' => $n[$i], 'pdfwidth' => $pdfwidth];
563
            } else {
564
                $str = $i + 1;
565
                $rankcols[] = (object)['width' => $width . '%', 'text' => $i + 1, 'pdfwidth' => $pdfwidth];
566
            }
567
        }
568
        $pagetags->averages->choicelabelrow = new \stdClass();
569
        $pagetags->averages->choicelabelrow->innertablewidth = $header2->pdfwidth;
570
        $pagetags->averages->choicelabelrow->column1 = (object)['width' => $header1->width, 'align' => $header1->align,
571
            'text' => '', 'pdfwidth' => $header1->pdfwidth];
572
        $pagetags->averages->choicelabelrow->column2 = (object)['width' => $header2->width, 'align' => $header2->align,
573
            'ranks' => $rankcols, 'pdfwidth' => $header2->pdfwidth];
574
        $pagetags->averages->choicelabelrow->column3 = (object)['width' => $header3->width, 'align' => $header3->align,
575
            'text' => '', 'pdfwidth' => $header3->pdfwidth];
576
        if ($isna) {
577
            $pagetags->averages->choicelabelrow->column4 = (object)['width' => $header4->width, 'align' => $header4->align,
578
                'text' => '', 'pdfwidth' => $header4->pdfwidth];
579
        }
580
 
581
        switch ($sort) {
582
            case 'ascending':
583
                uasort($this->counts, 'self::sortavgasc');
584
                break;
585
            case 'descending':
586
                uasort($this->counts, 'self::sortavgdesc');
587
                break;
588
        }
589
        reset ($this->counts);
590
 
591
        if (!empty($this->counts) && is_array($this->counts)) {
592
            $pagetags->averages->choiceaverages = [];
593
            foreach ($this->counts as $content => $contentobj) {
594
                // Eliminate potential named degrees on Likert scale.
595
                if (!preg_match("/^[0-9]{1,3}=/", $content)) {
596
                    if (isset($contentobj->avg)) {
597
                        $avg = $contentobj->avg;
598
                        // If named degrees were used, swap averages for display.
599
                        if (isset($contentobj->avgvalue)) {
600
                            $avg = $contentobj->avgvalue;
601
                            $avgvalue = $contentobj->avg;
602
                        } else {
603
                            $avgvalue = '';
604
                        }
605
                    } else {
606
                        $avg = '';
607
                    }
608
                    $nbna = $contentobj->nbna;
609
 
610
                    if ($avg) {
611
                        if (($j = $avg * $width) > 0) {
612
                            $marginposition = ($avg - 0.5 ) / ($this->question->length + $isrestricted);
613
                        }
614
                        if (!right_to_left()) {
615
                            $margin = 'margin-left:' . $marginposition * 100 . '%';
616
                            $marginpdf = $marginposition * $pagetags->averages->choicelabelrow->innertablewidth;
617
                        } else {
618
                            $margin = 'margin-right:' . $marginposition * 100 . '%';
619
                            $marginpdf = $pagetags->averages->choicelabelrow->innertablewidth -
620
                                ($marginposition * $pagetags->averages->choicelabelrow->innertablewidth);
621
                        }
622
                    } else {
623
                        $margin = '';
624
                    }
625
 
626
                    if ($osgood) {
627
                        // Ensure there are two bits of content.
628
                        list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' '));
629
                    } else {
630
                        $contents = questionnaire_choice_values($content);
631
                        if ($contents->modname) {
632
                            $content = $contents->text;
633
                        }
634
                    }
635
                    if ($osgood) {
636
                        $choicecol1 = new \stdClass();
637
                        $choicecol1->width = $header1->width;
638
                        $choicecol1->pdfwidth = $header1->pdfwidth;
639
                        $choicecol1->align = $header1->align;
640
                        $choicecol1->text = '<div class="mdl-right">' .
641
                            format_text($content, FORMAT_HTML, ['noclean' => true]) . '</div>';
642
                        $choicecol2 = new \stdClass();
643
                        $choicecol2->width = $header2->width;
644
                        $choicecol2->pdfwidth = $header2->pdfwidth;
645
                        $choicecol2->align = $header2->align;
646
                        $choicecol2->imageurl = $imageurl;
647
                        $choicecol2->spacerimage = $spacerimage;
648
                        $choicecol2->margin = $margin;
649
                        $choicecol2->marginpdf = $marginpdf;
650
                        $choicecol3 = new \stdClass();
651
                        $choicecol3->width = $header3->width;
652
                        $choicecol3->pdfwidth = $header3->pdfwidth;
653
                        $choicecol3->align = $header3->align;
654
                        $choicecol3->text = '<div class="mdl-left">' .
655
                            format_text($contentright, FORMAT_HTML, ['noclean' => true]) . '</div>';
656
                        $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1, 'column2' => $choicecol2,
657
                            'column3' => $choicecol3];
658
                        // JR JUNE 2012 do not display meaningless average rank values for Osgood.
659
                    } else if ($avg || ($nbna != 0)) {
660
                        $stravgval = '';
661
                        if ($avg) {
662
                            if ($stravgvalue) {
663
                                $stravgval = '('.sprintf('%.1f', $avgvalue).')';
664
                            }
665
                            $stravgval = sprintf('%.1f', $avg).'&nbsp;'.$stravgval;
666
                            if ($isna) {
667
                                $choicecol4 = new \stdClass();
668
                                $choicecol4->width = $header4->width;
669
                                $choicecol4->pdfwidth = $header4->pdfwidth;
670
                                $choicecol4->align = $header4->align;
671
                                $choicecol4->text = $nbna;
672
                            }
673
                        }
674
                        $choicecol1 = new \stdClass();
675
                        $choicecol1->width = $header1->width;
676
                        $choicecol1->pdfwidth = $header1->pdfwidth;
677
                        $choicecol1->align = $header1->align;
678
                        $choicecol1->text = format_text($content, FORMAT_HTML, ['noclean' => true]);
679
                        $choicecol2 = new \stdClass();
680
                        $choicecol2->width = $header2->width;
681
                        $choicecol2->pdfwidth = $header2->pdfwidth;
682
                        $choicecol2->align = $header2->align;
683
                        $choicecol2->imageurl = $imageurl;
684
                        $choicecol2->spacerimage = $spacerimage;
685
                        $choicecol2->margin = $margin;
686
                        $choicecol2->marginpdf = $marginpdf;
687
                        $choicecol3 = new \stdClass();
688
                        $choicecol3->width = $header3->width;
689
                        $choicecol3->pdfwidth = $header3->pdfwidth;
690
                        $choicecol3->align = $header3->align;
691
                        $choicecol3->text = $stravgval;
692
                        if ($avg) {
693
                            if (isset($choicecol4)) {
694
                                $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1,
695
                                    'column2' => $choicecol2, 'column3' => $choicecol3, 'column4' => $choicecol4];
696
                            } else {
697
                                $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1,
698
                                    'column2' => $choicecol2, 'column3' => $choicecol3];
699
                            }
700
                        } else {
701
                            $choicecol4 = new \stdClass();
702
                            $choicecol4->width = $header4->width;
703
                            $choicecol4->pdfwidth = $header4->pdfwidth;
704
                            $choicecol4->align = $header4->align;
705
                            $choicecol4->text = $nbna;
706
                            $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1, 'column2' => $choicecol2,
707
                                'column3' => $choicecol3];
708
                        }
709
                    }
710
                } // End if named degrees.
711
            } // End foreach.
712
        } else {
713
            $nodata1 = new \stdClass();
714
            $nodata1->width = $header1->width;
715
            $nodata1->align = $header1->align;
716
            $nodata1->text = '';
717
            $nodata2 = new \stdClass();
718
            $nodata2->width = $header2->width;
719
            $nodata2->align = $header2->align;
720
            $nodata2->text = get_string('noresponsedata', 'mod_questionnaire');
721
            $nodata3 = new \stdClass();
722
            $nodata3->width = $header3->width;
723
            $nodata3->align = $header3->align;
724
            $nodata3->text = '';
725
            if (isset($header4)) {
726
                $nodata4 = new \stdClass();
727
                $nodata4->width = $header4->width;
728
                $nodata4->align = $header4->align;
729
                $nodata4->text = '';
730
                $pagetags->averages->nodata = [$nodata1, $nodata2, $nodata3, $nodata4];
731
            } else {
732
                $pagetags->averages->nodata = [$nodata1, $nodata2, $nodata3];
733
            }
734
        }
735
        return $pagetags;
736
    }
737
 
738
    /**
739
     * Return a structure for counts.
740
     * @param array $rids
741
     * @param array $rows
742
     * @param string $sort
743
     * @return \stdClass
744
     */
745
    private function mkrescount($rids, $rows, $sort) {
746
        // Display number of responses to Rate questions - see http://moodle.org/mod/forum/discuss.php?d=185106.
747
        global $DB;
748
 
749
        $nbresponses = count($rids);
750
        // Prepare data to be displayed.
751
        $isrestricted = ($this->question->length < count($this->question->choices)) && $this->question->precise == 2;
752
 
753
        $rsql = '';
754
        if (!empty($rids)) {
755
            list($rsql, $params) = $DB->get_in_or_equal($rids);
756
            $rsql = ' AND response_id ' . $rsql;
757
        }
758
 
759
        array_unshift($params, $this->question->id); // This is question_id.
760
        $sql = 'SELECT r.id, c.content, r.rankvalue, c.id AS choiceid ' .
761
            'FROM {questionnaire_quest_choice} c , ' .
762
            '{questionnaire_response_rank} r ' .
763
            'WHERE c.question_id = ?' .
764
            ' AND r.question_id = c.question_id' .
765
            ' AND r.choice_id = c.id ' .
766
            $rsql .
767
            ' ORDER BY choiceid, rankvalue ASC';
768
        $choices = $DB->get_records_sql($sql, $params);
769
 
770
        // Sort rows (results) by average value.
771
        if ($sort != 'default') {
772
            $sortarray = array();
773
            foreach ($rows as $row) {
774
                foreach ($row as $key => $value) {
775
                    if (!isset($sortarray[$key])) {
776
                        $sortarray[$key] = array();
777
                    }
778
                    $sortarray[$key][] = $value;
779
                }
780
            }
781
            $orderby = "average";
782
            switch ($sort) {
783
                case 'ascending':
784
                    array_multisort($sortarray[$orderby], SORT_ASC, $rows);
785
                    break;
786
                case 'descending':
787
                    array_multisort($sortarray[$orderby], SORT_DESC, $rows);
788
                    break;
789
            }
790
        }
791
        $nbranks = $this->question->length;
792
        $ranks = [];
793
        $rankvalue = [];
794
        if (!empty($this->question->nameddegrees)) {
795
            $rankvalue = array_flip(array_keys($this->question->nameddegrees));
796
        }
797
        foreach ($rows as $row) {
798
            $choiceid = $row->id;
799
            foreach ($choices as $choice) {
800
                if ($choice->choiceid == $choiceid) {
801
                    $n = 0;
802
                    for ($i = 1; $i <= $nbranks; $i++) {
803
                        if ((isset($rankvalue[$choice->rankvalue]) && ($rankvalue[$choice->rankvalue] == ($i - 1))) ||
804
                            (empty($rankvalue) && ($choice->rankvalue == $i))) {
805
                            $n++;
806
                            if (!isset($ranks[$choice->content][$i])) {
807
                                $ranks[$choice->content][$i] = 0;
808
                            }
809
                            $ranks[$choice->content][$i] += $n;
810
                        } else if (!isset($ranks[$choice->content][$i])) {
811
                            $ranks[$choice->content][$i] = 0;
812
                        }
813
                    }
814
                }
815
            }
816
        }
817
 
818
        // Psettings for display.
819
        $strtotal = '<strong>'.get_string('total', 'questionnaire').'</strong>';
820
        $isna = $this->question->precise == 1;
821
        $osgood = false;
822
        if ($this->question->precise == 3) { // Osgood's semantic differential.
823
            $osgood = true;
824
        }
825
        if ($this->question->precise == 1) {
826
            $na = get_string('notapplicable', 'questionnaire');
827
        } else {
828
            $na = '';
829
        }
830
        $nameddegrees = 0;
831
        $n = array();
832
        foreach ($this->question->nameddegrees as $degree) {
833
            $content = $degree;
834
            $n[$nameddegrees] = format_text($content, FORMAT_HTML, ['noclean' => true]);
835
            $nameddegrees++;
836
        }
837
        foreach ($this->question->choices as $choice) {
838
            $contents = questionnaire_choice_values($choice->content);
839
            if ($contents->modname) {
840
                $choice->content = $contents->text;
841
            }
842
        }
843
 
844
        $pagetags = new \stdClass();
845
        $pagetags->totals = new \stdClass();
846
        $pagetags->totals->headers = [];
847
        if ($osgood) {
848
            $align = 'right';
849
        } else {
850
            $align = 'left';
851
        }
852
        $pagetags->totals->headers[] = (object)['align' => $align,
853
            'text' => '<span class="smalltext">'.get_string('responses', 'questionnaire').'</span>'];
854
 
855
        // Display the column titles.
856
        for ($j = 0; $j < $this->question->length; $j++) {
857
            if (isset($n[$j])) {
858
                $str = $n[$j];
859
            } else {
860
                $str = $j + 1;
861
            }
862
            $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => '<span class="smalltext">'.$str.'</span>'];
863
        }
864
        if ($osgood) {
865
            $pagetags->totals->headers[] = (object)['align' => 'left', 'text' => ''];
866
        }
867
        $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => $strtotal];
868
        if ($isrestricted) {
869
            $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => get_string('notapplicable', 'questionnaire')];
870
        }
871
        if ($na) {
872
            $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => $na];
873
        }
874
 
875
        // Now display the responses.
876
        $pagetags->totals->choices = [];
877
        foreach ($ranks as $content => $rank) {
878
            $totalcols = [];
879
            // Eliminate potential named degrees on Likert scale.
880
            if (!preg_match("/^[0-9]{1,3}=/", $content)) {
881
                // First display the list of degrees (named or un-named)
882
                // number of NOT AVAILABLE responses for this possible answer.
883
                $nbna = $this->counts[$content]->nbna;
884
                // TOTAL number of responses for this possible answer.
885
                $total = $this->counts[$content]->num;
886
                $nbresp = '<strong>'.$total.'</strong>';
887
                if ($osgood) {
888
                    // Ensure there are two bits of content.
889
                    list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' '));
890
                    $header = reset($pagetags->totals->headers);
891
                    $totalcols[] = (object)['align' => $header->align,
892
                        'text' => format_text($content, FORMAT_HTML, ['noclean' => true])];
893
                } else {
894
                    // Eliminate potentially short-named choices.
895
                    $contents = questionnaire_choice_values($content);
896
                    if ($contents->modname) {
897
                        $content = $contents->text;
898
                    }
899
                    $header = reset($pagetags->totals->headers);
900
                    $totalcols[] = (object)['align' => $header->align,
901
                        'text' => format_text($content, FORMAT_HTML, ['noclean' => true])];
902
                }
903
                // Display ranks/rates numbers.
904
                $maxrank = max($rank);
905
                for ($i = 1; $i <= $this->question->length; $i++) {
906
                    $percent = '';
907
                    if (isset($rank[$i])) {
908
                        $str = $rank[$i];
909
                        if ($total !== 0 && $str !== 0) {
910
                            $percent = ' (<span class="percent">'.number_format(($str * 100) / $total).'%</span>)';
911
                        }
912
                        // Emphasize responses with max rank value.
913
                        if ($str == $maxrank) {
914
                            $str = '<strong>'.$str.'</strong>';
915
                        }
916
                    } else {
917
                        $str = 0;
918
                    }
919
                    $header = next($pagetags->totals->headers);
920
                    $totalcols[] = (object)['align' => $header->align, 'text' => $str.$percent];
921
                }
922
                if ($osgood) {
923
                    $header = next($pagetags->totals->headers);
924
                    $totalcols[] = (object)['align' => $header->align,
925
                        'text' => format_text($contentright, FORMAT_HTML, ['noclean' => true])];
926
                }
927
                $header = next($pagetags->totals->headers);
928
                $totalcols[] = (object)['align' => $header->align, 'text' => $nbresp];
929
                if ($isrestricted) {
930
                    $header = next($pagetags->totals->headers);
931
                    $totalcols[] = (object)['align' => $header->align, 'text' => $nbresponses - $total];
932
                }
933
                if (!$osgood) {
934
                    if ($na) {
935
                        $header = next($pagetags->totals->headers);
936
                        $totalcols[] = (object)['align' => $header->align, 'text' => $nbna];
937
                    }
938
                }
939
            } // End named degrees.
940
            $pagetags->totals->choices[] = (object)['totalcols' => $totalcols];
941
        }
942
        return $pagetags;
943
    }
944
 
945
    /**
946
     * Sorting function for ascending.
947
     * @param \stdClass $a
948
     * @param \stdClass $b
949
     * @return int
950
     */
951
    private static function sortavgasc($a, $b) {
952
        if (isset($a->avg) && isset($b->avg)) {
953
            if ( $a->avg < $b->avg ) {
954
                return -1;
955
            } else if ($a->avg > $b->avg ) {
956
                return 1;
957
            } else {
958
                return 0;
959
            }
960
        }
961
    }
962
 
963
    /**
964
     * Sorting function for descending.
965
     * @param \stdClass $a
966
     * @param \stdClass $b
967
     * @return int
968
     */
969
    private static function sortavgdesc($a, $b) {
970
        if (isset($a->avg) && isset($b->avg)) {
971
            if ( $a->avg > $b->avg ) {
972
                return -1;
973
            } else if ($a->avg < $b->avg) {
974
                return 1;
975
            } else {
976
                return 0;
977
            }
978
        }
979
    }
980
}