getResponseDegreeInfo()   B
last analyzed

Complexity

Conditions 10
Paths 13

Size

Total Lines 53
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 41
nc 13
nop 3
dl 0
loc 53
rs 7.6666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

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