getExerciseAttemptInfo()   A
last analyzed

Complexity

Conditions 4
Paths 8

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 16
nc 8
nop 3
dl 0
loc 25
rs 9.7333
c 0
b 0
f 0
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