Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace mod_questionnaire\question;
18
 
19
/**
20
 * This file contains the parent class for rate question types.
21
 *
22
 * @author Mike Churchward
23
 * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org)
24
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
25
 * @package mod_questionnaire
26
 */
27
class rate extends question {
28
 
29
    /** @var array $nameddegrees */
30
    public $nameddegrees = [];
31
 
32
    /**
33
     * The class constructor
34
     * @param int $id
35
     * @param \stdClass $question
36
     * @param \context $context
37
     * @param array $params
38
     */
39
    public function __construct($id = 0, $question = null, $context = null, $params = array()) {
40
        $this->length = 5;
41
        parent::__construct($id, $question, $context, $params);
42
        $this->add_nameddegrees_from_extradata();
43
    }
44
 
45
    /**
46
     * Each question type must define its response class.
47
     * @return object The response object based off of questionnaire_response_base.
48
     */
49
    protected function responseclass() {
50
        return '\\mod_questionnaire\\responsetype\\rank';
51
    }
52
 
53
    /**
54
     * Short name for this question type - no spaces, etc..
55
     * @return string
56
     */
57
    public function helpname() {
58
        return 'ratescale';
59
    }
60
 
61
    /**
62
     * Return true if the question has choices.
63
     */
64
    public function has_choices() {
65
        return true;
66
    }
67
 
68
    /**
69
     * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this.
70
     * @return boolean | string
71
     */
72
    public function question_template() {
73
        return 'mod_questionnaire/question_rate';
74
    }
75
 
76
    /**
77
     * Override and return a response template if provided. Output of response_survey_display is iterpreted based on this.
78
     * @return boolean | string
79
     */
80
    public function response_template() {
81
        return 'mod_questionnaire/response_rate';
82
    }
83
 
84
    /**
85
     * Return true if rate scale type is set to "Normal".
86
     * @param int $scaletype
87
     * @return bool
88
     */
89
    public static function type_is_normal_rate_scale($scaletype) {
90
        return ($scaletype == 0);
91
    }
92
 
93
    /**
94
     * Return true if rate scale type is set to "N/A column".
95
     * @param int $scaletype
96
     * @return bool
97
     */
98
    public static function type_is_na_column($scaletype) {
99
        return ($scaletype == 1);
100
    }
101
 
102
    /**
103
     * Return true if rate scale type is set to "No duplicate choices".
104
     * @param int $scaletype
105
     * @return bool
106
     */
107
    public static function type_is_no_duplicate_choices($scaletype) {
108
        return ($scaletype == 2);
109
    }
110
 
111
    /**
112
     * Return true if rate scale type is set to "Osgood".
113
     * @param int $scaletype
114
     * @return bool
115
     */
116
    public static function type_is_osgood_rate_scale($scaletype) {
117
        return ($scaletype == 3);
118
    }
119
 
120
    /**
121
     * Return true if rate scale type is set to "Normal".
122
     * @return bool
123
     */
124
    public function normal_rate_scale() {
125
        return self::type_is_normal_rate_scale($this->precise);
126
    }
127
 
128
    /**
129
     * Return true if rate scale type is set to "N/A column".
130
     * @return bool
131
     */
132
    public function has_na_column() {
133
        return self::type_is_na_column($this->precise);
134
    }
135
 
136
    /**
137
     * Return true if rate scale type is set to "No duplicate choices".
138
     * @return bool
139
     */
140
    public function no_duplicate_choices() {
141
        return self::type_is_no_duplicate_choices($this->precise);
142
    }
143
 
144
    /**
145
     * Return true if rate scale type is set to "Osgood".
146
     * @return bool
147
     */
148
    public function osgood_rate_scale() {
149
        return self::type_is_osgood_rate_scale($this->precise);
150
    }
151
 
152
    /**
153
     * True if question type supports feedback options. False by default.
154
     */
155
    public function supports_feedback() {
156
        return true;
157
    }
158
 
159
    /**
160
     * True if the question supports feedback and has valid settings for feedback. Override if the default logic is not enough.
161
     */
162
    public function valid_feedback() {
163
        return $this->supports_feedback() && $this->has_choices() && $this->required() && !empty($this->name) &&
164
            ($this->normal_rate_scale() || $this->osgood_rate_scale()) && !empty($this->nameddegrees);
165
    }
166
 
167
    /**
168
     * Get the maximum score possible for feedback if appropriate. Override if default behaviour is not correct.
169
     * @return int | boolean
170
     */
171
    public function get_feedback_maxscore() {
172
        if ($this->valid_feedback()) {
173
            $maxscore = 0;
174
            $nbchoices = count($this->choices);
175
            foreach ($this->nameddegrees as $value => $label) {
176
                if ($value > $maxscore) {
177
                    $maxscore = $value;
178
                }
179
            }
180
            // The maximum score needs to be multiplied by the number of items to rate.
181
            $maxscore = $maxscore * $nbchoices;
182
        } else {
183
            $maxscore = false;
184
        }
185
        return $maxscore;
186
    }
187
 
188
    /**
189
     * Return the context tags for the check question template.
190
     * @param \mod_questionnaire\responsetype\response\response $response
191
     * @param string $descendantsdata
192
     * @param boolean $blankquestionnaire
193
     * @return object The check question context tags.
194
     *
195
     * TODO: This function needs to be rewritten. It is a mess!
196
     *
197
     */
198
    protected function question_survey_display($response, $descendantsdata, $blankquestionnaire=false) {
199
        $choicetags = new \stdClass();
200
        $choicetags->qelements = [];
201
 
202
        $disabled = '';
203
        if ($blankquestionnaire) {
204
            $disabled = ' disabled="disabled"';
205
        }
206
        if (!empty($data) && ( !isset($data->{'q'.$this->id}) || !is_array($data->{'q'.$this->id}) ) ) {
207
            $data->{'q'.$this->id} = [];
208
        }
209
 
210
        // Check if rate question has one line only to display full width columns of choices.
211
        $nocontent = false;
212
        $nameddegrees = count($this->nameddegrees);
213
        $n = [];
214
        $v = [];
215
        $maxndlen = 0;
216
        foreach ($this->choices as $cid => $choice) {
217
            $content = $choice->content;
218
            if (!$nocontent && $content == '') {
219
                $nocontent = true;
220
            }
221
            if ($nameddegrees == 0) {
222
                // Determine if the choices have named values.
223
                $contents = questionnaire_choice_values($content);
224
                if ($contents->modname) {
225
                    $choice->content = $contents->text;
226
                }
227
            }
228
        }
229
 
230
        // The 0.1% right margin is needed to avoid the horizontal scrollbar in Chrome!
231
        // A one-line rate question (no content) does not need to span more than 50%.
232
        $width = $nocontent ? "50%" : "99.9%";
233
        $choicetags->qelements['twidth'] = $width;
234
        $choicetags->qelements['headerrow'] = [];
235
        // If Osgood, adjust central columns to width of named degrees if any.
236
        if ($this->osgood_rate_scale()) {
237
            if ($maxndlen < 4) {
238
                $width = 45;
239
            } else if ($maxndlen < 13) {
240
                $width = 40;
241
            } else {
242
                $width = 30;
243
            }
244
            $nn = 100 - ($width * 2);
245
            $colwidth = ($nn / $this->length).'%';
246
            $textalign = 'right';
247
            $width = $width . '%';
248
        } else if ($nocontent) {
249
            $width = '0%';
250
            $colwidth = (100 / $this->length).'%';
251
            $textalign = 'right';
252
        } else {
253
            $width = '59%';
254
            $colwidth = (40 / $this->length).'%';
255
            $textalign = 'left';
256
        }
257
 
258
        $choicetags->qelements['headerrow']['col1width'] = $width;
259
 
260
        if ($this->has_na_column()) {
261
            $na = get_string('notapplicable', 'questionnaire');
262
        } else {
263
            $na = '';
264
        }
265
        if ($this->no_duplicate_choices()) {
266
            $order = 'other_rate_uncheck(name, value)';
267
        } else {
268
            $order = '';
269
        }
270
 
271
        if (!$this->no_duplicate_choices()) {
272
            $nbchoices = count($this->choices);
273
        } else { // If "No duplicate choices", can restrict nbchoices to number of rate items specified.
274
            $nbchoices = $this->length;
275
        }
276
 
277
        // Display empty td for Not yet answered column.
278
        if (($nbchoices > 1) && !$this->no_duplicate_choices() && !$blankquestionnaire) {
279
            $choicetags->qelements['headerrow']['colnya'] = true;
280
        }
281
 
282
        $collabel = [];
283
        if ($nameddegrees > 0) {
284
            $currentdegree = reset($this->nameddegrees);
285
        }
286
        for ($j = 1; $j <= $this->length; $j++) {
287
            $col = [];
288
            if (($nameddegrees > 0) && ($currentdegree !== false)) {
289
                $str = format_text($currentdegree, FORMAT_HTML, ['noclean' => true]);
290
                $currentdegree = next($this->nameddegrees);
291
            } else {
292
                $str = $j;
293
            }
294
            $val = $j;
295
            if ($blankquestionnaire) {
296
                $val = '<br />('.$val.')';
297
            } else {
298
                $val = '';
299
            }
300
            $col['colwidth'] = $colwidth;
301
            $col['coltext'] = $str.$val;
302
            $collabel[$j] = $col['coltext'];
303
            $choicetags->qelements['headerrow']['cols'][] = $col;
304
        }
305
        if ($na) {
306
            $choicetags->qelements['headerrow']['cols'][] = ['colwidth' => $colwidth, 'coltext' => $na];
307
            $collabel[$j] = $na;
308
        }
309
 
310
        $num = 0;
311
        foreach ($this->choices as $cid => $choice) {
312
            $num += (isset($response->answers[$this->id][$cid]) && ($response->answers[$this->id][$cid]->value != -999));
313
        }
314
 
315
        $notcomplete = false;
316
        if ( ($num != $nbchoices) && ($num != 0) ) {
317
            $this->add_notification(get_string('checkallradiobuttons', 'questionnaire', $nbchoices));
318
            $notcomplete = true;
319
        }
320
 
321
        $row = 0;
322
        $choicetags->qelements['rows'] = [];
323
        foreach ($this->choices as $cid => $choice) {
324
            $cols = [];
325
            if (isset($choice->content)) {
326
                $row++;
327
                $str = 'q'."{$this->id}_$cid";
328
                $content = $choice->content;
329
                if ($this->osgood_rate_scale()) {
330
                    list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' '));
331
                }
332
                $cols[] = ['colstyle' => 'text-align: '.$textalign.';',
333
                           'coltext' => format_text($content, FORMAT_HTML, ['noclean' => true]).'&nbsp;'];
334
 
335
                $bg = 'c0 raterow';
336
                if (($nbchoices > 1) && !$this->no_duplicate_choices()  && !$blankquestionnaire) {
337
                    $checked = ' checked="checked"';
338
                    $completeclass = 'notanswered';
339
                    $title = '';
340
                    if ($notcomplete && isset($response->answers[$this->id][$cid]) &&
341
                        ($response->answers[$this->id][$cid]->value == -999)) {
342
                        $completeclass = 'notcompleted';
343
                        $title = get_string('pleasecomplete', 'questionnaire');
344
                    }
345
                    // Set value of notanswered button to -999 in order to eliminate it from form submit later on.
346
                    $colinput = ['name' => $str, 'value' => -999];
347
                    if (!empty($checked)) {
348
                        $colinput['checked'] = true;
349
                    }
350
                    if (!empty($order)) {
351
                        $colinput['onclick'] = $order;
352
                    }
353
                    $cols[] = ['colstyle' => 'width:1%;', 'colclass' => $completeclass, 'coltitle' => $title,
354
                        'colinput' => $colinput];
355
                }
356
                if ($nameddegrees > 0) {
357
                    reset($this->nameddegrees);
358
                }
359
                for ($j = 1; $j <= $this->length + $this->has_na_column(); $j++) {
360
                    if (!isset($collabel[$j])) {
361
                        // If not using this value, continue.
362
                        continue;
363
                    }
364
                    $col = [];
365
                    $checked = '';
366
                    // If isna column then set na choice to -1 value. This needs work!
367
                    if (!empty($this->nameddegrees) && (key($this->nameddegrees) !== null)) {
368
                        $value = key($this->nameddegrees);
369
                        next($this->nameddegrees);
370
                    } else {
371
                        $value = ($j <= $this->length ? $j : -1);
372
                    }
373
                    if (isset($response->answers[$this->id][$cid]) && ($value == $response->answers[$this->id][$cid]->value)) {
374
                        $checked = ' checked="checked"';
375
                    }
376
                    $col['colstyle'] = 'text-align:center';
377
                    $col['colclass'] = $bg;
378
                    $col['colhiddentext'] = get_string('option', 'questionnaire', $j);
379
                    $col['colinput']['name'] = $str;
380
                    $col['colinput']['value'] = $value;
381
                    $col['colinput']['id'] = $str.'_'.$value;
382
                    if (!empty($checked)) {
383
                        $col['colinput']['checked'] = true;
384
                    }
385
                    if (!empty($disabled)) {
386
                        $col['colinput']['disabled'] = true;
387
                    }
388
                    if (!empty($order)) {
389
                        $col['colinput']['onclick'] = $order;
390
                    }
391
                    $col['colinput']['label'] = 'Choice '.$collabel[$j].' for row '.format_text($content, FORMAT_PLAIN);
392
                    if ($bg == 'c0 raterow') {
393
                        $bg = 'c1 raterow';
394
                    } else {
395
                        $bg = 'c0 raterow';
396
                    }
397
                    $cols[] = $col;
398
                }
399
                if ($this->osgood_rate_scale()) {
400
                    $cols[] = ['coltext' => '&nbsp;'.format_text($contentright, FORMAT_HTML, ['noclean' => true])];
401
                }
402
                $choicetags->qelements['rows'][] = ['cols' => $cols];
403
            }
404
        }
405
 
406
        return $choicetags;
407
    }
408
 
409
    /**
410
     * Return the context tags for the rate response template.
411
     * @param \mod_questionnaire\responsetype\response\response $response
412
     * @return \stdClass The rate question response context tags.
413
     * @throws \coding_exception
414
     */
415
    protected function response_survey_display($response) {
416
        static $uniquetag = 0;  // To make sure all radios have unique names.
417
 
418
        $resptags = new \stdClass();
419
        $resptags->headers = [];
420
        $resptags->rows = [];
421
 
422
        if (!isset($response->answers[$this->id])) {
423
            $response->answers[$this->id][] = new \mod_questionnaire\responsetype\answer\answer();
424
        }
425
        // Check if rate question has one line only to display full width columns of choices.
426
        $nocontent = false;
427
        foreach ($this->choices as $cid => $choice) {
428
            if ($choice->content == '') {
429
                $nocontent = true;
430
                break;
431
            }
432
        }
433
        $resptags->twidth = $nocontent ? "50%" : "99.9%";
434
 
435
        $bg = 'c0';
436
        $nameddegrees = 0;
437
        $cidnamed = array();
438
        // Max length of potential named degree in column head.
439
        $maxndlen = 0;
440
        if ($this->osgood_rate_scale()) {
441
            $resptags->osgood = 1;
442
            if ($maxndlen < 4) {
443
                $sidecolwidth = '45%';
444
                $sidecolwidthn = 45;
445
            } else if ($maxndlen < 13) {
446
                $sidecolwidth = '40%';
447
                $sidecolwidthn = 40;
448
            } else {
449
                $sidecolwidth = '30%';
450
                $sidecolwidthn = 30;
451
            }
452
            $nn = 100 - ($sidecolwidthn * 2);
453
            $resptags->sidecolwidth = $sidecolwidth;
454
            $resptags->colwidth = ($nn / $this->length).'%';
455
            $resptags->textalign = 'right';
456
        } else {
457
            $resptags->sidecolwidth = '49%';
458
            $resptags->colwidth = (50 / $this->length).'%';
459
            $resptags->textalign = 'left';
460
        }
461
        if (!empty($this->nameddegrees)) {
462
            $this->length = count($this->nameddegrees);
463
            reset($this->nameddegrees);
464
        }
465
        for ($j = 1; $j <= $this->length; $j++) {
466
            $cellobj = new \stdClass();
467
            $cellobj->bg = $bg;
468
            if (!empty($this->nameddegrees)) {
469
                $cellobj->str = current($this->nameddegrees);
470
                next($this->nameddegrees);
471
            } else {
472
                $cellobj->str = $j;
473
            }
474
            if ($bg == 'c0') {
475
                $bg = 'c1';
476
            } else {
477
                $bg = 'c0';
478
            }
479
            $resptags->headers[] = $cellobj;
480
        }
481
        if ($this->has_na_column()) {
482
            $cellobj = new \stdClass();
483
            $cellobj->bg = $bg;
484
            $cellobj->str = get_string('notapplicable', 'questionnaire');
485
            $resptags->headers[] = $cellobj;
486
        }
487
 
488
        foreach ($this->choices as $cid => $choice) {
489
            $rowobj = new \stdClass();
490
            // Do not print column names if named column exist.
491
            if (!array_key_exists($cid, $cidnamed)) {
492
                $str = 'q'."{$this->id}_$cid";
493
                $content = $choice->content;
494
                $contents = questionnaire_choice_values($content);
495
                if ($contents->modname) {
496
                    $content = $contents->text;
497
                }
498
                if ($this->osgood_rate_scale()) {
499
                    list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' '));
500
                }
501
                $rowobj->content = format_text($content, FORMAT_HTML, ['noclean' => true]).'&nbsp;';
502
                $bg = 'c0';
503
                $cols = [];
504
                if (!empty($this->nameddegrees)) {
505
                    $this->length = count($this->nameddegrees);
506
                    reset($this->nameddegrees);
507
                }
508
                for ($j = 1; $j <= $this->length; $j++) {
509
                    $cellobj = new \stdClass();
510
                    if (isset($response->answers[$this->id][$cid])) {
511
                        if (!empty($this->nameddegrees)) {
512
                            if ($response->answers[$this->id][$cid]->value == key($this->nameddegrees)) {
513
                                $cellobj->checked = 1;
514
                            }
515
                            next($this->nameddegrees);
516
                        } else if ($j == $response->answers[$this->id][$cid]->value) {
517
                            $cellobj->checked = 1;
518
                        }
519
                    }
520
                    $cellobj->str = $str.$j.$uniquetag++;
521
                    $cellobj->bg = $bg;
522
                    // N/A column checked.
523
                    $checkedna = (isset($response->answers[$this->id][$cid]) && ($response->answers[$this->id][$cid]->value == -1));
524
                    if ($bg == 'c0') {
525
                        $bg = 'c1';
526
                    } else {
527
                        $bg = 'c0';
528
                    }
529
                    $cols[] = $cellobj;
530
                }
531
                if ($this->has_na_column()) { // N/A column.
532
                    $cellobj = new \stdClass();
533
                    if ($checkedna) {
534
                        $cellobj->checked = 1;
535
                    }
536
                    $cellobj->str = $str.$j.$uniquetag++.'na';
537
                    $cellobj->bg = $bg;
538
                    $cols[] = $cellobj;
539
                }
540
                $rowobj->cols = $cols;
541
                if ($this->osgood_rate_scale()) {
542
                    $rowobj->osgoodstr = '&nbsp;'.format_text($contentright, FORMAT_HTML, ['noclean' => true]);
543
                }
544
                $resptags->rows[] = $rowobj;
545
            }
546
        }
547
        return $resptags;
548
    }
549
 
550
    /**
551
     * Check question's form data for complete response.
552
     *
553
     * @param \stdClass $responsedata The data entered into the response.
554
     * @return boolean
555
     *
556
     */
557
    public function response_complete($responsedata) {
558
        if (!is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) {
559
            $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this]);
560
        } else {
561
            $response = $responsedata;
562
        }
563
 
564
        // To make it easier, create an array of answers by choiceid.
565
        $answers = [];
566
        if (isset($response->answers[$this->id])) {
567
            foreach ($response->answers[$this->id] as $answer) {
568
                $answers[$answer->choiceid] = $answer;
569
            }
570
        }
571
 
572
        $answered = true;
573
        $num = 0;
574
        $nbchoices = count($this->choices);
575
        $na = get_string('notapplicable', 'questionnaire');
576
        foreach ($this->choices as $cid => $choice) {
577
            // In case we have named degrees on the Likert scale, count them to substract from nbchoices.
578
            $nameddegrees = 0;
579
            $content = $choice->content;
580
            if (preg_match("/^[0-9]{1,3}=/", $content)) {
581
                $nameddegrees++;
582
            } else {
583
                if (isset($answers[$cid]) && !empty($answers[$cid]) && ($answers[$cid]->value == $na)) {
584
                    $answers[$cid]->value = -1;
585
                }
586
                // If choice value == -999 this is a not yet answered choice.
587
                $num += (isset($answers[$cid]) && ($answers[$cid]->value != -999));
588
            }
589
            $nbchoices -= $nameddegrees;
590
        }
591
 
592
        if ($num == 0) {
593
            if ($this->required()) {
594
                $answered = false;
595
            }
596
        }
597
        return $answered;
598
    }
599
 
600
    /**
601
     * Check question's form data for valid response. Override this is type has specific format requirements.
602
     *
603
     * @param \stdClass $responsedata The data entered into the response.
604
     * @return boolean
605
     */
606
    public function response_valid($responsedata) {
607
        // Work with a response object.
608
        if (!is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) {
609
            $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this]);
610
        } else {
611
            $response = $responsedata;
612
        }
613
        $num = 0;
614
        $nbchoices = count($this->choices);
615
        $na = get_string('notapplicable', 'questionnaire');
616
 
617
        // Create an answers array indexed by choiceid for ease.
618
        $answers = [];
619
        $nodups = [];
620
        if (isset($response->answers[$this->id])) {
621
            foreach ($response->answers[$this->id] as $answer) {
622
                $answers[$answer->choiceid] = $answer;
623
                $nodups[] = $answer->value;
624
            }
625
        }
626
 
627
        foreach ($this->choices as $cid => $choice) {
628
            // In case we have named degrees on the Likert scale, count them to substract from nbchoices.
629
            $nameddegrees = 0;
630
            $content = $choice->content;
631
            if (preg_match("/^[0-9]{1,3}=/", $content)) {
632
                $nameddegrees++;
633
            } else {
634
                if (isset($answers[$cid]) && ($answers[$cid]->value == $na)) {
635
                    $answers[$cid]->value = -1;
636
                }
637
                // If choice value == -999 this is a not yet answered choice.
638
                $num += (isset($answers[$cid]) && ($answers[$cid]->value != -999));
639
            }
640
            $nbchoices -= $nameddegrees;
641
        }
642
        // If nodupes and nb choice restricted, nbchoices may be > actual choices, so limit it to $question->length.
643
        $isrestricted = ($this->length < count($this->choices)) && $this->no_duplicate_choices();
644
        if ($isrestricted) {
645
            $nbchoices = min ($nbchoices, $this->length);
646
        }
647
 
648
        // Test for duplicate answers in a no duplicate question type.
649
        if ($this->no_duplicate_choices()) {
650
            foreach ($answers as $answer) {
651
                if (count(array_keys($nodups, $answer->value)) > 1) {
652
                    return false;
653
                }
654
            }
655
        }
656
 
657
        if (($num != $nbchoices) && ($num != 0)) {
658
            return false;
659
        } else {
660
            return parent::response_valid($responsedata);
661
        }
662
    }
663
 
664
    /**
665
     * Return the length form element.
666
     * @param \MoodleQuickForm $mform
667
     * @param string $helptext
668
     */
669
    protected function form_length(\MoodleQuickForm $mform, $helptext = '') {
670
        return parent::form_length($mform, 'numberscaleitems');
671
    }
672
 
673
    /**
674
     * Return the precision form element.
675
     * @param \MoodleQuickForm $mform
676
     * @param string $helptext
677
     */
678
    protected function form_precise(\MoodleQuickForm $mform, $helptext = '') {
679
        $precoptions = array("0" => get_string('normal', 'questionnaire'),
680
                             "1" => get_string('notapplicablecolumn', 'questionnaire'),
681
                             "2" => get_string('noduplicates', 'questionnaire'),
682
                             "3" => get_string('osgood', 'questionnaire'));
683
        $mform->addElement('select', 'precise', get_string('kindofratescale', 'questionnaire'), $precoptions);
684
        $mform->addHelpButton('precise', 'kindofratescale', 'questionnaire');
685
        $mform->setType('precise', PARAM_INT);
686
 
687
        return $mform;
688
    }
689
 
690
    /**
691
     * Override if the question uses the extradata field.
692
     * @param \MoodleQuickForm $mform
693
     * @param string $helpname
694
     * @return \MoodleQuickForm
695
     */
696
    protected function form_extradata(\MoodleQuickForm $mform, $helpname = '') {
697
        $defaultvalue = '';
698
        foreach ($this->nameddegrees as $value => $label) {
699
            $defaultvalue .= $value . '=' . $label . "\n";
700
        }
701
 
702
        $options = ['wrap' => 'virtual'];
703
        $mform->addElement('textarea', 'allnameddegrees', get_string('allnameddegrees', 'questionnaire'), $options);
704
        $mform->setDefault('allnameddegrees', $defaultvalue);
705
        $mform->setType('allnameddegrees', PARAM_RAW);
706
        $mform->addHelpButton('allnameddegrees', 'allnameddegrees', 'questionnaire');
707
 
708
        return $mform;
709
    }
710
 
711
    /**
712
     * Any preprocessing of general data.
713
     * @param \stdClass $formdata
714
     * @return bool
715
     */
716
    protected function form_preprocess_data($formdata) {
717
        $nameddegrees = [];
718
        // Named degrees are put one per line in the form "[value]=[label]".
719
        if (!empty($formdata->allnameddegrees)) {
720
            $nameddegreelines = explode("\n", $formdata->allnameddegrees);
721
            foreach ($nameddegreelines as $nameddegreeline) {
722
                $nameddegreeline = trim($nameddegreeline);
723
                if (($nameddegree = \mod_questionnaire\question\choice::content_is_named_degree_choice($nameddegreeline)) !==
724
                    false) {
725
                    $nameddegrees += $nameddegree;
726
                }
727
            }
728
        }
729
 
730
        // Now store the new named degrees in extradata.
731
        $formdata->extradata = json_encode($nameddegrees);
732
        return parent::form_preprocess_data($formdata);
733
    }
734
 
735
    /**
736
     * Override this function for question specific choice preprocessing.
737
     * @param \stdClass $formdata
738
     * @return false
739
     */
740
    protected function form_preprocess_choicedata($formdata) {
741
        if (empty($formdata->allchoices)) {
742
            // Add dummy blank space character for empty value.
743
            $formdata->allchoices = " ";
744
        } else {
745
            $allchoices = $formdata->allchoices;
746
            $allchoices = explode("\n", $allchoices);
747
            $ispossibleanswer = false;
748
            $nbnameddegrees = 0;
749
            $nbvalues = 0;
750
            foreach ($allchoices as $choice) {
751
                if ($choice) {
752
                    // Check for number from 1 to 3 digits, followed by the equal sign =.
753
                    if (preg_match("/^[0-9]{1,3}=/", $choice)) {
754
                        $nbnameddegrees++;
755
                    } else {
756
                        $nbvalues++;
757
                        $ispossibleanswer = true;
758
                    }
759
                }
760
            }
761
            // Add carriage return and dummy blank space character for empty value.
762
            if (!$ispossibleanswer) {
763
                $formdata->allchoices .= "\n ";
764
            }
765
 
766
            // Sanity checks for correct number of values in $formdata->length.
767
 
768
            // Sanity check for named degrees.
769
            if ($nbnameddegrees && $nbnameddegrees != $formdata->length) {
770
                $formdata->length = $nbnameddegrees;
771
            }
772
            // Sanity check for "no duplicate choices"".
773
            if (self::type_is_no_duplicate_choices($formdata->precise) && ($formdata->length > $nbvalues || !$formdata->length)) {
774
                $formdata->length = $nbvalues;
775
            }
776
        }
777
        return true;
778
    }
779
 
780
    /**
781
     * Update the choice with the choicerecord.
782
     * @param \stdClass $choicerecord
783
     * @return bool
784
     */
785
    public function update_choice($choicerecord) {
786
        if ($nameddegree = \mod_questionnaire\question\choice::content_is_named_degree_choice($choicerecord->content)) {
787
            // Preserve any existing value from the new array.
788
            $this->nameddegrees = $nameddegree + $this->nameddegrees;
789
            $this->insert_nameddegrees($this->nameddegrees);
790
        }
791
        return parent::update_choice($choicerecord);
792
    }
793
 
794
    /**
795
     * Add a new choice to the database.
796
     * @param \stdClass $choicerecord
797
     * @return bool
798
     */
799
    public function add_choice($choicerecord) {
800
        if ($nameddegree = \mod_questionnaire\question\choice::content_is_named_degree_choice($choicerecord->content)) {
801
            // Preserve any existing value from the new array.
802
            $this->nameddegrees = $nameddegree + $this->nameddegrees;
803
            $this->insert_nameddegrees($this->nameddegrees);
804
        }
805
        return parent::add_choice($choicerecord);
806
    }
807
 
808
    /**
809
     * True if question provides mobile support.
810
     *
811
     * @return bool
812
     */
813
    public function supports_mobile() {
814
        return true;
815
    }
816
 
817
    /**
818
     * Override and return false if not supporting mobile app.
819
     * @param int $qnum
820
     * @param bool $autonum
821
     * @return \stdClass
822
     */
823
    public function mobile_question_display($qnum, $autonum = false) {
824
        $mobiledata = parent::mobile_question_display($qnum, $autonum);
825
        $mobiledata->rates = $this->mobile_question_rates_display();
826
        if ($this->has_na_column()) {
827
            $mobiledata->hasnacolumn = (object)['value' => -1, 'label' => get_string('notapplicable', 'questionnaire')];
828
        }
829
 
830
        $mobiledata->israte = true;
831
        return $mobiledata;
832
    }
833
 
834
    /**
835
     * Override and return false if not supporting mobile app.
836
     * @return array
837
     */
838
    public function mobile_question_choices_display() {
839
        $choices = [];
840
        $excludes = [];
841
        $vals = $extracontents = [];
842
        $cnum = 0;
843
        foreach ($this->choices as $choiceid => $choice) {
844
            $choice->na = false;
845
            $choice->choice_id = $choiceid;
846
            $choice->id = $choiceid;
847
            $choice->question_id = $this->id;
848
 
849
            // Add a fieldkey for each choice.
850
            $choice->fieldkey = $this->mobile_fieldkey($choiceid);
851
 
852
            if ($this->osgood_rate_scale()) {
853
                list($choice->leftlabel, $choice->rightlabel) = array_merge(preg_split('/[|]/', $choice->content), []);
854
            }
855
 
856
            if ($this->normal_rate_scale() || $this->no_duplicate_choices()) {
857
                $choices[$cnum] = $choice;
858
                if ($this->required()) {
859
                    $choices[$cnum]->min = 0;
860
                    $choices[$cnum]->minstr = 1;
861
                } else {
862
                    $choices[$cnum]->min = 0;
863
                    $choices[$cnum]->minstr = 1;
864
                }
865
                $choices[$cnum]->max = intval($this->length) - 1;
866
                $choices[$cnum]->maxstr = intval($this->length);
867
 
868
            } else if ($this->has_na_column()) {
869
                $choices[$cnum] = $choice;
870
                if ($this->required()) {
871
                    $choices[$cnum]->min = 0;
872
                    $choices[$cnum]->minstr = 1;
873
                } else {
874
                    $choices[$cnum]->min = 0;
875
                    $choices[$cnum]->minstr = 1;
876
                }
877
                $choices[$cnum]->max = intval($this->length);
878
                $choices[$cnum]->na = true;
879
 
880
            } else {
881
                $excludes[$choiceid] = $choiceid;
882
                if ($choice->value == null) {
883
                    if ($arr = explode('|', $choice->content)) {
884
                        if (count($arr) == 2) {
885
                            $choices[$cnum] = $choice;
886
                            $choices[$cnum]->content = '';
887
                            $choices[$cnum]->minstr = $arr[0];
888
                            $choices[$cnum]->maxstr = $arr[1];
889
                        }
890
                    }
891
                } else {
892
                    $val = intval($choice->value);
893
                    $vals[$val] = $val;
894
                    $extracontents[] = $choice->content;
895
                }
896
            }
897
            if ($vals) {
898
                if ($q = $choices) {
899
                    foreach (array_keys($q) as $itemid) {
900
                        $choices[$itemid]->min = min($vals);
901
                        $choices[$itemid]->max = max($vals);
902
                    }
903
                }
904
            }
905
            if ($extracontents) {
906
                $extracontents = array_unique($extracontents);
907
                $extrahtml = '<br><ul>';
908
                foreach ($extracontents as $extracontent) {
909
                    $extrahtml .= '<li>'.$extracontent.'</li>';
910
                }
911
                $extrahtml .= '</ul>';
912
                $options = ['noclean' => true, 'para' => false, 'filter' => true,
913
                    'context' => $this->context, 'overflowdiv' => true];
914
                $choice->content .= format_text($extrahtml, FORMAT_HTML, $options);
915
            }
916
 
917
            if (!in_array($choiceid, $excludes)) {
918
                $choice->choice_id = $choiceid;
919
                if ($choice->value == null) {
920
                    $choice->value = '';
921
                }
922
                $choices[$cnum] = $choice;
923
            }
924
            $cnum++;
925
        }
926
 
927
        return $choices;
928
    }
929
 
930
    /**
931
     * Display the rates question for mobile.
932
     * @return array
933
     */
934
    public function mobile_question_rates_display() {
935
        $rates = [];
936
        if (!empty($this->nameddegrees)) {
937
            foreach ($this->nameddegrees as $value => $label) {
938
                $rates[] = (object)['value' => $value, 'label' => $label];
939
            }
940
        } else {
941
            for ($i = 1; $i <= $this->length; $i++) {
942
                $rates[] = (object)['value' => $i, 'label' => $i];
943
            }
944
        }
945
        return $rates;
946
    }
947
 
948
    /**
949
     * Return the mobile response data.
950
     * @param response $response
951
     * @return array
952
     */
953
    public function get_mobile_response_data($response) {
954
        $resultdata = [];
955
        if (isset($response->answers[$this->id])) {
956
            foreach ($response->answers[$this->id] as $answer) {
957
                // Add a fieldkey for each choice.
958
                if (!empty($this->nameddegrees)) {
959
                    if (isset($this->nameddegrees[$answer->value])) {
960
                        $resultdata[$this->mobile_fieldkey($answer->choiceid)] = $this->nameddegrees[$answer->value];
961
                    } else {
962
                        $resultdata[$this->mobile_fieldkey($answer->choiceid)] = $answer->value;
963
                    }
964
                } else {
965
                    $resultdata[$this->mobile_fieldkey($answer->choiceid)] = $answer->value;
966
                }
967
            }
968
        }
969
        return $resultdata;
970
    }
971
 
972
    /**
973
     * Add the nameddegrees property.
974
     */
975
    private function add_nameddegrees_from_extradata() {
976
        if (!empty($this->extradata)) {
977
            $this->nameddegrees = json_decode($this->extradata, true);
978
        }
979
    }
980
 
981
    /**
982
     * Insert nameddegress to the extradata database field.
983
     * @param array $nameddegrees
984
     * @return bool
985
     * @throws \dml_exception
986
     */
987
    public function insert_nameddegrees(array $nameddegrees) {
988
        return $this->insert_extradata(json_encode($nameddegrees));
989
    }
990
 
991
    /**
992
     * Helper function used to move existing named degree choices for the specified question from the "quest_choice" table to the
993
     * "question" table.
994
     * @param int $qid
995
     * @param null|\stdClass $questionrec
996
     */
997
    public static function move_nameddegree_choices(int $qid = 0, \stdClass $questionrec = null) {
998
        global $DB;
999
 
1000
        if ($qid !== 0) {
1001
            $question = new rate($qid);
1002
        } else {
1003
            $question = new rate(0, $questionrec);
1004
        }
1005
        $nameddegrees = [];
1006
        $oldchoiceids = [];
1007
        // There was an issue where rate values were being stored as 1..n, no matter what the named degree value was. We need to fix
1008
        // the old responses now. This also assumes that the values are now 1 based rather than 0 based.
1009
        $newvalues = [];
1010
        $oldval = 1;
1011
        foreach ($question->choices as $choice) {
1012
            if ($nameddegree = $choice->is_named_degree_choice()) {
1013
                $nameddegrees += $nameddegree;
1014
                $oldchoiceids[] = $choice->id;
1015
                reset($nameddegree);
1016
                $newvalues[$oldval++] = key($nameddegree);
1017
            }
1018
        }
1019
 
1020
        if (!empty($nameddegrees)) {
1021
            if ($question->insert_nameddegrees($nameddegrees)) {
1022
                // Remove the old named desgree from the choices table.
1023
                foreach ($oldchoiceids as $choiceid) {
1024
                    \mod_questionnaire\question\choice::delete_from_db_by_id($choiceid);
1025
                }
1026
 
1027
                // First get all existing rank responses for this question.
1028
                $responses = $DB->get_recordset('questionnaire_response_rank', ['question_id' => $question->id]);
1029
                // Iterating over each response record ensures we won't change an existing record more than once.
1030
                foreach ($responses as $response) {
1031
                    // Then, if the old value exists, set it to the new one.
1032
                    if (isset($newvalues[$response->rankvalue])) {
1033
                        $DB->set_field('questionnaire_response_rank', 'rankvalue', $newvalues[$response->rankvalue],
1034
                            ['id' => $response->id]);
1035
                    }
1036
                }
1037
                $responses->close();
1038
            }
1039
        }
1040
    }
1041
 
1042
    /**
1043
     * Helper function to move named degree choices for all questions, optionally for a specific surveyid.
1044
     * This should only be called for an upgrade from before '2018110103', or from a restore operation for a version of a
1045
     * questionnaire before '2018110103'.
1046
     * @param int|null $surveyid
1047
     */
1048
    public static function move_all_nameddegree_choices(int $surveyid = null) {
1049
        global $DB;
1050
 
1051
        // This operation might take a while. Cancel PHP timeouts for this.
1052
        \core_php_time_limit::raise();
1053
 
1054
        // First, let's adjust all rate answers from zero based to one based (see GHI223).
1055
        // If a specific survey is being dealt with, only use the questions from that survey.
1056
        $skip = false;
1057
        if ($surveyid !== null) {
1058
            $qids = $DB->get_records_menu('questionnaire_question', ['surveyid' => $surveyid, 'type_id' => QUESRATE],
1059
                '', 'id,surveyid');
1060
            if (!empty($qids)) {
1061
                list($qsql, $qparams) = $DB->get_in_or_equal(array_keys($qids));
1062
            } else {
1063
                // No relevant questions, so no need to do this step.
1064
                $skip = true;
1065
            }
1066
        }
1067
 
1068
        // If we're doing this step, let's do it.
1069
        if (!$skip) {
1070
            $select = 'UPDATE {questionnaire_response_rank} ' .
1071
                'SET rankvalue = (rankvalue + 1) ' .
1072
                'WHERE (rankvalue >= 0)';
1073
            if ($surveyid !== null) {
1074
                $select .= ' AND (question_id ' . $qsql . ')';
1075
            } else {
1076
                $qparams = [];
1077
            }
1078
            $DB->execute($select, $qparams);
1079
        }
1080
 
1081
        $args = ['type_id' => QUESRATE];
1082
        if ($surveyid !== null) {
1083
            $args['surveyid'] = $surveyid;
1084
        }
1085
        $ratequests = $DB->get_recordset('questionnaire_question', $args);
1086
        foreach ($ratequests as $questionrec) {
1087
            self::move_nameddegree_choices(0, $questionrec);
1088
        }
1089
        $ratequests->close();
1090
    }
1091
}