displayDegreeChartChildren()   F
last analyzed

Complexity

Conditions 20
Paths 15360

Size

Total Lines 169
Code Lines 110

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 110
nc 15360
nop 9
dl 0
loc 169
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Framework\Container;
6
use Chamilo\CourseBundle\Entity\CQuizQuestion;
7
use ChamiloSession as Session;
8
9
/**
10
 * Class MultipleAnswerTrueFalseDegreeCertainty
11
 * This class allows to instantiate an object of type MULTIPLE_ANSWER
12
 * (MULTIPLE CHOICE, MULTIPLE ANSWER), extending the class question.
13
 */
14
class MultipleAnswerTrueFalseDegreeCertainty extends Question
15
{
16
    public const LEVEL_DARKGREEN = 1;
17
    public const LEVEL_LIGHTGREEN = 2;
18
    public const LEVEL_WHITE = 3;
19
    public const LEVEL_LIGHTRED = 4;
20
    public const LEVEL_DARKRED = 5;
21
22
    public $typePicture = 'mccert.png';
23
    public $explanationLangVar = 'Multiple answer true/false/degree of certainty';
24
    public $optionsTitle;
25
    public $options;
26
27
    public function __construct()
28
    {
29
        parent::__construct();
30
        $this->type = MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY;
31
        $this->isContent = $this->getIsContent();
32
        $this->optionsTitle = [1 => 'Answers', 2 => 'DegreeOfCertaintyThatMyAnswerIsCorrect'];
33
        $this->options = [
34
            1 => 'True',
35
            2 => 'False',
36
            3 => '50%',
37
            4 => '60%',
38
            5 => '70%',
39
            6 => '80%',
40
            7 => '90%',
41
            8 => '100%',
42
        ];
43
    }
44
45
    /**
46
     * Redefines Question::createAnswersForm: creates the HTML form to answer the question.
47
     *
48
     * @param FormValidator $form
49
     */
50
    public function createAnswersForm($form)
51
    {
52
        global $text;
53
54
        $nbAnswers = (int) ($_POST['nb_answers'] ?? 4);
55
        // The previous default value was 2. See task #1759.
56
        $nbAnswers += (isset($_POST['lessAnswers']) ? -1 : (isset($_POST['moreAnswers']) ? 1 : 0));
57
58
        $courseId = api_get_course_int_id();
59
        $objEx = Session::read('objExercise');
60
        $renderer = &$form->defaultRenderer();
61
        $defaults = [];
62
63
        $form->addHeader(get_lang('Answers'));
64
65
        // Determine if options exist already (edit) or not (first creation).
66
        $hasOptions = false;
67
        if (!empty($this->id)) {
68
            try {
69
                $opt = Question::readQuestionOption($this->id, $courseId);
70
            } catch (\Throwable $e) {
71
                $opt = Question::readQuestionOption($this->id);
72
            }
73
            $hasOptions = !empty($opt);
74
        }
75
76
        $tfIids = $this->getTrueFalseOptionIids($courseId);
77
78
        $html = '<table class="table table-striped table-hover">';
79
        $html .= '<thead><tr>';
80
        $html .= '<th width="10px">'.get_lang('number').'</th>';
81
        $html .= '<th width="10px">'.get_lang('True').'</th>';
82
        $html .= '<th width="10px">'.get_lang('False').'</th>';
83
        $html .= '<th width="50%">'.get_lang('Answer').'</th>';
84
85
        // Show column comment when feedback is enabled
86
        if (EXERCISE_FEEDBACK_TYPE_EXAM != $objEx->getFeedbackType()) {
87
            $html .= '<th width="50%">'.get_lang('Comment').'</th>';
88
        }
89
90
        $html .= '</tr></thead><tbody>';
91
        $form->addHtml($html);
92
93
        $answer = null;
94
        if (!empty($this->id)) {
95
            $answer = new Answer($this->id);
96
            $answer->read();
97
            if ($answer->nbrAnswers > 0 && !$form->isSubmitted()) {
98
                $nbAnswers = (int) $answer->nbrAnswers;
99
            }
100
        }
101
102
        $form->addElement('hidden', 'nb_answers');
103
        if ($nbAnswers < 1) {
104
            $nbAnswers = 1;
105
            echo Display::return_message(get_lang('You have to create at least one answer'));
106
        }
107
108
        for ($i = 1; $i <= $nbAnswers; $i++) {
109
            $form->addElement('html', '<tr>');
110
111
            $renderer->setElementTemplate(
112
                '<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
113
                'counter['.$i.']'
114
            );
115
116
            // Two radios will render as two <td> cells (True / False).
117
            $renderer->setElementTemplate(
118
                '<td style="text-align:center;"><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
119
                'correct['.$i.']'
120
            );
121
122
            $renderer->setElementTemplate(
123
                '<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
124
                'answer['.$i.']'
125
            );
126
127
            $renderer->setElementTemplate(
128
                '<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
129
                'comment['.$i.']'
130
            );
131
132
            $answerNumber = $form->addElement('text', 'counter['.$i.']', null, 'value="'.$i.'"');
133
            $answerNumber->freeze();
134
135
            $defaults['answer['.$i.']'] = '';
136
            $defaults['comment['.$i.']'] = '';
137
            $defaults['correct['.$i.']'] = '';
138
139
            if (is_object($answer)) {
140
                $defaults['answer['.$i.']'] = $answer->answer[$i] ?? '';
141
                $defaults['comment['.$i.']'] = $answer->comment[$i] ?? '';
142
                $defaults['correct['.$i.']'] = $answer->correct[$i] ?? '';
143
144
                if (isset($_POST['answer'][$i])) {
145
                    $defaults['answer['.$i.']'] = Security::remove_XSS($_POST['answer'][$i]);
146
                }
147
                if (isset($_POST['comment'][$i])) {
148
                    $defaults['comment['.$i.']'] = Security::remove_XSS($_POST['comment'][$i]);
149
                }
150
                if (isset($_POST['correct'][$i])) {
151
                    $defaults['correct['.$i.']'] = Security::remove_XSS($_POST['correct'][$i]);
152
                }
153
            }
154
155
            $trueValue = $hasOptions ? (int) $tfIids[1] : 1;
156
            $falseValue = $hasOptions ? (int) $tfIids[2] : 2;
157
158
            $form->addElement('radio', 'correct['.$i.']', null, null, $trueValue);
159
            $form->addElement('radio', 'correct['.$i.']', null, null, $falseValue);
160
161
            $form->addHtmlEditor(
162
                'answer['.$i.']',
163
                null,
164
                true,
165
                false,
166
                ['ToolbarSet' => 'TestProposedAnswer', 'Width' => '100%', 'Height' => '100'],
167
                ['style' => 'vertical-align:middle;']
168
            );
169
            $form->addRule('answer['.$i.']', get_lang('Required field'), 'required');
170
            $form->applyFilter("answer[$i]", 'attr_on_filter');
171
172
            if (isset($_POST['answer'][$i])) {
173
                $form->getElement("answer[$i]")->setValue(Security::remove_XSS($_POST['answer'][$i]));
174
            }
175
176
            // Show comment when feedback is enabled
177
            if (EXERCISE_FEEDBACK_TYPE_EXAM != $objEx->getFeedbackType()) {
178
                $form->addHtmlEditor(
179
                    'comment['.$i.']',
180
                    null,
181
                    false,
182
                    false,
183
                    ['ToolbarSet' => 'TestProposedAnswer', 'Width' => '100%', 'Height' => '100'],
184
                    ['style' => 'vertical-align:middle;']
185
                );
186
187
                if (isset($_POST['comment'][$i])) {
188
                    $form->getElement("comment[$i]")->setValue(Security::remove_XSS($_POST['comment'][$i]));
189
                }
190
191
                $form->applyFilter("comment[$i]", 'attr_on_filter');
192
            }
193
194
            $form->addElement('html', '</tr>');
195
        }
196
197
        $form->addElement('html', '</tbody></table>');
198
        $form->addElement('html', '<br />');
199
200
        // Scores (Correct/Wrong). Option[3] is fixed to 0 here.
201
        $txtOption1 = $form->addElement('text', 'option[1]', get_lang('Correct'), ['value' => '1']);
202
        $txtOption2 = $form->addElement('text', 'option[2]', get_lang('Wrong'), ['value' => '-0.5']);
203
        $form->addElement('hidden', 'option[3]', 0);
204
205
        $form->addRule('option[1]', get_lang('Required field'), 'required');
206
        $form->addRule('option[2]', get_lang('Required field'), 'required');
207
208
        $form->addElement('hidden', 'options_count', 3);
209
        $form->addElement('html', '<br /><br />');
210
211
        // Load stored score values if present
212
        if (!empty($this->extra)) {
213
            $scores = explode(':', $this->extra);
214
            if (!empty($scores)) {
215
                $txtOption1->setValue($scores[0] ?? '1');
216
                $txtOption2->setValue($scores[1] ?? '-0.5');
217
            }
218
        }
219
220
        if (true === $objEx->edit_exercise_in_lp ||
221
            (empty($this->exerciseList) && empty($objEx->id))
222
        ) {
223
            $form->addElement('submit', 'lessAnswers', get_lang('Remove answer option'), 'class="btn btn--danger minus"');
224
            $form->addElement('submit', 'moreAnswers', get_lang('Add answer option'), 'class="btn btn--primary plus"');
225
            $form->addElement('submit', 'submitQuestion', $text, 'class="btn btn--primary"');
226
        }
227
228
        $renderer->setElementTemplate('{element}&nbsp;', 'lessAnswers');
229
        $renderer->setElementTemplate('{element}&nbsp;', 'submitQuestion');
230
        $renderer->setElementTemplate('{element}&nbsp;', 'moreAnswers');
231
232
        if (!empty($this->id) && !$form->isSubmitted()) {
233
            $form->setDefaults($defaults);
234
        }
235
236
        $form->setConstants(['nb_answers' => $nbAnswers]);
237
    }
238
239
    /**
240
     * abstract function which creates the form to create / edit the answers of the question.
241
     *
242
     * @param FormValidator $form
243
     * @param Exercise      $exercise
244
     */
245
    public function processAnswersCreation($form, $exercise)
246
    {
247
        $questionWeighting = 0.0;
248
        $objAnswer = new Answer($this->id);
249
250
        $nbAnswers = (int) $form->getSubmitValue('nb_answers');
251
        $courseId = api_get_course_int_id();
252
253
        $repo = Container::getQuestionRepository();
254
        /** @var CQuizQuestion $question */
255
        $question = $repo->find($this->id);
256
257
        $optionsCollection = $question->getOptions();
258
        $isFirstCreation = $optionsCollection->isEmpty();
259
260
        // Ensure default options exist on first creation (True/False + certainty levels).
261
        if ($isFirstCreation) {
262
            for ($i = 1; $i <= 8; $i++) {
263
                Question::saveQuestionOption($question, $this->options[$i], $i);
264
            }
265
        }
266
267
        // Load options and index them by position for mapping (1/2 => iid).
268
        $newOptions = Question::readQuestionOption($this->id, $courseId);
269
        $sortedByPosition = [];
270
        foreach ($newOptions as $item) {
271
            $sortedByPosition[(int) ($item['position'] ?? 0)] = $item;
272
        }
273
274
        // Save extra score values in the format "Correct:Wrong:0".
275
        $extraValues = [];
276
        for ($i = 1; $i <= 3; $i++) {
277
            $score = trim((string) $form->getSubmitValue('option['.$i.']'));
278
            $extraValues[] = $score;
279
        }
280
        $this->setExtra(implode(':', $extraValues));
281
282
        for ($i = 1; $i <= $nbAnswers; $i++) {
283
            $answer = trim((string) $form->getSubmitValue('answer['.$i.']'));
284
            $comment = trim((string) $form->getSubmitValue('comment['.$i.']'));
285
            $goodAnswer = trim((string) $form->getSubmitValue('correct['.$i.']'));
286
287
            if ($isFirstCreation) {
288
                // First creation: map submitted position (1/2) to the real option iid.
289
                $pos = (int) $goodAnswer;
290
                $goodAnswer = isset($sortedByPosition[$pos]) ? (string) ($sortedByPosition[$pos]['iid'] ?? '') : '';
291
            }
292
293
            // Total weighting = nbAnswers * "Correct" score (option[1]).
294
            $questionWeighting += (float) ($extraValues[0] ?? 0);
295
296
            $objAnswer->createAnswer($answer, $goodAnswer, $comment, '', $i);
297
        }
298
299
        // Save answers to DB.
300
        $objAnswer->save();
301
302
        // Save total weighting and question.
303
        $this->updateWeighting($questionWeighting);
304
        $this->save($exercise);
305
    }
306
307
    public function return_header(Exercise $exercise, $counter = null, $score = [])
308
    {
309
        $header = parent::return_header($exercise, $counter, $score);
310
        $header .= '<table class="'.$this->questionTableClass.'"><tr>';
311
        $header .= '<th>'.get_lang('Your choice').'</th>';
312
313
        if ($exercise->showExpectedChoiceColumn()) {
314
            $header .= '<th>'.get_lang('Expected choice').'</th>';
315
        }
316
317
        $header .= '<th>'
318
            .get_lang('Answer')
319
            .'</th><th colspan="2" style="text-align:center;">'
320
            .get_lang('Your degree of certainty')
321
            .'</th>'
322
        ;
323
        if (false === $exercise->hideComment) {
324
            if (EXERCISE_FEEDBACK_TYPE_EXAM != $exercise->getFeedbackType()) {
325
                $header .= '<th>'.get_lang('Comment').'</th>';
326
            }
327
        }
328
        $header .= '</tr>';
329
330
        return $header;
331
    }
332
333
    /**
334
     * Get color code, status, label and description for the current answer.
335
     *
336
     * @param string $studentAnswer
337
     * @param string $expectedAnswer
338
     * @param int    $studentDegreeChoicePosition
339
     *
340
     * @return array An array with indexes 'color', 'background-color', 'status', 'label' and 'description'
341
     */
342
    public function getResponseDegreeInfo($studentAnswer, $expectedAnswer, $studentDegreeChoicePosition)
343
    {
344
        $result = [];
345
        if (3 == $studentDegreeChoicePosition) {
346
            $result = [
347
                'color' => '#000000',
348
                'background-color' => '#F6BA2A',
349
                'status' => self::LEVEL_WHITE,
350
                'label' => get_lang('Declared ignorance'),
351
                'description' => get_lang('You didn\'t know the answer - only 50% sure'),
352
            ];
353
        } else {
354
            $checkResult = $studentAnswer == $expectedAnswer ? true : false;
355
            if ($checkResult) {
356
                if ($studentDegreeChoicePosition >= 6) {
357
                    $result = [
358
                        'color' => '#FFFFFF',
359
                        'background-color' => '#1E9C55',
360
                        'status' => self::LEVEL_DARKGREEN,
361
                        'label' => get_lang('Very sure'),
362
                        'description' => get_lang('Your answer was correct and you were 80% sure about it. Congratulations!'),
363
                    ];
364
                } elseif ($studentDegreeChoicePosition >= 4 && $studentDegreeChoicePosition <= 5) {
365
                    $result = [
366
                        'color' => '#000000',
367
                        'background-color' => '#B1E183',
368
                        'status' => self::LEVEL_LIGHTGREEN,
369
                        'label' => get_lang('Pretty sure'),
370
                        'description' => get_lang('Your answer was correct but you were not completely sure (only 60% to 70% sure)'),
371
                    ];
372
                }
373
            } else {
374
                if ($studentDegreeChoicePosition >= 6) {
375
                    $result = [
376
                        'color' => '#FFFFFF',
377
                        'background-color' => '#ED4040',
378
                        'status' => self::LEVEL_DARKRED,
379
                        'label' => get_lang('Very unsure'),
380
                        'description' => get_lang('Your answer was incorrect although you were about 80% (or more) sure it was wrong'),
381
                    ];
382
                } elseif ($studentDegreeChoicePosition >= 4 && $studentDegreeChoicePosition <= 5) {
383
                    $result = [
384
                        'color' => '#000000',
385
                        'background-color' => '#F79B88',
386
                        'status' => self::LEVEL_LIGHTRED,
387
                        'label' => get_lang('Unsure'),
388
                        'description' => get_lang('Your answer was incorrect, but you guessed it was (60% to 70% sure)'),
389
                    ];
390
                }
391
            }
392
        }
393
394
        return $result;
395
    }
396
397
    /**
398
     * Method to show the code color and his meaning for the test result.
399
     */
400
    public static function showColorCodes()
401
    {
402
        ?>
403
        <table class="fc-border-separate" cellspacing="0" style="width:600px;
404
            margin: auto; border: 3px solid #A39E9E;" >
405
            <tr style="border-bottom: 1px solid #A39E9E;">
406
                <td style="width:15%; height:30px; background-color: #088A08; border-right: 1px solid #A39E9E;">
407
                    &nbsp;
408
                </td>
409
                <td style="padding-left:10px;">
410
                    <b><?php echo get_lang('Very sure'); ?> :</b>
411
                    <?php echo get_lang('Your answer was correct and you were 80% sure about it. Congratulations!'); ?>
412
                </td>
413
            </tr>
414
            <tr style="border-bottom: 1px solid #A39E9E;">
415
                <td style="width:15%; height:30px; background-color: #A9F5A9; border-right: 1px solid #A39E9E;">
416
                    &nbsp;
417
                </td>
418
                <td style="padding-left:10px;">
419
                    <b><?php echo get_lang('Pretty sure'); ?> :</b>
420
                    <?php echo get_lang('Your answer was correct but you were not completely sure (only 60% to 70% sure)'); ?>
421
                </td>
422
            </tr>
423
            <tr style="border: 1px solid #A39E9E;">
424
                <td style="width:15%; height:30px; background-color: #FFFFFF; border-right: 1px solid #A39E9E;">
425
                    &nbsp;
426
                </td>
427
                <td style="padding-left:10px;">
428
                    <b><?php echo get_lang('Declared ignorance'); ?> :</b>
429
                    <?php echo get_lang('You didn\'t know the answer - only 50% sure'); ?>
430
                </td>
431
            </tr>
432
            <tr style="border: 1px solid #A39E9E;">
433
                <td style="width:15%; height:30px; background-color: #F6CECE; border-right: 1px solid #A39E9E;">
434
                    &nbsp;
435
                </td>
436
                <td style="padding-left:10px;">
437
                    <b><?php echo get_lang('Unsure'); ?> :</b>
438
                    <?php echo get_lang('Your answer was incorrect, but you guessed it was (60% to 70% sure)'); ?>
439
                </td>
440
            </tr>
441
            <tr style="border-bottom: 1px solid #A39E9E;">
442
                <td style="width:15%; height:30px; background-color: #FE2E2E; border-right: 1px solid #A39E9E;">
443
                    &nbsp;
444
                </td>
445
                <td style="padding-left:10px;">
446
                    <b><?php echo get_lang('Very unsure'); ?> :</b>
447
                    <?php echo get_lang('Your answer was incorrect although you were about 80% (or more) sure it was wrong'); ?>
448
                </td>
449
            </tr>
450
        </table><br/>
451
        <?php
452
    }
453
454
    /**
455
     * Display basic bar charts of results by category of questions.
456
     *
457
     * @param array  $scoreListAll
458
     * @param string $title        The block title
459
     * @param int    $sizeRatio
460
     *
461
     * @return string The HTML/CSS code for the charts block
462
     */
463
    public static function displayDegreeChartByCategory($scoreListAll, $title, $sizeRatio = 1)
464
    {
465
        $maxHeight = 0;
466
        $groupCategoriesByBracket = false;
467
        if ($groupCategoriesByBracket) {
468
            $scoreList = [];
469
            $categoryPrefixList = [];
470
            // categoryPrefix['Math'] = firstCategoryId for this prefix
471
            // rebuild $scoreList factorizing data with category prefix
472
            foreach ($scoreListAll as $categoryId => $scoreListForCategory) {
473
                $objCategory = new Testcategory();
474
                $objCategoryNum = $objCategory->getCategory($categoryId);
475
                preg_match("/^\[([^]]+)\]/", $objCategoryNum->name, $matches);
476
477
                if (count($matches) > 1) {
478
                    // check if we have already see this prefix
479
                    if (array_key_exists($matches[1], $categoryPrefixList)) {
480
                        // add the result color for this entry
481
                        $scoreList[$categoryPrefixList[$matches[1]]][self::LEVEL_DARKGREEN] +=
482
                            $scoreListForCategory[self::LEVEL_DARKGREEN];
483
                        $scoreList[$categoryPrefixList[$matches[1]]][self::LEVEL_LIGHTGREEN] +=
484
                            $scoreListForCategory[self::LEVEL_LIGHTGREEN];
485
                        $scoreList[$categoryPrefixList[$matches[1]]][self::LEVEL_WHITE] +=
486
                            $scoreListForCategory[self::LEVEL_WHITE];
487
                        $scoreList[$categoryPrefixList[$matches[1]]][self::LEVEL_LIGHTRED] +=
488
                            $scoreListForCategory[self::LEVEL_LIGHTRED];
489
                        $scoreList[$categoryPrefixList[$matches[1]]][self::LEVEL_DARKRED] +=
490
                            $scoreListForCategory[self::LEVEL_DARKRED];
491
                    } else {
492
                        $categoryPrefixList[$matches[1]] = $categoryId;
493
                        $scoreList[$categoryId] = $scoreListAll[$categoryId];
494
                    }
495
                } else {
496
                    // doesn't match the prefix '[math] Math category'
497
                    $scoreList[$categoryId] = $scoreListAll[$categoryId];
498
                }
499
            }
500
        } else {
501
            $scoreList = $scoreListAll;
502
        }
503
504
        // get the max height of item to have each table the same height if displayed side by side
505
        foreach ($scoreList as $categoryId => $scoreListForCategory) {
506
            [$noValue, $height] = self::displayDegreeChartChildren(
507
                $scoreListForCategory,
508
                300,
509
                '',
510
                1,
511
                0,
512
                false,
513
                true,
514
                0
515
            );
516
            if ($height > $maxHeight) {
517
                $maxHeight = $height;
518
            }
519
        }
520
521
        $html = '<div class="row-chart">';
522
        $html .= '<h4 class="chart-title">'.$title.'</h4>';
523
524
        $legendTitle = [
525
            'Very unsure',
526
            'Unsure',
527
            'Declared ignorance',
528
            'Pretty sure',
529
            'Very sure',
530
        ];
531
        $html .= '<ul class="chart-legend">';
532
        foreach ($legendTitle as $i => $item) {
533
            $html .= '<li><i class="fa fa-square square_color'.$i.'" aria-hidden="true"></i> '.get_lang($item).'</li>';
534
        }
535
        $html .= '</ul>';
536
537
        // get the html of items
538
        $i = 0;
539
        $testCategory = new Testcategory();
540
        foreach ($scoreList as $categoryId => $scoreListForCategory) {
541
            $category = $testCategory->getCategory($categoryId);
542
            $categoryQuestionName = '';
543
            if ($category) {
544
                $categoryQuestionName = $category->name;
545
            }
546
547
            if ('' === $categoryQuestionName) {
548
                $categoryName = get_lang('Without category');
549
            } else {
550
                $categoryName = $categoryQuestionName;
551
            }
552
553
            $html .= '<div class="col-md-4">';
554
            $html .= self::displayDegreeChartChildren(
555
                $scoreListForCategory,
556
                300,
557
                $categoryName,
558
                1,
559
                $maxHeight,
560
                false,
561
                false,
562
                $groupCategoriesByBracket
563
            );
564
            $html .= '</div>';
565
566
            if (2 == $i) {
567
                $html .= '<div style="clear:both; height: 10px;">&nbsp;</div>';
568
                $i = 0;
569
            } else {
570
                $i++;
571
            }
572
        }
573
        $html .= '</div>';
574
575
        return $html.'<div style="clear:both; height: 10px;" >&nbsp;</div>';
576
    }
577
578
    /**
579
     * Return HTML code for the $scoreList of MultipleAnswerTrueFalseDegreeCertainty questions.
580
     *
581
     * @param        $scoreList
582
     * @param        $widthTable
583
     * @param string $title
584
     * @param int    $sizeRatio
585
     * @param int    $minHeight
586
     * @param bool   $displayExplanationText
587
     * @param bool   $returnHeight
588
     * @param bool   $groupCategoriesByBracket
589
     * @param int    $numberOfQuestions
590
     *
591
     * @return array|string
592
     */
593
    public static function displayDegreeChart(
594
        $scoreList,
595
        $widthTable,
596
        $title = '',
597
        $sizeRatio = 1,
598
        $minHeight = 0,
599
        $displayExplanationText = true,
600
        $returnHeight = false,
601
        $groupCategoriesByBracket = false,
602
        $numberOfQuestions = 0
603
    ) {
604
        $topAndBottomMargin = 10;
605
        $colorList = [
606
            self::LEVEL_DARKRED,
607
            self::LEVEL_LIGHTRED,
608
            self::LEVEL_WHITE,
609
            self::LEVEL_LIGHTGREEN,
610
            self::LEVEL_DARKGREEN,
611
        ];
612
613
        // get total attempt number
614
        $highterColorHeight = 0;
615
        foreach ($scoreList as $color => $number) {
616
            if ($number > $highterColorHeight) {
617
                $highterColorHeight = $number;
618
            }
619
        }
620
621
        $totalAttemptNumber = $numberOfQuestions;
622
        $verticalLineHeight = $highterColorHeight * $sizeRatio * 2 + 122 + $topAndBottomMargin * 2;
623
        if ($verticalLineHeight < $minHeight) {
624
            $minHeightCorrection = $minHeight - $verticalLineHeight;
625
            $verticalLineHeight += $minHeightCorrection;
626
        }
627
628
        // draw chart
629
        $html = '';
630
631
        if ($groupCategoriesByBracket) {
632
            $title = api_preg_replace('/[^]]*$/', '', $title);
633
            $title = ucfirst(api_preg_replace("/[\[\]]/", '', $title));
634
        }
635
636
        $titleDisplay = strpos($title, 'ensemble') > 0 ?
637
            $title."<br/>($totalAttemptNumber questions)" :
638
            $title;
639
        $textSize = strpos($title, 'ensemble') > 0 ||
640
            strpos($title, 'votre dernier résultat à ce test') > 0 ? 100 : 80;
641
642
        $html .= '<div class="row-chart">';
643
        $html .= '<h4 class="chart-title">'.$titleDisplay.'</h4>';
644
645
        $nbResponsesInc = 0;
646
        if (isset($scoreList[4])) {
647
            $nbResponsesInc += (int) $scoreList[4];
648
        }
649
        if (isset($scoreList[5])) {
650
            $nbResponsesInc += (int) $scoreList[5];
651
        }
652
653
        $nbResponsesIng = isset($scoreList[3]) ? $scoreList[3] : 0;
654
655
        $nbResponsesCor = 0;
656
        if (isset($scoreList[1])) {
657
            $nbResponsesCor += (int) $scoreList[1];
658
        }
659
        if (isset($scoreList[2])) {
660
            $nbResponsesCor += (int) $scoreList[2];
661
        }
662
663
        $IncorrectAnswers = sprintf(get_lang('Incorrect answers: %s'), $nbResponsesInc);
664
        $IgnoranceAnswers = sprintf(get_lang('Ignorance: %s'), $nbResponsesIng);
665
        $CorrectAnswers = sprintf(get_lang('Correct answers: %s'), $nbResponsesCor);
666
667
        $html .= '<div class="chart-grid">';
668
669
        $explainHistoList = null;
670
        if ($displayExplanationText) {
671
            // Display of histogram text
672
            $explainHistoList = [
673
                'Very unsure',
674
                'Unsure',
675
                'Declared ignorance',
676
                'Pretty sure',
677
                'Very sure',
678
            ];
679
        }
680
681
        foreach ($colorList as $i => $color) {
682
            if (array_key_exists($color, $scoreList)) {
683
                $scoreOnBottom = $scoreList[$color]; // height of the colored area on the bottom
684
            } else {
685
                $scoreOnBottom = 0;
686
            }
687
            $sizeBar = ($scoreOnBottom * $sizeRatio * 2).'px;';
688
689
            if (0 == $i) {
690
                $html .= '<div class="item">';
691
                $html .= '<div class="panel-certaint" style="min-height:'.$verticalLineHeight.'px; position: relative;">';
692
                $html .= '<div class="answers-title">'.$IncorrectAnswers.'</div>';
693
                $html .= '<ul class="certaint-list-two">';
694
            } elseif (3 == $i) {
695
                $html .= '<div class="item">';
696
                $html .= '<div class="panel-certaint" style="height:'.$verticalLineHeight.'px;  position: relative;">';
697
                $html .= '<div class="answers-title">'.$CorrectAnswers.'</div>';
698
                $html .= '<ul class="certaint-list-two">';
699
            } elseif (2 == $i) {
700
                $html .= '<div class="item">';
701
                $html .= '<div class="panel-certaint" style="height:'.$verticalLineHeight.'px;  position: relative;">';
702
                $html .= '<div class="answers-title">'.$IgnoranceAnswers.'</div>';
703
                $html .= '<ul class="certaint-list">';
704
            }
705
            $html .= '<li>';
706
            $html .= '<div class="certaint-score">';
707
            $html .= $scoreOnBottom;
708
            $html .= '</div>';
709
            $html .= '<div class="levelbar_'.$color.'" style="height:'.$sizeBar.'">&nbsp;</div>';
710
            $html .= '<div class="certaint-text">'.get_lang($explainHistoList[$i]).'</div>';
711
            $html .= '</li>';
712
713
            if (1 == $i || 2 == $i || 4 == $i) {
714
                $html .= '</ul>';
715
                $html .= '</div>';
716
                $html .= '</div>';
717
            }
718
        }
719
720
        $html .= '</div>';
721
        $html .= '</div>';
722
723
        if ($returnHeight) {
724
            return [$html, $verticalLineHeight];
725
        } else {
726
            return $html;
727
        }
728
    }
729
730
    /**
731
     * Return HTML code for the $scoreList of MultipleAnswerTrueFalseDegreeCertainty questions.
732
     *
733
     * @param        $scoreList
734
     * @param        $widthTable
735
     * @param string $title
736
     * @param int    $sizeRatio
737
     * @param int    $minHeight
738
     * @param bool   $displayExplanationText
739
     * @param bool   $returnHeight
740
     * @param bool   $groupCategoriesByBracket
741
     * @param int    $numberOfQuestions
742
     *
743
     * @return array|string
744
     */
745
    public static function displayDegreeChartChildren(
746
        $scoreList,
747
        $widthTable,
748
        $title = '',
749
        $sizeRatio = 1,
750
        $minHeight = 0,
751
        $displayExplanationText = true,
752
        $returnHeight = false,
753
        $groupCategoriesByBracket = false,
754
        $numberOfQuestions = 0
755
    ) {
756
        $topAndBottomMargin = 10;
757
        $colorList = [
758
            self::LEVEL_DARKRED,
759
            self::LEVEL_LIGHTRED,
760
            self::LEVEL_WHITE,
761
            self::LEVEL_LIGHTGREEN,
762
            self::LEVEL_DARKGREEN,
763
        ];
764
765
        // get total attempt number
766
        $highterColorHeight = 0;
767
        foreach ($scoreList as $color => $number) {
768
            if ($number > $highterColorHeight) {
769
                $highterColorHeight = $number;
770
            }
771
        }
772
773
        $totalAttemptNumber = $numberOfQuestions;
774
        $verticalLineHeight = $highterColorHeight * $sizeRatio * 2 + 122 + $topAndBottomMargin * 2;
775
        if ($verticalLineHeight < $minHeight) {
776
            $minHeightCorrection = $minHeight - $verticalLineHeight;
777
            $verticalLineHeight += $minHeightCorrection;
778
        }
779
780
        // draw chart
781
        $html = '';
782
783
        if ($groupCategoriesByBracket) {
784
            $title = api_preg_replace('/[^]]*$/', '', $title);
785
            $title = ucfirst(api_preg_replace("/[\[\]]/", '', $title));
786
        }
787
788
        $textSize = 80;
789
790
        $classGlobalChart = '';
791
        if ($displayExplanationText) {
792
            // global chart
793
            $classGlobalChart = 'globalChart';
794
        }
795
796
        $html .= '<table class="certaintyTable" style="height :'.$verticalLineHeight.'px; margin-bottom: 10px;" >';
797
        $html .= '<tr><th colspan="5" class="'.$classGlobalChart.'">'
798
            .$title
799
            .'</th><tr>'
800
        ;
801
802
        $nbResponsesInc = 0;
803
        if (isset($scoreList[4])) {
804
            $nbResponsesInc += (int) $scoreList[4];
805
        }
806
        if (isset($scoreList[5])) {
807
            $nbResponsesInc += (int) $scoreList[5];
808
        }
809
810
        $nbResponsesIng = isset($scoreList[3]) ? $scoreList[3] : 0;
811
812
        $nbResponsesCor = 0;
813
        if (isset($scoreList[1])) {
814
            $nbResponsesCor += (int) $scoreList[1];
815
        }
816
        if (isset($scoreList[2])) {
817
            $nbResponsesCor += (int) $scoreList[2];
818
        }
819
820
        $colWidth = $widthTable / 5;
821
822
        $html .= '<tr>
823
                <td class="firstLine borderRight '.$classGlobalChart.'"
824
                    colspan="2"
825
                    style="width:'.($colWidth * 2).'px; line-height: 15px; font-size:'.$textSize.'%;">'.
826
            sprintf(get_lang('Incorrect answers: %s'), $nbResponsesInc).'
827
                </td>
828
                <td class="firstLine borderRight '.$classGlobalChart.'"
829
                    style="width:'.$colWidth.'px; line-height: 15px; font-size :'.$textSize.'%;">'.
830
            sprintf(get_lang('Ignorance: %s'), $nbResponsesIng).'
831
                </td>
832
                <td class="firstLine '.$classGlobalChart.'"
833
                    colspan="2"
834
                    style="width:'.($colWidth * 2).'px; line-height: 15px; font-size:'.$textSize.'%;">'.
835
            sprintf(get_lang('Correct answers: %s'), $nbResponsesCor).'
836
                </td>
837
            </tr>';
838
        $html .= '<tr>';
839
840
        foreach ($colorList as $i => $color) {
841
            if (array_key_exists($color, $scoreList)) {
842
                $scoreOnBottom = $scoreList[$color]; // height of the colored area on the bottom
843
            } else {
844
                $scoreOnBottom = 0;
845
            }
846
            $sizeOnBottom = $scoreOnBottom * $sizeRatio * 2;
847
            if (1 == $i || 2 == $i) {
848
                $html .= '<td width="'
849
                    .$colWidth
850
                    .'px" style="border-right: 1px dotted #7FC5FF; vertical-align: bottom;font-size: '
851
                    .$textSize
852
                    .'%;">'
853
                ;
854
            } else {
855
                $html .= '<td width="'
856
                    .$colWidth
857
                    .'px" style="vertical-align: bottom;font-size: '
858
                    .$textSize
859
                    .'%;">'
860
                ;
861
            }
862
            $html .= '<div class="certaint-score">'
863
                .$scoreOnBottom
864
                .'</div><div class="levelbar_'
865
                .$color
866
                .'" style="height: '
867
                .$sizeOnBottom
868
                .'px;">&nbsp;</div>'
869
            ;
870
            $html .= '</td>';
871
        }
872
873
        $html .= '</tr>';
874
875
        if ($displayExplanationText) {
876
            // Display of histogram text
877
            $explainHistoList = [
878
                'Very unsure',
879
                'Unsure',
880
                'Declared ignorance',
881
                'Pretty sure',
882
                'Very sure',
883
            ];
884
            $html .= '<tr>';
885
            $i = 0;
886
            foreach ($explainHistoList as $explain) {
887
                if (1 == $i || 2 == $i) {
888
                    $class = 'borderRight';
889
                } else {
890
                    $class = '';
891
                }
892
                $html .= '<td class="firstLine '
893
                    .$class
894
                    .' '
895
                    .$classGlobalChart
896
                    .'" style="width="'
897
                    .$colWidth
898
                    .'px; font-size:'
899
                    .$textSize
900
                    .'%;">'
901
                ;
902
                $html .= get_lang($explain);
903
                $html .= '</td>';
904
                $i++;
905
            }
906
            $html .= '</tr>';
907
        }
908
        $html .= '</table></center>';
909
910
        if ($returnHeight) {
911
            return [$html, $verticalLineHeight];
912
        } else {
913
            return $html;
914
        }
915
    }
916
917
    /**
918
     * return previous attempt id for this test for student, 0 if no previous attempt.
919
     *
920
     * @param $exeId
921
     *
922
     * @return int
923
     */
924
    public static function getPreviousAttemptId($exeId)
925
    {
926
        $tblTrackEExercise = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
927
        $exeId = (int) $exeId;
928
        $sql = "SELECT * FROM $tblTrackEExercise
929
                WHERE exe_id = ".$exeId;
930
        $res = Database::query($sql);
931
932
        if (empty(Database::num_rows($res))) {
933
            // if we cannot find the exe_id
934
            return 0;
935
        }
936
937
        $data = Database::fetch_assoc($res);
938
        $courseCode = $data['c_id'];
939
        $exerciseId = $data['exe_exo_id'];
940
        $userId = $data['exe_user_id'];
941
        $attemptDate = $data['exe_date'];
942
943
        if ('0000-00-00 00:00:00' === $attemptDate) {
944
            // incomplete attempt, close it before continue
945
            return 0;
946
        }
947
948
        // look for previous attempt
949
        $exerciseId = (int) $exerciseId;
950
        $userId = (int) $userId;
951
        $sql = "SELECT *
952
                FROM $tblTrackEExercise
953
                WHERE
954
                      c_id = '$courseCode' AND
955
                      exe_exo_id = $exerciseId AND
956
                      exe_user_id = $userId AND
957
                      status = '' AND
958
                      exe_date > '0000-00-00 00:00:00' AND
959
                      exe_date < '$attemptDate'
960
                ORDER BY exe_date DESC";
961
962
        $res = Database::query($sql);
963
964
        if (0 == Database::num_rows($res)) {
965
            // no previous attempt
966
            return 0;
967
        }
968
969
        $data = Database::fetch_assoc($res);
970
971
        return $data['exe_id'];
972
    }
973
974
    /**
975
     * return an array of number of answer color for exe attempt
976
     * for question type = MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
977
     * e.g.
978
     * [LEVEL_DARKGREEN => 3, LEVEL_LIGHTGREEN => 0, LEVEL_WHITE => 5, LEVEL_LIGHTRED => 12, LEVEL_DARKTRED => 0].
979
     *
980
     * @param $exeId
981
     *
982
     * @return array
983
     */
984
    public static function getColorNumberListForAttempt($exeId)
985
    {
986
        $result = [
987
            self::LEVEL_DARKGREEN => 0,
988
            self::LEVEL_LIGHTGREEN => 0,
989
            self::LEVEL_WHITE => 0,
990
            self::LEVEL_LIGHTRED => 0,
991
            self::LEVEL_DARKRED => 0,
992
        ];
993
994
        $attemptInfoList = self::getExerciseAttemptInfo($exeId);
995
996
        foreach ($attemptInfoList as $attemptInfo) {
997
            $oQuestion = new self();
998
            $oQuestion->read($attemptInfo['question_id']);
999
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $oQuestion->type) {
1000
                $answerColor = self::getAnswerColor($exeId, $attemptInfo['question_id'], $attemptInfo['position']);
1001
                if ($answerColor) {
1002
                    $result[$answerColor]++;
1003
                }
1004
            }
1005
        }
1006
1007
        return $result;
1008
    }
1009
1010
    /**
1011
     * return an array of number of color for question type = MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY
1012
     * for each question category.
1013
     *
1014
     * e.g.
1015
     * [
1016
     *      (categoryId=)5 => [LEVEL_DARKGREEN => 3, LEVEL_WHITE => 5, LEVEL_LIGHTRED => 12]
1017
     *      (categoryId=)2 => [LEVEL_DARKGREEN => 8, LEVEL_LIGHTRED => 2, LEVEL_DARKTRED => 8]
1018
     *      (categoryId=)0 => [LEVEL_DARKGREEN => 1,
1019
     *          LEVEL_LIGHTGREEN => 2,
1020
     *          LEVEL_WHITE => 6,
1021
     *          LEVEL_LIGHTRED => 1,
1022
     *          LEVEL_DARKTRED => 9]
1023
     * ]
1024
     *
1025
     * @param int $exeId
1026
     *
1027
     * @return array
1028
     */
1029
    public static function getColorNumberListForAttemptByCategory($exeId)
1030
    {
1031
        $result = [];
1032
        $attemptInfoList = self::getExerciseAttemptInfo($exeId);
1033
1034
        foreach ($attemptInfoList as $attemptInfo) {
1035
            $oQuestion = new self();
1036
            $oQuestion->read($attemptInfo['question_id']);
1037
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $oQuestion->type) {
1038
                $questionCategory = Testcategory::getCategoryForQuestion($attemptInfo['question_id']);
1039
1040
                if (!array_key_exists($questionCategory, $result)) {
1041
                    $result[$questionCategory] = [];
1042
                }
1043
1044
                $answerColor = self::getAnswerColor($exeId, $attemptInfo['question_id'], $attemptInfo['position']);
1045
                if ($answerColor && isset($result[$questionCategory])) {
1046
                    if (!isset($result[$questionCategory][$answerColor])) {
1047
                        $result[$questionCategory][$answerColor] = 0;
1048
                    }
1049
                    $result[$questionCategory][$answerColor]++;
1050
                }
1051
            }
1052
        }
1053
1054
        return $result;
1055
    }
1056
1057
    /**
1058
     * Return true if answer of $exeId, $questionId, $position is correct, otherwise return false.
1059
     *
1060
     * @param $exeId
1061
     * @param $questionId
1062
     * @param $position
1063
     *
1064
     * @return int
1065
     */
1066
    public static function getAnswerColor($exeId, $questionId, $position)
1067
    {
1068
        $attemptInfoList = self::getExerciseAttemptInfo($exeId, $questionId, $position);
1069
1070
        if (1 != count($attemptInfoList)) {
1071
            // havent got the answer
1072
            return 0;
1073
        }
1074
1075
        $answerCodes = $attemptInfoList[0]['answer'];
1076
1077
        // student answer
1078
        $splitAnswer = preg_split('/:/', $answerCodes);
1079
        // get correct answer option id
1080
        $correctAnswerOptionId = self::getCorrectAnswerOptionId($splitAnswer[0]);
1081
        if (0 == $correctAnswerOptionId) {
1082
            // error returning the correct answer option id
1083
            return 0;
1084
        }
1085
1086
        // get student answer option id
1087
        $studentAnswerOptionId = $splitAnswer[1] ?? null;
1088
1089
        // we got the correct answer option id, let's compare ti with the student answer
1090
        $percentage = null;
1091
        if (isset($splitAnswer[2])) {
1092
            $percentage = self::getPercentagePosition($splitAnswer[2]);
1093
        }
1094
1095
        if ($studentAnswerOptionId == $correctAnswerOptionId) {
1096
            // yeah, student got correct answer
1097
            switch ($percentage) {
1098
                case 3:
1099
                    return self::LEVEL_WHITE;
1100
                case 4:
1101
                case 5:
1102
                    return self::LEVEL_LIGHTGREEN;
1103
                case 6:
1104
                case 7:
1105
                case 8:
1106
                    return self::LEVEL_DARKGREEN;
1107
                default:
1108
                    return 0;
1109
            }
1110
        } else {
1111
            // bummer, wrong answer dude
1112
            switch ($percentage) {
1113
                case 3:
1114
                    return self::LEVEL_WHITE;
1115
                case 4:
1116
                case 5:
1117
                    return self::LEVEL_LIGHTRED;
1118
                case 6:
1119
                case 7:
1120
                case 8:
1121
                    return self::LEVEL_DARKRED;
1122
                default:
1123
                    return 0;
1124
            }
1125
        }
1126
    }
1127
1128
    /**
1129
     * Return the position of certitude %age choose by student.
1130
     *
1131
     * @param $optionId
1132
     *
1133
     * @return int
1134
     */
1135
    public static function getPercentagePosition($optionId)
1136
    {
1137
        $tblAnswerOption = Database::get_course_table(TABLE_QUIZ_QUESTION_OPTION);
1138
        $courseId = api_get_course_int_id();
1139
        $optionId = (int) $optionId;
1140
        $sql = "SELECT position
1141
                FROM $tblAnswerOption
1142
                WHERE c_id = $courseId AND id = $optionId";
1143
        $res = Database::query($sql);
1144
1145
        if (0 == Database::num_rows($res)) {
1146
            return 0;
1147
        }
1148
1149
        $data = Database::fetch_assoc($res);
1150
1151
        return $data['position'];
1152
    }
1153
1154
    /**
1155
     * return the correct id from c_quiz_question_option for question idAuto.
1156
     *
1157
     * @param $idAuto
1158
     *
1159
     * @return int
1160
     */
1161
    public static function getCorrectAnswerOptionId($idAuto)
1162
    {
1163
        $tblAnswer = Database::get_course_table(TABLE_QUIZ_ANSWER);
1164
        $idAuto = (int) $idAuto;
1165
        $sql = "SELECT correct FROM $tblAnswer
1166
                WHERE iid = $idAuto";
1167
1168
        $res = Database::query($sql);
1169
        $data = Database::fetch_assoc($res);
1170
        if (Database::num_rows($res) > 0) {
1171
            return $data['correct'];
1172
        }
1173
1174
        return 0;
1175
    }
1176
1177
    /**
1178
     * return an array of exe info from track_e_attempt.
1179
     *
1180
     * @param int $exeId
1181
     * @param int $questionId
1182
     * @param int $position
1183
     *
1184
     * @return array
1185
     */
1186
    public static function getExerciseAttemptInfo($exeId, $questionId = -1, $position = -1)
1187
    {
1188
        $result = [];
1189
        $and = '';
1190
        $questionId = (int) $questionId;
1191
        $position = (int) $position;
1192
        $exeId = (int) $exeId;
1193
1194
        if ($questionId >= 0) {
1195
            $and .= " AND question_id = $questionId";
1196
        }
1197
        if ($position >= 0) {
1198
            $and .= " AND position = $position";
1199
        }
1200
1201
        $tblExeAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1202
        $sql = "SELECT * FROM $tblExeAttempt
1203
                WHERE exe_id = $exeId $and";
1204
1205
        $res = Database::query($sql);
1206
        while ($data = Database::fetch_assoc($res)) {
1207
            $result[] = $data;
1208
        }
1209
1210
        return $result;
1211
    }
1212
1213
    /**
1214
     * @param int $exeId
1215
     *
1216
     * @return int
1217
     */
1218
    public static function getNumberOfQuestionsForExeId($exeId)
1219
    {
1220
        $tableTrackEExercise = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1221
        $exeId = (int) $exeId;
1222
1223
        $sql = "SELECT exe_exo_id
1224
                FROM $tableTrackEExercise
1225
                WHERE exe_id=".$exeId;
1226
        $res = Database::query($sql);
1227
        $data = Database::fetch_assoc($res);
1228
        if ($data) {
1229
            $exerciseId = $data['exe_exo_id'];
1230
1231
            $objectExercise = new Exercise();
1232
            $objectExercise->read($exerciseId);
1233
1234
            return $objectExercise->getQuestionCount();
1235
        }
1236
1237
        return 0;
1238
    }
1239
1240
    /**
1241
     * Display student chart results for these question types.
1242
     *
1243
     * @param int      $exeId
1244
     * @param Exercise $objExercice
1245
     *
1246
     * @return string
1247
     */
1248
    public static function displayStudentsChartResults($exeId, $objExercice)
1249
    {
1250
        $numberOfQuestions = self::getNumberOfQuestionsForExeId($exeId);
1251
        $globalScoreList = self::getColorNumberListForAttempt($exeId);
1252
        $html = self::displayDegreeChart(
1253
            $globalScoreList,
1254
            600,
1255
            get_lang('Your overall results for the test'),
1256
            2,
1257
            0,
1258
            true,
1259
            false,
1260
            false,
1261
            $numberOfQuestions
1262
        );
1263
        $html .= '<br/>';
1264
1265
        $previousAttemptId = self::getPreviousAttemptId($exeId);
1266
        if ($previousAttemptId > 0) {
1267
            $previousAttemptScoreList = self::getColorNumberListForAttempt(
1268
                $previousAttemptId
1269
            );
1270
            $html .= self::displayDegreeChart(
1271
                $previousAttemptScoreList,
1272
                600,
1273
                get_lang('In comparison, your latest results for this test'),
1274
                2
1275
            );
1276
            $html .= '<br/>';
1277
        }
1278
1279
        $list = self::getColorNumberListForAttemptByCategory($exeId);
1280
        $html .= self::displayDegreeChartByCategory(
1281
            $list,
1282
            get_lang('Your results by discipline'),
1283
            1,
1284
            $objExercice
1285
        );
1286
        $html .= '<br/>';
1287
1288
        return $html;
1289
    }
1290
1291
    /**
1292
     * send mail to student with degre certainty result test.
1293
     *
1294
     * @param int      $userId
1295
     * @param Exercise $objExercise
1296
     * @param int      $exeId
1297
     */
1298
    public static function sendQuestionCertaintyNotification($userId, $objExercise, $exeId)
1299
    {
1300
        $userInfo = api_get_user_info($userId);
1301
        $recipientName = api_get_person_name($userInfo['firstname'],
1302
            $userInfo['lastname'],
1303
            null,
1304
            PERSON_NAME_EMAIL_ADDRESS
1305
        );
1306
        $subject = '['.get_lang('Please do not reply').'] '
1307
            .html_entity_decode(get_lang('Results for the accomplished test').' "'.$objExercise->title.'"');
1308
1309
        // message sended to the student
1310
        $message = get_lang('Dear').' '.$recipientName.',<br /><br />';
1311
        $exerciseLink = "<a href='".api_get_path(WEB_CODE_PATH).'/exercise/result.php?show_headers=1&'
1312
            .api_get_cidreq()
1313
            ."&id=$exeId'>";
1314
        $exerciseTitle = $objExercise->title;
1315
1316
        $message .= sprintf(
1317
            get_lang('Please follow the instructions below to check your results for test %s.<br /><br />'),
1318
            $exerciseTitle,
1319
            api_get_path(WEB_PATH),
1320
            $exerciseLink
1321
        );
1322
1323
        // show histogram
1324
        $message .= self::displayStudentsChartResults($exeId, $objExercise);
1325
        $message .= get_lang('Kind regards,');
1326
        $message = api_preg_replace("/\\\n/", '', $message);
1327
1328
        MessageManager::send_message_simple($userId, $subject, $message);
1329
    }
1330
1331
    /**
1332
     * Return True/False option iids when they exist.
1333
     * Fallback to positions 1/2 when options are not available yet.
1334
     */
1335
    private function getTrueFalseOptionIids(int $courseId): array
1336
    {
1337
        // Fallback for first-creation cases (positions)
1338
        $map = [1 => 1, 2 => 2];
1339
1340
        if (empty($this->id)) {
1341
            return $map;
1342
        }
1343
1344
        // Try reading options with courseId if supported, fallback otherwise.
1345
        try {
1346
            $optionData = Question::readQuestionOption($this->id, $courseId);
1347
        } catch (\Throwable $e) {
1348
            $optionData = Question::readQuestionOption($this->id);
1349
        }
1350
1351
        if (!empty($optionData)) {
1352
            foreach ($optionData as $row) {
1353
                $pos = (int) ($row['position'] ?? 0);
1354
                $iid = (int) ($row['iid'] ?? 0);
1355
1356
                if (($pos === 1 || $pos === 2) && $iid > 0) {
1357
                    $map[$pos] = $iid;
1358
                }
1359
            }
1360
        }
1361
1362
        return $map;
1363
    }
1364
}
1365