Passed
Push — master ( 16296d...c50ca1 )
by
unknown
17:01 queued 08:11
created

ExerciseLib::displayGroupMenu()   A

Complexity

Conditions 3

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 18
c 0
b 0
f 0
nop 3
dl 0
loc 24
rs 9.6666
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\Asset;
6
use Chamilo\CoreBundle\Entity\Course as CourseEntity;
7
use Chamilo\CoreBundle\Entity\Session as SessionEntity;
8
use Chamilo\CoreBundle\Entity\GradebookCategory;
9
use Chamilo\CoreBundle\Entity\TrackEExercise;
10
use Chamilo\CoreBundle\Enums\ActionIcon;
11
use Chamilo\CoreBundle\Framework\Container;
12
use Chamilo\CoreBundle\Helpers\ChamiloHelper;
13
use Chamilo\CourseBundle\Entity\CLpItem;
14
use Chamilo\CourseBundle\Entity\CLpItemView;
15
use Chamilo\CourseBundle\Entity\CQuiz;
16
use ChamiloSession as Session;
17
18
/**
19
 * Class ExerciseLib
20
 * shows a question and its answers.
21
 *
22
 * @author Olivier Brouckaert <[email protected]>
23
 * @author Hubert Borderiou 2011-10-21
24
 * @author ivantcholakov2009-07-20
25
 * @author Julio Montoya
26
 */
27
class ExerciseLib
28
{
29
    /**
30
     * Shows a question.
31
     *
32
     * @param Exercise $exercise
33
     * @param int      $questionId     $questionId question id
34
     * @param bool     $only_questions if true only show the questions, no exercise title
35
     * @param bool     $origin         i.e = learnpath
36
     * @param string   $current_item   current item from the list of questions
37
     * @param bool     $show_title
38
     * @param bool     $freeze
39
     * @param array    $user_choice
40
     * @param bool     $show_comment
41
     * @param bool     $show_answers
42
     *
43
     * @throws \Exception
44
     *
45
     * @return bool|int
46
     */
47
    public static function showQuestion(
48
        $exercise,
49
        $questionId,
50
        $only_questions = false,
51
        $origin = false,
52
        $current_item = '',
53
        $show_title = true,
54
        $freeze = false,
55
        $user_choice = [],
56
        $show_comment = false,
57
        $show_answers = false,
58
        $show_icon = false
59
    ) {
60
        $course_id = $exercise->course_id;
61
        $exerciseId = $exercise->iId;
62
63
        if (empty($course_id)) {
64
            return '';
65
        }
66
        $course = $exercise->course;
67
68
        // Change false to true in the following line to enable answer hinting
69
        $debug_mark_answer = $show_answers;
70
        // Reads question information
71
        if (!$objQuestionTmp = Question::read($questionId, $course)) {
72
            // Question not found
73
            return false;
74
        }
75
76
        if (EXERCISE_FEEDBACK_TYPE_END != $exercise->getFeedbackType()) {
77
            $show_comment = false;
78
        }
79
80
        $answerType = $objQuestionTmp->selectType();
81
82
        if (MEDIA_QUESTION === $answerType) {
83
            $mediaHtml = $objQuestionTmp->selectDescription();
84
            if (!empty($mediaHtml)) {
85
                echo '<div class="media-content wysiwyg">'. $mediaHtml .'</div>';
86
            }
87
            return 0;
88
        }
89
90
        if (PAGE_BREAK === $answerType) {
91
            $description = $objQuestionTmp->selectDescription();
92
            if (!$only_questions && !empty($description)) {
93
                echo '<div class="page-break-content wysiwyg">'
94
                    . $description .
95
                    '</div>';
96
            }
97
            return 0;
98
        }
99
100
        $s = '';
101
        if (HOT_SPOT != $answerType &&
102
            HOT_SPOT_DELINEATION != $answerType &&
103
             HOT_SPOT_COMBINATION != $answerType &&
104
            ANNOTATION != $answerType
105
        ) {
106
            // Question is not a hotspot
107
            if (!$only_questions) {
108
                $questionDescription = $objQuestionTmp->selectDescription();
109
                if ($show_title) {
110
                    if ($exercise->display_category_name) {
111
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
112
                    }
113
                    $titleToDisplay = $objQuestionTmp->getTitleToDisplay($exercise, $current_item);
114
                    if (READING_COMPREHENSION == $answerType) {
115
                        // In READING_COMPREHENSION, the title of the question
116
                        // contains the question itself, which can only be
117
                        // shown at the end of the given time, so hide for now
118
                        $titleToDisplay = Display::div(
119
                            $current_item.'. '.get_lang('Reading comprehension'),
120
                            ['class' => 'question_title']
121
                        );
122
                    }
123
                    echo $titleToDisplay;
124
                }
125
126
                if (!empty($questionDescription) && READING_COMPREHENSION != $answerType) {
127
                    echo Display::div(
128
                        $questionDescription,
129
                        ['class' => 'question_description wysiwyg']
130
                    );
131
                }
132
            }
133
134
            if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, UPLOAD_ANSWER]) && $freeze) {
135
                return '';
136
            }
137
138
            echo '<div class="question_options type-'.$answerType.'">';
139
            // construction of the Answer object (also gets all answers details)
140
            $objAnswerTmp = new Answer($questionId, $course_id, $exercise);
141
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
142
            $quizQuestionOptions = Question::readQuestionOption($questionId, $course_id);
143
            $selectableOptions = [];
144
145
            for ($i = 1; $i <= $objAnswerTmp->nbrAnswers; $i++) {
146
                $selectableOptions[$objAnswerTmp->iid[$i]] = $objAnswerTmp->answer[$i];
147
            }
148
149
            // For "matching" type here, we need something a little bit special
150
            // because the match between the suggestions and the answers cannot be
151
            // done easily (suggestions and answers are in the same table), so we
152
            // have to go through answers first (elems with "correct" value to 0).
153
            $select_items = [];
154
            //This will contain the number of answers on the left side. We call them
155
            // suggestions here, for the sake of comprehensions, while the ones
156
            // on the right side are called answers
157
            $num_suggestions = 0;
158
            switch ($answerType) {
159
                case MATCHING:
160
                case MATCHING_COMBINATION:
161
                case DRAGGABLE:
162
                case MATCHING_DRAGGABLE:
163
                case MATCHING_DRAGGABLE_COMBINATION:
164
                    if (DRAGGABLE == $answerType) {
165
                        $isVertical = 'v' === $objQuestionTmp->extra;
166
                        $s .= '<p class="small">'
167
                            .get_lang('Sort the following options from the list as you see fit by dragging them to the lower areas. You can put them back in this area to modify your answer.')
168
                            .'</p>
169
                            <div class="w-full ui-widget ui-helper-clearfix">
170
                                <div class="clearfix">
171
                                    <ul class="exercise-draggable-answer '.($isVertical ? 'vertical' : 'list-inline w-full').'"
172
                                        id="question-'.$questionId.'" data-question="'.$questionId.'">
173
                            ';
174
                    } else {
175
                        $s .= '<div id="drag'.$questionId.'_question" class="drag_question">
176
                               <table class="table table-hover table-striped data_table">';
177
                    }
178
179
                    // Iterate through answers.
180
                    $x = 1;
181
                    // Mark letters for each answer.
182
                    $letter = 'A';
183
                    $answer_matching = [];
184
                    $cpt1 = [];
185
                    for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
186
                        $answerCorrect = $objAnswerTmp->isCorrect($answerId);
187
                        $numAnswer = $objAnswerTmp->selectAutoId($answerId);
188
                        if (0 == $answerCorrect) {
189
                            // options (A, B, C, ...) that will be put into the list-box
190
                            // have the "correct" field set to 0 because they are answer
191
                            $cpt1[$x] = $letter;
192
                            $answer_matching[$x] = $objAnswerTmp->selectAnswerByAutoId($numAnswer);
193
                            $x++;
194
                            $letter++;
195
                        }
196
                    }
197
198
                    $i = 1;
199
                    $select_items[0]['id'] = 0;
200
                    $select_items[0]['letter'] = '--';
201
                    $select_items[0]['answer'] = '';
202
                    foreach ($answer_matching as $id => $value) {
203
                        $select_items[$i]['id'] = $value['iid'];
204
                        $select_items[$i]['letter'] = $cpt1[$id];
205
                        $select_items[$i]['answer'] = $value['answer'];
206
                        $i++;
207
                    }
208
209
                    $user_choice_array_position = [];
210
                    if (!empty($user_choice)) {
211
                        foreach ($user_choice as $item) {
212
                            $user_choice_array_position[$item['position']] = $item['answer'];
213
                        }
214
                    }
215
                    $num_suggestions = ($nbrAnswers - $x) + 1;
216
                    break;
217
                case FREE_ANSWER:
218
                    $fck_content = isset($user_choice[0]) && !empty($user_choice[0]['answer']) ? $user_choice[0]['answer'] : null;
219
                    $form = new FormValidator('free_choice_'.$questionId);
220
                    $config = [
221
                        'ToolbarSet' => 'TestFreeAnswer',
222
                    ];
223
                    $form->addHtmlEditor(
224
                        'choice['.$questionId.']',
225
                        null,
226
                        false,
227
                        false,
228
                        $config
229
                    );
230
                    $form->setDefaults(["choice[".$questionId."]" => $fck_content]);
231
                    $s .= $form->returnForm();
232
                    break;
233
                case UPLOAD_ANSWER:
234
                    $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=upload_answer&question_id='.$questionId;
235
                    $multipleForm = new FormValidator(
236
                        'drag_drop',
237
                        'post',
238
                        '#',
239
                        '',
240
                        ['enctype' => 'multipart/form-data', 'id' => 'drag_drop']
241
                    );
242
243
                    $iconDelete = Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_SMALL);
244
                    $multipleForm->addMultipleUpload($url);
245
246
                    $s .= '<script>
247
                        function setRemoveLink(dataContext) {
248
                            var removeLink = $("<a>", {
249
                                html: "&nbsp;'.addslashes($iconDelete).'",
250
                                href: "#",
251
                                click: function(e) {
252
                                  e.preventDefault();
253
                                  dataContext.parent().remove();
254
                                }
255
                            });
256
                            dataContext.append(removeLink);
257
                        }
258
259
                        $(function() {
260
                            $("#input_file_upload").bind("fileuploaddone", function (e, data) {
261
                                $.each(data.result.files, function (index, file) {
262
                                    // El backend ahora devuelve asset_id y url
263
                                    if (file.asset_id) {
264
                                        var input = $("<input>", {
265
                                            type: "hidden",
266
                                            name: "uploadAsset['.$questionId.'][]",
267
                                            value: file.asset_id
268
                                        });
269
                                        $(data.context.children()[index]).parent().append(input);
270
                                        // set the remove link
271
                                        setRemoveLink($(data.context.children()[index]).parent());
272
                                    }
273
                                });
274
                            });
275
                        });
276
                    </script>';
277
                    $sessionKey = 'upload_answer_assets_'.$questionId;
278
                    $assetIds = (array) ChamiloSession::read($sessionKey);
279
280
                    if (!empty($assetIds)) {
281
                        $icon = Display::return_icon('file_txt.gif');
282
                        $default = "";
283
                        $assetRepo = Container::getAssetRepository();
284
                        $basePath = rtrim(api_get_path(WEB_PATH), "/");
285
286
                        foreach ($assetIds as $id) {
287
                            try {
288
                                $asset = $assetRepo->find(\Symfony\Component\Uid\Uuid::fromRfc4122((string)$id));
289
                            } catch (\Throwable $e) {
290
                                $asset = null;
291
                            }
292
                            if (!$asset) { continue; }
293
294
                            $title = Security::remove_XSS($asset->getTitle());
295
                            $urlAsset = $basePath.$assetRepo->getAssetUrl($asset);
296
297
                            $default .= Display::tag(
298
                                "a",
299
                                Display::div(
300
                                    Display::div($icon, ['class' => 'col-sm-4'])
301
                                    . Display::div($title, ['class' => 'col-sm-5 file_name'])
302
                                    . Display::tag("input", "", [
303
                                        "type" => "hidden",
304
                                        "name" => "uploadAsset['.$questionId.'][]",
305
                                        "value" => (string)$id
306
                                    ])
307
                                    . Display::div("", ["class" => "col-sm-3"]),
308
                                    ["class" => "row"]
309
                                ),
310
                                ["target" => "_blank", "class" => "panel-image", "href" => $urlAsset]
311
                            );
312
                        }
313
314
                        $s .= '<script>
315
                            $(function() {
316
                                if ($("#files").length > 0) {
317
                                    $("#files").html("'.addslashes($default).'");
318
                                    var links = $("#files").children();
319
                                    links.each(function(index) {
320
                                        var dataContext = $(links[index]).find(".row");
321
                                        setRemoveLink(dataContext);
322
                                    });
323
                                }
324
                            });
325
                        </script>';
326
                    }
327
328
                    $s .= $multipleForm->returnForm();
329
                    break;
330
                case ORAL_EXPRESSION:
331
                    // Add nanog
332
                    //@todo pass this as a parameter
333
                    global $exercise_stat_info;
334
                    if (!empty($exercise_stat_info)) {
335
                        echo $objQuestionTmp->returnRecorder((int) $exercise_stat_info['exe_id']);
336
                        $generatedFile = self::getOralFileAudio($exercise_stat_info['exe_id'], $questionId);
337
                        if (!empty($generatedFile)) {
338
                            echo $generatedFile;
339
                        }
340
                    }
341
342
                    $form = new FormValidator('free_choice_'.$questionId);
343
                    $config = ['ToolbarSet' => 'TestFreeAnswer'];
344
345
                    $form->addHtml('<div id="'.'hide_description_'.$questionId.'_options" style="display: none;">');
346
                    $form->addHtmlEditor(
347
                        "choice[$questionId]",
348
                        null,
349
                        false,
350
                        false,
351
                        $config
352
                    );
353
                    $form->addHtml('</div>');
354
                    $s .= $form->returnForm();
355
                    break;
356
                case MULTIPLE_ANSWER_DROPDOWN:
357
                case MULTIPLE_ANSWER_DROPDOWN_COMBINATION:
358
                    if ($debug_mark_answer) {
359
                        $s .= '<p><strong>'
360
                            .(
361
                                MULTIPLE_ANSWER_DROPDOWN == $answerType
362
                                    ? '<span class="pull-right">'.get_lang('Score').'</span>'
363
                                    : ''
364
                            )
365
                            .get_lang('Correct answer').'</strong></p>';
366
                    }
367
                    break;
368
            }
369
370
            // Now navigate through the possible answers, using the max number of
371
            // answers for the question as a limiter
372
            $lines_count = 1; // a counter for matching-type answers
373
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
374
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
375
            ) {
376
                $header = Display::tag('th', get_lang('Options'));
377
                foreach ($objQuestionTmp->options as $item) {
378
                    if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
379
                        if (in_array($item, $objQuestionTmp->options)) {
380
                            $header .= Display::tag('th', get_lang($item));
381
                        } else {
382
                            $header .= Display::tag('th', $item);
383
                        }
384
                    } else {
385
                        $header .= Display::tag('th', $item);
386
                    }
387
                }
388
                if ($show_comment) {
389
                    $header .= Display::tag('th', get_lang('Feedback'));
390
                }
391
                $s .= '<table class="table table-hover table-striped">';
392
                $s .= Display::tag(
393
                    'tr',
394
                    $header,
395
                    ['style' => 'text-align:left;']
396
                );
397
            } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
398
                $header = Display::tag('th', get_lang('Options'), ['width' => '50%']);
399
                echo "
400
                <script>
401
                    function RadioValidator(question_id, answer_id)
402
                    {
403
                        var ShowAlert = '';
404
                        var typeRadioB = '';
405
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
406
407
                        for (i = 0; i < AllFormElements.length; i++) {
408
                            if (AllFormElements[i].type == 'radio') {
409
                                var ThisRadio = AllFormElements[i].name;
410
                                var ThisChecked = 'No';
411
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
412
413
                                for (x = 0; x < AllRadioOptions.length; x++) {
414
                                     if (AllRadioOptions[x].checked && ThisChecked == 'No') {
415
                                         ThisChecked = 'Yes';
416
                                         break;
417
                                     }
418
                                }
419
420
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
421
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
422
                                    ShowAlert = ShowAlert + ThisRadio;
423
                                }
424
                            }
425
                        }
426
                        if (ShowAlert != '') {
427
428
                        } else {
429
                            $('.question-validate-btn').removeAttr('disabled');
430
                        }
431
                    }
432
433
                    function handleRadioRow(event, question_id, answer_id) {
434
                        var t = event.target;
435
                        if (t && t.tagName == 'INPUT')
436
                            return;
437
                        while (t && t.tagName != 'TD') {
438
                            t = t.parentElement;
439
                        }
440
                        var r = t.getElementsByTagName('INPUT')[0];
441
                        r.click();
442
                        RadioValidator(question_id, answer_id);
443
                    }
444
445
                    $(function() {
446
                        var ShowAlert = '';
447
                        var typeRadioB = '';
448
                        var question_id = $('input[name=question_id]').val();
449
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
450
451
                        for (i = 0; i < AllFormElements.length; i++) {
452
                            if (AllFormElements[i].type == 'radio') {
453
                                var ThisRadio = AllFormElements[i].name;
454
                                var ThisChecked = 'No';
455
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
456
457
                                for (x = 0; x < AllRadioOptions.length; x++) {
458
                                    if (AllRadioOptions[x].checked && ThisChecked == 'No') {
459
                                        ThisChecked = \"Yes\";
460
                                        break;
461
                                    }
462
                                }
463
464
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
465
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
466
                                    ShowAlert = ShowAlert + ThisRadio;
467
                                }
468
                            }
469
                        }
470
471
                        if (ShowAlert != '') {
472
                             $('.question-validate-btn').attr('disabled', 'disabled');
473
                        } else {
474
                            $('.question-validate-btn').removeAttr('disabled');
475
                        }
476
                    });
477
                </script>";
478
479
                foreach ($objQuestionTmp->optionsTitle as $item) {
480
                    if (in_array($item, $objQuestionTmp->optionsTitle)) {
481
                        $properties = [];
482
                        if ('Answers' === $item) {
483
                            $properties['colspan'] = 2;
484
                            $properties['style'] = 'background-color: #F56B2A; color: #ffffff;';
485
                        } elseif ('DegreeOfCertaintyThatMyAnswerIsCorrect' === $item) {
486
                            $properties['colspan'] = 6;
487
                            $properties['style'] = 'background-color: #330066; color: #ffffff;';
488
                        }
489
                        $header .= Display::tag('th', get_lang($item), $properties);
490
                    } else {
491
                        $header .= Display::tag('th', $item);
492
                    }
493
                }
494
495
                if ($show_comment) {
496
                    $header .= Display::tag('th', get_lang('Feedback'));
497
                }
498
499
                $s .= '<table class="table table-hover table-striped data_table">';
500
                $s .= Display::tag('tr', $header, ['style' => 'text-align:left;']);
501
502
                // ajout de la 2eme ligne d'entête pour true/falss et les pourcentages de certitude
503
                $header1 = Display::tag('th', '&nbsp;');
504
                $cpt1 = 0;
505
                foreach ($objQuestionTmp->options as $item) {
506
                    $colorBorder1 = ($cpt1 == (count($objQuestionTmp->options) - 1))
507
                        ? '' : 'border-right: solid #FFFFFF 1px;';
508
                    if ('True' === $item || 'False' === $item) {
509
                        $header1 .= Display::tag(
510
                            'th',
511
                            get_lang($item),
512
                            ['style' => 'background-color: #F7C9B4; color: black;'.$colorBorder1]
513
                        );
514
                    } else {
515
                        $header1 .= Display::tag(
516
                            'th',
517
                            $item,
518
                            ['style' => 'background-color: #e6e6ff; color: black;padding:5px; '.$colorBorder1]
519
                        );
520
                    }
521
                    $cpt1++;
522
                }
523
                if ($show_comment) {
524
                    $header1 .= Display::tag('th', '&nbsp;');
525
                }
526
527
                $s .= Display::tag('tr', $header1);
528
529
                // add explanation
530
                $header2 = Display::tag('th', '&nbsp;');
531
                $descriptionList = [
532
                    get_lang('I don\'t know the answer and I\'ve picked at random'),
533
                    get_lang('I am very unsure'),
534
                    get_lang('I am unsure'),
535
                    get_lang('I am pretty sure'),
536
                    get_lang('I am almost 100% sure'),
537
                    get_lang('I am totally sure'),
538
                ];
539
                $counter2 = 0;
540
                foreach ($objQuestionTmp->options as $item) {
541
                    if ('True' === $item || 'False' === $item) {
542
                        $header2 .= Display::tag('td',
543
                            '&nbsp;',
544
                            ['style' => 'background-color: #F7E1D7; color: black;border-right: solid #FFFFFF 1px;']);
545
                    } else {
546
                        $color_border2 = ($counter2 == (count($objQuestionTmp->options) - 1)) ?
547
                            '' : 'border-right: solid #FFFFFF 1px;font-size:11px;';
548
                        $header2 .= Display::tag(
549
                            'td',
550
                            nl2br($descriptionList[$counter2]),
551
                            ['style' => 'background-color: #EFEFFC; color: black; width: 110px; text-align:center;
552
                                vertical-align: top; padding:5px; '.$color_border2]);
553
                        $counter2++;
554
                    }
555
                }
556
                if ($show_comment) {
557
                    $header2 .= Display::tag('th', '&nbsp;');
558
                }
559
                $s .= Display::tag('tr', $header2);
560
            }
561
562
            if ($show_comment) {
563
                if (in_array(
564
                    $answerType,
565
                    [
566
                        MULTIPLE_ANSWER,
567
                        MULTIPLE_ANSWER_COMBINATION,
568
                        UNIQUE_ANSWER,
569
                        UNIQUE_ANSWER_IMAGE,
570
                        UNIQUE_ANSWER_NO_OPTION,
571
                        GLOBAL_MULTIPLE_ANSWER,
572
                    ]
573
                )) {
574
                    $header = Display::tag('th', get_lang('Options'));
575
                    if (EXERCISE_FEEDBACK_TYPE_END == $exercise->getFeedbackType()) {
576
                        $header .= Display::tag('th', get_lang('Feedback'));
577
                    }
578
                    $s .= '<table class="table table-hover table-striped">';
579
                    $s .= Display::tag(
580
                        'tr',
581
                        $header,
582
                        ['style' => 'text-align:left;']
583
                    );
584
                }
585
            }
586
587
            $matching_correct_answer = 0;
588
            $userChoiceList = [];
589
            if (!empty($user_choice)) {
590
                foreach ($user_choice as $item) {
591
                    $userChoiceList[] = $item['answer'];
592
                }
593
            }
594
595
            $hidingClass = '';
596
            if (READING_COMPREHENSION == $answerType) {
597
                /** @var ReadingComprehension */
598
                $objQuestionTmp->setExerciseType($exercise->selectType());
599
                $objQuestionTmp->processText($objQuestionTmp->selectDescription());
600
                $hidingClass = 'hide-reading-answers';
601
                $s .= Display::div(
602
                    $objQuestionTmp->selectTitle(),
603
                    ['class' => 'question_title '.$hidingClass]
604
                );
605
            }
606
607
            for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
608
                $answer = $objAnswerTmp->selectAnswer($answerId);
609
                $answerCorrect = $objAnswerTmp->isCorrect($answerId);
610
                $numAnswer = $objAnswerTmp->selectAutoId($answerId);
611
                $comment = $objAnswerTmp->selectComment($answerId);
612
                $attributes = [];
613
614
                switch ($answerType) {
615
                    case UNIQUE_ANSWER:
616
                    case UNIQUE_ANSWER_NO_OPTION:
617
                    case UNIQUE_ANSWER_IMAGE:
618
                    case READING_COMPREHENSION:
619
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
620
                        if (isset($user_choice[0]['answer']) && $user_choice[0]['answer'] == $numAnswer) {
621
                            $attributes = [
622
                                'id' => $input_id,
623
                                'checked' => 1,
624
                                'selected' => 1,
625
                            ];
626
                        } else {
627
                            $attributes = ['id' => $input_id];
628
                        }
629
630
                        if ($debug_mark_answer) {
631
                            if ($answerCorrect) {
632
                                $attributes['checked'] = 1;
633
                                $attributes['selected'] = 1;
634
                            }
635
                        }
636
637
                        if ($show_comment) {
638
                            $s .= '<tr><td>';
639
                        }
640
641
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
642
                            if ($show_comment) {
643
                                if (empty($comment)) {
644
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
645
                                            class="exercise-unique-answer-image text-center">';
646
                                } else {
647
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
648
                                            class="exercise-unique-answer-image col-xs-6 col-sm-12 text-center">';
649
                                }
650
                            } else {
651
                                $s .= '<div id="answer'.$questionId.$numAnswer.'"
652
                                        class="exercise-unique-answer-image col-xs-6 col-md-3 text-center">';
653
                            }
654
                        }
655
656
                        if (UNIQUE_ANSWER_IMAGE != $answerType) {
657
                            $userStatus = STUDENT;
658
                            // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
659
                            // see BT#18242
660
                            if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
661
                                $userStatus = COURSEMANAGERLOWSECURITY;
662
                            }
663
                            $answer = Security::remove_XSS($answer, $userStatus);
664
                        }
665
                        $s .= Display::input(
666
                            'hidden',
667
                            'choice2['.$questionId.']',
668
                            '0'
669
                        );
670
671
                        $answer_input = null;
672
                        $attributes['class'] = 'checkradios';
673
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
674
                            $attributes['class'] = '';
675
                            $attributes['style'] = 'display: none;';
676
                            $answer = '<div class="thumbnail">'.$answer.'</div>';
677
                        }
678
679
                        $answer_input .= '<label class="radio '.$hidingClass.'">';
680
                        $answer_input .= Display::input(
681
                            'radio',
682
                            'choice['.$questionId.']',
683
                            $numAnswer,
684
                            $attributes
685
                        );
686
                        $answer_input .= $answer;
687
                        $answer_input .= '</label>';
688
689
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
690
                            $answer_input .= "</div>";
691
                        }
692
693
                        if ($show_comment) {
694
                            $s .= $answer_input;
695
                            $s .= '</td>';
696
                            $s .= '<td>';
697
                            $s .= $comment;
698
                            $s .= '</td>';
699
                            $s .= '</tr>';
700
                        } else {
701
                            $s .= $answer_input;
702
                        }
703
                        break;
704
                    case MULTIPLE_ANSWER:
705
                    case MULTIPLE_ANSWER_TRUE_FALSE:
706
                    case GLOBAL_MULTIPLE_ANSWER:
707
                    case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
708
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
709
                        $userStatus = STUDENT;
710
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
711
                        // see BT#18242
712
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
713
                            $userStatus = COURSEMANAGERLOWSECURITY;
714
                        }
715
                        $answer = Security::remove_XSS($answer, $userStatus);
716
717
                        if (in_array($numAnswer, $userChoiceList)) {
718
                            $attributes = [
719
                                'id' => $input_id,
720
                                'checked' => 1,
721
                                'selected' => 1,
722
                            ];
723
                        } else {
724
                            $attributes = ['id' => $input_id];
725
                        }
726
727
                        if ($debug_mark_answer) {
728
                            if ($answerCorrect) {
729
                                $attributes['checked'] = 1;
730
                                $attributes['selected'] = 1;
731
                            }
732
                        }
733
734
                        if (MULTIPLE_ANSWER == $answerType || GLOBAL_MULTIPLE_ANSWER == $answerType) {
735
                            $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
736
                            $attributes['class'] = 'checkradios';
737
                            $answer_input = '<label class="checkbox">';
738
                            $answer_input .= Display::input(
739
                                'checkbox',
740
                                'choice['.$questionId.']['.$numAnswer.']',
741
                                $numAnswer,
742
                                $attributes
743
                            );
744
                            $answer_input .= $answer;
745
                            $answer_input .= '</label>';
746
747
                            if ($show_comment) {
748
                                $s .= '<tr><td>';
749
                                $s .= $answer_input;
750
                                $s .= '</td>';
751
                                $s .= '<td>';
752
                                $s .= $comment;
753
                                $s .= '</td>';
754
                                $s .= '</tr>';
755
                            } else {
756
                                $s .= $answer_input;
757
                            }
758
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
759
                            $myChoice = [];
760
                            if (!empty($userChoiceList)) {
761
                                foreach ($userChoiceList as $item) {
762
                                    $item = explode(':', $item);
763
                                    if (!empty($item)) {
764
                                        $myChoice[$item[0]] = isset($item[1]) ? $item[1] : '';
765
                                    }
766
                                }
767
                            }
768
769
                            $s .= '<tr>';
770
                            $s .= Display::tag('td', $answer);
771
772
                            if (!empty($quizQuestionOptions)) {
773
                                $j = 1;
774
                                foreach ($quizQuestionOptions as $id => $item) {
775
                                    if (isset($myChoice[$numAnswer]) && $item['iid'] == $myChoice[$numAnswer]) {
776
                                        $attributes = [
777
                                            'checked' => 1,
778
                                            'selected' => 1,
779
                                        ];
780
                                    } else {
781
                                        $attributes = [];
782
                                    }
783
784
                                    if ($debug_mark_answer) {
785
                                        if ($j == $answerCorrect) {
786
                                            $attributes['checked'] = 1;
787
                                            $attributes['selected'] = 1;
788
                                        }
789
                                    }
790
                                    $s .= Display::tag(
791
                                        'td',
792
                                        Display::input(
793
                                            'radio',
794
                                            'choice['.$questionId.']['.$numAnswer.']',
795
                                            $item['iid'],
796
                                            $attributes
797
                                        ),
798
                                        ['style' => '']
799
                                    );
800
                                    $j++;
801
                                }
802
                            }
803
804
                            if ($show_comment) {
805
                                $s .= '<td>';
806
                                $s .= $comment;
807
                                $s .= '</td>';
808
                            }
809
                            $s .= '</tr>';
810
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
811
                            $myChoice = [];
812
                            if (!empty($userChoiceList)) {
813
                                foreach ($userChoiceList as $item) {
814
                                    $item = explode(':', $item);
815
                                    $myChoice[$item[0]] = $item[1];
816
                                }
817
                            }
818
                            $myChoiceDegreeCertainty = [];
819
                            if (!empty($userChoiceList)) {
820
                                foreach ($userChoiceList as $item) {
821
                                    $item = explode(':', $item);
822
                                    $myChoiceDegreeCertainty[$item[0]] = $item[2];
823
                                }
824
                            }
825
                            $s .= '<tr>';
826
                            $s .= Display::tag('td', $answer);
827
828
                            if (!empty($quizQuestionOptions)) {
829
                                $j = 1;
830
                                foreach ($quizQuestionOptions as $id => $item) {
831
                                    if (isset($myChoice[$numAnswer]) && $id == $myChoice[$numAnswer]) {
832
                                        $attributes = ['checked' => 1, 'selected' => 1];
833
                                    } else {
834
                                        $attributes = [];
835
                                    }
836
                                    $attributes['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
837
838
                                    // radio button selection
839
                                    if (isset($myChoiceDegreeCertainty[$numAnswer]) &&
840
                                        $id == $myChoiceDegreeCertainty[$numAnswer]
841
                                    ) {
842
                                        $attributes1 = ['checked' => 1, 'selected' => 1];
843
                                    } else {
844
                                        $attributes1 = [];
845
                                    }
846
847
                                    $attributes1['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
848
849
                                    if ($debug_mark_answer) {
850
                                        if ($j == $answerCorrect) {
851
                                            $attributes['checked'] = 1;
852
                                            $attributes['selected'] = 1;
853
                                        }
854
                                    }
855
856
                                    if ('True' == $item['name'] || 'False' == $item['name']) {
857
                                        $s .= Display::tag('td',
858
                                            Display::input('radio',
859
                                                'choice['.$questionId.']['.$numAnswer.']',
860
                                                $id,
861
                                                $attributes
862
                                            ),
863
                                            ['style' => 'text-align:center; background-color:#F7E1D7;',
864
                                                'onclick' => 'handleRadioRow(event, '.
865
                                                    $questionId.', '.
866
                                                    $numAnswer.')',
867
                                            ]
868
                                        );
869
                                    } else {
870
                                        $s .= Display::tag('td',
871
                                            Display::input('radio',
872
                                                'choiceDegreeCertainty['.$questionId.']['.$numAnswer.']',
873
                                                $id,
874
                                                $attributes1
875
                                            ),
876
                                            ['style' => 'text-align:center; background-color:#EFEFFC;',
877
                                                'onclick' => 'handleRadioRow(event, '.
878
                                                    $questionId.', '.
879
                                                    $numAnswer.')',
880
                                            ]
881
                                        );
882
                                    }
883
                                    $j++;
884
                                }
885
                            }
886
887
                            if ($show_comment) {
888
                                $s .= '<td>';
889
                                $s .= $comment;
890
                                $s .= '</td>';
891
                            }
892
                            $s .= '</tr>';
893
                        }
894
                        break;
895
                    case MULTIPLE_ANSWER_COMBINATION:
896
                        // multiple answers
897
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
898
899
                        if (in_array($numAnswer, $userChoiceList)) {
900
                            $attributes = [
901
                                'id' => $input_id,
902
                                'checked' => 1,
903
                                'selected' => 1,
904
                            ];
905
                        } else {
906
                            $attributes = ['id' => $input_id];
907
                        }
908
909
                        if ($debug_mark_answer) {
910
                            if ($answerCorrect) {
911
                                $attributes['checked'] = 1;
912
                                $attributes['selected'] = 1;
913
                            }
914
                        }
915
916
                        $userStatus = STUDENT;
917
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
918
                        // see BT#18242
919
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
920
                            $userStatus = COURSEMANAGERLOWSECURITY;
921
                        }
922
                        $answer = Security::remove_XSS($answer, $userStatus);
923
                        $answer_input = '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
924
                        $answer_input .= '<label class="checkbox">';
925
                        $answer_input .= Display::input(
926
                            'checkbox',
927
                            'choice['.$questionId.']['.$numAnswer.']',
928
                            1,
929
                            $attributes
930
                        );
931
                        $answer_input .= $answer;
932
                        $answer_input .= '</label>';
933
934
                        if ($show_comment) {
935
                            $s .= '<tr>';
936
                            $s .= '<td>';
937
                            $s .= $answer_input;
938
                            $s .= '</td>';
939
                            $s .= '<td>';
940
                            $s .= $comment;
941
                            $s .= '</td>';
942
                            $s .= '</tr>';
943
                        } else {
944
                            $s .= $answer_input;
945
                        }
946
                        break;
947
                    case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
948
                        $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
949
                        $myChoice = [];
950
                        if (!empty($userChoiceList)) {
951
                            foreach ($userChoiceList as $item) {
952
                                $item = explode(':', $item);
953
                                if (isset($item[1]) && isset($item[0])) {
954
                                    $myChoice[$item[0]] = $item[1];
955
                                }
956
                            }
957
                        }
958
                        $userStatus = STUDENT;
959
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
960
                        // see BT#18242
961
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
962
                            $userStatus = COURSEMANAGERLOWSECURITY;
963
                        }
964
                        $answer = Security::remove_XSS($answer, $userStatus);
965
                        $s .= '<tr>';
966
                        $s .= Display::tag('td', $answer);
967
                        foreach ($objQuestionTmp->options as $key => $item) {
968
                            if (isset($myChoice[$numAnswer]) && $key == $myChoice[$numAnswer]) {
969
                                $attributes = [
970
                                    'checked' => 1,
971
                                    'selected' => 1,
972
                                ];
973
                            } else {
974
                                $attributes = [];
975
                            }
976
977
                            if ($debug_mark_answer) {
978
                                if ($key == $answerCorrect) {
979
                                    $attributes['checked'] = 1;
980
                                    $attributes['selected'] = 1;
981
                                }
982
                            }
983
                            $s .= Display::tag(
984
                                'td',
985
                                Display::input(
986
                                    'radio',
987
                                    'choice['.$questionId.']['.$numAnswer.']',
988
                                    $key,
989
                                    $attributes
990
                                )
991
                            );
992
                        }
993
994
                        if ($show_comment) {
995
                            $s .= '<td>';
996
                            $s .= $comment;
997
                            $s .= '</td>';
998
                        }
999
                        $s .= '</tr>';
1000
                        break;
1001
                    case FILL_IN_BLANKS:
1002
                    case FILL_IN_BLANKS_COMBINATION:
1003
                        // display the question, with field empty, for student to fill it,
1004
                        // or filled to display the answer in the Question preview of the exercise/admin.php page
1005
                        $displayForStudent = true;
1006
                        $listAnswerInfo = FillBlanks::getAnswerInfo($answer);
1007
                        // Correct answers
1008
                        $correctAnswerList = $listAnswerInfo['words'];
1009
                        // Student's answer
1010
                        $studentAnswerList = [];
1011
                        if (isset($user_choice[0]['answer'])) {
1012
                            $arrayStudentAnswer = FillBlanks::getAnswerInfo(
1013
                                $user_choice[0]['answer'],
1014
                                true
1015
                            );
1016
                            $studentAnswerList = $arrayStudentAnswer['student_answer'];
1017
                        }
1018
1019
                        // If the question must be shown with the answer (in page exercise/admin.php)
1020
                        // for teacher preview set the student-answer to the correct answer
1021
                        if ($debug_mark_answer) {
1022
                            $studentAnswerList = $correctAnswerList;
1023
                            $displayForStudent = false;
1024
                        }
1025
1026
                        if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
1027
                            $answer = '';
1028
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
1029
                                // display the common word
1030
                                $answer .= $listAnswerInfo['common_words'][$i];
1031
                                // display the blank word
1032
                                $correctItem = $listAnswerInfo['words'][$i];
1033
                                if (isset($studentAnswerList[$i])) {
1034
                                    // If student already started this test and answered this question,
1035
                                    // fill the blank with his previous answers
1036
                                    // may be "" if student viewed the question, but did not fill the blanks
1037
                                    $correctItem = $studentAnswerList[$i];
1038
                                }
1039
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
1040
                                $answer .= FillBlanks::getFillTheBlankHtml(
1041
                                    $current_item,
1042
                                    $questionId,
1043
                                    $correctItem,
1044
                                    $attributes,
1045
                                    $answer,
1046
                                    $listAnswerInfo,
1047
                                    $displayForStudent,
1048
                                    $i
1049
                                );
1050
                            }
1051
                            // display the last common word
1052
                            $answer .= $listAnswerInfo['common_words'][$i];
1053
                        } else {
1054
                            // display empty [input] with the right width for student to fill it
1055
                            $answer = '';
1056
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
1057
                                // display the common words
1058
                                $answer .= $listAnswerInfo['common_words'][$i];
1059
                                // display the blank word
1060
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
1061
                                $answer .= FillBlanks::getFillTheBlankHtml(
1062
                                    $current_item,
1063
                                    $questionId,
1064
                                    '',
1065
                                    $attributes,
1066
                                    $answer,
1067
                                    $listAnswerInfo,
1068
                                    $displayForStudent,
1069
                                    $i
1070
                                );
1071
                            }
1072
                            // display the last common word
1073
                            $answer .= $listAnswerInfo['common_words'][$i];
1074
                        }
1075
                        $s .= $answer;
1076
                        break;
1077
                    case CALCULATED_ANSWER:
1078
                        /*
1079
                         * In the CALCULATED_ANSWER test
1080
                         * you mustn't have [ and ] in the textarea
1081
                         * you mustn't have @@ in the textarea
1082
                         * the text to find mustn't be empty or contains only spaces
1083
                         * the text to find mustn't contains HTML tags
1084
                         * the text to find mustn't contains char "
1085
                         */
1086
                        if (null !== $origin) {
1087
                            global $exe_id;
1088
                            $exe_id = (int) $exe_id;
1089
                            $trackAttempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1090
                            $sql = "SELECT answer FROM $trackAttempts
1091
                                    WHERE exe_id = $exe_id AND question_id= $questionId";
1092
                            $rsLastAttempt = Database::query($sql);
1093
                            $rowLastAttempt = Database::fetch_array($rsLastAttempt);
1094
1095
                            $answer = null;
1096
                            if (isset($rowLastAttempt['answer'])) {
1097
                                $answer = $rowLastAttempt['answer'];
1098
                            }
1099
1100
                            if (empty($answer)) {
1101
                                $_SESSION['calculatedAnswerId'][$questionId] = mt_rand(
1102
                                    1,
1103
                                    $nbrAnswers
1104
                                );
1105
                                $answer = $objAnswerTmp->selectAnswer(
1106
                                    $_SESSION['calculatedAnswerId'][$questionId]
1107
                                );
1108
                            }
1109
                        }
1110
1111
                        [$answer] = explode('@@', $answer);
1112
                        // $correctAnswerList array of array with correct anwsers 0=> [0=>[\p] 1=>[plop]]
1113
                        api_preg_match_all(
1114
                            '/\[[^]]+\]/',
1115
                            $answer,
1116
                            $correctAnswerList
1117
                        );
1118
1119
                        // get student answer to display it if student go back
1120
                        // to previous calculated answer question in a test
1121
                        if (isset($user_choice[0]['answer'])) {
1122
                            api_preg_match_all(
1123
                                '/\[[^]]+\]/',
1124
                                $answer,
1125
                                $studentAnswerList
1126
                            );
1127
                            $studentAnswerListToClean = $studentAnswerList[0];
1128
                            $studentAnswerList = [];
1129
1130
                            $maxStudents = count($studentAnswerListToClean);
1131
                            for ($i = 0; $i < $maxStudents; $i++) {
1132
                                $answerCorrected = $studentAnswerListToClean[$i];
1133
                                $answerCorrected = api_preg_replace(
1134
                                    '| / <font color="green"><b>.*$|',
1135
                                    '',
1136
                                    $answerCorrected
1137
                                );
1138
                                $answerCorrected = api_preg_replace(
1139
                                    '/^\[/',
1140
                                    '',
1141
                                    $answerCorrected
1142
                                );
1143
                                $answerCorrected = api_preg_replace(
1144
                                    '|^<font color="red"><s>|',
1145
                                    '',
1146
                                    $answerCorrected
1147
                                );
1148
                                $answerCorrected = api_preg_replace(
1149
                                    '|</s></font>$|',
1150
                                    '',
1151
                                    $answerCorrected
1152
                                );
1153
                                $answerCorrected = '['.$answerCorrected.']';
1154
                                $studentAnswerList[] = $answerCorrected;
1155
                            }
1156
                        }
1157
1158
                        // If display preview of answer in test view for exemple,
1159
                        // set the student answer to the correct answers
1160
                        if ($debug_mark_answer) {
1161
                            // contain the rights answers surronded with brackets
1162
                            $studentAnswerList = $correctAnswerList[0];
1163
                        }
1164
1165
                        /*
1166
                        Split the response by bracket
1167
                        tabComments is an array with text surrounding the text to find
1168
                        we add a space before and after the answerQuestion to be sure to
1169
                        have a block of text before and after [xxx] patterns
1170
                        so we have n text to find ([xxx]) and n+1 block of texts before,
1171
                        between and after the text to find
1172
                        */
1173
                        $tabComments = api_preg_split(
1174
                            '/\[[^]]+\]/',
1175
                            ' '.$answer.' '
1176
                        );
1177
                        if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
1178
                            $answer = '';
1179
                            $i = 0;
1180
                            foreach ($studentAnswerList as $studentItem) {
1181
                                // Remove surronding brackets
1182
                                $studentResponse = api_substr(
1183
                                    $studentItem,
1184
                                    1,
1185
                                    api_strlen($studentItem) - 2
1186
                                );
1187
                                $size = strlen($studentItem);
1188
                                $attributes['class'] = self::detectInputAppropriateClass($size);
1189
                                $answer .= $tabComments[$i].
1190
                                    Display::input(
1191
                                        'text',
1192
                                        "choice[$questionId][]",
1193
                                        $studentResponse,
1194
                                        $attributes
1195
                                    );
1196
                                $i++;
1197
                            }
1198
                            $answer .= $tabComments[$i];
1199
                        } else {
1200
                            // display exercise with empty input fields
1201
                            // every [xxx] are replaced with an empty input field
1202
                            foreach ($correctAnswerList[0] as $item) {
1203
                                $size = strlen($item);
1204
                                $attributes['class'] = self::detectInputAppropriateClass($size);
1205
                                if (EXERCISE_FEEDBACK_TYPE_POPUP == $exercise->getFeedbackType()) {
1206
                                    $attributes['id'] = "question_$questionId";
1207
                                    $attributes['class'] .= ' checkCalculatedQuestionOnEnter ';
1208
                                }
1209
1210
                                $answer = str_replace(
1211
                                    $item,
1212
                                    Display::input(
1213
                                        'text',
1214
                                        "choice[$questionId][]",
1215
                                        '',
1216
                                        $attributes
1217
                                    ),
1218
                                    $answer
1219
                                );
1220
                            }
1221
                        }
1222
                        if (null !== $origin) {
1223
                            $s = $answer;
1224
                            break;
1225
                        } else {
1226
                            $s .= $answer;
1227
                        }
1228
                        break;
1229
                    case MATCHING:
1230
                    case MATCHING_COMBINATION:
1231
                        // matching type, showing suggestions and answers
1232
                        // TODO: replace $answerId by $numAnswer
1233
                        if (0 != $answerCorrect) {
1234
                            // only show elements to be answered (not the contents of
1235
                            // the select boxes, who are correct = 0)
1236
                            $s .= '<tr><td width="45%" valign="top">';
1237
                            $parsed_answer = $answer;
1238
                            // Left part questions
1239
                            $s .= '<p class="indent">'.$lines_count.'.&nbsp;'.$parsed_answer.'</p></td>';
1240
                            // Middle part (matches selects)
1241
                            // Id of select is # question + # of option
1242
                            $s .= '<td width="10%" valign="top" align="center">
1243
                                <div class="select-matching">
1244
                                <select
1245
                                    class="form-control"
1246
                                    id="choice_id_'.$current_item.'_'.$lines_count.'"
1247
                                    name="choice['.$questionId.']['.$numAnswer.']">';
1248
1249
                            // fills the list-box
1250
                            foreach ($select_items as $key => $val) {
1251
                                // set $debug_mark_answer to true at function start to
1252
                                // show the correct answer with a suffix '-x'
1253
                                $selected = '';
1254
                                if ($debug_mark_answer) {
1255
                                    if ($val['id'] == $answerCorrect) {
1256
                                        $selected = 'selected="selected"';
1257
                                    }
1258
                                }
1259
                                //$user_choice_array_position
1260
                                if (isset($user_choice_array_position[$numAnswer]) &&
1261
                                    $val['id'] == $user_choice_array_position[$numAnswer]
1262
                                ) {
1263
                                    $selected = 'selected="selected"';
1264
                                }
1265
                                $s .= '<option value="'.$val['id'].'" '.$selected.'>'.$val['letter'].'</option>';
1266
                            }
1267
1268
                            $s .= '</select></div></td><td width="5%" class="separate">&nbsp;</td>';
1269
                            $s .= '<td width="40%" valign="top" >';
1270
                            if (isset($select_items[$lines_count])) {
1271
                                $s .= '<div class="text-right">
1272
                                        <p class="indent">'.
1273
                                    $select_items[$lines_count]['letter'].'.&nbsp; '.
1274
                                    $select_items[$lines_count]['answer'].'
1275
                                        </p>
1276
                                        </div>';
1277
                            } else {
1278
                                $s .= '&nbsp;';
1279
                            }
1280
                            $s .= '</td>';
1281
                            $s .= '</tr>';
1282
                            $lines_count++;
1283
                            // If the left side of the "matching" has been completely
1284
                            // shown but the right side still has values to show...
1285
                            if (($lines_count - 1) == $num_suggestions) {
1286
                                // if it remains answers to shown at the right side
1287
                                while (isset($select_items[$lines_count])) {
1288
                                    $s .= '<tr>
1289
                                      <td colspan="2"></td>
1290
                                      <td valign="top">';
1291
                                    $s .= '<b>'.$select_items[$lines_count]['letter'].'.</b> '.
1292
                                        $select_items[$lines_count]['answer'];
1293
                                    $s .= "</td>
1294
                                </tr>";
1295
                                    $lines_count++;
1296
                                }
1297
                            }
1298
                            $matching_correct_answer++;
1299
                        }
1300
                        break;
1301
                    case DRAGGABLE:
1302
                        if ($answerCorrect) {
1303
                            $windowId = $questionId.'_'.$lines_count;
1304
                            $s .= '<li class="touch-items" id="'.$windowId.'">';
1305
                            $s .= Display::div(
1306
                                $answer,
1307
                                [
1308
                                    'id' => "window_$windowId",
1309
                                    'class' => "window{$questionId}_question_draggable exercise-draggable-answer-option",
1310
                                ]
1311
                            );
1312
1313
                            $draggableSelectOptions = [];
1314
                            $selectedValue = 0;
1315
                            $selectedIndex = 0;
1316
                            if ($user_choice) {
1317
                                foreach ($user_choice as $userChoiceKey => $chosen) {
1318
                                    $userChoiceKey++;
1319
                                    if ($lines_count != $userChoiceKey) {
1320
                                        continue;
1321
                                    }
1322
                                    /*if ($answerCorrect != $chosen['answer']) {
1323
                                        continue;
1324
                                    }*/
1325
                                    $selectedValue = $chosen['answer'];
1326
                                }
1327
                            }
1328
                            foreach ($select_items as $key => $select_item) {
1329
                                $draggableSelectOptions[$select_item['id']] = $select_item['letter'];
1330
                            }
1331
1332
                            foreach ($draggableSelectOptions as $value => $text) {
1333
                                if ($value == $selectedValue) {
1334
                                    break;
1335
                                }
1336
                                $selectedIndex++;
1337
                            }
1338
1339
                            $s .= Display::select(
1340
                                "choice[$questionId][$numAnswer]",
1341
                                $draggableSelectOptions,
1342
                                $selectedValue,
1343
                                [
1344
                                    'id' => "window_{$windowId}_select",
1345
                                    'class' => 'select_option hidden',
1346
                                ],
1347
                                false
1348
                            );
1349
1350
                            if ($selectedValue && $selectedIndex) {
1351
                                $s .= "
1352
                                    <script>
1353
                                        $(function() {
1354
                                            DraggableAnswer.deleteItem(
1355
                                                $('#{$questionId}_$lines_count'),
1356
                                                $('#drop_{$questionId}_{$selectedIndex}')
1357
                                            );
1358
                                        });
1359
                                    </script>
1360
                                ";
1361
                            }
1362
1363
                            if (isset($select_items[$lines_count])) {
1364
                                $s .= Display::div(
1365
                                    Display::tag(
1366
                                        'b',
1367
                                        $select_items[$lines_count]['letter']
1368
                                    ).$select_items[$lines_count]['answer'],
1369
                                    [
1370
                                        'id' => "window_{$windowId}_answer",
1371
                                        'class' => 'hidden',
1372
                                    ]
1373
                                );
1374
                            } else {
1375
                                $s .= '&nbsp;';
1376
                            }
1377
1378
                            $lines_count++;
1379
                            if (($lines_count - 1) == $num_suggestions) {
1380
                                while (isset($select_items[$lines_count])) {
1381
                                    $s .= Display::tag('b', $select_items[$lines_count]['letter']);
1382
                                    $s .= $select_items[$lines_count]['answer'];
1383
                                    $lines_count++;
1384
                                }
1385
                            }
1386
1387
                            $matching_correct_answer++;
1388
                            $s .= '</li>';
1389
                        }
1390
                        break;
1391
                    case MATCHING_DRAGGABLE:
1392
                    case MATCHING_DRAGGABLE_COMBINATION:
1393
                        if (1 == $answerId) {
1394
                            echo $objAnswerTmp->getJs();
1395
                        }
1396
                        if (0 != $answerCorrect) {
1397
                            $windowId = "{$questionId}_{$lines_count}";
1398
                            $s .= <<<HTML
1399
                            <tr>
1400
                                <td width="45%">
1401
                                    <div id="window_{$windowId}"
1402
                                        class="window window_left_question window{$questionId}_question">
1403
                                        <strong>$lines_count.</strong>
1404
                                        $answer
1405
                                    </div>
1406
                                </td>
1407
                                <td width="10%">
1408
HTML;
1409
1410
                            $draggableSelectOptions = [];
1411
                            $selectedValue = 0;
1412
                            $selectedIndex = 0;
1413
1414
                            if ($user_choice) {
1415
                                foreach ($user_choice as $chosen) {
1416
                                    if ($numAnswer == $chosen['position']) {
1417
                                        $selectedValue = $chosen['answer'];
1418
                                        break;
1419
                                    }
1420
                                }
1421
                            }
1422
1423
                            foreach ($select_items as $key => $selectItem) {
1424
                                $draggableSelectOptions[$selectItem['id']] = $selectItem['letter'];
1425
                            }
1426
1427
                            foreach ($draggableSelectOptions as $value => $text) {
1428
                                if ($value == $selectedValue) {
1429
                                    break;
1430
                                }
1431
                                $selectedIndex++;
1432
                            }
1433
1434
                            $s .= Display::select(
1435
                                "choice[$questionId][$numAnswer]",
1436
                                $draggableSelectOptions,
1437
                                $selectedValue,
1438
                                [
1439
                                    'id' => "window_{$windowId}_select",
1440
                                    'class' => 'hidden',
1441
                                ],
1442
                                false
1443
                            );
1444
1445
                            if (!empty($answerCorrect) && !empty($selectedValue)) {
1446
                                // Show connect if is not freeze (question preview)
1447
                                if (!$freeze) {
1448
                                    $s .= "
1449
                                        <script>
1450
                                            $(function() {
1451
                                                MatchingDraggable.instances['$questionId'].connect({
1452
                                                    source: 'window_$windowId',
1453
                                                    target: 'window_{$questionId}_{$selectedIndex}_answer',
1454
                                                    endpoint: ['Dot', {radius: 12}],
1455
                                                    anchors: ['RightMiddle', 'LeftMiddle'],
1456
                                                    paintStyle: {stroke: '#8A8888', strokeWidth: 8},
1457
                                                    connector: [
1458
                                                        MatchingDraggable.connectorType,
1459
                                                        {curvines: MatchingDraggable.curviness}
1460
                                                    ]
1461
                                                });
1462
                                            });
1463
                                        </script>
1464
                                    ";
1465
                                }
1466
                            }
1467
1468
                            $s .= '</td><td width="45%">';
1469
                            if (isset($select_items[$lines_count])) {
1470
                                $s .= <<<HTML
1471
                                <div id="window_{$windowId}_answer" class="window window_right_question">
1472
                                    <strong>{$select_items[$lines_count]['letter']}.</strong>
1473
                                    {$select_items[$lines_count]['answer']}
1474
                                </div>
1475
HTML;
1476
                            } else {
1477
                                $s .= '&nbsp;';
1478
                            }
1479
1480
                            $s .= '</td></tr>';
1481
                            $lines_count++;
1482
                            if (($lines_count - 1) == $num_suggestions) {
1483
                                while (isset($select_items[$lines_count])) {
1484
                                    $s .= <<<HTML
1485
                                    <tr>
1486
                                        <td colspan="2"></td>
1487
                                        <td>
1488
                                            <strong>{$select_items[$lines_count]['letter']}</strong>
1489
                                            {$select_items[$lines_count]['answer']}
1490
                                        </td>
1491
                                    </tr>
1492
HTML;
1493
                                    $lines_count++;
1494
                                }
1495
                            }
1496
                            $matching_correct_answer++;
1497
                        }
1498
                        break;
1499
                    case MULTIPLE_ANSWER_DROPDOWN:
1500
                    case MULTIPLE_ANSWER_DROPDOWN_COMBINATION:
1501
                        if ($debug_mark_answer && $answerCorrect) {
1502
                            $s .= '<p>'
1503
                                .(
1504
                                    MULTIPLE_ANSWER_DROPDOWN == $answerType
1505
                                        ? '<span class="pull-right">'.$objAnswerTmp->weighting[$answerId].'</span>'
1506
                                        : ''
1507
                                )
1508
                                .Display::returnFontAwesomeIcon('check-square-o', '', true);
1509
                            $s .= Security::remove_XSS($objAnswerTmp->answer[$answerId]).'</p>';
1510
                        }
1511
                        break;
1512
                }
1513
            }
1514
1515
            if (in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION]) && !$debug_mark_answer) {
1516
                $userChoiceList = array_unique($userChoiceList);
1517
                $input_id = "choice-$questionId";
1518
                $clear_id = "clear-$questionId";
1519
1520
                $s .= Display::input('hidden', "choice2[$questionId]", '0')
1521
                    .'<div class="mb-4">'
1522
                    .'<div class="flex items-center justify-between mb-2">'
1523
                    .'<label for="'.$input_id.'" class="text-sm font-medium text-gray-90">'
1524
                    .get_lang('Please select an option')
1525
                    .'</label>'
1526
                    .'<button type="button" id="'.$clear_id.'" '
1527
                    .'class="inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium '
1528
                    .'bg-primary text-white hover:opacity-90 border border-primary">'
1529
                    .'<span class="fa fa-times" aria-hidden="true"></span>'
1530
                    .'<span>'.get_lang('Clear').'</span>'
1531
                    .'</button>'
1532
                    .'</div>'
1533
                    .Display::select(
1534
                        "choice[$questionId][]",
1535
                        $selectableOptions,
1536
                        $userChoiceList,
1537
                        [
1538
                            'id'       => $input_id,
1539
                            'multiple' => 'multiple',
1540
                            'class'    => 'w-full', // full width before Select2 mounts
1541
                        ],
1542
                        false
1543
                    )
1544
                    .'</div>'
1545
                    .'<script>
1546
            $(function () {
1547
                var $el = $("#'.$input_id.'");
1548
                if (!$.fn.select2) return;
1549
1550
                $el.select2({
1551
                    width: "100%",
1552
                    placeholder: { id: "-2", text: "'.get_lang('None').'" },
1553
                    allowClear: true,
1554
                    selectOnClose: false,
1555
                    containerCssClass: "select2-tw",
1556
                    selectionCssClass: "select2-tw",
1557
                    dropdownCssClass: "select2-tw-dd"
1558
                });
1559
1560
                $("#'.$clear_id.'").on("click", function(e){
1561
                    e.preventDefault();
1562
                    $el.val(null).trigger("change");
1563
                });
1564
            });
1565
        </script>';
1566
            }
1567
1568
            if ($show_comment) {
1569
                $s .= '</table>';
1570
            } elseif (in_array(
1571
                $answerType,
1572
                [
1573
                    MATCHING,
1574
                    MATCHING_COMBINATION,
1575
                    MATCHING_DRAGGABLE,
1576
                    MATCHING_DRAGGABLE_COMBINATION,
1577
                    UNIQUE_ANSWER_NO_OPTION,
1578
                    MULTIPLE_ANSWER_TRUE_FALSE,
1579
                    MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE,
1580
                    MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY,
1581
                ]
1582
            )) {
1583
                $s .= '</table>';
1584
            }
1585
1586
            if (DRAGGABLE == $answerType) {
1587
                $isVertical = 'v' == $objQuestionTmp->extra;
1588
                $s .= "</ul></div>";
1589
                $counterAnswer = 1;
1590
                $s .= '<div class="question-answer__items question-answer__items--'.($isVertical ? 'vertical' : 'horizontal').'">';
1591
                for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
1592
                    $answerCorrect = $objAnswerTmp->isCorrect($answerId);
1593
                    $windowId = $questionId.'_'.$counterAnswer;
1594
                    if ($answerCorrect) {
1595
                        $s .= '<div class="droppable-item '.($isVertical ? 'w-full' : '').' flex items-center justify-between p-4 mb-4 bg-gray-200 rounded-md">';
1596
                        $s .= '<span class="number text-lg font-bold">'.$counterAnswer.'</span>';
1597
                        $s .= '<div id="drop_'.$windowId.'" class="droppable border-2 border-dashed border-gray-400 p-4 bg-white rounded-md"></div>';
1598
                        $s .= '</div>';
1599
                        $counterAnswer++;
1600
                    }
1601
                }
1602
1603
                $s .= '</div>';
1604
//                $s .= '</div>';
1605
            }
1606
1607
            if (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
1608
                $s .= '</div>'; //drag_question
1609
            }
1610
1611
            $s .= '</div>'; //question_options row
1612
1613
            // destruction of the Answer object
1614
            unset($objAnswerTmp);
1615
            // destruction of the Question object
1616
            unset($objQuestionTmp);
1617
            if ('export' == $origin) {
1618
                return $s;
1619
            }
1620
            echo $s;
1621
        } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_DELINEATION, HOT_SPOT_COMBINATION])) {
1622
            global $exe_id;
1623
            $questionDescription = $objQuestionTmp->selectDescription();
1624
            // Get the answers, make a list
1625
            $objAnswerTmp = new Answer($questionId, $course_id);
1626
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
1627
1628
            // get answers of hotpost
1629
            $answers_hotspot = [];
1630
            for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
1631
                $answers = $objAnswerTmp->selectAnswerByAutoId(
1632
                    $objAnswerTmp->selectAutoId($answerId)
1633
                );
1634
                $answers_hotspot[$answers['iid']] = $objAnswerTmp->selectAnswer(
1635
                    $answerId
1636
                );
1637
            }
1638
1639
            $answerList = '';
1640
            $hotspotColor = 0;
1641
            if (HOT_SPOT_DELINEATION != $answerType) {
1642
                $answerList = '
1643
        <div class="p-4 rounded-md border border-gray-25">
1644
            <h5 class="font-bold text-lg mb-2 text-primary">'.get_lang('Image zones').'</h5>
1645
            <ol class="list-decimal ml-6 space-y-2 text-primary">
1646
        ';
1647
1648
                if (!empty($answers_hotspot)) {
1649
                    Session::write("hotspot_ordered$questionId", array_keys($answers_hotspot));
1650
                    foreach ($answers_hotspot as $value) {
1651
                        $answerList .= '<li class="flex items-center space-x-2">';
1652
                        if ($freeze) {
1653
                            $answerList .= '<span class="text-support-5 fa fa-square" aria-hidden="true"></span>';
1654
                        }
1655
                        $answerList .= '<span>'.$value.'</span>';
1656
                        $answerList .= '</li>';
1657
                        $hotspotColor++;
1658
                    }
1659
                }
1660
1661
                $answerList .= '
1662
                        </ol>
1663
                    </div>
1664
                ';
1665
            }
1666
            if ($freeze) {
1667
                $relPath = api_get_path(WEB_CODE_PATH);
1668
                echo "
1669
        <div class=\"flex space-x-4\">
1670
            <div class=\"w-3/4\">
1671
                <div id=\"hotspot-preview-$questionId\" class=\"bg-gray-10 w-full bg-center bg-no-repeat bg-contain border border-gray-25\"></div>
1672
            </div>
1673
            <div class=\"w-1/4\">
1674
                $answerList
1675
            </div>
1676
        </div>
1677
        <script>
1678
            new ".(in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION]) ? "HotspotQuestion" : "DelineationQuestion")."({
1679
                questionId: $questionId,
1680
                exerciseId: $exerciseId,
1681
                exeId: 0,
1682
                selector: '#hotspot-preview-$questionId',
1683
                for: 'preview',
1684
                relPath: '$relPath'
1685
            });
1686
        </script>
1687
    ";
1688
                return;
1689
            }
1690
1691
            if (!$only_questions) {
1692
                if ($show_title) {
1693
                    if ($exercise->display_category_name) {
1694
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
1695
                    }
1696
                    echo $objQuestionTmp->getTitleToDisplay($exercise, $current_item);
1697
                }
1698
1699
                //@todo I need to the get the feedback type
1700
                echo <<<HOTSPOT
1701
        <input type="hidden" name="hidden_hotspot_id" value="$questionId" />
1702
        <div class="exercise_questions">
1703
            $questionDescription
1704
            <div class="flex space-x-4">
1705
HOTSPOT;
1706
            }
1707
1708
            $relPath = api_get_path(WEB_CODE_PATH);
1709
            $s .= "<div class=\"w-3/4\">
1710
           <div class=\"hotspot-image bg-gray-10 border border-gray-25 bg-center bg-no-repeat bg-contain\"></div>
1711
            <script>
1712
                $(function() {
1713
                    new ".(HOT_SPOT_DELINEATION == $answerType ? 'DelineationQuestion' : 'HotspotQuestion')."({
1714
                        questionId: $questionId,
1715
                        exerciseId: $exerciseId,
1716
                        exeId: 0,
1717
                        selector: '#question_div_' + $questionId + ' .hotspot-image',
1718
                        for: 'user',
1719
                        relPath: '$relPath'
1720
                    });
1721
                });
1722
            </script>
1723
        </div>
1724
        <div class=\"w-1/4\">
1725
            $answerList
1726
        </div>
1727
    ";
1728
1729
            echo <<<HOTSPOT
1730
        $s
1731
    </div>
1732
</div>
1733
HOTSPOT;
1734
        } elseif (ANNOTATION == $answerType) {
1735
            global $exe_id;
1736
            $relPath = api_get_path(WEB_CODE_PATH);
1737
            if (api_is_platform_admin() || api_is_course_admin()) {
1738
                $questionRepo = Container::getQuestionRepository();
1739
                $questionEntity = $questionRepo->find($questionId);
1740
                if ($freeze) {
1741
                    echo '
1742
            <div id="annotation-canvas-'.$questionId.'" class="annotation-canvas center-block"></div>
1743
            <script>
1744
                AnnotationQuestion({
1745
                    questionId: '.(int)$questionId.',
1746
                    exerciseId: 0,
1747
                    relPath: \''.$relPath.'\',
1748
                    courseId: '.(int)$course_id.',
1749
                    mode: "preview"
1750
                });
1751
            </script>
1752
        ';
1753
                    return 0;
1754
                }
1755
            }
1756
1757
            if (!$only_questions) {
1758
                if ($show_title) {
1759
                    if ($exercise->display_category_name) {
1760
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
1761
                    }
1762
                    echo $objQuestionTmp->getTitleToDisplay($exercise, $current_item);
1763
                }
1764
1765
                echo '
1766
                    <input type="hidden" name="hidden_hotspot_id" value="'.$questionId.'" />
1767
                    <div class="exercise_questions">
1768
                        '.$objQuestionTmp->selectDescription().'
1769
                        <div class="row">
1770
                            <div class="col-sm-8 col-md-9">
1771
                                <div id="annotation-canvas-'.$questionId.'" class="annotation-canvas center-block">
1772
                                </div>
1773
                                <script>
1774
                                    AnnotationQuestion({
1775
                                        questionId: '.$questionId.',
1776
                                        exerciseId: '.$exerciseId.',
1777
                                        relPath: \''.$relPath.'\',
1778
                                        courseId: '.$course_id.',
1779
                                    });
1780
                                </script>
1781
                            </div>
1782
                            <div class="col-sm-4 col-md-3">
1783
                                <div class="well well-sm" id="annotation-toolbar-'.$questionId.'">
1784
                                    <div class="btn-toolbar">
1785
                                        <div class="btn-group" data-toggle="buttons">
1786
                                            <label class="btn btn--plain active"
1787
                                                aria-label="'.get_lang('Add annotation path').'">
1788
                                                <input
1789
                                                    type="radio" value="0"
1790
                                                    name="'.$questionId.'-options" autocomplete="off" checked>
1791
                                                <span class="fas fa-pencil-alt" aria-hidden="true"></span>
1792
                                            </label>
1793
                                            <label class="btn btn--plain"
1794
                                                aria-label="'.get_lang('Add annotation text').'">
1795
                                                <input
1796
                                                    type="radio" value="1"
1797
                                                    name="'.$questionId.'-options" autocomplete="off">
1798
                                                <span class="fa fa-font fa-fw" aria-hidden="true"></span>
1799
                                            </label>
1800
                                        </div>
1801
                                    </div>
1802
                                    <ul class="list-unstyled"></ul>
1803
                                </div>
1804
                            </div>
1805
                        </div>
1806
                    </div>
1807
                ';
1808
            }
1809
            $objAnswerTmp = new Answer($questionId);
1810
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
1811
            unset($objAnswerTmp, $objQuestionTmp);
1812
        }
1813
1814
            return $nbrAnswers;
1815
    }
1816
1817
    /**
1818
     * Displays a table listing the quizzes where a question is used.
1819
     */
1820
    public static function showTestsWhereQuestionIsUsed(int $questionId, int $excludeTestId = 0): void
1821
    {
1822
        $em = Database::getManager();
1823
        $quizRepo = $em->getRepository(CQuiz::class);
1824
        $quizzes = $quizRepo->findQuizzesUsingQuestion($questionId, $excludeTestId);
1825
1826
        if (empty($quizzes)) {
1827
            echo '';
1828
            return;
1829
        }
1830
1831
        $result = [];
1832
1833
        foreach ($quizzes as $quiz) {
1834
            $link = $quiz->getFirstResourceLink();
1835
            $course = $link?->getCourse();
1836
            $session = $link?->getSession();
1837
            $courseId = $course?->getId() ?? 0;
1838
            $sessionId = $session?->getId() ?? 0;
1839
1840
            $url = api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.
1841
                'cid='.$courseId.'&sid='.$sessionId.'&gid=0&gradebook=0&origin='.
1842
                '&exerciseId='.$quiz->getIid().'&r=1';
1843
1844
1845
            $result[] = [
1846
                $course?->getTitle() ?? '-',
1847
                $session?->getTitle() ?? '-',
1848
                $quiz->getTitle(),
1849
                '<a href="'.$url.'">'.Display::getMdiIcon(
1850
                    'order-bool-ascending-variant',
1851
                    'ch-tool-icon',
1852
                    null,
1853
                    ICON_SIZE_SMALL,
1854
                    get_lang('Edit')
1855
                ).'</a>',
1856
            ];
1857
        }
1858
1859
        $headers = [
1860
            get_lang('Course'),
1861
            get_lang('Session'),
1862
            get_lang('Test'),
1863
            get_lang('Link to test edition'),
1864
        ];
1865
1866
        $title = Display::div(
1867
            get_lang('Question also used in the following tests'),
1868
            ['class' => 'section-title', 'style' => 'margin-top: 25px; border-bottom: none']
1869
        );
1870
1871
        echo $title.Display::table($headers, $result);
1872
    }
1873
1874
    /**
1875
     * @param int $exeId
1876
     *
1877
     * @return array
1878
     */
1879
    public static function get_exercise_track_exercise_info($exeId)
1880
    {
1881
        $quizTable = Database::get_course_table(TABLE_QUIZ_TEST);
1882
        $trackExerciseTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1883
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
1884
        $exeId = (int) $exeId;
1885
        $result = [];
1886
        if (!empty($exeId)) {
1887
            $sql = " SELECT q.*, tee.*
1888
                FROM $quizTable as q
1889
                INNER JOIN $trackExerciseTable as tee
1890
                ON q.iid = tee.exe_exo_id
1891
                WHERE
1892
                    tee.exe_id = $exeId";
1893
1894
            $sqlResult = Database::query($sql);
1895
            if (Database::num_rows($sqlResult)) {
1896
                $result = Database::fetch_assoc($sqlResult);
1897
                $result['duration_formatted'] = '';
1898
                if (!empty($result['exe_duration'])) {
1899
                    $time = api_format_time($result['exe_duration'], 'js');
1900
                    $result['duration_formatted'] = $time;
1901
                }
1902
            }
1903
        }
1904
1905
        return $result;
1906
    }
1907
1908
    /**
1909
     * Validates the time control key.
1910
     *
1911
     * @param int $lp_id
1912
     * @param int $lp_item_id
1913
     *
1914
     * @return bool
1915
     */
1916
    public static function exercise_time_control_is_valid(Exercise $exercise, $lp_id = 0, $lp_item_id = 0)
1917
    {
1918
        $exercise_id = $exercise->getId();
1919
        $expiredTime = $exercise->expired_time;
1920
1921
        // If the exercise has no time control configured, it's valid.
1922
        if (empty($expiredTime)) {
1923
            return true;
1924
        }
1925
1926
        // Build a stable session key for the LP/exercise context
1927
        $current_expired_time_key = self::get_time_control_key(
1928
            $exercise_id,
1929
            $lp_id,
1930
            $lp_item_id
1931
        );
1932
1933
        // If the key isn't present, time control cannot be validated -> not valid
1934
        if (!isset($_SESSION['expired_time'][$current_expired_time_key])) {
1935
            return false;
1936
        }
1937
1938
        // Normalize the stored value (can be DateTime, unix timestamp, or string)
1939
        $raw = $_SESSION['expired_time'][$current_expired_time_key];
1940
        if ($raw instanceof \DateTimeInterface) {
1941
            $expiredAtStr = $raw->format('Y-m-d H:i:s');
1942
        } elseif (is_int($raw) || ctype_digit((string) $raw)) {
1943
            // Treat numeric as unix timestamp (UTC)
1944
            $expiredAtStr = gmdate('Y-m-d H:i:s', (int) $raw);
1945
        } else {
1946
            // Assume parsable datetime string
1947
            $expiredAtStr = (string) $raw;
1948
        }
1949
1950
        // Compute remaining time (defensive: handle parse failure as already expired)
1951
        $expired_time = api_strtotime($expiredAtStr, 'UTC');
1952
        if ($expired_time === false || $expired_time === null) {
1953
            return false;
1954
        }
1955
1956
        $current_time = time();
1957
        $total_time_allowed = $expired_time + 30; // small grace period
1958
1959
        if ($total_time_allowed < $current_time) {
1960
            return false;
1961
        }
1962
1963
        return true;
1964
    }
1965
1966
    /**
1967
     * Deletes the time control token.
1968
     *
1969
     * @param int $exercise_id
1970
     * @param int $lp_id
1971
     * @param int $lp_item_id
1972
     */
1973
    public static function exercise_time_control_delete(
1974
        $exercise_id,
1975
        $lp_id = 0,
1976
        $lp_item_id = 0
1977
    ) {
1978
        $current_expired_time_key = self::get_time_control_key(
1979
            $exercise_id,
1980
            $lp_id,
1981
            $lp_item_id
1982
        );
1983
        unset($_SESSION['expired_time'][$current_expired_time_key]);
1984
    }
1985
1986
    /**
1987
     * Generates the time control key.
1988
     *
1989
     * @param int $exercise_id
1990
     * @param int $lp_id
1991
     * @param int $lp_item_id
1992
     *
1993
     * @return string
1994
     */
1995
    public static function get_time_control_key(
1996
        $exercise_id,
1997
        $lp_id = 0,
1998
        $lp_item_id = 0
1999
    ) {
2000
        $exercise_id = (int) $exercise_id;
2001
        $lp_id = (int) $lp_id;
2002
        $lp_item_id = (int) $lp_item_id;
2003
2004
        return
2005
            api_get_course_int_id().'_'.
2006
            api_get_session_id().'_'.
2007
            $exercise_id.'_'.
2008
            api_get_user_id().'_'.
2009
            $lp_id.'_'.
2010
            $lp_item_id;
2011
    }
2012
2013
    /**
2014
     * Get session time control.
2015
     *
2016
     * @param int $exercise_id
2017
     * @param int $lp_id
2018
     * @param int $lp_item_id
2019
     *
2020
     * @return int
2021
     */
2022
    public static function get_session_time_control_key(
2023
        $exercise_id,
2024
        $lp_id = 0,
2025
        $lp_item_id = 0
2026
    ) {
2027
        $return_value = 0;
2028
        $time_control_key = self::get_time_control_key(
2029
            $exercise_id,
2030
            $lp_id,
2031
            $lp_item_id
2032
        );
2033
        if (isset($_SESSION['expired_time']) && isset($_SESSION['expired_time'][$time_control_key])) {
2034
            $return_value = $_SESSION['expired_time'][$time_control_key];
2035
        }
2036
2037
        return $return_value;
2038
    }
2039
2040
    /**
2041
     * Gets count of exam results.
2042
     *
2043
     * @param int   $exerciseId
2044
     * @param array $conditions
2045
     * @param int   $courseId
2046
     * @param bool  $showSession
2047
     *
2048
     * @return array
2049
     */
2050
    public static function get_count_exam_results($exerciseId, $conditions, $courseId, $showSession = false)
2051
    {
2052
        $count = self::get_exam_results_data(
2053
            null,
2054
            null,
2055
            null,
2056
            null,
2057
            $exerciseId,
2058
            $conditions,
2059
            true,
2060
            $courseId,
2061
            $showSession
2062
        );
2063
2064
        return $count;
2065
    }
2066
2067
    /**
2068
     * Gets the exam'data results.
2069
     *
2070
     * @todo this function should be moved in a library  + no global calls
2071
     *
2072
     * @param int    $from
2073
     * @param int    $number_of_items
2074
     * @param int    $column
2075
     * @param string $direction
2076
     * @param int    $exercise_id
2077
     * @param null   $extra_where_conditions
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $extra_where_conditions is correct as it would always require null to be passed?
Loading history...
2078
     * @param bool   $get_count
2079
     * @param int    $courseId
2080
     * @param bool   $showSessionField
2081
     * @param bool   $showExerciseCategories
2082
     * @param array  $userExtraFieldsToAdd
2083
     * @param bool   $useCommaAsDecimalPoint
2084
     * @param bool   $roundValues
2085
     * @param bool   $getOnyIds
2086
     *
2087
     * @return array
2088
     */
2089
    public static function get_exam_results_data(
2090
        $from,
2091
        $number_of_items,
2092
        $column,
2093
        $direction,
2094
        $exercise_id,
2095
        $extra_where_conditions = null,
2096
        $get_count = false,
2097
        $courseId = null,
2098
        $showSessionField = false,
2099
        $showExerciseCategories = false,
2100
        $userExtraFieldsToAdd = [],
2101
        $useCommaAsDecimalPoint = false,
2102
        $roundValues = false,
2103
        $getOnyIds = false
2104
    ) {
2105
        //@todo replace all this globals
2106
        global $filter;
2107
        $courseId = (int) $courseId;
2108
        $course = api_get_course_entity($courseId);
2109
        if (null === $course) {
2110
            return [];
2111
        }
2112
2113
        $sessionId = api_get_session_id();
2114
        $exercise_id = (int) $exercise_id;
2115
2116
        $is_allowedToEdit =
2117
            api_is_allowed_to_edit(null, true) ||
2118
            api_is_allowed_to_edit(true) ||
2119
            api_is_drh() ||
2120
            api_is_student_boss() ||
2121
            api_is_session_admin();
2122
        $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
2123
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
2124
        $TBL_GROUP_REL_USER = Database::get_course_table(TABLE_GROUP_USER);
2125
        $TBL_GROUP = Database::get_course_table(TABLE_GROUP);
2126
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2127
        $tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
2128
2129
        $session_id_and = '';
2130
        $sessionCondition = '';
2131
        if (!$showSessionField) {
2132
            $session_id_and = api_get_session_condition($sessionId, true, false, 'te.session_id');
2133
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'ttte.session_id');
2134
        }
2135
2136
        $exercise_where = '';
2137
        if (!empty($exercise_id)) {
2138
            $exercise_where .= ' AND te.exe_exo_id = '.$exercise_id.'  ';
2139
        }
2140
2141
        // sql for chamilo-type tests for teacher / tutor view
2142
        $sql_inner_join_tbl_track_exercices = "
2143
        (
2144
            SELECT DISTINCT ttte.*, if(tr.exe_id,1, 0) as revised
2145
            FROM $TBL_TRACK_EXERCISES ttte
2146
            LEFT JOIN $tblTrackAttemptQualify tr
2147
            ON (ttte.exe_id = tr.exe_id) AND tr.author > 0
2148
            WHERE
2149
                c_id = $courseId AND
2150
                exe_exo_id = $exercise_id
2151
                $sessionCondition
2152
        )";
2153
2154
        if ($is_allowedToEdit) {
2155
            //@todo fix to work with COURSE_RELATION_TYPE_RRHH in both queries
2156
            // Hack in order to filter groups
2157
            $sql_inner_join_tbl_user = '';
2158
            if (strpos($extra_where_conditions, 'group_id')) {
2159
                $sql_inner_join_tbl_user = "
2160
                (
2161
                    SELECT
2162
                        u.id as user_id,
2163
                        firstname,
2164
                        lastname,
2165
                        official_code,
2166
                        email,
2167
                        username,
2168
                        g.name as group_name,
2169
                        g.id as group_id
2170
                    FROM $TBL_USER u
2171
                    INNER JOIN $TBL_GROUP_REL_USER gru
2172
                    ON (gru.user_id = u.id AND gru.c_id= $courseId )
2173
                    INNER JOIN $TBL_GROUP g
2174
                    ON (gru.group_id = g.id AND g.c_id= $courseId )
2175
                    WHERE u.active <> ".USER_SOFT_DELETED."
2176
                )";
2177
            }
2178
2179
            if (strpos($extra_where_conditions, 'group_all')) {
2180
                $extra_where_conditions = str_replace(
2181
                    "AND (  group_id = 'group_all'  )",
2182
                    '',
2183
                    $extra_where_conditions
2184
                );
2185
                $extra_where_conditions = str_replace(
2186
                    "AND group_id = 'group_all'",
2187
                    '',
2188
                    $extra_where_conditions
2189
                );
2190
                $extra_where_conditions = str_replace(
2191
                    "group_id = 'group_all' AND",
2192
                    '',
2193
                    $extra_where_conditions
2194
                );
2195
2196
                $sql_inner_join_tbl_user = "
2197
                (
2198
                    SELECT
2199
                        u.id as user_id,
2200
                        firstname,
2201
                        lastname,
2202
                        official_code,
2203
                        email,
2204
                        username,
2205
                        '' as group_name,
2206
                        '' as group_id
2207
                    FROM $TBL_USER u
2208
                    WHERE u.active <> ".USER_SOFT_DELETED."
2209
                )";
2210
                $sql_inner_join_tbl_user = null;
2211
            }
2212
2213
            if (strpos($extra_where_conditions, 'group_none')) {
2214
                $extra_where_conditions = str_replace(
2215
                    "AND (  group_id = 'group_none'  )",
2216
                    "AND (  group_id is null  )",
2217
                    $extra_where_conditions
2218
                );
2219
                $extra_where_conditions = str_replace(
2220
                    "AND group_id = 'group_none'",
2221
                    "AND (  group_id is null  )",
2222
                    $extra_where_conditions
2223
                );
2224
                $sql_inner_join_tbl_user = "
2225
            (
2226
                SELECT
2227
                    u.id as user_id,
2228
                    firstname,
2229
                    lastname,
2230
                    official_code,
2231
                    email,
2232
                    username,
2233
                    g.name as group_name,
2234
                    g.iid as group_id
2235
                FROM $TBL_USER u
2236
                LEFT OUTER JOIN $TBL_GROUP_REL_USER gru
2237
                ON (gru.user_id = u.id AND gru.c_id= $courseId )
2238
                LEFT OUTER JOIN $TBL_GROUP g
2239
                ON (gru.group_id = g.id AND g.c_id = $courseId )
2240
                WHERE u.active <> ".USER_SOFT_DELETED."
2241
            )";
2242
            }
2243
2244
            // All
2245
            $is_empty_sql_inner_join_tbl_user = false;
2246
            if (empty($sql_inner_join_tbl_user)) {
2247
                $is_empty_sql_inner_join_tbl_user = true;
2248
                $sql_inner_join_tbl_user = "
2249
            (
2250
                SELECT u.id as user_id, firstname, lastname, email, username, ' ' as group_name, '' as group_id, official_code
2251
                FROM $TBL_USER u
2252
                WHERE u.active <> ".USER_SOFT_DELETED." AND u.status NOT IN(".api_get_users_status_ignored_in_reports('string').")
2253
            )";
2254
            }
2255
2256
            $sqlFromOption = " , $TBL_GROUP_REL_USER AS gru ";
2257
            $sqlWhereOption = "  AND gru.c_id = $courseId AND gru.user_id = user.id ";
2258
            $first_and_last_name = api_is_western_name_order() ? "firstname, lastname" : "lastname, firstname";
2259
2260
            if ($get_count) {
2261
                $sql_select = 'SELECT count(te.exe_id) ';
2262
            } else {
2263
                $sql_select = "SELECT DISTINCT
2264
                user.user_id,
2265
                $first_and_last_name,
2266
                official_code,
2267
                ce.title,
2268
                username,
2269
                te.score,
2270
                te.max_score,
2271
                te.exe_date,
2272
                te.exe_id,
2273
                te.session_id,
2274
                email as exemail,
2275
                te.start_date,
2276
                ce.expired_time,
2277
                steps_counter,
2278
                exe_user_id,
2279
                te.exe_duration,
2280
                te.status as completion_status,
2281
                propagate_neg,
2282
                revised,
2283
                group_name,
2284
                user.group_id AS group_id,
2285
                orig_lp_id,
2286
                te.user_ip";
2287
            }
2288
2289
            $sql = " $sql_select
2290
            FROM $TBL_EXERCISES AS ce
2291
            INNER JOIN $sql_inner_join_tbl_track_exercices AS te
2292
            ON (te.exe_exo_id = ce.iid)
2293
            INNER JOIN $sql_inner_join_tbl_user AS user
2294
            ON (user.user_id = exe_user_id)
2295
            INNER JOIN resource_node rn
2296
                ON rn.id = ce.resource_node_id
2297
            INNER JOIN resource_link rl
2298
                ON rl.resource_node_id = rn.id
2299
            WHERE
2300
                te.c_id = $courseId $session_id_and AND
2301
                rl.deleted_at IS NULL
2302
                $exercise_where
2303
                $extra_where_conditions
2304
            ";
2305
        }
2306
2307
        if (empty($sql)) {
2308
            return false;
2309
        }
2310
2311
        if ($get_count) {
2312
            $resx = Database::query($sql);
2313
            $rowx = Database::fetch_row($resx, 'ASSOC');
2314
2315
            return $rowx[0];
2316
        }
2317
2318
        $teacher_list = CourseManager::get_teacher_list_from_course_code($course->getCode());
2319
        $teacher_id_list = [];
2320
        if (!empty($teacher_list)) {
2321
            foreach ($teacher_list as $teacher) {
2322
                $teacher_id_list[] = $teacher['user_id'];
2323
            }
2324
        }
2325
2326
        $scoreDisplay = new ScoreDisplay();
2327
        $decimalSeparator = '.';
2328
        $thousandSeparator = ',';
2329
2330
        if ($useCommaAsDecimalPoint) {
2331
            $decimalSeparator = ',';
2332
            $thousandSeparator = '';
2333
        }
2334
2335
        $listInfo = [];
2336
        $column = !empty($column) ? Database::escape_string($column) : null;
2337
        $from = (int) $from;
2338
        $number_of_items = (int) $number_of_items;
2339
        $direction = !in_array(strtolower(trim($direction)), ['asc', 'desc']) ? 'asc' : $direction;
2340
2341
        if (!empty($column)) {
2342
            $sql .= " ORDER BY `$column` $direction ";
2343
        }
2344
2345
        if (!$getOnyIds) {
2346
            $sql .= " LIMIT $from, $number_of_items";
2347
        }
2348
2349
        $results = [];
2350
        $resx = Database::query($sql);
2351
        while ($rowx = Database::fetch_assoc($resx)) {
2352
            $results[] = $rowx;
2353
        }
2354
2355
        $group_list = GroupManager::get_group_list(null, $course);
2356
        $clean_group_list = [];
2357
        if (!empty($group_list)) {
2358
            foreach ($group_list as $group) {
2359
                $clean_group_list[$group['iid']] = $group['title'];
2360
            }
2361
        }
2362
2363
        $lp_list_obj = new LearnpathList(api_get_user_id());
2364
        $lp_list = $lp_list_obj->get_flat_list();
2365
        $oldIds = array_column($lp_list, 'lp_old_id', 'iid');
2366
2367
        if (is_array($results)) {
2368
            $users_array_id = [];
2369
            $from_gradebook = false;
2370
            if (isset($_GET['gradebook']) && 'view' === $_GET['gradebook']) {
2371
                $from_gradebook = true;
2372
            }
2373
            $sizeof = count($results);
2374
            $locked = api_resource_is_locked_by_gradebook(
2375
                $exercise_id,
2376
                LINK_EXERCISE
2377
            );
2378
2379
            $timeNow = strtotime(api_get_utc_datetime());
2380
            // Looping results
2381
            for ($i = 0; $i < $sizeof; $i++) {
2382
                $revised = $results[$i]['revised'];
2383
                if ('incomplete' === $results[$i]['completion_status']) {
2384
                    // If the exercise was incomplete, we need to determine
2385
                    // if it is still into the time allowed, or if its
2386
                    // allowed time has expired and it can be closed
2387
                    // (it's "unclosed")
2388
                    $minutes = $results[$i]['expired_time'];
2389
                    if (0 == $minutes) {
2390
                        // There's no time limit, so obviously the attempt
2391
                        // can still be "ongoing", but the teacher should
2392
                        // be able to choose to close it, so mark it as
2393
                        // "unclosed" instead of "ongoing"
2394
                        $revised = 2;
2395
                    } else {
2396
                        $allowedSeconds = $minutes * 60;
2397
                        $timeAttemptStarted = strtotime($results[$i]['start_date']);
2398
                        $secondsSinceStart = $timeNow - $timeAttemptStarted;
2399
                        if ($secondsSinceStart > $allowedSeconds) {
2400
                            $revised = 2; // mark as "unclosed"
2401
                        } else {
2402
                            $revised = 3; // mark as "ongoing"
2403
                        }
2404
                    }
2405
                }
2406
2407
                if ($from_gradebook && ($is_allowedToEdit)) {
2408
                    if (in_array(
2409
                        $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'],
2410
                        $users_array_id
2411
                    )) {
2412
                        continue;
2413
                    }
2414
                    $users_array_id[] = $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'];
2415
                }
2416
2417
                $lp_obj = isset($results[$i]['orig_lp_id']) && isset($lp_list[$results[$i]['orig_lp_id']]) ? $lp_list[$results[$i]['orig_lp_id']] : null;
2418
                if (empty($lp_obj)) {
2419
                    // Try to get the old id (id instead of iid)
2420
                    $lpNewId = isset($results[$i]['orig_lp_id']) && isset($oldIds[$results[$i]['orig_lp_id']]) ? $oldIds[$results[$i]['orig_lp_id']] : null;
2421
                    if ($lpNewId) {
2422
                        $lp_obj = isset($lp_list[$lpNewId]) ? $lp_list[$lpNewId] : null;
2423
                    }
2424
                }
2425
                $lp_name = null;
2426
                if ($lp_obj) {
2427
                    $url = api_get_path(WEB_CODE_PATH).
2428
                        'lp/lp_controller.php?'.api_get_cidreq().'&action=view&lp_id='.$results[$i]['orig_lp_id'];
2429
                    $lp_name = Display::url(
2430
                        $lp_obj['lp_name'],
2431
                        $url,
2432
                        ['target' => '_blank']
2433
                    );
2434
                }
2435
2436
                // Add all groups by user
2437
                $group_name_list = '';
2438
                if ($is_empty_sql_inner_join_tbl_user) {
2439
                    $group_list = GroupManager::get_group_ids(
2440
                        api_get_course_int_id(),
2441
                        $results[$i]['user_id']
2442
                    );
2443
2444
                    foreach ($group_list as $id) {
2445
                        if (isset($clean_group_list[$id])) {
2446
                            $group_name_list .= $clean_group_list[$id].'<br/>';
2447
                        }
2448
                    }
2449
                    $results[$i]['group_name'] = $group_name_list;
2450
                }
2451
2452
                $results[$i]['exe_duration'] = !empty($results[$i]['exe_duration']) ? round($results[$i]['exe_duration'] / 60) : 0;
2453
                $id = $results[$i]['exe_id'];
2454
                $dt = api_convert_and_format_date($results[$i]['max_score']);
2455
2456
                // we filter the results if we have the permission to
2457
                $result_disabled = 0;
2458
                if (isset($results[$i]['results_disabled'])) {
2459
                    $result_disabled = (int) $results[$i]['results_disabled'];
2460
                }
2461
                if (0 == $result_disabled) {
2462
                    $my_res = $results[$i]['score'];
2463
                    $my_total = $results[$i]['max_score'];
2464
                    $results[$i]['start_date'] = api_get_local_time($results[$i]['start_date']);
2465
                    $results[$i]['exe_date'] = api_get_local_time($results[$i]['exe_date']);
2466
2467
                    if (!$results[$i]['propagate_neg'] && $my_res < 0) {
2468
                        $my_res = 0;
2469
                    }
2470
2471
                    $score = self::show_score(
2472
                        $my_res,
2473
                        $my_total,
2474
                        true,
2475
                        true,
2476
                        false,
2477
                        false,
2478
                        $decimalSeparator,
2479
                        $thousandSeparator,
2480
                        $roundValues
2481
                    );
2482
2483
                    $actions = '<div class="pull-right">';
2484
                    if ($is_allowedToEdit) {
2485
                        if (isset($teacher_id_list)) {
2486
                            if (in_array(
2487
                                $results[$i]['exe_user_id'],
2488
                                $teacher_id_list
2489
                            )) {
2490
                                $actions .= Display::getMdiIcon('human-male-board', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Trainer'));
2491
                            }
2492
                        }
2493
                        $revisedLabel = '';
2494
                        switch ($revised) {
2495
                            case 0:
2496
                                $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=qualify&id=$id'>".
2497
                                    Display::getMdiIcon(ActionIcon::GRADE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Grade activity')
2498
                                    );
2499
                                $actions .= '</a>';
2500
                                $revisedLabel = Display::label(
2501
                                    get_lang('Not validated'),
2502
                                    'info'
2503
                                );
2504
                                break;
2505
                            case 1:
2506
                                $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=edit&id=$id'>".
2507
                                    Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit'));
2508
                                $actions .= '</a>';
2509
                                $revisedLabel = Display::label(
2510
                                    get_lang('Validated'),
2511
                                    'success'
2512
                                );
2513
                                break;
2514
                            case 2: //finished but not marked as such
2515
                                $actions .= '<a href="exercise_report.php?'
2516
                                    .api_get_cidreq()
2517
                                    .'&exerciseId='
2518
                                    .$exercise_id
2519
                                    .'&a=close&id='
2520
                                    .$id
2521
                                    .'">'.
2522
                                    Display::getMdiIcon(ActionIcon::LOCK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Mark attempt as closed'));
2523
                                $actions .= '</a>';
2524
                                $revisedLabel = Display::label(
2525
                                    get_lang('Unclosed'),
2526
                                    'warning'
2527
                                );
2528
                                break;
2529
                            case 3: //still ongoing
2530
                                $actions .= Display::getMdiIcon('clock', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Attempt still going on. Please wait.'));
2531
                                $actions .= '';
2532
                                $revisedLabel = Display::label(
2533
                                    get_lang('Ongoing'),
2534
                                    'danger'
2535
                                );
2536
                                break;
2537
                        }
2538
2539
                        if (2 == $filter) {
2540
                            $actions .= ' <a href="exercise_history.php?'.api_get_cidreq().'&exe_id='.$id.'">'.
2541
                                Display::getMdiIcon('clipboard-text-clock', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('View changes history')
2542
                                ).'</a>';
2543
                        }
2544
2545
                        // Admin can always delete the attempt
2546
                        if ((false == $locked || api_is_platform_admin()) && !api_is_student_boss()) {
2547
                            $ip = Tracking::get_ip_from_user_event(
2548
                                $results[$i]['exe_user_id'],
2549
                                api_get_utc_datetime(),
2550
                                false
2551
                            );
2552
                            $actions .= '<a href="http://www.whatsmyip.org/ip-geo-location/?ip='.$ip.'" target="_blank">'
2553
                                .Display::getMdiIcon('information', 'ch-tool-icon', null, ICON_SIZE_SMALL, $ip)
2554
                                .'</a>';
2555
2556
                            $recalculateUrl = api_get_path(WEB_CODE_PATH).'exercise/recalculate.php?'.
2557
                                api_get_cidreq().'&'.
2558
                                http_build_query([
2559
                                    'id' => $id,
2560
                                    'exercise' => $exercise_id,
2561
                                    'user' => $results[$i]['exe_user_id'],
2562
                                ]);
2563
                            $actions .= Display::url(
2564
                                Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Recalculate results')),
2565
                                $recalculateUrl,
2566
                                [
2567
                                    'data-exercise' => $exercise_id,
2568
                                    'data-user' => $results[$i]['exe_user_id'],
2569
                                    'data-id' => $id,
2570
                                    'class' => 'exercise-recalculate',
2571
                                ]
2572
                            );
2573
2574
                            $exportPdfUrl = api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'.
2575
                                api_get_cidreq().'&exerciseId='.$exercise_id.'&action=export_pdf&attemptId='.$id.'&userId='.(int) $results[$i]['exe_user_id'];
2576
                            $actions .= '<a href="'.$exportPdfUrl.'" target="_blank">'
2577
                                .Display::getMdiIcon(ActionIcon::EXPORT_PDF, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Export to PDF'))
2578
                                .'</a>';
2579
2580
                            $sendMailUrl =  api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'.api_get_cidreq().'&action=send_email&exerciseId='.$exercise_id.'&attemptId='.$results[$i]['exe_id'];
2581
                            $emailLink = '<a href="'.$sendMailUrl.'">'
2582
                                .Display::getMdiIcon(ActionIcon::SEND_SINGLE_EMAIL, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Send by e-mail'))
2583
                                .'</a>';
2584
2585
                            $filterByUser = isset($_GET['filter_by_user']) ? (int) $_GET['filter_by_user'] : 0;
2586
                            $delete_link = '<a
2587
                            href="exercise_report.php?'.api_get_cidreq().'&filter_by_user='.$filterByUser.'&filter='.$filter.'&exerciseId='.$exercise_id.'&delete=delete&did='.$id.'"
2588
                            onclick=
2589
                            "javascript:if(!confirm(\''.sprintf(addslashes(get_lang('Delete attempt?')), $results[$i]['username'], $dt).'\')) return false;"
2590
                            >';
2591
                            $delete_link .= Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, addslashes(get_lang('Delete'))).'</a>';
2592
2593
                            if (api_is_drh() && !api_is_platform_admin()) {
2594
                                $delete_link = null;
2595
                            }
2596
                            if (api_is_session_admin()) {
2597
                                $delete_link = '';
2598
                            }
2599
                            if (3 == $revised) {
2600
                                $delete_link = null;
2601
                            }
2602
                            if (1 !== $revised) {
2603
                                $emailLink = '';
2604
                            }
2605
                            $actions .= $delete_link;
2606
                            $actions .= $emailLink;
2607
                        }
2608
                    } else {
2609
                        $attempt_url = api_get_path(WEB_CODE_PATH).'exercise/result.php?'.api_get_cidreq().'&id='.$results[$i]['exe_id'].'&sid='.$sessionId;
2610
                        $attempt_link = Display::url(
2611
                            get_lang('Show'),
2612
                            $attempt_url,
2613
                            [
2614
                                'class' => 'ajax btn btn--plain',
2615
                                'data-title' => get_lang('Show'),
2616
                            ]
2617
                        );
2618
                        $actions .= $attempt_link;
2619
                    }
2620
                    $actions .= '</div>';
2621
2622
                    if (!empty($userExtraFieldsToAdd)) {
2623
                        foreach ($userExtraFieldsToAdd as $variable) {
2624
                            $extraFieldValue = new ExtraFieldValue('user');
2625
                            $values = $extraFieldValue->get_values_by_handler_and_field_variable(
2626
                                $results[$i]['user_id'],
2627
                                $variable
2628
                            );
2629
                            if (isset($values['value'])) {
2630
                                $results[$i][$variable] = $values['value'];
2631
                            }
2632
                        }
2633
                    }
2634
2635
                    $exeId = $results[$i]['exe_id'];
2636
                    $results[$i]['id'] = $exeId;
2637
                    $category_list = [];
2638
                    if ($is_allowedToEdit) {
2639
                        $sessionName = '';
2640
                        $sessionStartAccessDate = '';
2641
                        if (!empty($results[$i]['session_id'])) {
2642
                            $sessionInfo = api_get_session_info($results[$i]['session_id']);
2643
                            if (!empty($sessionInfo)) {
2644
                                $sessionName = $sessionInfo['title'];
2645
                                $sessionStartAccessDate = api_get_local_time($sessionInfo['access_start_date']);
2646
                            }
2647
                        }
2648
2649
                        $objExercise = new Exercise($courseId);
2650
                        if ($showExerciseCategories) {
2651
                            // Getting attempt info
2652
                            $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
2653
                            if (!empty($exercise_stat_info['data_tracking'])) {
2654
                                $question_list = explode(',', $exercise_stat_info['data_tracking']);
2655
                                if (!empty($question_list)) {
2656
                                    foreach ($question_list as $questionId) {
2657
                                        $objQuestionTmp = Question::read($questionId, $objExercise->course);
2658
                                        // We're inside *one* question. Go through each possible answer for this question
2659
                                        $result = $objExercise->manage_answer(
2660
                                            $exeId,
2661
                                            $questionId,
2662
                                            null,
2663
                                            'exercise_result',
2664
                                            false,
2665
                                            false,
2666
                                            true,
2667
                                            false,
2668
                                            $objExercise->selectPropagateNeg(),
2669
                                            null,
2670
                                            true
2671
                                        );
2672
2673
                                        $my_total_score = $result['score'];
2674
                                        $my_total_weight = $result['weight'];
2675
2676
                                        // Category report
2677
                                        $category_was_added_for_this_test = false;
2678
                                        if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
2679
                                            if (!isset($category_list[$objQuestionTmp->category]['score'])) {
2680
                                                $category_list[$objQuestionTmp->category]['score'] = 0;
2681
                                            }
2682
                                            if (!isset($category_list[$objQuestionTmp->category]['total'])) {
2683
                                                $category_list[$objQuestionTmp->category]['total'] = 0;
2684
                                            }
2685
                                            $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
2686
                                            $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
2687
                                            $category_was_added_for_this_test = true;
2688
                                        }
2689
2690
                                        if (isset($objQuestionTmp->category_list) &&
2691
                                            !empty($objQuestionTmp->category_list)
2692
                                        ) {
2693
                                            foreach ($objQuestionTmp->category_list as $category_id) {
2694
                                                $category_list[$category_id]['score'] += $my_total_score;
2695
                                                $category_list[$category_id]['total'] += $my_total_weight;
2696
                                                $category_was_added_for_this_test = true;
2697
                                            }
2698
                                        }
2699
2700
                                        // No category for this question!
2701
                                        if (false == $category_was_added_for_this_test) {
2702
                                            if (!isset($category_list['none']['score'])) {
2703
                                                $category_list['none']['score'] = 0;
2704
                                            }
2705
                                            if (!isset($category_list['none']['total'])) {
2706
                                                $category_list['none']['total'] = 0;
2707
                                            }
2708
2709
                                            $category_list['none']['score'] += $my_total_score;
2710
                                            $category_list['none']['total'] += $my_total_weight;
2711
                                        }
2712
                                    }
2713
                                }
2714
                            }
2715
                        }
2716
2717
                        foreach ($category_list as $categoryId => $result) {
2718
                            $scoreToDisplay = self::show_score(
2719
                                $result['score'],
2720
                                $result['total'],
2721
                                true,
2722
                                true,
2723
                                false,
2724
                                false,
2725
                                $decimalSeparator,
2726
                                $thousandSeparator,
2727
                                $roundValues
2728
                            );
2729
                            $results[$i]['category_'.$categoryId] = $scoreToDisplay;
2730
                            $results[$i]['category_'.$categoryId.'_score_percentage'] = self::show_score(
2731
                                $result['score'],
2732
                                $result['total'],
2733
                                true,
2734
                                true,
2735
                                true, // $show_only_percentage = false
2736
                                true, // hide % sign
2737
                                $decimalSeparator,
2738
                                $thousandSeparator,
2739
                                $roundValues
2740
                            );
2741
                            $results[$i]['category_'.$categoryId.'_only_score'] = $result['score'];
2742
                            $results[$i]['category_'.$categoryId.'_total'] = $result['total'];
2743
                        }
2744
                        $results[$i]['session'] = $sessionName;
2745
                        $results[$i]['session_access_start_date'] = $sessionStartAccessDate;
2746
                        $results[$i]['status'] = $revisedLabel;
2747
                        $results[$i]['score'] = $score;
2748
                        $results[$i]['score_percentage'] = self::show_score(
2749
                            $my_res,
2750
                            $my_total,
2751
                            true,
2752
                            true,
2753
                            true,
2754
                            true,
2755
                            $decimalSeparator,
2756
                            $thousandSeparator,
2757
                            $roundValues
2758
                        );
2759
2760
                        if ($roundValues) {
2761
                            $whole = floor($my_res); // 1
2762
                            $fraction = $my_res - $whole; // .25
2763
                            if ($fraction >= 0.5) {
2764
                                $onlyScore = ceil($my_res);
2765
                            } else {
2766
                                $onlyScore = round($my_res);
2767
                            }
2768
                        } else {
2769
                            $onlyScore = $scoreDisplay->format_score(
2770
                                $my_res,
2771
                                false,
2772
                                $decimalSeparator,
2773
                                $thousandSeparator
2774
                            );
2775
                        }
2776
2777
                        $results[$i]['only_score'] = $onlyScore;
2778
2779
                        if ($roundValues) {
2780
                            $whole = floor($my_total); // 1
2781
                            $fraction = $my_total - $whole; // .25
2782
                            if ($fraction >= 0.5) {
2783
                                $onlyTotal = ceil($my_total);
2784
                            } else {
2785
                                $onlyTotal = round($my_total);
2786
                            }
2787
                        } else {
2788
                            $onlyTotal = $scoreDisplay->format_score(
2789
                                $my_total,
2790
                                false,
2791
                                $decimalSeparator,
2792
                                $thousandSeparator
2793
                            );
2794
                        }
2795
                        $results[$i]['total'] = $onlyTotal;
2796
                        $results[$i]['lp'] = $lp_name;
2797
                        $results[$i]['actions'] = $actions;
2798
                        $listInfo[] = $results[$i];
2799
                    } else {
2800
                        $results[$i]['status'] = $revisedLabel;
2801
                        $results[$i]['score'] = $score;
2802
                        $results[$i]['actions'] = $actions;
2803
                        $listInfo[] = $results[$i];
2804
                    }
2805
                }
2806
            }
2807
        }
2808
2809
        return $listInfo;
2810
    }
2811
2812
    /**
2813
     * Returns email content for a specific attempt.
2814
     */
2815
    public static function getEmailContentForAttempt(int $attemptId): array
2816
    {
2817
        $trackExerciseInfo = self::get_exercise_track_exercise_info($attemptId);
2818
2819
        if (empty($trackExerciseInfo)) {
2820
            return [
2821
                'to' => '',
2822
                'subject' => 'No exercise info found',
2823
                'message' => 'Attempt ID not found or invalid.',
2824
            ];
2825
        }
2826
2827
        $studentId = $trackExerciseInfo['exe_user_id'];
2828
        $courseInfo = api_get_course_info();
2829
        $teacherId = api_get_user_id();
2830
2831
        if (
2832
            empty($trackExerciseInfo['orig_lp_id']) ||
2833
            empty($trackExerciseInfo['orig_lp_item_id'])
2834
        ) {
2835
            $url = api_get_path(WEB_CODE_PATH).'exercise/result.php?id='.$trackExerciseInfo['exe_id'].'&'.api_get_cidreq()
2836
                .'&show_headers=1&id_session='.api_get_session_id();
2837
        } else {
2838
            $url = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?action=view&item_id='
2839
                .$trackExerciseInfo['orig_lp_item_id'].'&lp_id='.$trackExerciseInfo['orig_lp_id'].'&'.api_get_cidreq()
2840
                .'&id_session='.api_get_session_id();
2841
        }
2842
2843
        $message = self::getEmailNotification(
2844
            $teacherId,
2845
            $courseInfo,
2846
            $trackExerciseInfo['title'],
2847
            $url
2848
        );
2849
2850
        return [
2851
            'to' => $studentId,
2852
            'subject' => get_lang('Corrected test result'),
2853
            'message' => $message,
2854
        ];
2855
    }
2856
2857
    /**
2858
     * Sends the exercise result email to the student.
2859
     */
2860
    public static function sendExerciseResultByEmail(int $attemptId): void
2861
    {
2862
        $content = self::getEmailContentForAttempt($attemptId);
2863
2864
        if (empty($content['to'])) {
2865
            return;
2866
        }
2867
2868
        MessageManager::send_message_simple(
2869
            $content['to'],
2870
            $content['subject'],
2871
            $content['message'],
2872
            api_get_user_id()
2873
        );
2874
    }
2875
2876
    /**
2877
     * Returns all reviewed attempts for a given exercise and session.
2878
     */
2879
    public static function getReviewedAttemptsInfo(int $exerciseId, int $sessionId): array
2880
    {
2881
        $courseId = api_get_course_int_id();
2882
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2883
        $qualifyTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
2884
2885
        $sessionCondition = api_get_session_condition($sessionId, true, false, 't.session_id');
2886
2887
        $sql = "
2888
            SELECT DISTINCT t.exe_id
2889
            FROM $trackTable t
2890
            INNER JOIN $qualifyTable q ON (t.exe_id = q.exe_id AND q.author > 0)
2891
            WHERE
2892
                t.c_id = $courseId AND
2893
                t.exe_exo_id = $exerciseId
2894
                $sessionCondition
2895
        ";
2896
2897
        return Database::store_result(Database::query($sql));
2898
    }
2899
2900
    /**
2901
     * @param $score
2902
     * @param $weight
2903
     *
2904
     * @return array
2905
     */
2906
    public static function convertScoreToPlatformSetting($score, $weight)
2907
    {
2908
        $maxNote = api_get_setting('exercise_max_score');
2909
        $minNote = api_get_setting('exercise_min_score');
2910
2911
        if ('' != $maxNote && '' != $minNote) {
2912
            if (!empty($weight) && (float) $weight !== (float) 0) {
2913
                $score = $minNote + ($maxNote - $minNote) * $score / $weight;
2914
            } else {
2915
                $score = $minNote;
2916
            }
2917
            $weight = $maxNote;
2918
        }
2919
2920
        return ['score' => $score, 'weight' => $weight];
2921
    }
2922
2923
    /**
2924
     * Converts the score with the exercise_max_note and exercise_min_score
2925
     * the platform settings + formats the results using the float_format function.
2926
     *
2927
     * @param float  $score
2928
     * @param float  $weight
2929
     * @param bool   $show_percentage       show percentage or not
2930
     * @param bool   $use_platform_settings use or not the platform settings
2931
     * @param bool   $show_only_percentage
2932
     * @param bool   $hidePercentageSign    hide "%" sign
2933
     * @param string $decimalSeparator
2934
     * @param string $thousandSeparator
2935
     * @param bool   $roundValues           This option rounds the float values into a int using ceil()
2936
     * @param bool   $removeEmptyDecimals
2937
     *
2938
     * @return string an html with the score modified
2939
     */
2940
    public static function show_score(
2941
        $score,
2942
        $weight,
2943
        $show_percentage = true,
2944
        $use_platform_settings = true,
2945
        $show_only_percentage = false,
2946
        $hidePercentageSign = false,
2947
        $decimalSeparator = '.',
2948
        $thousandSeparator = ',',
2949
        $roundValues = false,
2950
        $removeEmptyDecimals = false
2951
    ) {
2952
        if (is_null($score) && is_null($weight)) {
2953
            return '-';
2954
        }
2955
2956
        $decimalSeparator = empty($decimalSeparator) ? '.' : $decimalSeparator;
2957
        $thousandSeparator = empty($thousandSeparator) ? ',' : $thousandSeparator;
2958
2959
        if ($use_platform_settings) {
2960
            $result = self::convertScoreToPlatformSetting($score, $weight);
2961
            $score = $result['score'];
2962
            $weight = $result['weight'];
2963
        }
2964
2965
        // Keep a raw numeric percentage for model mapping BEFORE string formatting
2966
        $percentageRaw = (100 * (float) $score) / ((0 != (float) $weight) ? (float) $weight : 1);
2967
2968
        // Formats values
2969
        $percentage = float_format($percentageRaw, 1);
2970
        $score      = float_format($score, 1);
2971
        $weight     = float_format($weight, 1);
2972
2973
        if ($roundValues) {
2974
            $whole = floor($percentage);
2975
            $fraction = $percentage - $whole;
2976
            $percentage = ($fraction >= 0.5) ? ceil($percentage) : round($percentage);
2977
2978
            $whole = floor($score);
2979
            $fraction = $score - $whole;
2980
            $score = ($fraction >= 0.5) ? ceil($score) : round($score);
2981
2982
            $whole = floor($weight);
2983
            $fraction = $weight - $whole;
2984
            $weight = ($fraction >= 0.5) ? ceil($weight) : round($weight);
2985
        } else {
2986
            $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator);
2987
            $score      = float_format($score, 1, $decimalSeparator, $thousandSeparator);
2988
            $weight     = float_format($weight, 1, $decimalSeparator, $thousandSeparator);
2989
        }
2990
2991
        // Build base HTML (percentage or score/weight)
2992
        if ($show_percentage) {
2993
            $percentageSign = $hidePercentageSign ? '' : ' %';
2994
            $html = $show_only_percentage
2995
                ? ($percentage . $percentageSign)
2996
                : ($percentage . $percentageSign . ' (' . $score . ' / ' . $weight . ')');
2997
        } else {
2998
            if ($removeEmptyDecimals && ScoreDisplay::hasEmptyDecimals($weight)) {
2999
                $weight = round($weight);
3000
            }
3001
            $html = $score . ' / ' . $weight;
3002
        }
3003
3004
        $bucket = self::convertScoreToModel($percentageRaw);
3005
        if ($bucket !== null) {
3006
            $html = self::getModelStyle($bucket, $percentageRaw);
3007
        }
3008
3009
        // If the platform forces a format, it overrides everything (including the model badge)
3010
        $format = (int) api_get_setting('exercise.exercise_score_format');
3011
        if (!empty($format)) {
3012
            $html = ScoreDisplay::instance()->display_score([$score, $weight], $format);
3013
        }
3014
3015
        return Display::span($html, ['class' => 'score_exercise']);
3016
    }
3017
3018
    /**
3019
     * @param array $model
3020
     * @param float $percentage
3021
     *
3022
     * @return string
3023
     */
3024
    public static function getModelStyle($bucket, $percentage)
3025
    {
3026
        $rawClass = (string) ($bucket['css_class'] ?? '');
3027
        $twClass  = self::mapScoreCssClass($rawClass);
3028
3029
        // Accept both 'name' and 'variable'
3030
        $key   = isset($bucket['name']) ? 'name' : (isset($bucket['variable']) ? 'variable' : null);
3031
        $raw   = $key ? (string) $bucket[$key] : '';
3032
        $label = $raw !== '' ? get_lang($raw) : '';
3033
        $show  = (int) ($bucket['display_score_name'] ?? 0) === 1;
3034
3035
        $base = 'inline-block px-2 py-1 rounded';
3036
3037
        if ($show && $label !== '') {
3038
            return '<span class="' . htmlspecialchars($base . ' ' . $twClass) . '">' .
3039
                htmlspecialchars($label) . '</span>';
3040
        }
3041
3042
        return '<span class="' . htmlspecialchars($base . ' ' . $twClass) . '" ' .
3043
            'title="' . htmlspecialchars($label) . '" aria-label="' . htmlspecialchars($label) . '">' .
3044
            '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;' .
3045
            '</span>';
3046
    }
3047
3048
    /**
3049
     * Map legacy css_class (e.g., "btn-danger") to Tailwind utility classes
3050
     * defined in Chamilo 2's theme (danger/success/warning/info).
3051
     * If a Tailwind class list is already provided, pass-through.
3052
     */
3053
    private static function mapScoreCssClass(string $cssClass): string
3054
    {
3055
        $cssClass = trim($cssClass);
3056
3057
        // Legacy → Tailwind mapping
3058
        $map = [
3059
            'btn-success' => 'bg-success text-success-button-text',
3060
            'btn-warning' => 'bg-warning text-warning-button-text',
3061
            'btn-danger'  => 'bg-danger text-danger-button-text',
3062
            'btn-info'    => 'bg-info text-info-button-text',
3063
3064
            // Also accept short tokens if someone uses "success" directly
3065
            'success' => 'bg-success text-success-button-text',
3066
            'warning' => 'bg-warning text-warning-button-text',
3067
            'danger'  => 'bg-danger text-danger-button-text',
3068
            'info'    => 'bg-info text-info-button-text',
3069
        ];
3070
3071
        if (isset($map[$cssClass])) {
3072
            return $map[$cssClass];
3073
        }
3074
3075
        // If it already looks like Tailwind utility classes, keep as-is
3076
        if (strpos($cssClass, ' ') !== false || preg_match('/[a-z]+-[a-z0-9\-]+/i', $cssClass)) {
3077
            return $cssClass;
3078
        }
3079
3080
        // Neutral fallback
3081
        return 'bg-gray-20 text-gray-90';
3082
    }
3083
3084
    /**
3085
     * @param float $percentage value between 0 and 100
3086
     *
3087
     * @return string
3088
     */
3089
    public static function convertScoreToModel($percentage): ?array
3090
    {
3091
        $model = self::getCourseScoreModel();
3092
        if (empty($model) || empty($model['score_list'])) {
3093
            return null;
3094
        }
3095
3096
        foreach ($model['score_list'] as $bucket) {
3097
            $min = (float) ($bucket['min'] ?? 0);
3098
            $max = (float) ($bucket['max'] ?? 0);
3099
3100
            if ($percentage >= $min && $percentage <= $max) {
3101
                // Propagate the model flag to the bucket
3102
                $bucket['display_score_name'] = (int) ($model['display_score_name'] ?? 0);
3103
                // Precompute label for convenience (optional)
3104
                $bucket['label'] = self::scoreLabel($bucket);
3105
                return $bucket;
3106
            }
3107
        }
3108
3109
        return null;
3110
    }
3111
3112
    private static function scoreLabel(array $row): string
3113
    {
3114
        $key = isset($row['name']) ? 'name' : (isset($row['variable']) ? 'variable' : null);
3115
        if (!$key) {
3116
            return '';
3117
        }
3118
        $value = (string) $row[$key];
3119
        return get_lang($value);
3120
    }
3121
3122
    /**
3123
     * @return array
3124
     */
3125
    public static function getCourseScoreModel(): array
3126
    {
3127
        $modelList = self::getScoreModels();
3128
        if (empty($modelList) || empty($modelList['models'])) {
3129
            return [];
3130
        }
3131
3132
        // Read the configured model id from course settings
3133
        $scoreModelId = (int) api_get_course_setting('score_model_id');
3134
3135
        // first available model
3136
        $selected = $modelList['models'][0];
3137
3138
        if ($scoreModelId !== -1) {
3139
            foreach ($modelList['models'] as $m) {
3140
                if ((int) ($m['id'] ?? 0) === $scoreModelId) {
3141
                    $selected = $m;
3142
                    break;
3143
                }
3144
            }
3145
        }
3146
3147
        // do NOT show name unless explicitly enabled
3148
        $selected['display_score_name'] = (int) ($selected['display_score_name'] ?? 0);
3149
3150
        return $selected;
3151
    }
3152
3153
    /**
3154
     * @return array
3155
     */
3156
    public static function getScoreModels()
3157
    {
3158
        return api_get_setting('exercise.score_grade_model', true);
3159
    }
3160
3161
    /**
3162
     * @param float  $score
3163
     * @param float  $weight
3164
     * @param string $passPercentage
3165
     *
3166
     * @return bool
3167
     */
3168
    public static function isSuccessExerciseResult($score, $weight, $passPercentage)
3169
    {
3170
        $percentage = float_format(
3171
            ($score / (0 != $weight ? $weight : 1)) * 100,
3172
            1
3173
        );
3174
        if (isset($passPercentage) && !empty($passPercentage)) {
3175
            if ($percentage >= $passPercentage) {
3176
                return true;
3177
            }
3178
        }
3179
3180
        return false;
3181
    }
3182
3183
    /**
3184
     * @param string $name
3185
     * @param $weight
3186
     * @param $selected
3187
     *
3188
     * @return bool
3189
     */
3190
    public static function addScoreModelInput(
3191
        FormValidator $form,
3192
        $name,
3193
        $weight,
3194
        $selected
3195
    ) {
3196
        $model = self::getCourseScoreModel();
3197
        if (empty($model)) {
3198
            return false;
3199
        }
3200
3201
        /** @var HTML_QuickForm_select $element */
3202
        $element = $form->createElement(
3203
            'select',
3204
            $name,
3205
            get_lang('Score'),
3206
            [],
3207
            ['class' => 'exercise_mark_select']
3208
        );
3209
3210
        foreach ($model['score_list'] as $item) {
3211
            $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2);
3212
            $label = self::getModelStyle($item, $i);
3213
            $attributes = [
3214
                'class' => $item['css_class'],
3215
            ];
3216
            if ($selected == $i) {
3217
                $attributes['selected'] = 'selected';
3218
            }
3219
            $element->addOption($label, $i, $attributes);
3220
        }
3221
        $form->addElement($element);
3222
    }
3223
3224
    /**
3225
     * @return string
3226
     */
3227
    public static function getJsCode()
3228
    {
3229
        // Filling the scores with the right colors.
3230
        $models = self::getCourseScoreModel();
3231
        $cssListToString = '';
3232
        if (!empty($models)) {
3233
            $cssList = array_column($models['score_list'], 'css_class');
3234
            $cssListToString = implode(' ', $cssList);
3235
        }
3236
3237
        if (empty($cssListToString)) {
3238
            return '';
3239
        }
3240
        $js = <<<EOT
3241
3242
        function updateSelect(element) {
3243
            var spanTag = element.parent().find('span.filter-option');
3244
            var value = element.val();
3245
            var selectId = element.attr('id');
3246
            var optionClass = $('#' + selectId + ' option[value="'+value+'"]').attr('class');
3247
            spanTag.removeClass('$cssListToString');
3248
            spanTag.addClass(optionClass);
3249
        }
3250
3251
        $(function() {
3252
            // Loading values
3253
            $('.exercise_mark_select').on('loaded.bs.select', function() {
3254
                updateSelect($(this));
3255
            });
3256
            // On change
3257
            $('.exercise_mark_select').on('changed.bs.select', function() {
3258
                updateSelect($(this));
3259
            });
3260
        });
3261
EOT;
3262
3263
        return $js;
3264
    }
3265
3266
    /**
3267
     * @param float  $score
3268
     * @param float  $weight
3269
     * @param string $pass_percentage
3270
     *
3271
     * @return string
3272
     */
3273
    public static function showSuccessMessage($score, $weight, $pass_percentage)
3274
    {
3275
        $res = '';
3276
        if (self::isPassPercentageEnabled($pass_percentage)) {
3277
            $isSuccess = self::isSuccessExerciseResult(
3278
                $score,
3279
                $weight,
3280
                $pass_percentage
3281
            );
3282
3283
            if ($isSuccess) {
3284
                $html = get_lang('Congratulations you passed the test!');
3285
                $icon = Display::getMdiIcon('check-circle', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Correct'));
3286
            } else {
3287
                $html = get_lang('You didn\'t reach the minimum score');
3288
                $icon = Display::getMdiIcon('alert', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Wrong'));
3289
            }
3290
            $html = Display::tag('h4', $html);
3291
            $html .= Display::tag(
3292
                'h5',
3293
                $icon,
3294
                ['style' => 'width:40px; padding:2px 10px 0px 0px']
3295
            );
3296
            $res = $html;
3297
        }
3298
3299
        return $res;
3300
    }
3301
3302
    /**
3303
     * Return true if pass_pourcentage activated (we use the pass pourcentage feature
3304
     * return false if pass_percentage = 0 (we don't use the pass pourcentage feature.
3305
     *
3306
     * @param $value
3307
     *
3308
     * @return bool
3309
     *              In this version, pass_percentage and show_success_message are disabled if
3310
     *              pass_percentage is set to 0
3311
     */
3312
    public static function isPassPercentageEnabled($value)
3313
    {
3314
        return $value > 0;
3315
    }
3316
3317
    /**
3318
     * Converts a numeric value in a percentage example 0.66666 to 66.67 %.
3319
     *
3320
     * @param $value
3321
     *
3322
     * @return float Converted number
3323
     */
3324
    public static function convert_to_percentage($value)
3325
    {
3326
        $return = '-';
3327
        if ('' != $value) {
3328
            $return = float_format($value * 100, 1).' %';
3329
        }
3330
3331
        return $return;
3332
    }
3333
3334
    /**
3335
     * Getting all active exercises from a course from a session
3336
     * (if a session_id is provided we will show all the exercises in the course +
3337
     * all exercises in the session).
3338
     *
3339
     * @param array  $course_info
3340
     * @param int    $session_id
3341
     * @param bool   $check_publication_dates
3342
     * @param string $search                  Search exercise name
3343
     * @param bool   $search_all_sessions     Search exercises in all sessions
3344
     * @param   int     0 = only inactive exercises
0 ignored issues
show
Documentation Bug introduced by
The doc comment 0 at position 0 could not be parsed: Unknown type name '0' at position 0 in 0.
Loading history...
3345
     *                  1 = only active exercises,
3346
     *                  2 = all exercises
3347
     *                  3 = active <> -1
3348
     *
3349
     * @return CQuiz[]
3350
     */
3351
    public static function get_all_exercises(
3352
        $course_info = null,
3353
        $session_id = 0,
3354
        $check_publication_dates = false,
3355
        $search = '',
3356
        $search_all_sessions = false,
3357
        $active = 2
3358
    ) {
3359
        $course_id = api_get_course_int_id();
3360
        if (!empty($course_info) && !empty($course_info['real_id'])) {
3361
            $course_id = $course_info['real_id'];
3362
        }
3363
3364
        if (-1 == $session_id) {
3365
            $session_id = 0;
3366
        }
3367
        $course = api_get_course_entity($course_id);
3368
        $session = api_get_session_entity($session_id);
3369
3370
        if (null === $course) {
3371
            return [];
3372
        }
3373
3374
        $repo = Container::getQuizRepository();
3375
3376
        return $repo->findAllByCourse($course, $session, (string) $search, $active)
3377
            ->getQuery()
3378
            ->getResult();
3379
    }
3380
3381
    /**
3382
     * Getting all exercises (active only or all)
3383
     * from a course from a session
3384
     * (if a session_id is provided we will show all the exercises in the
3385
     * course + all exercises in the session).
3386
     */
3387
    public static function get_all_exercises_for_course_id(
3388
        int $courseId,
3389
        int $sessionId = 0,
3390
        bool $onlyActiveExercises = true
3391
    ): array {
3392
        if ($courseId <= 0) {
3393
            return [];
3394
        }
3395
3396
        $course  = api_get_course_entity($courseId);
3397
        $session = api_get_session_entity($sessionId);
3398
3399
        $repo = Container::getQuizRepository();
3400
3401
        $qb = $repo->getResourcesByCourse($course, $session);
3402
3403
        $qb->andWhere('resourceLink.endVisibilityAt IS NULL');
3404
3405
        if ($onlyActiveExercises) {
3406
            $qb->andWhere('resourceLink.visibility = 2');
3407
        } else {
3408
            $qb->andWhere('resourceLink.visibility IN (0,2)');
3409
        }
3410
3411
        $qb->orderBy('resource.title', 'ASC');
3412
3413
        $exercises = $qb->getQuery()->getResult();
3414
3415
        $exerciseList = [];
3416
        foreach ($exercises as $exercise) {
3417
            $exerciseList[] = [
3418
                'iid' => $exercise->getIid(),
3419
                'title' => $exercise->getTitle(),
3420
            ];
3421
        }
3422
3423
        return $exerciseList;
3424
    }
3425
3426
    /**
3427
     * Gets the position of the score based in a given score (result/weight)
3428
     * and the exe_id based in the user list
3429
     * (NO Exercises in LPs ).
3430
     *
3431
     * @param float  $my_score      user score to be compared *attention*
3432
     *                              $my_score = score/weight and not just the score
3433
     * @param int    $my_exe_id     exe id of the exercise
3434
     *                              (this is necessary because if 2 students have the same score the one
3435
     *                              with the minor exe_id will have a best position, just to be fair and FIFO)
3436
     * @param int    $exercise_id
3437
     * @param string $course_code
3438
     * @param int    $session_id
3439
     * @param array  $user_list
3440
     * @param bool   $return_string
3441
     *
3442
     * @return int the position of the user between his friends in a course
3443
     *             (or course within a session)
3444
     */
3445
    public static function get_exercise_result_ranking(
3446
        $my_score,
3447
        $my_exe_id,
3448
        $exercise_id,
3449
        $course_code,
3450
        $session_id = 0,
3451
        $user_list = [],
3452
        $return_string = true
3453
    ) {
3454
        //No score given we return
3455
        if (is_null($my_score)) {
3456
            return '-';
3457
        }
3458
        if (empty($user_list)) {
3459
            return '-';
3460
        }
3461
3462
        $best_attempts = [];
3463
        foreach ($user_list as $user_data) {
3464
            $user_id = $user_data['user_id'];
3465
            $best_attempts[$user_id] = self::get_best_attempt_by_user(
3466
                $user_id,
3467
                $exercise_id,
3468
                $course_code,
3469
                $session_id
3470
            );
3471
        }
3472
3473
        if (empty($best_attempts)) {
3474
            return 1;
3475
        } else {
3476
            $position = 1;
3477
            $my_ranking = [];
3478
            foreach ($best_attempts as $user_id => $result) {
3479
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3480
                    $my_ranking[$user_id] = $result['score'] / $result['max_score'];
3481
                } else {
3482
                    $my_ranking[$user_id] = 0;
3483
                }
3484
            }
3485
            //if (!empty($my_ranking)) {
3486
            asort($my_ranking);
3487
            $position = count($my_ranking);
3488
            if (!empty($my_ranking)) {
3489
                foreach ($my_ranking as $user_id => $ranking) {
3490
                    if ($my_score >= $ranking) {
3491
                        if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) {
3492
                            $exe_id = $best_attempts[$user_id]['exe_id'];
3493
                            if ($my_exe_id < $exe_id) {
3494
                                $position--;
3495
                            }
3496
                        } else {
3497
                            $position--;
3498
                        }
3499
                    }
3500
                }
3501
            }
3502
            //}
3503
            $return_value = [
3504
                'position' => $position,
3505
                'count' => count($my_ranking),
3506
            ];
3507
3508
            if ($return_string) {
3509
                if (!empty($position) && !empty($my_ranking)) {
3510
                    $return_value = $position.'/'.count($my_ranking);
3511
                } else {
3512
                    $return_value = '-';
3513
                }
3514
            }
3515
3516
            return $return_value;
3517
        }
3518
    }
3519
3520
    /**
3521
     * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts
3522
     * (NO Exercises in LPs ) old functionality by attempt.
3523
     *
3524
     * @param   float   user score to be compared attention => score/weight
3525
     * @param   int     exe id of the exercise
3526
     * (this is necessary because if 2 students have the same score the one
3527
     * with the minor exe_id will have a best position, just to be fair and FIFO)
3528
     * @param   int     exercise id
3529
     * @param   string  course code
3530
     * @param   int     session id
3531
     * @param bool $return_string
3532
     *
3533
     * @return int the position of the user between his friends in a course (or course within a session)
3534
     */
3535
    public static function get_exercise_result_ranking_by_attempt(
3536
        $my_score,
3537
        $my_exe_id,
3538
        $exercise_id,
3539
        $courseId,
3540
        $session_id = 0,
3541
        $return_string = true
3542
    ) {
3543
        if (empty($session_id)) {
3544
            $session_id = 0;
3545
        }
3546
        if (is_null($my_score)) {
3547
            return '-';
3548
        }
3549
        $user_results = Event::get_all_exercise_results(
3550
            $exercise_id,
3551
            $courseId,
3552
            $session_id,
3553
            false
3554
        );
3555
        $position_data = [];
3556
        if (empty($user_results)) {
3557
            return 1;
3558
        } else {
3559
            $position = 1;
3560
            $my_ranking = [];
3561
            foreach ($user_results as $result) {
3562
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3563
                    $my_ranking[$result['exe_id']] = $result['score'] / $result['max_score'];
3564
                } else {
3565
                    $my_ranking[$result['exe_id']] = 0;
3566
                }
3567
            }
3568
            asort($my_ranking);
3569
            $position = count($my_ranking);
3570
            if (!empty($my_ranking)) {
3571
                foreach ($my_ranking as $exe_id => $ranking) {
3572
                    if ($my_score >= $ranking) {
3573
                        if ($my_score == $ranking) {
3574
                            if ($my_exe_id < $exe_id) {
3575
                                $position--;
3576
                            }
3577
                        } else {
3578
                            $position--;
3579
                        }
3580
                    }
3581
                }
3582
            }
3583
            $return_value = [
3584
                'position' => $position,
3585
                'count' => count($my_ranking),
3586
            ];
3587
3588
            if ($return_string) {
3589
                if (!empty($position) && !empty($my_ranking)) {
3590
                    return $position.'/'.count($my_ranking);
3591
                }
3592
            }
3593
3594
            return $return_value;
3595
        }
3596
    }
3597
3598
    /**
3599
     * Get the best attempt in a exercise (NO Exercises in LPs ).
3600
     *
3601
     * @param int $exercise_id
3602
     * @param int $courseId
3603
     * @param int $session_id
3604
     *
3605
     * @return array
3606
     */
3607
    public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id)
3608
    {
3609
        $user_results = Event::get_all_exercise_results(
3610
            $exercise_id,
3611
            $courseId,
3612
            $session_id,
3613
            false
3614
        );
3615
3616
        $best_score_data = [];
3617
        $best_score = 0;
3618
        if (!empty($user_results)) {
3619
            foreach ($user_results as $result) {
3620
                if (!empty($result['max_score']) &&
3621
                    0 != intval($result['max_score'])
3622
                ) {
3623
                    $score = $result['score'] / $result['max_score'];
3624
                    if ($score >= $best_score) {
3625
                        $best_score = $score;
3626
                        $best_score_data = $result;
3627
                    }
3628
                }
3629
            }
3630
        }
3631
3632
        return $best_score_data;
3633
    }
3634
3635
    /**
3636
     * Get the best score in a exercise (NO Exercises in LPs ).
3637
     *
3638
     * @param int $user_id
3639
     * @param int $exercise_id
3640
     * @param int $courseId
3641
     * @param int $session_id
3642
     *
3643
     * @return array
3644
     */
3645
    public static function get_best_attempt_by_user(
3646
        $user_id,
3647
        $exercise_id,
3648
        $courseId,
3649
        $session_id
3650
    ) {
3651
        $user_results = Event::get_all_exercise_results(
3652
            $exercise_id,
3653
            $courseId,
3654
            $session_id,
3655
            false,
3656
            $user_id
3657
        );
3658
        $best_score_data = [];
3659
        $best_score = 0;
3660
        if (!empty($user_results)) {
3661
            foreach ($user_results as $result) {
3662
                if (!empty($result['max_score']) && 0 != (float) $result['max_score']) {
3663
                    $score = $result['score'] / $result['max_score'];
3664
                    if ($score >= $best_score) {
3665
                        $best_score = $score;
3666
                        $best_score_data = $result;
3667
                    }
3668
                }
3669
            }
3670
        }
3671
3672
        return $best_score_data;
3673
    }
3674
3675
    /**
3676
     * Get average score (NO Exercises in LPs ).
3677
     *
3678
     * @param    int    exercise id
3679
     * @param int $courseId
3680
     * @param    int    session id
3681
     *
3682
     * @return float Average score
3683
     */
3684
    public static function get_average_score($exercise_id, $courseId, $session_id)
3685
    {
3686
        $user_results = Event::get_all_exercise_results(
3687
            $exercise_id,
3688
            $courseId,
3689
            $session_id
3690
        );
3691
        $avg_score = 0;
3692
        if (!empty($user_results)) {
3693
            foreach ($user_results as $result) {
3694
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3695
                    $score = $result['score'] / $result['max_score'];
3696
                    $avg_score += $score;
3697
                }
3698
            }
3699
            $avg_score = float_format($avg_score / count($user_results), 1);
3700
        }
3701
3702
        return $avg_score;
3703
    }
3704
3705
    /**
3706
     * Get average score by score (NO Exercises in LPs ).
3707
     *
3708
     * @param int $courseId
3709
     * @param    int    session id
3710
     *
3711
     * @return float Average score
3712
     */
3713
    public static function get_average_score_by_course($courseId, $session_id)
3714
    {
3715
        $user_results = Event::get_all_exercise_results_by_course(
3716
            $courseId,
3717
            $session_id,
3718
            false
3719
        );
3720
        $avg_score = 0;
3721
        if (!empty($user_results)) {
3722
            foreach ($user_results as $result) {
3723
                if (!empty($result['max_score']) && 0 != intval(
3724
                        $result['max_score']
3725
                    )
3726
                ) {
3727
                    $score = $result['score'] / $result['max_score'];
3728
                    $avg_score += $score;
3729
                }
3730
            }
3731
            // We assume that all max_score
3732
            $avg_score = $avg_score / count($user_results);
3733
        }
3734
3735
        return $avg_score;
3736
    }
3737
3738
    /**
3739
     * @param int $user_id
3740
     * @param int $courseId
3741
     * @param int $session_id
3742
     *
3743
     * @return float|int
3744
     */
3745
    public static function get_average_score_by_course_by_user(
3746
        $user_id,
3747
        $courseId,
3748
        $session_id
3749
    ) {
3750
        $user_results = Event::get_all_exercise_results_by_user(
3751
            $user_id,
3752
            $courseId,
3753
            $session_id
3754
        );
3755
        $avg_score = 0;
3756
        if (!empty($user_results)) {
3757
            foreach ($user_results as $result) {
3758
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3759
                    $score = $result['score'] / $result['max_score'];
3760
                    $avg_score += $score;
3761
                }
3762
            }
3763
            // We assume that all max_score
3764
            $avg_score = ($avg_score / count($user_results));
3765
        }
3766
3767
        return $avg_score;
3768
    }
3769
3770
    /**
3771
     * Get average score by score (NO Exercises in LPs ).
3772
     *
3773
     * @param int $exercise_id
3774
     * @param int $courseId
3775
     * @param int $session_id
3776
     * @param int $user_count
3777
     *
3778
     * @return float Best average score
3779
     */
3780
    public static function get_best_average_score_by_exercise(
3781
        $exercise_id,
3782
        $courseId,
3783
        $session_id,
3784
        $user_count
3785
    ) {
3786
        $user_results = Event::get_best_exercise_results_by_user(
3787
            $exercise_id,
3788
            $courseId,
3789
            $session_id
3790
        );
3791
        $avg_score = 0;
3792
        if (!empty($user_results)) {
3793
            foreach ($user_results as $result) {
3794
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3795
                    $score = $result['score'] / $result['max_score'];
3796
                    $avg_score += $score;
3797
                }
3798
            }
3799
            // We asumme that all max_score
3800
            if (!empty($user_count)) {
3801
                $avg_score = float_format($avg_score / $user_count, 1) * 100;
3802
            } else {
3803
                $avg_score = 0;
3804
            }
3805
        }
3806
3807
        return $avg_score;
3808
    }
3809
3810
    /**
3811
     * Get average score by score (NO Exercises in LPs ).
3812
     *
3813
     * @param int $exercise_id
3814
     * @param int $courseId
3815
     * @param int $session_id
3816
     *
3817
     * @return float Best average score
3818
     */
3819
    public static function getBestScoreByExercise(
3820
        $exercise_id,
3821
        $courseId,
3822
        $session_id
3823
    ) {
3824
        $user_results = Event::get_best_exercise_results_by_user(
3825
            $exercise_id,
3826
            $courseId,
3827
            $session_id
3828
        );
3829
        $avg_score = 0;
3830
        if (!empty($user_results)) {
3831
            foreach ($user_results as $result) {
3832
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3833
                    $score = $result['score'] / $result['max_score'];
3834
                    $avg_score += $score;
3835
                }
3836
            }
3837
        }
3838
3839
        return $avg_score;
3840
    }
3841
3842
    /**
3843
     * Get student results (only in completed exercises) stats by question.
3844
     *
3845
     * @throws \Doctrine\DBAL\Exception
3846
     */
3847
    public static function getStudentStatsByQuestion(
3848
        int $questionId,
3849
        int $exerciseId,
3850
        string $courseCode,
3851
        int $sessionId,
3852
        bool $onlyStudent = false
3853
    ): array
3854
    {
3855
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3856
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3857
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3858
3859
        $questionId = (int) $questionId;
3860
        $exerciseId = (int) $exerciseId;
3861
        $courseCode = Database::escape_string($courseCode);
3862
        $sessionId = (int) $sessionId;
3863
        $courseId = api_get_course_int_id($courseCode);
3864
3865
        $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average
3866
                FROM $trackExercises e ";
3867
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
3868
        if ($onlyStudent) {
3869
            $courseCondition = '';
3870
            if (empty($sessionId)) {
3871
                $courseCondition = "
3872
                INNER JOIN $courseUser c
3873
                ON (
3874
                    e.exe_user_id = c.user_id AND
3875
                    e.c_id = c.c_id AND
3876
                    c.status = ".STUDENT." AND
3877
                    relation_type <> 2
3878
                )";
3879
            } else {
3880
                $sessionRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3881
                $courseCondition = "
3882
            INNER JOIN $sessionRelCourse sc
3883
            ON (
3884
                        e.exe_user_id = sc.user_id AND
3885
                        e.c_id = sc.c_id AND
3886
                        e.session_id = sc.session_id AND
3887
                        sc.status = ".SessionEntity::STUDENT."
3888
                )";
3889
            }
3890
            $sql .= $courseCondition;
3891
        }
3892
        $sql .= "
3893
    		INNER JOIN $trackAttempt a
3894
    		ON (
3895
    		    a.exe_id = e.exe_id
3896
            )
3897
    		WHERE
3898
    		    exe_exo_id 	= $exerciseId AND
3899
                e.c_id = $courseId AND
3900
                question_id = $questionId AND
3901
                e.status = ''
3902
                $sessionCondition
3903
            LIMIT 1";
3904
        $result = Database::query($sql);
3905
        $return = [];
3906
        if ($result) {
3907
            $return = Database::fetch_assoc($result);
3908
        }
3909
3910
        return $return;
3911
    }
3912
3913
    /**
3914
     * Get the correct answer count for a fill blanks question.
3915
     *
3916
     * @param int $question_id
3917
     * @param int $exercise_id
3918
     *
3919
     * @return array
3920
     */
3921
    public static function getNumberStudentsFillBlanksAnswerCount(
3922
        $question_id,
3923
        $exercise_id
3924
    ) {
3925
        $listStudentsId = [];
3926
        $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3927
            api_get_course_id(),
3928
            true
3929
        );
3930
        foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3931
            $listStudentsId[] = $listStudentInfo['user_id'];
3932
        }
3933
3934
        $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3935
            $exercise_id,
3936
            $question_id,
3937
            $listStudentsId,
3938
            '1970-01-01',
3939
            '3000-01-01'
3940
        );
3941
3942
        $arrayCount = [];
3943
3944
        foreach ($listFillTheBlankResult as $resultCount) {
3945
            foreach ($resultCount as $index => $count) {
3946
                //this is only for declare the array index per answer
3947
                $arrayCount[$index] = 0;
3948
            }
3949
        }
3950
3951
        foreach ($listFillTheBlankResult as $resultCount) {
3952
            foreach ($resultCount as $index => $count) {
3953
                $count = (0 === $count) ? 1 : 0;
3954
                $arrayCount[$index] += $count;
3955
            }
3956
        }
3957
3958
        return $arrayCount;
3959
    }
3960
3961
    /**
3962
     * Get the number of questions with answers.
3963
     *
3964
     * @param int    $question_id
3965
     * @param int    $exercise_id
3966
     * @param string $course_code
3967
     * @param int    $session_id
3968
     * @param string $questionType
3969
     *
3970
     * @return int
3971
     */
3972
    public static function get_number_students_question_with_answer_count(
3973
        $question_id,
3974
        $exercise_id,
3975
        $course_code,
3976
        $session_id,
3977
        $questionType = ''
3978
    ) {
3979
        $track_exercises   = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3980
        $track_attempt     = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3981
        $courseUser        = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3982
        $courseTable       = Database::get_main_table(TABLE_MAIN_COURSE);
3983
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3984
3985
        $question_id = (int) $question_id;
3986
        $exercise_id = (int) $exercise_id;
3987
        $courseId    = (int) api_get_course_int_id($course_code);
3988
        $session_id  = (int) $session_id;
3989
3990
        if (in_array($questionType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION], true)) {
3991
            $listStudentsId     = [];
3992
            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(api_get_course_id(), true);
3993
            foreach ($listAllStudentInfo as $listStudentInfo) {
3994
                $listStudentsId[] = (int) $listStudentInfo['user_id'];
3995
            }
3996
3997
            $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3998
                $exercise_id,
3999
                $question_id,
4000
                $listStudentsId,
4001
                '1970-01-01',
4002
                '3000-01-01'
4003
            );
4004
4005
            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
4006
        }
4007
4008
        if (empty($session_id)) {
4009
            $courseCondition = "
4010
            INNER JOIN $courseUser cu
4011
                ON cu.c_id = c.id AND cu.user_id = exe_user_id";
4012
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4013
        } else {
4014
            $courseCondition = "
4015
            INNER JOIN $courseUserSession cu
4016
                ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4017
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4018
        }
4019
4020
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
4021
        $sql = "SELECT DISTINCT exe_user_id
4022
            FROM $track_exercises e
4023
            INNER JOIN $track_attempt a
4024
                ON (a.exe_id = e.exe_id AND a.c_id = e.c_id)
4025
            INNER JOIN $courseTable c
4026
                ON c.id = e.c_id
4027
            $courseCondition
4028
            WHERE
4029
                exe_exo_id  = $exercise_id AND
4030
                e.c_id      = $courseId AND
4031
                question_id = $question_id AND
4032
                answer <> '0' AND
4033
                e.status = ''
4034
                $courseConditionWhere
4035
                $sessionCondition
4036
    ";
4037
4038
        $result = Database::query($sql);
4039
4040
        return $result ? (int) Database::num_rows($result) : 0;
4041
    }
4042
4043
    /**
4044
     * Get number of answers to hotspot questions.
4045
     */
4046
    public static function getNumberStudentsAnswerHotspotCount(
4047
        int    $answerId,
4048
        int    $questionId,
4049
        int    $exerciseId,
4050
        string $courseCode,
4051
        int $sessionId
4052
    ): int
4053
    {
4054
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4055
        $trackHotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4056
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4057
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4058
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4059
4060
        $questionId = (int) $questionId;
4061
        $answerId = (int) $answerId;
4062
        $exerciseId = (int) $exerciseId;
4063
        $courseId = api_get_course_int_id($courseCode);
4064
        $sessionId = (int) $sessionId;
4065
4066
        if (empty($sessionId)) {
4067
            $courseCondition = "
4068
            INNER JOIN $courseUser cu
4069
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4070
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4071
        } else {
4072
            $courseCondition = "
4073
            INNER JOIN $courseUserSession cu
4074
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4075
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4076
        }
4077
4078
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
4079
        $sql = "SELECT DISTINCT exe_user_id
4080
                FROM $trackExercises e
4081
                INNER JOIN $trackHotspot a
4082
                ON (a.hotspot_exe_id = e.exe_id)
4083
                INNER JOIN $courseTable c
4084
                ON (a.c_id = c.id)
4085
                $courseCondition
4086
                WHERE
4087
                    exe_exo_id              = $exerciseId AND
4088
                    a.c_id 	= $courseId AND
4089
                    hotspot_answer_id       = $answerId AND
4090
                    hotspot_question_id     = $questionId AND
4091
                    hotspot_correct         =  1 AND
4092
                    e.status                = ''
4093
                    $courseConditionWhere
4094
                    $sessionCondition
4095
            ";
4096
        $result = Database::query($sql);
4097
        $return = 0;
4098
        if ($result) {
4099
            $return = Database::num_rows($result);
4100
        }
4101
4102
        return $return;
4103
    }
4104
4105
    /**
4106
     * @param int    $answer_id
4107
     * @param int    $question_id
4108
     * @param int    $exercise_id
4109
     * @param string $course_code
4110
     * @param int    $session_id
4111
     * @param string $question_type
4112
     * @param string $correct_answer
4113
     * @param string $current_answer
4114
     *
4115
     * @return int
4116
     */
4117
    public static function get_number_students_answer_count(
4118
        $answer_id,
4119
        $question_id,
4120
        $exercise_id,
4121
        $course_code,
4122
        $session_id,
4123
        $question_type = null,
4124
        $correct_answer = null,
4125
        $current_answer = null
4126
    ) {
4127
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4128
        $track_attempt   = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4129
        $courseTable     = Database::get_main_table(TABLE_MAIN_COURSE);
4130
        $courseUser      = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4131
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4132
4133
        $question_id = (int) $question_id;
4134
        $answer_id   = (int) $answer_id;
4135
        $exercise_id = (int) $exercise_id;
4136
        $courseId    = (int) api_get_course_int_id($course_code);
4137
        $session_id  = (int) $session_id;
4138
4139
        switch ($question_type) {
4140
            case FILL_IN_BLANKS:
4141
            case FILL_IN_BLANKS_COMBINATION:
4142
                $answer_condition = '';
4143
                $select_condition = ' e.exe_id, answer ';
4144
                break;
4145
            case MATCHING:
4146
            case MATCHING_COMBINATION:
4147
            case MATCHING_DRAGGABLE:
4148
            case MATCHING_DRAGGABLE_COMBINATION:
4149
            default:
4150
                $answer_condition = " answer = $answer_id AND ";
4151
                $select_condition = ' DISTINCT exe_user_id ';
4152
        }
4153
4154
        if (empty($session_id)) {
4155
            $courseCondition = "
4156
            INNER JOIN $courseUser cu
4157
                ON cu.c_id = e.c_id AND cu.user_id = e.exe_user_id";
4158
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4159
        } else {
4160
            $courseCondition = "
4161
            INNER JOIN $courseUserSession cu
4162
                ON (cu.c_id = e.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4163
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4164
        }
4165
4166
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
4167
        $sql = "SELECT $select_condition
4168
            FROM $track_exercises e
4169
            INNER JOIN $track_attempt a
4170
                ON (a.exe_id = e.exe_id AND a.c_id = e.c_id)
4171
            INNER JOIN $courseTable c
4172
                ON c.id = e.c_id
4173
            $courseCondition
4174
            WHERE
4175
                exe_exo_id = $exercise_id AND
4176
                e.c_id = $courseId AND
4177
                $answer_condition
4178
                question_id = $question_id AND
4179
                e.status = ''
4180
                $courseConditionWhere
4181
                $sessionCondition
4182
    ";
4183
4184
        $result = Database::query($sql);
4185
        $return = 0;
4186
        if ($result) {
4187
            switch ($question_type) {
4188
                case FILL_IN_BLANKS:
4189
                case FILL_IN_BLANKS_COMBINATION:
4190
                    $good_answers = 0;
4191
                    while ($row = Database::fetch_assoc($result)) {
4192
                        $fill_blank = self::check_fill_in_blanks(
4193
                            $correct_answer,
4194
                            $row['answer'],
4195
                            $current_answer
4196
                        );
4197
                        if (isset($fill_blank[$current_answer]) && 1 == (int) $fill_blank[$current_answer]) {
4198
                            $good_answers++;
4199
                        }
4200
                    }
4201
4202
                    return $good_answers;
4203
4204
                case MATCHING:
4205
                case MATCHING_COMBINATION:
4206
                case MATCHING_DRAGGABLE:
4207
                case MATCHING_DRAGGABLE_COMBINATION:
4208
                default:
4209
                    $return = Database::num_rows($result);
4210
            }
4211
        }
4212
4213
        return $return;
4214
    }
4215
4216
    /**
4217
     * Get the number of times an answer was selected.
4218
     */
4219
    public static function getCountOfAnswers(
4220
        int $answerId,
4221
        int $questionId,
4222
        int $exerciseId,
4223
        string $courseCode,
4224
        int $sessionId,
4225
        $questionType = null,
4226
    ): int
4227
    {
4228
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4229
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4230
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4231
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4232
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4233
4234
        $answerId = (int) $answerId;
4235
        $questionId = (int) $questionId;
4236
        $exerciseId = (int) $exerciseId;
4237
        $courseId = api_get_course_int_id($courseCode);
4238
        $sessionId = (int) $sessionId;
4239
        $return = 0;
4240
4241
        $answerCondition = match ($questionType) {
4242
            FILL_IN_BLANKS => '',
4243
            default => " answer = $answerId AND ",
4244
        };
4245
4246
        if (empty($sessionId)) {
4247
            $courseCondition = "
4248
            INNER JOIN $courseUser cu
4249
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
4250
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4251
        } else {
4252
            $courseCondition = "
4253
            INNER JOIN $courseUserSession cu
4254
            ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4255
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4256
        }
4257
4258
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
4259
        $sql = "SELECT count(a.answer) as total
4260
                FROM $trackExercises e
4261
                INNER JOIN $trackAttempt a
4262
                ON (
4263
                    a.exe_id = e.exe_id
4264
                )
4265
                INNER JOIN $courseTable c
4266
                ON c.id = e.c_id
4267
                $courseCondition
4268
                WHERE
4269
                    exe_exo_id = $exerciseId AND
4270
                    e.c_id = $courseId AND
4271
                    $answerCondition
4272
                    question_id = $questionId AND
4273
                    e.status = ''
4274
                    $courseConditionWhere
4275
                    $sessionCondition
4276
            ";
4277
        $result = Database::query($sql);
4278
        if ($result) {
4279
            $count = Database::fetch_array($result);
4280
            $return = (int) $count['total'];
4281
        }
4282
        return $return;
4283
    }
4284
4285
    /**
4286
     * @param array  $answer
4287
     * @param string $user_answer
4288
     *
4289
     * @return array
4290
     */
4291
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
4292
    {
4293
        // the question is encoded like this
4294
        // [A] B [C] D [E] F::10,10,10@1
4295
        // number 1 before the "@" means that is a switchable fill in blank question
4296
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4297
        // means that is a normal fill blank question
4298
        // first we explode the "::"
4299
        $pre_array = explode('::', $answer);
4300
        // is switchable fill blank or not
4301
        $last = count($pre_array) - 1;
4302
        $is_set_switchable = explode('@', $pre_array[$last]);
4303
        $switchable_answer_set = false;
4304
        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4305
            $switchable_answer_set = true;
4306
        }
4307
        $answer = '';
4308
        for ($k = 0; $k < $last; $k++) {
4309
            $answer .= $pre_array[$k];
4310
        }
4311
        // splits weightings that are joined with a comma
4312
        $answerWeighting = explode(',', $is_set_switchable[0]);
4313
4314
        // we save the answer because it will be modified
4315
        //$temp = $answer;
4316
        $temp = $answer;
4317
4318
        $answer = '';
4319
        $j = 0;
4320
        //initialise answer tags
4321
        $user_tags = $correct_tags = $real_text = [];
4322
        // the loop will stop at the end of the text
4323
        while (1) {
4324
            // quits the loop if there are no more blanks (detect '[')
4325
            if (false === ($pos = api_strpos($temp, '['))) {
4326
                // adds the end of the text
4327
                $answer = $temp;
4328
                $real_text[] = $answer;
4329
                break; //no more "blanks", quit the loop
4330
            }
4331
            // adds the piece of text that is before the blank
4332
            //and ends with '[' into a general storage array
4333
            $real_text[] = api_substr($temp, 0, $pos + 1);
4334
            $answer .= api_substr($temp, 0, $pos + 1);
4335
            //take the string remaining (after the last "[" we found)
4336
            $temp = api_substr($temp, $pos + 1);
4337
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4338
            if (false === ($pos = api_strpos($temp, ']'))) {
4339
                // adds the end of the text
4340
                $answer .= $temp;
4341
                break;
4342
            }
4343
4344
            $str = $user_answer;
4345
4346
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
4347
            $str = str_replace('\r\n', '', $str);
4348
            $choices = $arr[1];
4349
            $choice = [];
4350
            $check = false;
4351
            $i = 0;
4352
            foreach ($choices as $item) {
4353
                if ($current_answer === $item) {
4354
                    $check = true;
4355
                }
4356
                if ($check) {
4357
                    $choice[] = $item;
4358
                    $i++;
4359
                }
4360
                if (3 == $i) {
4361
                    break;
4362
                }
4363
            }
4364
            $tmp = api_strrpos($choice[$j], ' / ');
4365
4366
            if (false !== $tmp) {
4367
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
4368
            }
4369
4370
            $choice[$j] = trim($choice[$j]);
4371
4372
            //Needed to let characters ' and " to work as part of an answer
4373
            $choice[$j] = stripslashes($choice[$j]);
4374
4375
            $user_tags[] = api_strtolower($choice[$j]);
4376
            //put the contents of the [] answer tag into correct_tags[]
4377
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
4378
            $j++;
4379
            $temp = api_substr($temp, $pos + 1);
4380
        }
4381
4382
        $answer = '';
4383
        $real_correct_tags = $correct_tags;
4384
        $chosen_list = [];
4385
        $good_answer = [];
4386
4387
        for ($i = 0; $i < count($real_correct_tags); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4388
            if (!$switchable_answer_set) {
4389
                //needed to parse ' and " characters
4390
                $user_tags[$i] = stripslashes($user_tags[$i]);
4391
                if ($correct_tags[$i] == $user_tags[$i]) {
4392
                    $good_answer[$correct_tags[$i]] = 1;
4393
                } elseif (!empty($user_tags[$i])) {
4394
                    $good_answer[$correct_tags[$i]] = 0;
4395
                } else {
4396
                    $good_answer[$correct_tags[$i]] = 0;
4397
                }
4398
            } else {
4399
                // switchable fill in the blanks
4400
                if (in_array($user_tags[$i], $correct_tags)) {
4401
                    $correct_tags = array_diff($correct_tags, $chosen_list);
4402
                    $good_answer[$correct_tags[$i]] = 1;
4403
                } elseif (!empty($user_tags[$i])) {
4404
                    $good_answer[$correct_tags[$i]] = 0;
4405
                } else {
4406
                    $good_answer[$correct_tags[$i]] = 0;
4407
                }
4408
            }
4409
            // adds the correct word, followed by ] to close the blank
4410
            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4411
            if (isset($real_text[$i + 1])) {
4412
                $answer .= $real_text[$i + 1];
4413
            }
4414
        }
4415
4416
        return $good_answer;
4417
    }
4418
4419
    /**
4420
     * Return an HTML select menu with the student groups.
4421
     *
4422
     * @param string $name     is the name and the id of the <select>
4423
     * @param string $default  default value for option
4424
     * @param string $onchange
4425
     *
4426
     * @return string the html code of the <select>
4427
     */
4428
    public static function displayGroupMenu($name, $default, $onchange = "")
4429
    {
4430
        // check the default value of option
4431
        $tabSelected = [$default => " selected='selected' "];
4432
        $res = "<select name='$name' id='$name' onchange='".$onchange."' >";
4433
        $res .= "<option value='-1'".$tabSelected["-1"].">-- ".get_lang('All groups')." --</option>";
4434
        $res .= "<option value='0'".$tabSelected["0"].">- ".get_lang('Not in a group')." -</option>";
4435
        $groups = GroupManager::get_group_list();
4436
        $currentCatId = 0;
4437
        $countGroups = count($groups);
4438
        for ($i = 0; $i < $countGroups; $i++) {
4439
            $category = GroupManager::get_category_from_group($groups[$i]['iid']);
4440
            if ($category['id'] != $currentCatId) {
4441
                $res .= "<option value='-1' disabled='disabled'>".$category['title']."</option>";
4442
                $currentCatId = $category['id'];
4443
            }
4444
            $res .= "<option ".$tabSelected[$groups[$i]['id']]."style='margin-left:40px' value='".
4445
                $groups[$i]["iid"]."'>".
4446
                $groups[$i]["name"].
4447
                "</option>";
4448
        }
4449
        $res .= "</select>";
4450
4451
        return $res;
4452
    }
4453
4454
    /**
4455
     * @param int $exe_id
4456
     */
4457
    public static function create_chat_exercise_session($exe_id)
4458
    {
4459
        if (!isset($_SESSION['current_exercises'])) {
4460
            $_SESSION['current_exercises'] = [];
4461
        }
4462
        $_SESSION['current_exercises'][$exe_id] = true;
4463
    }
4464
4465
    /**
4466
     * @param int $exe_id
4467
     */
4468
    public static function delete_chat_exercise_session($exe_id)
4469
    {
4470
        if (isset($_SESSION['current_exercises'])) {
4471
            $_SESSION['current_exercises'][$exe_id] = false;
4472
        }
4473
    }
4474
4475
    /**
4476
     * Display the exercise results.
4477
     *
4478
     * @param Exercise $objExercise
4479
     * @param int      $exeId
4480
     * @param bool     $save_user_result save users results (true) or just show the results (false)
4481
     * @param string   $remainingMessage
4482
     * @param bool     $allowSignature
4483
     * @param bool     $allowExportPdf
4484
     * @param bool     $isExport
4485
     */
4486
    public static function displayQuestionListByAttempt(
4487
        $objExercise,
4488
        $exeId,
4489
        $save_user_result = false,
4490
        $remainingMessage = '',
4491
        $allowSignature = false,
4492
        $allowExportPdf = false,
4493
        $isExport = false
4494
    ) {
4495
        $origin = api_get_origin();
4496
        $courseId = api_get_course_int_id();
4497
        $courseCode = api_get_course_id();
4498
        $sessionId = api_get_session_id();
4499
4500
        // Getting attempt info
4501
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
4502
4503
        // Getting question list
4504
        $question_list = [];
4505
        $studentInfo = [];
4506
        if (!empty($exercise_stat_info['data_tracking'])) {
4507
            $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
4508
            $question_list = explode(',', $exercise_stat_info['data_tracking']);
4509
        } else {
4510
            // Try getting the question list only if save result is off
4511
            if (false == $save_user_result) {
4512
                $question_list = $objExercise->get_validated_question_list();
4513
            }
4514
            if (in_array(
4515
                $objExercise->getFeedbackType(),
4516
                [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4517
            )) {
4518
                $question_list = $objExercise->get_validated_question_list();
4519
            }
4520
        }
4521
4522
        if ($objExercise->getResultAccess()) {
4523
            if (false === $objExercise->hasResultsAccess($exercise_stat_info)) {
4524
                echo Display::return_message(
4525
                    sprintf(get_lang('You have passed the %s minutes limit to see the results.'), $objExercise->getResultsAccess())
4526
                );
4527
4528
                return false;
4529
            }
4530
4531
            if (!empty($objExercise->getResultAccess())) {
4532
                $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id;
4533
                echo $objExercise->returnTimeLeftDiv();
4534
                echo $objExercise->showSimpleTimeControl(
4535
                    $objExercise->getResultAccessTimeDiff($exercise_stat_info),
4536
                    $url
4537
                );
4538
            }
4539
        }
4540
4541
        $counter = 1;
4542
        $total_score = $total_weight = 0;
4543
        $exerciseContent = null;
4544
4545
        // Hide results
4546
        $show_results = false;
4547
        $show_only_score = false;
4548
        if (in_array($objExercise->results_disabled,
4549
            [
4550
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4551
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
4552
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4553
            ]
4554
        )) {
4555
            $show_results = true;
4556
        }
4557
4558
        if (in_array(
4559
            $objExercise->results_disabled,
4560
            [
4561
                RESULT_DISABLE_SHOW_SCORE_ONLY,
4562
                RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
4563
                RESULT_DISABLE_RANKING,
4564
            ]
4565
        )
4566
        ) {
4567
            $show_only_score = true;
4568
        }
4569
4570
        // Not display expected answer, but score, and feedback
4571
        $show_all_but_expected_answer = false;
4572
        if (RESULT_DISABLE_SHOW_SCORE_ONLY == $objExercise->results_disabled &&
4573
            EXERCISE_FEEDBACK_TYPE_END == $objExercise->getFeedbackType()
4574
        ) {
4575
            $show_all_but_expected_answer = true;
4576
            $show_results = true;
4577
            $show_only_score = false;
4578
        }
4579
4580
        $showTotalScoreAndUserChoicesInLastAttempt = true;
4581
        $showTotalScore = true;
4582
        $showQuestionScore = true;
4583
        $attemptResult = [];
4584
4585
        if (in_array(
4586
            $objExercise->results_disabled,
4587
            [
4588
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
4589
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
4590
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
4591
            ])
4592
        ) {
4593
            $show_only_score = true;
4594
            $show_results = true;
4595
            $numberAttempts = 0;
4596
            if ($objExercise->attempts > 0) {
4597
                $attempts = Event::getExerciseResultsByUser(
4598
                    api_get_user_id(),
4599
                    $objExercise->id,
4600
                    $courseId,
4601
                    $sessionId,
4602
                    $exercise_stat_info['orig_lp_id'],
4603
                    $exercise_stat_info['orig_lp_item_id'],
4604
                    'desc'
4605
                );
4606
                if ($attempts) {
4607
                    $numberAttempts = count($attempts);
4608
                }
4609
4610
                if ($save_user_result) {
4611
                    $numberAttempts++;
4612
                }
4613
4614
                $showTotalScore = false;
4615
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT == $objExercise->results_disabled) {
4616
                    $showTotalScore = true;
4617
                }
4618
                $showTotalScoreAndUserChoicesInLastAttempt = false;
4619
                if ($numberAttempts >= $objExercise->attempts) {
4620
                    $showTotalScore = true;
4621
                    $show_results = true;
4622
                    $show_only_score = false;
4623
                    $showTotalScoreAndUserChoicesInLastAttempt = true;
4624
                }
4625
4626
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $objExercise->results_disabled) {
4627
                    $showTotalScore = true;
4628
                    $show_results = true;
4629
                    $show_only_score = false;
4630
                    $showTotalScoreAndUserChoicesInLastAttempt = false;
4631
                    if ($numberAttempts >= $objExercise->attempts) {
4632
                        $showTotalScoreAndUserChoicesInLastAttempt = true;
4633
                    }
4634
4635
                    // Check if the current attempt is the last.
4636
                    if (false === $save_user_result && !empty($attempts)) {
4637
                        $showTotalScoreAndUserChoicesInLastAttempt = false;
4638
                        $position = 1;
4639
                        foreach ($attempts as $attempt) {
4640
                            if ($exeId == $attempt['exe_id']) {
4641
                                break;
4642
                            }
4643
                            $position++;
4644
                        }
4645
4646
                        if ($position == $objExercise->attempts) {
4647
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
4648
                        }
4649
                    }
4650
                }
4651
            }
4652
4653
            if (RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK ==
4654
                $objExercise->results_disabled
4655
            ) {
4656
                $show_only_score = false;
4657
                $show_results = true;
4658
                $show_all_but_expected_answer = false;
4659
                $showTotalScore = false;
4660
                $showQuestionScore = false;
4661
                if ($numberAttempts >= $objExercise->attempts) {
4662
                    $showTotalScore = true;
4663
                    $showQuestionScore = true;
4664
                }
4665
            }
4666
        }
4667
4668
        // When exporting to PDF hide feedback/comment/score show warning in hotspot.
4669
        if ($allowExportPdf && $isExport) {
4670
            $showTotalScore = false;
4671
            $showQuestionScore = false;
4672
            $objExercise->feedback_type = 2;
4673
            $objExercise->hideComment = true;
4674
            $objExercise->hideNoAnswer = true;
4675
            $objExercise->results_disabled = 0;
4676
            $objExercise->hideExpectedAnswer = true;
4677
            $show_results = true;
4678
        }
4679
4680
        if ('embeddable' !== $origin &&
4681
            !empty($exercise_stat_info['exe_user_id']) &&
4682
            !empty($studentInfo)
4683
        ) {
4684
            // Shows exercise header.
4685
            echo $objExercise->showExerciseResultHeader(
4686
                $studentInfo,
4687
                $exercise_stat_info,
4688
                $save_user_result,
4689
                $allowSignature,
4690
                $allowExportPdf
4691
            );
4692
        }
4693
4694
        $question_list_answers = [];
4695
        $category_list = [];
4696
        $loadChoiceFromSession = false;
4697
        $fromDatabase = true;
4698
        $exerciseResult = null;
4699
        $exerciseResultCoordinates = null;
4700
        $delineationResults = null;
4701
        if (true === $save_user_result && in_array(
4702
            $objExercise->getFeedbackType(),
4703
            [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4704
        )) {
4705
            $loadChoiceFromSession = true;
4706
            $fromDatabase = false;
4707
            $exerciseResult = Session::read('exerciseResult');
4708
            $exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
4709
            $delineationResults = Session::read('hotspot_delineation_result');
4710
            $delineationResults = isset($delineationResults[$objExercise->id]) ? $delineationResults[$objExercise->id] : null;
4711
        }
4712
4713
        $countPendingQuestions = 0;
4714
        $result = [];
4715
        $panelsByParent = [];
4716
        // Loop over all question to show results for each of them, one by one
4717
        if (!empty($question_list)) {
4718
            foreach ($question_list as $questionId) {
4719
                // Creates a temporary Question object
4720
                $objQuestionTmp = Question::read($questionId, $objExercise->course);
4721
                // This variable came from exercise_submit_modal.php
4722
                ob_start();
4723
                $choice = null;
4724
                $delineationChoice = null;
4725
                if ($loadChoiceFromSession) {
4726
                    $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null;
4727
                    $delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null;
4728
                }
4729
4730
                // We're inside *one* question. Go through each possible answer for this question
4731
                $result = $objExercise->manage_answer(
4732
                    $exeId,
4733
                    $questionId,
4734
                    $choice,
4735
                    'exercise_result',
4736
                    $exerciseResultCoordinates,
4737
                    $save_user_result,
4738
                    $fromDatabase,
4739
                    $show_results,
4740
                    $objExercise->selectPropagateNeg(),
4741
                    $delineationChoice,
4742
                    $showTotalScoreAndUserChoicesInLastAttempt
4743
                );
4744
4745
                if (empty($result)) {
4746
                    continue;
4747
                }
4748
4749
                $total_score += $result['score'];
4750
                $total_weight += $result['weight'];
4751
4752
                $question_list_answers[] = [
4753
                    'question' => $result['open_question'],
4754
                    'answer' => $result['open_answer'],
4755
                    'answer_type' => $result['answer_type'],
4756
                    'generated_oral_file' => $result['generated_oral_file'],
4757
                ];
4758
4759
                $my_total_score = $result['score'];
4760
                $my_total_weight = $result['weight'];
4761
                $scorePassed = self::scorePassed($my_total_score, $my_total_weight);
4762
4763
                // Category report
4764
                $category_was_added_for_this_test = false;
4765
                if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
4766
                    if (!isset($category_list[$objQuestionTmp->category]['score'])) {
4767
                        $category_list[$objQuestionTmp->category]['score'] = 0;
4768
                    }
4769
                    if (!isset($category_list[$objQuestionTmp->category]['total'])) {
4770
                        $category_list[$objQuestionTmp->category]['total'] = 0;
4771
                    }
4772
                    if (!isset($category_list[$objQuestionTmp->category]['total_questions'])) {
4773
                        $category_list[$objQuestionTmp->category]['total_questions'] = 0;
4774
                    }
4775
                    if (!isset($category_list[$objQuestionTmp->category]['passed'])) {
4776
                        $category_list[$objQuestionTmp->category]['passed'] = 0;
4777
                    }
4778
                    if (!isset($category_list[$objQuestionTmp->category]['wrong'])) {
4779
                        $category_list[$objQuestionTmp->category]['wrong'] = 0;
4780
                    }
4781
                    if (!isset($category_list[$objQuestionTmp->category]['no_answer'])) {
4782
                        $category_list[$objQuestionTmp->category]['no_answer'] = 0;
4783
                    }
4784
4785
                    $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
4786
                    $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
4787
                    if ($scorePassed) {
4788
                        // Only count passed if score is not empty
4789
                        if (!empty($my_total_score)) {
4790
                            $category_list[$objQuestionTmp->category]['passed']++;
4791
                        }
4792
                    } else {
4793
                        if ($result['user_answered']) {
4794
                            $category_list[$objQuestionTmp->category]['wrong']++;
4795
                        } else {
4796
                            $category_list[$objQuestionTmp->category]['no_answer']++;
4797
                        }
4798
                    }
4799
4800
                    $category_list[$objQuestionTmp->category]['total_questions']++;
4801
                    $category_was_added_for_this_test = true;
4802
                }
4803
                if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) {
4804
                    foreach ($objQuestionTmp->category_list as $category_id) {
4805
                        $category_list[$category_id]['score'] += $my_total_score;
4806
                        $category_list[$category_id]['total'] += $my_total_weight;
4807
                        $category_was_added_for_this_test = true;
4808
                    }
4809
                }
4810
4811
                // No category for this question!
4812
                if (false == $category_was_added_for_this_test) {
4813
                    if (!isset($category_list['none']['score'])) {
4814
                        $category_list['none']['score'] = 0;
4815
                    }
4816
                    if (!isset($category_list['none']['total'])) {
4817
                        $category_list['none']['total'] = 0;
4818
                    }
4819
4820
                    $category_list['none']['score'] += $my_total_score;
4821
                    $category_list['none']['total'] += $my_total_weight;
4822
                }
4823
4824
                if (0 == $objExercise->selectPropagateNeg() && $my_total_score < 0) {
4825
                    $my_total_score = 0;
4826
                }
4827
4828
                $comnt = null;
4829
                if ($show_results) {
4830
                    $comnt = Event::get_comments($exeId, $questionId);
4831
                    $teacherAudio = self::getOralFeedbackAudio(
4832
                        $exeId,
4833
                        $questionId
4834
                    );
4835
4836
                    if (!empty($comnt) || $teacherAudio) {
4837
                        echo '<b>'.get_lang('Feedback').'</b>';
4838
                    }
4839
4840
                    if (!empty($comnt)) {
4841
                        echo self::getFeedbackText($comnt);
4842
                    }
4843
4844
                    if ($teacherAudio) {
4845
                        echo $teacherAudio;
4846
                    }
4847
                }
4848
4849
                $calculatedScore = [
4850
                    'result' => self::show_score(
4851
                        $my_total_score,
4852
                        $my_total_weight,
4853
                        false
4854
                    ),
4855
                    'pass' => $scorePassed,
4856
                    'score' => $my_total_score,
4857
                    'weight' => $my_total_weight,
4858
                    'comments' => $comnt,
4859
                    'user_answered' => $result['user_answered'],
4860
                ];
4861
4862
                $score = [];
4863
                if ($show_results) {
4864
                    $score = $calculatedScore;
4865
                }
4866
                if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
4867
                    $reviewScore = [
4868
                        'score' => $my_total_score,
4869
                        'comments' => Event::get_comments($exeId, $questionId),
4870
                    ];
4871
                    $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore);
4872
                    if (false === $check) {
4873
                        $countPendingQuestions++;
4874
                    }
4875
                }
4876
4877
                $contents = ob_get_clean();
4878
                $questionContent = '';
4879
                if ($show_results) {
4880
                    $questionContent = '<div class="question-answer-result">';
4881
                    if (false === $showQuestionScore) {
4882
                        $score = [];
4883
                    }
4884
4885
                    // Shows question title an description
4886
                    $questionContent .= $objQuestionTmp->return_header(
4887
                        $objExercise,
4888
                        $counter,
4889
                        $score
4890
                    );
4891
                }
4892
                $counter++;
4893
                $questionContent .= $contents;
4894
                if ($show_results) {
4895
                    $questionContent .= '</div>';
4896
                }
4897
4898
                $calculatedScore['question_content'] = $questionContent;
4899
                $attemptResult[] = $calculatedScore;
4900
                $parentId = intval($objQuestionTmp->parent_id ?: 0);
4901
                $panelsByParent[$parentId][] = Display::panel($questionContent);
4902
            }
4903
4904
            foreach ($panelsByParent as $pid => $panels) {
4905
                if ($pid !== 0) {
4906
                    $mediaQ = Question::read($pid, $objExercise->course);
4907
                    echo '<div class="media-group">';
4908
                    echo '<div class="media-content">';
4909
                    ob_start();
4910
                    $objExercise->manage_answer(
4911
                        $exeId,
4912
                        $pid,
4913
                        null,
4914
                        'exercise_show',
4915
                        [],
4916
                        false,
4917
                        true,
4918
                        $show_results,
4919
                        $objExercise->selectPropagateNeg()
4920
                    );
4921
                    echo ob_get_clean();
4922
                    echo '</div>';
4923
                    if (!empty($mediaQ->description)) {
4924
                        echo '<div class="media-description">'
4925
                            . $mediaQ->description
4926
                            . '</div>';
4927
                    }
4928
                    echo '<div class="media-children">';
4929
                }
4930
4931
                foreach ($panels as $panelHtml) {
4932
                    echo $panelHtml;
4933
                }
4934
4935
                if ($pid !== 0) {
4936
                    echo '</div></div>';
4937
                }
4938
            }
4939
        }
4940
4941
        // Display text when test is finished #4074 and for LP #4227
4942
        $endOfMessage = $objExercise->getFinishText($total_score, $total_weight);
4943
        if (!empty($endOfMessage)) {
4944
            echo Display::div(
4945
                $endOfMessage,
4946
                ['id' => 'quiz_end_message']
4947
            );
4948
        }
4949
4950
        $totalScoreText = null;
4951
        $certificateBlock = '';
4952
        if (($show_results || $show_only_score) && $showTotalScore) {
4953
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4954
                echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('Your results').'</h1><br />';
4955
            }
4956
            $totalScoreText .= '<div class="question_row_score">';
4957
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4958
                $totalScoreText .= self::getQuestionDiagnosisRibbon(
4959
                    $objExercise,
4960
                    $total_score,
4961
                    $total_weight,
4962
                    true
4963
                );
4964
            } else {
4965
                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
4966
                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
4967
                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->getId());
4968
4969
                    if (!empty($formula)) {
4970
                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
4971
                        $total_weight = $pluginEvaluation->getMaxScore();
4972
                    }
4973
                }
4974
4975
                $totalScoreText .= self::getTotalScoreRibbon(
4976
                    $objExercise,
4977
                    $total_score,
4978
                    $total_weight,
4979
                    true,
4980
                    $countPendingQuestions
4981
                );
4982
            }
4983
            $totalScoreText .= '</div>';
4984
4985
            if (!empty($studentInfo)) {
4986
                $certificateBlock = self::generateAndShowCertificateBlock(
4987
                    $total_score,
4988
                    $total_weight,
4989
                    $objExercise,
4990
                    $studentInfo['id'],
4991
                    $courseId,
4992
                    $sessionId
4993
                );
4994
            }
4995
        }
4996
4997
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4998
            $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults(
4999
                $exeId,
5000
                $objExercise
5001
            );
5002
            echo $chartMultiAnswer;
5003
        }
5004
5005
        if (!empty($category_list) &&
5006
            ($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled)
5007
        ) {
5008
            // Adding total
5009
            $category_list['total'] = [
5010
                'score' => $total_score,
5011
                'total' => $total_weight,
5012
            ];
5013
            echo TestCategory::get_stats_table_by_attempt($objExercise, $category_list);
5014
        }
5015
5016
        if ($show_all_but_expected_answer) {
5017
            $exerciseContent .= Display::return_message(get_lang('Note: This test has been setup to hide the expected answers.'));
5018
        }
5019
5020
        // Remove audio auto play from questions on results page - refs BT#7939
5021
        $exerciseContent = preg_replace(
5022
            ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'],
5023
            '',
5024
            $exerciseContent
5025
        );
5026
5027
        echo $certificateBlock;
5028
5029
        // Ofaj change BT#11784
5030
        if (('true' === api_get_setting('exercise.quiz_show_description_on_results_page')) &&
5031
            !empty($objExercise->description)
5032
        ) {
5033
            echo Display::div($objExercise->description, ['class' => 'exercise_description']);
5034
        }
5035
5036
        echo $exerciseContent;
5037
        if (!$show_only_score) {
5038
            echo $totalScoreText;
5039
        }
5040
5041
        if ($save_user_result) {
5042
            // Tracking of results
5043
            if ($exercise_stat_info) {
5044
                $learnpath_id = $exercise_stat_info['orig_lp_id'];
5045
                $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'];
5046
                $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'];
5047
5048
                if (api_is_allowed_to_session_edit()) {
5049
                    Event::updateEventExercise(
5050
                        $exercise_stat_info['exe_id'],
5051
                        $objExercise->getId(),
5052
                        $total_score,
5053
                        $total_weight,
5054
                        $sessionId,
5055
                        $learnpath_id,
5056
                        $learnpath_item_id,
5057
                        $learnpath_item_view_id,
5058
                        $exercise_stat_info['exe_duration'],
5059
                        $question_list
5060
                    );
5061
5062
                    $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
5063
                    if ($allowStats) {
5064
                        $objExercise->generateStats(
5065
                            $objExercise->getId(),
5066
                            api_get_course_info(),
5067
                            $sessionId
5068
                        );
5069
                    }
5070
                }
5071
            }
5072
5073
            // Send notification at the end
5074
            if (!api_is_allowed_to_edit(null, true) &&
5075
                !api_is_excluded_user_type()
5076
            ) {
5077
                $objExercise->send_mail_notification_for_exam(
5078
                    'end',
5079
                    $question_list_answers,
5080
                    $origin,
5081
                    $exeId,
5082
                    $total_score,
5083
                    $total_weight
5084
                );
5085
            }
5086
        }
5087
5088
        if (in_array(
5089
            $objExercise->selectResultsDisabled(),
5090
            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
5091
        )) {
5092
            echo Display::page_header(get_lang('Ranking'), null, 'h4');
5093
            echo self::displayResultsInRanking(
5094
                $objExercise,
5095
                api_get_user_id(),
5096
                $courseId,
5097
                $sessionId
5098
            );
5099
        }
5100
5101
        if (!empty($remainingMessage)) {
5102
            echo Display::return_message($remainingMessage, 'normal', false);
5103
        }
5104
5105
        $failedAnswersCount = 0;
5106
        $wrongQuestionHtml = '';
5107
        $all = '';
5108
        foreach ($attemptResult as $item) {
5109
            if (false === $item['pass']) {
5110
                $failedAnswersCount++;
5111
                $wrongQuestionHtml .= $item['question_content'].'<br />';
5112
            }
5113
            $all .= $item['question_content'].'<br />';
5114
        }
5115
5116
        $passed = self::isPassPercentageAttemptPassed(
5117
            $objExercise,
5118
            $total_score,
5119
            $total_weight
5120
        );
5121
5122
        $percentage = 0;
5123
        if (!empty($total_weight)) {
5124
            $percentage = ($total_score / $total_weight) * 100;
5125
        }
5126
5127
        return [
5128
            'category_list' => $category_list,
5129
            'attempts_result_list' => $attemptResult, // array of results
5130
            'exercise_passed' => $passed, // boolean
5131
            'total_answers_count' => count($attemptResult), // int
5132
            'failed_answers_count' => $failedAnswersCount, // int
5133
            'failed_answers_html' => $wrongQuestionHtml,
5134
            'all_answers_html' => $all,
5135
            'total_score' => $total_score,
5136
            'total_weight' => $total_weight,
5137
            'total_percentage' => $percentage,
5138
            'count_pending_questions' => $countPendingQuestions,
5139
        ];
5140
    }
5141
5142
    /**
5143
     * Display the ranking of results in a exercise.
5144
     *
5145
     * @param Exercise $exercise
5146
     * @param int      $currentUserId
5147
     * @param int      $courseId
5148
     * @param int      $sessionId
5149
     *
5150
     * @return string
5151
     */
5152
    public static function displayResultsInRanking($exercise, $currentUserId, $courseId, $sessionId = 0)
5153
    {
5154
        $exerciseId = $exercise->iId;
5155
        $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId);
5156
5157
        $table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered']);
5158
        $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']);
5159
        $table->setHeaderContents(0, 1, get_lang('Username'));
5160
        $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']);
5161
        $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
5162
5163
        foreach ($data as $r => $item) {
5164
            if (!isset($item[1])) {
5165
                continue;
5166
            }
5167
            $selected = $item[1]->getId() == $currentUserId;
5168
5169
            foreach ($item as $c => $value) {
5170
                $table->setCellContents($r + 1, $c, $value);
5171
5172
                $attrClass = '';
5173
5174
                if (in_array($c, [0, 2])) {
5175
                    $attrClass = 'text-right';
5176
                } elseif (3 == $c) {
5177
                    $attrClass = 'text-center';
5178
                }
5179
5180
                if ($selected) {
5181
                    $attrClass .= ' warning';
5182
                }
5183
5184
                $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]);
5185
            }
5186
        }
5187
5188
        return $table->toHtml();
5189
    }
5190
5191
    /**
5192
     * Get the ranking for results in a exercise.
5193
     * Function used internally by ExerciseLib::displayResultsInRanking.
5194
     *
5195
     * @param int $exerciseId
5196
     * @param int $courseId
5197
     * @param int $sessionId
5198
     *
5199
     * @return array
5200
     */
5201
    public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0)
5202
    {
5203
        $em = Database::getManager();
5204
5205
        $dql = 'SELECT DISTINCT u.id FROM ChamiloCoreBundle:TrackEExercise te JOIN te.user u WHERE te.quiz = :id AND te.course = :cId';
5206
        $dql .= api_get_session_condition($sessionId, true, false, 'te.session');
5207
5208
        $result = $em
5209
            ->createQuery($dql)
5210
            ->setParameters(['id' => $exerciseId, 'cId' => $courseId])
5211
            ->getScalarResult();
5212
5213
        $data = [];
5214
5215
        foreach ($result as $item) {
5216
            $attempt = self::get_best_attempt_by_user($item['id'], $exerciseId, $courseId, $sessionId);
5217
            if (!empty($attempt) && isset($attempt['score']) && isset($attempt['exe_date'])) {
5218
                $data[] = $attempt;
5219
            }
5220
        }
5221
5222
        if (empty($data)) {
5223
            return [];
5224
        }
5225
5226
        usort(
5227
            $data,
5228
            function ($a, $b) {
5229
                if ($a['score'] != $b['score']) {
5230
                    return $a['score'] > $b['score'] ? -1 : 1;
5231
                }
5232
5233
                if ($a['exe_date'] != $b['exe_date']) {
5234
                    return $a['exe_date'] < $b['exe_date'] ? -1 : 1;
5235
                }
5236
5237
                return 0;
5238
            }
5239
        );
5240
5241
        // flags to display the same position in case of tie
5242
        $lastScore = $data[0]['score'];
5243
        $position = 1;
5244
        $data = array_map(
5245
            function ($item) use (&$lastScore, &$position) {
5246
                if ($item['score'] < $lastScore) {
5247
                    $position++;
5248
                }
5249
5250
                $lastScore = $item['score'];
5251
5252
                return [
5253
                    $position,
5254
                    api_get_user_entity($item['exe_user_id']),
5255
                    self::show_score($item['score'], $item['max_score'], true, true, true),
5256
                    api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT),
5257
                ];
5258
            },
5259
            $data
5260
        );
5261
5262
        return $data;
5263
    }
5264
5265
    /**
5266
     * Get a special ribbon on top of "degree of certainty" questions (
5267
     * variation from getTotalScoreRibbon() for other question types).
5268
     *
5269
     * @param Exercise $objExercise
5270
     * @param float    $score
5271
     * @param float    $weight
5272
     * @param bool     $checkPassPercentage
5273
     *
5274
     * @return string
5275
     */
5276
    public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false)
5277
    {
5278
        $displayChartDegree = true;
5279
        $ribbon = $displayChartDegree ? '<div class="ribbon">' : '';
5280
5281
        if ($checkPassPercentage) {
5282
            $passPercentage = $objExercise->selectPassPercentage();
5283
            $isSuccess = self::isSuccessExerciseResult($score, $weight, $passPercentage);
5284
            // Color the final test score if pass_percentage activated
5285
            $ribbonTotalSuccessOrError = '';
5286
            if (self::isPassPercentageEnabled($passPercentage)) {
5287
                if ($isSuccess) {
5288
                    $ribbonTotalSuccessOrError = ' ribbon-total-success';
5289
                } else {
5290
                    $ribbonTotalSuccessOrError = ' ribbon-total-error';
5291
                }
5292
            }
5293
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total '.$ribbonTotalSuccessOrError.'">' : '';
5294
        } else {
5295
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total">' : '';
5296
        }
5297
5298
        if ($displayChartDegree) {
5299
            $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
5300
            $ribbon .= self::show_score($score, $weight, false, true);
5301
            $ribbon .= '</h3>';
5302
            $ribbon .= '</div>';
5303
        }
5304
5305
        if ($checkPassPercentage) {
5306
            $ribbon .= self::showSuccessMessage(
5307
                $score,
5308
                $weight,
5309
                $objExercise->selectPassPercentage()
5310
            );
5311
        }
5312
5313
        $ribbon .= $displayChartDegree ? '</div>' : '';
5314
5315
        return $ribbon;
5316
    }
5317
5318
    public static function isPassPercentageAttemptPassed($objExercise, $score, $weight)
5319
    {
5320
        $passPercentage = $objExercise->selectPassPercentage();
5321
5322
        return self::isSuccessExerciseResult($score, $weight, $passPercentage);
5323
    }
5324
5325
    /**
5326
     * @param float $score
5327
     * @param float $weight
5328
     * @param bool  $checkPassPercentage
5329
     * @param int   $countPendingQuestions
5330
     *
5331
     * @return string
5332
     */
5333
    public static function getTotalScoreRibbon(
5334
        Exercise $objExercise,
5335
        $score,
5336
        $weight,
5337
        $checkPassPercentage = false,
5338
        $countPendingQuestions = 0
5339
    ) {
5340
        $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score');
5341
        if (1 === $hide) {
5342
            return '';
5343
        }
5344
5345
        $passPercentage = $objExercise->selectPassPercentage();
5346
        $ribbon = '<div class="title-score">';
5347
        if ($checkPassPercentage) {
5348
            $isSuccess = self::isSuccessExerciseResult(
5349
                $score,
5350
                $weight,
5351
                $passPercentage
5352
            );
5353
            // Color the final test score if pass_percentage activated
5354
            $class = '';
5355
            if (self::isPassPercentageEnabled($passPercentage)) {
5356
                if ($isSuccess) {
5357
                    $class = ' ribbon-total-success';
5358
                } else {
5359
                    $class = ' ribbon-total-error';
5360
                }
5361
            }
5362
            $ribbon .= '<div class="total '.$class.'">';
5363
        } else {
5364
            $ribbon .= '<div class="total">';
5365
        }
5366
        $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
5367
        $ribbon .= self::show_score($score, $weight, false, true);
5368
        $ribbon .= '</h3>';
5369
        $ribbon .= '</div>';
5370
        if ($checkPassPercentage) {
5371
            $ribbon .= self::showSuccessMessage(
5372
                $score,
5373
                $weight,
5374
                $passPercentage
5375
            );
5376
        }
5377
        $ribbon .= '</div>';
5378
5379
        if (!empty($countPendingQuestions)) {
5380
            $ribbon .= '<br />';
5381
            $ribbon .= Display::return_message(
5382
                sprintf(
5383
                    get_lang('Temporary score: %s open question(s) not corrected yet.'),
5384
                    $countPendingQuestions
5385
                ),
5386
                'warning'
5387
            );
5388
        }
5389
5390
        return $ribbon;
5391
    }
5392
5393
    /**
5394
     * @param int $countLetter
5395
     *
5396
     * @return mixed
5397
     */
5398
    public static function detectInputAppropriateClass($countLetter)
5399
    {
5400
        $limits = [
5401
            0 => 'input-mini',
5402
            10 => 'input-mini',
5403
            15 => 'input-medium',
5404
            20 => 'input-xlarge',
5405
            40 => 'input-xlarge',
5406
            60 => 'input-xxlarge',
5407
            100 => 'input-xxlarge',
5408
            200 => 'input-xxlarge',
5409
        ];
5410
5411
        foreach ($limits as $size => $item) {
5412
            if ($countLetter <= $size) {
5413
                return $item;
5414
            }
5415
        }
5416
5417
        return $limits[0];
5418
    }
5419
5420
    /**
5421
     * @param int    $senderId
5422
     * @param array  $course_info
5423
     * @param string $test
5424
     * @param string $url
5425
     *
5426
     * @return string
5427
     */
5428
    public static function getEmailNotification($senderId, $course_info, $test, $url)
5429
    {
5430
        $teacher_info = api_get_user_info($senderId);
5431
        $fromName = api_get_person_name(
5432
            $teacher_info['firstname'],
5433
            $teacher_info['lastname'],
5434
            null,
5435
            PERSON_NAME_EMAIL_ADDRESS
5436
        );
5437
5438
        $params = [
5439
            'course_title' => Security::remove_XSS($course_info['name']),
5440
            'test_title' => Security::remove_XSS($test),
5441
            'url' => $url,
5442
            'teacher_name' => $fromName,
5443
        ];
5444
5445
        return Container::getTwig()->render(
5446
            '@ChamiloCore/Mailer/Exercise/result_alert_body.html.twig',
5447
            $params
5448
        );
5449
    }
5450
5451
    /**
5452
     * @return string
5453
     */
5454
    public static function getNotCorrectedYetText()
5455
    {
5456
        return Display::return_message(get_lang('This answer has not yet been corrected. Meanwhile, your score for this question is set to 0, affecting the total score.'), 'warning');
5457
    }
5458
5459
    /**
5460
     * @param string $message
5461
     *
5462
     * @return string
5463
     */
5464
    public static function getFeedbackText($message)
5465
    {
5466
        return Display::return_message($message, 'warning', false);
5467
    }
5468
5469
    /**
5470
     * Get the recorder audio component for save a teacher audio feedback.
5471
     *
5472
     * @param int $attemptId
5473
     * @param int $questionId
5474
     *
5475
     * @return string
5476
     */
5477
    public static function getOralFeedbackForm($attemptId, $questionId)
5478
    {
5479
        $view = new Template('', false, false, false, false, false, false);
5480
        $view->assign('type', Asset::EXERCISE_FEEDBACK);
5481
        $view->assign('question_id', $questionId);
5482
        $view->assign('t_exercise_id', $attemptId);
5483
        $template = $view->get_template('exercise/oral_expression.html.twig');
5484
5485
        return $view->fetch($template);
5486
    }
5487
5488
    /**
5489
     * Retrieves the generated audio files for an oral question in an exercise attempt.
5490
     *
5491
     * @param int  $trackExerciseId The ID of the tracked exercise.
5492
     * @param int  $questionId      The ID of the question.
5493
     * @param bool $returnUrls      (Optional) If set to true, only the URLs of the audio files are returned. Default is false.
5494
     *
5495
     * @return array|string If $returnUrls is true, returns an array of URLs of the audio files. Otherwise, returns an HTML string with audio tags.
5496
     */
5497
    public static function getOralFileAudio(int $trackExerciseId, int $questionId, bool $returnUrls = false): array|string
5498
    {
5499
        /** @var TrackEExercise $trackExercise */
5500
        $trackExercise = Container::getTrackEExerciseRepository()->find($trackExerciseId);
5501
5502
        if (null === $trackExercise) {
5503
            return $returnUrls ? [] : '';
5504
        }
5505
5506
        $questionAttempt = $trackExercise->getAttemptByQuestionId($questionId);
5507
5508
        if (null === $questionAttempt) {
5509
            return $returnUrls ? [] : '';
5510
        }
5511
5512
        $basePath = rtrim(api_get_path(WEB_PATH), '/');
5513
        $assetRepo = Container::getAssetRepository();
5514
5515
        if ($returnUrls) {
5516
            $urls = [];
5517
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5518
                $urls[] = $basePath.$assetRepo->getAssetUrl($attemptFile->getAsset());
5519
            }
5520
5521
            return $urls;
5522
        } else {
5523
            $html = '';
5524
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5525
                $html .= Display::tag(
5526
                    'audio',
5527
                    '',
5528
                    [
5529
                        'src' => $basePath.$assetRepo->getAssetUrl($attemptFile->getAsset()),
5530
                        'controls' => '',
5531
                    ]
5532
                );
5533
            }
5534
5535
            return $html;
5536
        }
5537
    }
5538
5539
    /**
5540
     * Get the audio component for a teacher audio feedback.
5541
     */
5542
    public static function getOralFeedbackAudio(int $attemptId, int $questionId): string
5543
    {
5544
        /** @var TrackEExercise $tExercise */
5545
        $tExercise = Container::getTrackEExerciseRepository()->find($attemptId);
5546
5547
        if (null === $tExercise) {
5548
            return '';
5549
        }
5550
5551
        $qAttempt = $tExercise->getAttemptByQuestionId($questionId);
5552
5553
        if (null === $qAttempt) {
5554
            return '';
5555
        }
5556
5557
        $html = '';
5558
5559
        $assetRepo = Container::getAssetRepository();
5560
5561
        foreach ($qAttempt->getAttemptFeedbacks() as $attemptFeedback) {
5562
            $html .= Display::tag(
5563
                'audio',
5564
                '',
5565
                [
5566
                    'src' => $assetRepo->getAssetUrl($attemptFeedback->getAsset()),
5567
                    'controls' => '',
5568
                ]
5569
5570
            );
5571
        }
5572
5573
        return $html;
5574
    }
5575
5576
    public static function getUploadAnswerFiles(int $trackExerciseId, int $questionId, bool $returnUrls = false)
5577
    {
5578
        $trackExercise = Container::getTrackEExerciseRepository()->find($trackExerciseId);
5579
        if (!$trackExercise) { return $returnUrls ? [] : ''; }
5580
        $attempt = $trackExercise->getAttemptByQuestionId($questionId);
5581
        if (!$attempt) { return $returnUrls ? [] : ''; }
5582
5583
        $assetRepo = Container::getAssetRepository();
5584
        $basePath = rtrim(api_get_path(WEB_PATH), '/');
5585
5586
        if ($returnUrls) {
5587
            $urls = [];
5588
            foreach ($attempt->getAttemptFiles() as $af) {
5589
                $urls[] = $basePath.$assetRepo->getAssetUrl($af->getAsset());
5590
            }
5591
            return $urls;
5592
        }
5593
5594
        $html = '';
5595
        foreach ($attempt->getAttemptFiles() as $af) {
5596
            $url = $basePath.$assetRepo->getAssetUrl($af->getAsset());
5597
            $html .= Display::url(basename($url), $url, ['target' => '_blank']).'<br />';
5598
        }
5599
        return $html;
5600
    }
5601
5602
    public static function getNotificationSettings(): array
5603
    {
5604
        return [
5605
            2 => get_lang('Paranoid: E-mail teacher when a student starts an exercise'),
5606
            1 => get_lang('Aware: E-mail teacher when a student ends an exercise'), // default
5607
            3 => get_lang('Relaxed open: E-mail teacher when a student ends an exercise, only if an open question is answered'),
5608
            4 => get_lang('Relaxed audio: E-mail teacher when a student ends an exercise, only if an oral question is answered'),
5609
        ];
5610
    }
5611
5612
    /**
5613
     * Get the additional actions added in exercise_additional_teacher_modify_actions configuration.
5614
     *
5615
     * @param int $exerciseId
5616
     * @param int $iconSize
5617
     *
5618
     * @return string
5619
     */
5620
    public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL)
5621
    {
5622
        $additionalActions = api_get_setting('exercise.exercise_additional_teacher_modify_actions', true) ?: [];
5623
        $actions = [];
5624
5625
        if (is_array($additionalActions)) {
5626
            foreach ($additionalActions as $additionalAction) {
5627
                $actions[] = call_user_func(
5628
                    $additionalAction,
5629
                    $exerciseId,
5630
                    $iconSize
5631
                );
5632
            }
5633
        }
5634
5635
        return implode(PHP_EOL, $actions);
5636
    }
5637
5638
    /**
5639
     * @param int $userId
5640
     * @param int $courseId
5641
     * @param int $sessionId
5642
     *
5643
     * @return int
5644
     */
5645
    public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId)
5646
    {
5647
        $em = Database::getManager();
5648
5649
        if (empty($sessionId)) {
5650
            $sessionId = null;
5651
        }
5652
5653
        $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true);
5654
5655
        $result = $em
5656
            ->createQuery('
5657
                SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
5658
                WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
5659
                    AND ea.tms > :time
5660
            ')
5661
            ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
5662
            ->getSingleScalarResult();
5663
5664
        return $result;
5665
    }
5666
5667
    /**
5668
     * @param int $userId
5669
     * @param int $numberOfQuestions
5670
     * @param int $courseId
5671
     * @param int $sessionId
5672
     *
5673
     * @throws \Doctrine\ORM\Query\QueryException
5674
     *
5675
     * @return bool
5676
     */
5677
    public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId)
5678
    {
5679
        $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day');
5680
5681
        if ($questionsLimitPerDay <= 0) {
5682
            return false;
5683
        }
5684
5685
        $midnightTime = ChamiloHelper::getServerMidnightTime();
5686
5687
        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
5688
            $midnightTime,
5689
            $userId,
5690
            $courseId,
5691
            $sessionId
5692
        );
5693
5694
        return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
5695
    }
5696
5697
    /**
5698
     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
5699
     * By making sure it is set on one question per page and it only contains unique-answer or multiple-answer questions
5700
     * or unique-answer image. And that the exam does not have immediate feedback.
5701
     *
5702
     * @return bool
5703
     */
5704
    public static function isQuizEmbeddable(CQuiz $exercise)
5705
    {
5706
        $em = Database::getManager();
5707
5708
        if (ONE_PER_PAGE != $exercise->getType() ||
5709
            in_array($exercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
5710
        ) {
5711
            return false;
5712
        }
5713
5714
        $countAll = $em
5715
            ->createQuery('SELECT COUNT(qq)
5716
                FROM ChamiloCourseBundle:CQuizQuestion qq
5717
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5718
                   WITH qq.iid = qrq.question
5719
                WHERE qrq.quiz = :id'
5720
            )
5721
            ->setParameter('id', $exercise->getIid())
5722
            ->getSingleScalarResult();
5723
5724
        $countOfAllowed = $em
5725
            ->createQuery('SELECT COUNT(qq)
5726
                FROM ChamiloCourseBundle:CQuizQuestion qq
5727
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5728
                   WITH qq.iid = qrq.question
5729
                WHERE qrq.quiz = :id AND qq.type IN (:types)'
5730
            )
5731
            ->setParameters(
5732
                [
5733
                    'id' => $exercise->getIid(),
5734
                    'types' => [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE],
5735
                ]
5736
            )
5737
            ->getSingleScalarResult();
5738
5739
        return $countAll === $countOfAllowed;
5740
    }
5741
5742
    /**
5743
     * Generate a certificate linked to current quiz and.
5744
     * Return the HTML block with links to download and view the certificate.
5745
     *
5746
     * @param float $totalScore
5747
     * @param float $totalWeight
5748
     * @param int   $studentId
5749
     * @param int   $courseId
5750
     * @param int   $sessionId
5751
     *
5752
     * @return string
5753
     */
5754
    public static function generateAndShowCertificateBlock(
5755
        $totalScore,
5756
        $totalWeight,
5757
        Exercise $objExercise,
5758
        $studentId,
5759
        $courseId,
5760
        $sessionId = 0
5761
    ) {
5762
        if (('true' !== api_get_setting('exercise.quiz_generate_certificate_ending')) ||
5763
            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
5764
        ) {
5765
            return '';
5766
        }
5767
5768
        $repo = Container::getGradeBookCategoryRepository();
5769
        /** @var GradebookCategory $category */
5770
        $category = $repo->findOneBy(
5771
            ['course' => $courseId, 'session' => $sessionId]
5772
        );
5773
5774
        if (null === $category) {
5775
            return '';
5776
        }
5777
5778
        /*$category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
5779
        if (empty($category)) {
5780
            return '';
5781
        }*/
5782
        $categoryId = $category->getId();
5783
        /*$link = LinkFactory::load(
5784
            null,
5785
            null,
5786
            $objExercise->getId(),
5787
            null,
5788
            $courseCode,
5789
            $categoryId
5790
        );*/
5791
5792
        if (empty($category->getLinks()->count())) {
5793
            return '';
5794
        }
5795
5796
        $resourceDeletedMessage = Category::show_message_resource_delete($courseId);
5797
        if (!empty($resourceDeletedMessage) || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
5798
            return '';
5799
        }
5800
5801
        $certificate = Category::generateUserCertificate($category, $studentId);
5802
        if (!is_array($certificate)) {
5803
            return '';
5804
        }
5805
5806
        return Category::getDownloadCertificateBlock($certificate);
5807
    }
5808
5809
    /**
5810
     * @param int $exerciseId
5811
     */
5812
    public static function getExerciseTitleById($exerciseId)
5813
    {
5814
        $em = Database::getManager();
5815
5816
        return $em
5817
            ->createQuery('SELECT cq.title
5818
                FROM ChamiloCourseBundle:CQuiz cq
5819
                WHERE cq.iid = :iid'
5820
            )
5821
            ->setParameter('iid', $exerciseId)
5822
            ->getSingleScalarResult();
5823
    }
5824
5825
    /**
5826
     * @param int $exeId      ID from track_e_exercises
5827
     * @param int $userId     User ID
5828
     * @param int $exerciseId Exercise ID
5829
     * @param int $courseId   Optional. Coure ID.
5830
     *
5831
     * @return TrackEExercise|null
5832
     */
5833
    public static function recalculateResult($exeId, $userId, $exerciseId, $courseId = 0)
5834
    {
5835
        if (empty($userId) || empty($exerciseId)) {
5836
            return null;
5837
        }
5838
5839
        $em = Database::getManager();
5840
        /** @var TrackEExercise $trackedExercise */
5841
        $trackedExercise = $em->getRepository(TrackEExercise::class)->find($exeId);
5842
5843
        if (empty($trackedExercise)) {
5844
            return null;
5845
        }
5846
5847
        if ($trackedExercise->getUser()->getId() != $userId ||
5848
            $trackedExercise->getQuiz()?->getIid() != $exerciseId
5849
        ) {
5850
            return null;
5851
        }
5852
5853
        $questionList = $trackedExercise->getDataTracking();
5854
5855
        if (empty($questionList)) {
5856
            return null;
5857
        }
5858
5859
        $questionList = explode(',', $questionList);
5860
5861
        $exercise = new Exercise($courseId);
5862
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : [];
5863
5864
        if (false === $exercise->read($exerciseId)) {
5865
            return null;
5866
        }
5867
5868
        $totalScore = 0;
5869
        $totalWeight = 0;
5870
5871
        $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
5872
5873
        $formula = 'true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)
5874
            ? $pluginEvaluation->getFormulaForExercise($exerciseId)
5875
            : 0;
5876
5877
        if (empty($formula)) {
5878
            foreach ($questionList as $questionId) {
5879
                $question = Question::read($questionId, $courseInfo);
5880
5881
                if (false === $question) {
5882
                    continue;
5883
                }
5884
5885
                $totalWeight += $question->selectWeighting();
5886
5887
                // We're inside *one* question. Go through each possible answer for this question
5888
                $result = $exercise->manage_answer(
5889
                    $exeId,
5890
                    $questionId,
5891
                    [],
5892
                    'exercise_result',
5893
                    [],
5894
                    false,
5895
                    true,
5896
                    false,
5897
                    $exercise->selectPropagateNeg(),
5898
                    [],
5899
                    [],
5900
                    true
5901
                );
5902
5903
                //  Adding the new score.
5904
                $totalScore += $result['score'];
5905
            }
5906
        } else {
5907
            $totalScore = $pluginEvaluation->getResultWithFormula($exeId, $formula);
5908
            $totalWeight = $pluginEvaluation->getMaxScore();
5909
        }
5910
5911
        $trackedExercise
5912
            ->setScore($totalScore)
5913
            ->setMaxScore($totalWeight);
5914
5915
        $em->persist($trackedExercise);
5916
        $em->flush();
5917
        $lpItemId = $trackedExercise->getOrigLpItemId();
5918
        $lpId = $trackedExercise->getOrigLpId();
5919
        $lpItemViewId = $trackedExercise->getOrigLpItemViewId();
5920
        if ($lpId && $lpItemId && $lpItemViewId) {
5921
            $lpItem = $em->getRepository(CLpItem::class)->find($lpItemId);
5922
            if ($lpItem && 'quiz' === $lpItem->getItemType()) {
5923
                $lpItemView = $em->getRepository(CLpItemView::class)->find($lpItemViewId);
5924
                if ($lpItemView) {
5925
                    $lpItemView->setScore($totalScore);
5926
                    $em->persist($lpItemView);
5927
                    $em->flush();
5928
                }
5929
            }
5930
        }
5931
5932
        return $trackedExercise;
5933
    }
5934
5935
    public static function getTotalQuestionAnswered($courseId, $exerciseId, $questionId, $onlyStudents = false): int
5936
    {
5937
        $courseId = (int) $courseId;
5938
        $exerciseId = (int) $exerciseId;
5939
        $questionId = (int) $questionId;
5940
5941
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5942
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5943
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
5944
        $courseUserJoin = "";
5945
        $studentsWhere = "";
5946
        if ($onlyStudents) {
5947
            $courseUserJoin = "
5948
            INNER JOIN $courseUser cu
5949
            ON cu.c_id = te.c_id AND cu.user_id = exe_user_id";
5950
            $studentsWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
5951
        }
5952
5953
        $sql = "SELECT count(distinct (te.exe_id)) total
5954
            FROM $attemptTable t
5955
            INNER JOIN $trackTable te
5956
            ON (t.exe_id = te.exe_id)
5957
            $courseUserJoin
5958
            WHERE
5959
                te.c_id = $courseId AND
5960
                exe_exo_id = $exerciseId AND
5961
                t.question_id = $questionId AND
5962
                te.status != 'incomplete'
5963
                $studentsWhere
5964
        ";
5965
        $queryTotal = Database::query($sql);
5966
        $totalRow = Database::fetch_assoc($queryTotal);
5967
        $total = 0;
5968
        if ($totalRow) {
5969
            $total = (int) $totalRow['total'];
5970
        }
5971
5972
        return $total;
5973
    }
5974
5975
    public static function getWrongQuestionResults($courseId, $exerciseId, $sessionId = 0, $limit = 10)
5976
    {
5977
        $courseId = (int) $courseId;
5978
        $exerciseId = (int) $exerciseId;
5979
        $limit = (int) $limit;
5980
5981
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
5982
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5983
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5984
5985
        $sessionCondition = '';
5986
        if (!empty($sessionId)) {
5987
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5988
        }
5989
5990
        $sql = "SELECT q.question, question_id, count(q.iid) count
5991
                FROM $attemptTable t
5992
                INNER JOIN $questionTable q
5993
                ON (q.iid = t.question_id)
5994
                INNER JOIN $trackTable te
5995
                ON (t.exe_id = te.exe_id)
5996
                WHERE
5997
                    te.c_id = $courseId AND
5998
                    t.marks != q.ponderation AND
5999
                    exe_exo_id = $exerciseId AND
6000
                    status != 'incomplete'
6001
                    $sessionCondition
6002
                GROUP BY q.iid
6003
                ORDER BY count DESC
6004
                LIMIT $limit
6005
        ";
6006
6007
        $result = Database::query($sql);
6008
6009
        return Database::store_result($result, 'ASSOC');
6010
    }
6011
6012
    public static function getExerciseResultsCount($type, $courseId, $exerciseId, $sessionId = 0)
6013
    {
6014
        $courseId = (int) $courseId;
6015
        $exerciseId = (int) $exerciseId;
6016
6017
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6018
6019
        $sessionCondition = '';
6020
        if (!empty($sessionId)) {
6021
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
6022
        }
6023
6024
        $selectCount = 'count(DISTINCT te.exe_id)';
6025
        $scoreCondition = '';
6026
        switch ($type) {
6027
            case 'correct_student':
6028
                $selectCount = 'count(DISTINCT te.exe_user_id)';
6029
                $scoreCondition = ' AND score = max_score ';
6030
                break;
6031
            case 'wrong_student':
6032
                $selectCount = 'count(DISTINCT te.exe_user_id)';
6033
                $scoreCondition = ' AND score != max_score ';
6034
                break;
6035
            case 'correct':
6036
                $scoreCondition = ' AND score = max_score ';
6037
                break;
6038
            case 'wrong':
6039
                $scoreCondition = ' AND score != max_score ';
6040
                break;
6041
        }
6042
6043
        $sql = "SELECT $selectCount count
6044
                FROM $trackTable te
6045
                WHERE
6046
                    c_id = $courseId AND
6047
                    exe_exo_id = $exerciseId AND
6048
                    status != 'incomplete'
6049
                    $scoreCondition
6050
                    $sessionCondition
6051
        ";
6052
        $result = Database::query($sql);
6053
        $totalRow = Database::fetch_assoc($result);
6054
        $total = 0;
6055
        if ($totalRow) {
6056
            $total = (int) $totalRow['count'];
6057
        }
6058
6059
        return $total;
6060
    }
6061
6062
    public static function parseContent($content, $stats, Exercise $exercise, $trackInfo, $currentUserId = 0)
6063
    {
6064
        $wrongAnswersCount = $stats['failed_answers_count'];
6065
        $attemptDate = substr($trackInfo['exe_date'], 0, 10);
6066
        $exerciseId = $exercise->iId;
6067
        $resultsStudentUrl = api_get_path(WEB_CODE_PATH).
6068
            'exercise/result.php?id='.$exerciseId.'&'.api_get_cidreq();
6069
        $resultsTeacherUrl = api_get_path(WEB_CODE_PATH).
6070
            'exercise/exercise_show.php?action=edit&id='.$exerciseId.'&'.api_get_cidreq();
6071
6072
        $content = str_replace(
6073
            [
6074
                '((exercise_error_count))',
6075
                '((all_answers_html))',
6076
                '((all_answers_teacher_html))',
6077
                '((exercise_title))',
6078
                '((exercise_attempt_date))',
6079
                '((link_to_test_result_page_student))',
6080
                '((link_to_test_result_page_teacher))',
6081
            ],
6082
            [
6083
                $wrongAnswersCount,
6084
                $stats['all_answers_html'],
6085
                $stats['all_answers_teacher_html'],
6086
                $exercise->get_formated_title(),
6087
                $attemptDate,
6088
                $resultsStudentUrl,
6089
                $resultsTeacherUrl,
6090
            ],
6091
            $content
6092
        );
6093
6094
        $currentUserId = empty($currentUserId) ? api_get_user_id() : (int) $currentUserId;
6095
6096
        $content = AnnouncementManager::parseContent(
6097
            $currentUserId,
6098
            $content,
6099
            api_get_course_id(),
6100
            api_get_session_id()
6101
        );
6102
6103
        return $content;
6104
    }
6105
6106
    public static function sendNotification(
6107
        $currentUserId,
6108
        $objExercise,
6109
        $exercise_stat_info,
6110
        $courseInfo,
6111
        $attemptCountToSend,
6112
        $stats,
6113
        $statsTeacher
6114
    ) {
6115
        $notifications = api_get_configuration_value('exercise_finished_notification_settings');
6116
        if (empty($notifications)) {
6117
            return false;
6118
        }
6119
6120
        $studentId = $exercise_stat_info['exe_user_id'];
6121
        $exerciseExtraFieldValue = new ExtraFieldValue('exercise');
6122
        $wrongAnswersCount = $stats['failed_answers_count'];
6123
        $exercisePassed = $stats['exercise_passed'];
6124
        $countPendingQuestions = $stats['count_pending_questions'];
6125
        $stats['all_answers_teacher_html'] = $statsTeacher['all_answers_html'];
6126
6127
        // If there are no pending questions (Open questions).
6128
        if (0 === $countPendingQuestions) {
6129
            /*$extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6130
                $objExercise->iId,
6131
                'signature_mandatory'
6132
            );
6133
6134
            if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) {
6135
                if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) {
6136
                    $signature = ExerciseSignaturePlugin::getSignature($studentId, $exercise_stat_info);
6137
                    if (false !== $signature) {
6138
                        //return false;
6139
                    }
6140
                }
6141
            }*/
6142
6143
            // Notifications.
6144
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6145
                $objExercise->iId,
6146
                'notifications'
6147
            );
6148
            $exerciseNotification = '';
6149
            if ($extraFieldData && isset($extraFieldData['value'])) {
6150
                $exerciseNotification = $extraFieldData['value'];
6151
            }
6152
6153
            $subject = sprintf(get_lang('Failure on attempt %s at %s'), $attemptCountToSend, $courseInfo['title']);
6154
            if ($exercisePassed) {
6155
                $subject = sprintf(get_lang('Validation of exercise at %s'), $courseInfo['title']);
6156
            }
6157
6158
            if ($exercisePassed) {
6159
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6160
                    $objExercise->iId,
6161
                    'MailSuccess'
6162
                );
6163
            } else {
6164
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6165
                    $objExercise->iId,
6166
                    'MailAttempt'.$attemptCountToSend
6167
                );
6168
            }
6169
6170
            // Blocking exercise.
6171
            $blockPercentageExtra = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6172
                $objExercise->iId,
6173
                'blocking_percentage'
6174
            );
6175
            $blockPercentage = false;
6176
            if ($blockPercentageExtra && isset($blockPercentageExtra['value']) && $blockPercentageExtra['value']) {
6177
                $blockPercentage = $blockPercentageExtra['value'];
6178
            }
6179
            if ($blockPercentage) {
6180
                $passBlock = $stats['total_percentage'] > $blockPercentage;
6181
                if (false === $passBlock) {
6182
                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6183
                        $objExercise->iId,
6184
                        'MailIsBlockByPercentage'
6185
                    );
6186
                }
6187
            }
6188
6189
            $extraFieldValueUser = new ExtraFieldValue('user');
6190
6191
            if ($extraFieldData && isset($extraFieldData['value'])) {
6192
                $content = $extraFieldData['value'];
6193
                $content = self::parseContent($content, $stats, $objExercise, $exercise_stat_info, $studentId);
6194
                //if (false === $exercisePassed) {
6195
                if (0 !== $wrongAnswersCount) {
6196
                    $content .= $stats['failed_answers_html'];
6197
                }
6198
6199
                $sendMessage = true;
6200
                if (!empty($exerciseNotification)) {
6201
                    foreach ($notifications as $name => $notificationList) {
6202
                        if ($exerciseNotification !== $name) {
6203
                            continue;
6204
                        }
6205
                        foreach ($notificationList as $notificationName => $attemptData) {
6206
                            if ('student_check' === $notificationName) {
6207
                                $sendMsgIfInList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : '';
6208
                                if (!empty($sendMsgIfInList)) {
6209
                                    foreach ($sendMsgIfInList as $skipVariable => $skipValues) {
6210
                                        $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
6211
                                            $studentId,
6212
                                            $skipVariable
6213
                                        );
6214
6215
                                        if (empty($userExtraFieldValue)) {
6216
                                            $sendMessage = false;
6217
                                            break;
6218
                                        } else {
6219
                                            $sendMessage = false;
6220
                                            if (isset($userExtraFieldValue['value']) &&
6221
                                                in_array($userExtraFieldValue['value'], $skipValues)
6222
                                            ) {
6223
                                                $sendMessage = true;
6224
                                                break;
6225
                                            }
6226
                                        }
6227
                                    }
6228
                                }
6229
                                break;
6230
                            }
6231
                        }
6232
                    }
6233
                }
6234
6235
                // Send to student.
6236
                if ($sendMessage) {
6237
                    MessageManager::send_message($currentUserId, $subject, $content);
6238
                }
6239
            }
6240
6241
            if (!empty($exerciseNotification)) {
6242
                foreach ($notifications as $name => $notificationList) {
6243
                    if ($exerciseNotification !== $name) {
6244
                        continue;
6245
                    }
6246
                    foreach ($notificationList as $attemptData) {
6247
                        $skipNotification = false;
6248
                        $skipNotificationList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : [];
6249
                        if (!empty($skipNotificationList)) {
6250
                            foreach ($skipNotificationList as $skipVariable => $skipValues) {
6251
                                $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
6252
                                    $studentId,
6253
                                    $skipVariable
6254
                                );
6255
6256
                                if (empty($userExtraFieldValue)) {
6257
                                    $skipNotification = true;
6258
                                    break;
6259
                                } else {
6260
                                    if (isset($userExtraFieldValue['value'])) {
6261
                                        if (!in_array($userExtraFieldValue['value'], $skipValues)) {
6262
                                            $skipNotification = true;
6263
                                            break;
6264
                                        }
6265
                                    } else {
6266
                                        $skipNotification = true;
6267
                                        break;
6268
                                    }
6269
                                }
6270
                            }
6271
                        }
6272
6273
                        if ($skipNotification) {
6274
                            continue;
6275
                        }
6276
6277
                        $email = isset($attemptData['email']) ? $attemptData['email'] : '';
6278
                        $emailList = explode(',', $email);
6279
                        if (empty($emailList)) {
6280
                            continue;
6281
                        }
6282
                        $attempts = isset($attemptData['attempts']) ? $attemptData['attempts'] : [];
6283
                        foreach ($attempts as $attempt) {
6284
                            $sendMessage = false;
6285
                            if (isset($attempt['attempt']) && $attemptCountToSend !== (int) $attempt['attempt']) {
6286
                                continue;
6287
                            }
6288
6289
                            if (!isset($attempt['status'])) {
6290
                                continue;
6291
                            }
6292
6293
                            if ($blockPercentage && isset($attempt['is_block_by_percentage'])) {
6294
                                if ($attempt['is_block_by_percentage']) {
6295
                                    if ($passBlock) {
6296
                                        continue;
6297
                                    }
6298
                                } else {
6299
                                    if (false === $passBlock) {
6300
                                        continue;
6301
                                    }
6302
                                }
6303
                            }
6304
6305
                            switch ($attempt['status']) {
6306
                                case 'passed':
6307
                                    if ($exercisePassed) {
6308
                                        $sendMessage = true;
6309
                                    }
6310
                                    break;
6311
                                case 'failed':
6312
                                    if (false === $exercisePassed) {
6313
                                        $sendMessage = true;
6314
                                    }
6315
                                    break;
6316
                                case 'all':
6317
                                    $sendMessage = true;
6318
                                    break;
6319
                            }
6320
6321
                            if ($sendMessage) {
6322
                                $attachments = [];
6323
                                if (isset($attempt['add_pdf']) && $attempt['add_pdf']) {
6324
                                    // Get pdf content
6325
                                    $pdfExtraData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6326
                                        $objExercise->iId,
6327
                                        $attempt['add_pdf']
6328
                                    );
6329
6330
                                    if ($pdfExtraData && isset($pdfExtraData['value'])) {
6331
                                        $pdfContent = self::parseContent(
6332
                                            $pdfExtraData['value'],
6333
                                            $stats,
6334
                                            $objExercise,
6335
                                            $exercise_stat_info,
6336
                                            $studentId
6337
                                        );
6338
6339
                                        @$pdf = new PDF();
6340
                                        $filename = get_lang('Test');
6341
                                        $pdfPath = @$pdf->content_to_pdf(
6342
                                            "<html><body>$pdfContent</body></html>",
6343
                                            null,
6344
                                            $filename,
6345
                                            api_get_course_id(),
6346
                                            'F',
6347
                                            false,
6348
                                            null,
6349
                                            false,
6350
                                            true
6351
                                        );
6352
                                        $attachments[] = ['filename' => $filename, 'path' => $pdfPath];
6353
                                    }
6354
                                }
6355
6356
                                $content = isset($attempt['content_default']) ? $attempt['content_default'] : '';
6357
                                if (isset($attempt['content'])) {
6358
                                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6359
                                        $objExercise->iId,
6360
                                        $attempt['content']
6361
                                    );
6362
                                    if ($extraFieldData && isset($extraFieldData['value']) && !empty($extraFieldData['value'])) {
6363
                                        $content = $extraFieldData['value'];
6364
                                    }
6365
                                }
6366
6367
                                if (!empty($content)) {
6368
                                    $content = self::parseContent(
6369
                                        $content,
6370
                                        $stats,
6371
                                        $objExercise,
6372
                                        $exercise_stat_info,
6373
                                        $studentId
6374
                                    );
6375
                                    foreach ($emailList as $email) {
6376
                                        if (empty($email)) {
6377
                                            continue;
6378
                                        }
6379
                                        api_mail_html(
6380
                                            null,
6381
                                            $email,
6382
                                            $subject,
6383
                                            $content,
6384
                                            null,
6385
                                            null,
6386
                                            [],
6387
                                            $attachments
6388
                                        );
6389
                                    }
6390
                                }
6391
6392
                                if (isset($attempt['post_actions'])) {
6393
                                    foreach ($attempt['post_actions'] as $action => $params) {
6394
                                        switch ($action) {
6395
                                            case 'subscribe_student_to_courses':
6396
                                                foreach ($params as $code) {
6397
                                                    $courseInfo = api_get_course_info($code);
6398
                                                    CourseManager::subscribeUser(
6399
                                                        $currentUserId,
6400
                                                        $courseInfo['real_id']
6401
                                                    );
6402
                                                    break;
6403
                                                }
6404
                                                break;
6405
                                        }
6406
                                    }
6407
                                }
6408
                            }
6409
                        }
6410
                    }
6411
                }
6412
            }
6413
        }
6414
    }
6415
6416
    /**
6417
     * Delete an exercise attempt.
6418
     *
6419
     * Log the exe_id deleted with the exe_user_id related.
6420
     *
6421
     * @param int $exeId
6422
     */
6423
    public static function deleteExerciseAttempt($exeId)
6424
    {
6425
        $exeId = (int) $exeId;
6426
6427
        $trackExerciseInfo = self::get_exercise_track_exercise_info($exeId);
6428
6429
        if (empty($trackExerciseInfo)) {
6430
            return;
6431
        }
6432
6433
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6434
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6435
6436
        Database::query("DELETE FROM $tblTrackAttempt WHERE exe_id = $exeId");
6437
        Database::query("DELETE FROM $tblTrackExercises WHERE exe_id = $exeId");
6438
6439
        Event::addEvent(
6440
            LOG_EXERCISE_ATTEMPT_DELETE,
6441
            LOG_EXERCISE_ATTEMPT,
6442
            $exeId,
6443
            api_get_utc_datetime()
6444
        );
6445
        Event::addEvent(
6446
            LOG_EXERCISE_ATTEMPT_DELETE,
6447
            LOG_EXERCISE_AND_USER_ID,
6448
            $exeId.'-'.$trackExerciseInfo['exe_user_id'],
6449
            api_get_utc_datetime()
6450
        );
6451
    }
6452
6453
    public static function scorePassed($score, $total)
6454
    {
6455
        $compareResult = bccomp($score, $total, 3);
6456
        $scorePassed = 1 === $compareResult || 0 === $compareResult;
6457
        if (false === $scorePassed) {
6458
            $epsilon = 0.00001;
6459
            if (abs($score - $total) < $epsilon) {
6460
                $scorePassed = true;
6461
            }
6462
        }
6463
6464
        return $scorePassed;
6465
    }
6466
6467
    /**
6468
     * Returns the HTML for a specific exercise attempt, ready for PDF generation.
6469
     */
6470
    public static function getAttemptPdfHtml(int $exeId, int $courseId, int $sessionId): string
6471
    {
6472
        $_GET = [
6473
            'id'           => $exeId,
6474
            'action'       => 'export',
6475
            'export_type'  => 'all_results',
6476
            'cid'          => $courseId,
6477
            'sid'          => $sessionId,
6478
            'gid'          => 0,
6479
            'gradebook'    => 0,
6480
            'origin'       => '',
6481
        ];
6482
        $_REQUEST = $_GET + $_REQUEST;
6483
6484
        ob_start();
6485
        include __DIR__ . '/../../exercise/exercise_show.php';
6486
        return ob_get_clean();
6487
    }
6488
6489
    /**
6490
     * Generates and saves a PDF for a single exercise attempt
6491
     */
6492
    public static function saveFileExerciseResultPdfDirect(
6493
        int    $exeId,
6494
        int    $courseId,
6495
        int    $sessionId,
6496
        string $exportFolderPath
6497
    ): void {
6498
        // Retrieve the HTML for this attempt and convert it to PDF
6499
        $html = self::getAttemptPdfHtml($exeId, $courseId, $sessionId);
6500
6501
        // Determine filename and path based on user information
6502
        $track   = self::get_exercise_track_exercise_info($exeId);
6503
        $userId  = $track['exe_user_id'] ?? 0;
6504
        $user    = api_get_user_info($userId);
6505
        $pdfName = api_replace_dangerous_char(
6506
            ($user['firstname'] ?? 'user') . '_' .
6507
            ($user['lastname']  ?? 'unknown') .
6508
            '_attemptId' . $exeId . '.pdf'
6509
        );
6510
        $filePath = rtrim($exportFolderPath, '/') . '/' . $pdfName;
6511
6512
        if (file_exists($filePath)) {
6513
            return;
6514
        }
6515
6516
        // Ensure the directory exists
6517
        $dir = dirname($filePath);
6518
        if (!is_dir($dir)) {
6519
            mkdir($dir, 0755, true);
6520
        }
6521
6522
        // Use Chamilo's PDF class to generate and save the file
6523
        $params = [
6524
            'filename'    => $pdfName,
6525
            'course_code' => api_get_course_id(),
6526
        ];
6527
        $pdf = new PDF('A4', 'P', $params);
6528
        $pdf->html_to_pdf_with_template(
6529
            $html,
6530
            true,
6531
            false,
6532
            true,
6533
            [],
6534
            'F',
6535
            $filePath
6536
        );
6537
    }
6538
6539
    /**
6540
     * Exports all results of an exercise to a ZIP archive by generating PDFs on disk and then sending the ZIP to the browser.
6541
     */
6542
    public static function exportExerciseAllResultsZip(
6543
        int   $sessionId,
6544
        int   $courseId,
6545
        int   $exerciseId,
6546
        array $filterDates = [],
6547
        string $mainPath    = ''
6548
    ) {
6549
        $em = Container::getEntityManager();
6550
6551
        /** @var CourseEntity|null $course */
6552
        $course = $em->getRepository(CourseEntity::class)->find($courseId);
6553
        /** @var CQuiz|null $quiz */
6554
        $quiz   = $em->getRepository(CQuiz::class)->findOneBy(['iid' => $exerciseId]);
6555
        $session = null;
6556
6557
        if (!$course) {
6558
            Display::addFlash(Display::return_message(get_lang('Course not found'), 'warning', false));
6559
            return false;
6560
        }
6561
        if (!$quiz) {
6562
            Display::addFlash(Display::return_message(get_lang('Test not found'), 'warning', false));
6563
            return false;
6564
        }
6565
        if ($sessionId > 0) {
6566
            $session = $em->getRepository(SessionEntity::class)->find($sessionId);
6567
            if (!$session) {
6568
                Display::addFlash(Display::return_message(get_lang('Session not found'), 'warning', false));
6569
                return false;
6570
            }
6571
        }
6572
6573
        // Fetch exe_ids with Doctrine, accepting NULL/0 session when $sessionId == 0
6574
        $exeIds = self::findAttemptExeIdsForExport($course, $quiz, $session, $filterDates);
6575
6576
        // Optional: hard fallback with native SQL to catch legacy session_id=0 rows if needed
6577
        if (empty($exeIds) && $sessionId === 0) {
6578
            $exeIds = self::findAttemptExeIdsFallbackSql($courseId, $exerciseId, $filterDates);
6579
        }
6580
6581
        if (empty($exeIds)) {
6582
            Display::addFlash(
6583
                Display::return_message(
6584
                    get_lang('No result found for export in this test.'),
6585
                    'warning',
6586
                    false
6587
                )
6588
            );
6589
            return false;
6590
        }
6591
6592
        // Prepare a temporary folder for the PDFs
6593
        $exportName       = 'S' . (int)($sessionId) . '-C' . (int)($courseId) . '-T' . (int)($exerciseId);
6594
        $baseDir          = api_get_path(SYS_ARCHIVE_PATH);
6595
        $exportFolderPath = $baseDir . 'pdfexport-' . $exportName;
6596
        if (is_dir($exportFolderPath)) {
6597
            rmdirr($exportFolderPath);
6598
        }
6599
        mkdir($exportFolderPath, 0755, true);
6600
6601
        // Generate a PDF for each attempt
6602
        foreach ($exeIds as $exeId) {
6603
            self::saveFileExerciseResultPdfDirect(
6604
                (int)$exeId,
6605
                (int)$courseId,
6606
                (int)$sessionId,
6607
                $exportFolderPath
6608
            );
6609
        }
6610
6611
        // Create the ZIP archive containing all generated PDFs
6612
        $zipFilePath = $baseDir . 'pdfexport-' . $exportName . '.zip';
6613
        $zip = new \ZipArchive();
6614
        if ($zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
6615
            throw new \Exception('Failed to create ZIP file');
6616
        }
6617
        $files = new RecursiveIteratorIterator(
6618
            new RecursiveDirectoryIterator($exportFolderPath),
6619
            RecursiveIteratorIterator::LEAVES_ONLY
6620
        );
6621
        foreach ($files as $file) {
6622
            if (!$file->isDir()) {
6623
                $filePath     = $file->getRealPath();
6624
                $relativePath = substr($filePath, strlen($exportFolderPath) + 1);
6625
                $zip->addFile($filePath, $relativePath);
6626
            }
6627
        }
6628
        $zip->close();
6629
        rmdirr($exportFolderPath);
6630
6631
        // Send the ZIP file to the browser or move it to mainPath
6632
        if (!empty($mainPath)) {
6633
            @rename($zipFilePath, $mainPath . '/pdfexport-' . $exportName . '.zip');
6634
            return true;
6635
        }
6636
6637
        session_write_close();
6638
        while (ob_get_level()) {
6639
            @ob_end_clean();
6640
        }
6641
6642
        header('Content-Description: File Transfer');
6643
        header('Content-Type: application/zip');
6644
        header('Content-Disposition: attachment; filename="pdfexport-' . $exportName . '.zip"');
6645
        header('Content-Transfer-Encoding: binary');
6646
        header('Expires: 0');
6647
        header('Cache-Control: must-revalidate');
6648
        header('Pragma: public');
6649
        header('Content-Length: ' . filesize($zipFilePath));
6650
6651
        readfile($zipFilePath);
6652
        @unlink($zipFilePath);
6653
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
6654
    }
6655
6656
    /**
6657
     * Return exe_ids for export using Doctrine (handles NULL/0 sessions safely).
6658
     */
6659
    private static function findAttemptExeIdsForExport(
6660
        CourseEntity $course,
6661
        CQuiz $quiz,
6662
        ?SessionEntity $session,
6663
        array $filterDates
6664
    ): array {
6665
        $em = Container::getEntityManager();
6666
6667
        $qb = $em->createQueryBuilder()
6668
            ->select('te.exeId AS exeId')
6669
            ->from(TrackEExercise::class, 'te')
6670
            ->where('te.course = :course')
6671
            ->andWhere('te.quiz = :quiz')
6672
            ->setParameter('course', $course)
6673
            ->setParameter('quiz', $quiz);
6674
6675
        // Session filter:
6676
        if ($session) {
6677
            $qb->andWhere('te.session = :session')->setParameter('session', $session);
6678
        } else {
6679
            // Accept both NULL and legacy "0" values
6680
            // IDENTITY() extracts the FK raw value to match 0 if present
6681
            $qb->andWhere('(te.session IS NULL OR IDENTITY(te.session) = 0)');
6682
        }
6683
6684
        // Date filters on exeDate
6685
        if (!empty($filterDates['start_date'])) {
6686
            $qb->andWhere('te.exeDate >= :start')
6687
                ->setParameter('start', new DateTime($filterDates['start_date']));
6688
        }
6689
        if (!empty($filterDates['end_date'])) {
6690
            $qb->andWhere('te.exeDate <= :end')
6691
                ->setParameter('end', new DateTime($filterDates['end_date']));
6692
        }
6693
6694
        $qb->orderBy('te.exeDate', 'DESC')->setMaxResults(5000);
6695
6696
        $rows = $qb->getQuery()->getScalarResult();
6697
        $exeIds = array_map(static fn($r) => (int)$r['exeId'], $rows);
6698
6699
6700
        return array_values(array_unique($exeIds));
6701
    }
6702
6703
    /**
6704
     * Fallback with native SQL for very legacy rows (session_id=0 and column names).
6705
     */
6706
    private static function findAttemptExeIdsFallbackSql(
6707
        int $courseId,
6708
        int $quizIid,
6709
        array $filterDates
6710
    ): array {
6711
        $conn = Container::getEntityManager()->getConnection();
6712
6713
        $sql = 'SELECT te.exe_id
6714
            FROM track_e_exercises te
6715
            WHERE te.c_id = :cid
6716
              AND te.exe_exo_id = :iid
6717
              AND (te.session_id IS NULL OR te.session_id = 0)';
6718
6719
        $params = ['cid' => $courseId, 'iid' => $quizIid];
6720
        $types  = [];
6721
6722
        if (!empty($filterDates['start_date'])) {
6723
            $sql .= ' AND te.exe_date >= :start';
6724
            $params['start'] = $filterDates['start_date'];
6725
        }
6726
        if (!empty($filterDates['end_date'])) {
6727
            $sql .= ' AND te.exe_date <= :end';
6728
            $params['end'] = $filterDates['end_date'];
6729
        }
6730
6731
        $sql .= ' ORDER BY te.exe_date DESC LIMIT 5000';
6732
6733
        $rows = $conn->fetchAllAssociative($sql, $params, $types);
6734
        $exeIds = array_map(static fn($r) => (int)$r['exe_id'], $rows);
6735
6736
        return $exeIds;
6737
    }
6738
6739
    /**
6740
     * Calculates the overall score for Combination-type questions.
6741
     */
6742
    public static function getUserQuestionScoreGlobal(
6743
        int   $answerType,
6744
        array $listCorrectAnswers,
6745
        int   $exeId,
6746
        int   $questionId,
6747
        float $questionWeighting,
6748
        array $choice = [],
6749
        int $nbrAnswers = 0
6750
    ): float
6751
    {
6752
        $nbrCorrect = 0;
6753
        $nbrOptions = 0;
6754
        $choice = is_array($choice) ? $choice : [];
6755
        switch ($answerType) {
6756
            case FILL_IN_BLANKS_COMBINATION:
6757
                if (!empty($listCorrectAnswers)) {
6758
                    if (!empty($listCorrectAnswers['student_score']) && is_array($listCorrectAnswers['student_score'])) {
6759
                        foreach ($listCorrectAnswers['student_score'] as $val) {
6760
                            if ((int) $val === 1) {
6761
                                $nbrCorrect++;
6762
                            }
6763
                        }
6764
                    }
6765
                    if (!empty($listCorrectAnswers['words_count'])) {
6766
                        $nbrOptions = (int) $listCorrectAnswers['words_count'];
6767
                    } elseif (!empty($listCorrectAnswers['words']) && is_array($listCorrectAnswers['words'])) {
6768
                        $nbrOptions = count($listCorrectAnswers['words']);
6769
                    }
6770
                }
6771
                break;
6772
6773
            case HOT_SPOT_COMBINATION:
6774
                if (!empty($listCorrectAnswers) && is_array($listCorrectAnswers) && is_array($choice)) {
6775
                    foreach ($listCorrectAnswers as $idx => $val) {
6776
                        if (isset($choice[$idx]) && (int) $choice[$idx] === 1) {
6777
                            $nbrCorrect++;
6778
                        }
6779
                    }
6780
                } else {
6781
                    $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
6782
                    $exeIdEsc = Database::escape_string($exeId);
6783
                    $qIdEsc   = Database::escape_string($questionId);
6784
                    $sql = "SELECT COUNT(hotspot_id) AS ct
6785
                        FROM $TBL_TRACK_HOTSPOT
6786
                        WHERE hotspot_exe_id = '$exeIdEsc'
6787
                          AND hotspot_question_id = '$qIdEsc'
6788
                          AND hotspot_correct = 1";
6789
                    $result = Database::query($sql);
6790
                    $nbrCorrect = (int) Database::result($result, 0, 0);
6791
                }
6792
                $nbrOptions = (int) $nbrAnswers;
6793
                break;
6794
6795
            case MATCHING_COMBINATION:
6796
            case MATCHING_DRAGGABLE_COMBINATION:
6797
                if (isset($listCorrectAnswers['form_values'])) {
6798
                    if (isset($listCorrectAnswers['form_values']['correct'])) {
6799
                        $nbrCorrect = count($listCorrectAnswers['form_values']['correct']);
6800
                        $nbrOptions = (int) $listCorrectAnswers['form_values']['count_options'];
6801
                    }
6802
                } else {
6803
                    if (isset($listCorrectAnswers['from_database'])) {
6804
                        if (isset($listCorrectAnswers['from_database']['correct'])) {
6805
                            $nbrCorrect = count($listCorrectAnswers['from_database']['correct']);
6806
                            $nbrOptions = (int) $listCorrectAnswers['from_database']['count_options'];
6807
                        }
6808
                    }
6809
                }
6810
                break;
6811
        }
6812
6813
        $questionScore = 0.0;
6814
        if ($nbrOptions > 0 && $nbrCorrect === $nbrOptions) {
6815
            $questionScore = (float) $questionWeighting;
6816
        }
6817
6818
        return $questionScore;
6819
    }
6820
}
6821