Passed
Push — master ( 15a517...3e63d2 )
by
unknown
16:13 queued 07:43
created

ExerciseLib::getReviewedAttemptsInfo()   A

Complexity

Conditions 1

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 11
nop 2
dl 0
loc 19
rs 9.9
c 0
b 0
f 0
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
        $percentage = (100 * $score) / (0 != $weight ? $weight : 1);
2966
        // Formats values
2967
        $percentage = float_format($percentage, 1);
2968
        $score = float_format($score, 1);
2969
        $weight = float_format($weight, 1);
2970
2971
        if ($roundValues) {
2972
            $whole = floor($percentage); // 1
2973
            $fraction = $percentage - $whole; // .25
2974
2975
            // Formats values
2976
            if ($fraction >= 0.5) {
2977
                $percentage = ceil($percentage);
2978
            } else {
2979
                $percentage = round($percentage);
2980
            }
2981
2982
            $whole = floor($score); // 1
2983
            $fraction = $score - $whole; // .25
2984
            if ($fraction >= 0.5) {
2985
                $score = ceil($score);
2986
            } else {
2987
                $score = round($score);
2988
            }
2989
2990
            $whole = floor($weight); // 1
2991
            $fraction = $weight - $whole; // .25
2992
            if ($fraction >= 0.5) {
2993
                $weight = ceil($weight);
2994
            } else {
2995
                $weight = round($weight);
2996
            }
2997
        } else {
2998
            // Formats values
2999
            $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator);
3000
            $score = float_format($score, 1, $decimalSeparator, $thousandSeparator);
3001
            $weight = float_format($weight, 1, $decimalSeparator, $thousandSeparator);
3002
        }
3003
3004
        if ($show_percentage) {
3005
            $percentageSign = ' %';
3006
            if ($hidePercentageSign) {
3007
                $percentageSign = '';
3008
            }
3009
            $html = $percentage."$percentageSign ($score / $weight)";
3010
            if ($show_only_percentage) {
3011
                $html = $percentage.$percentageSign;
3012
            }
3013
        } else {
3014
            if ($removeEmptyDecimals) {
3015
                if (ScoreDisplay::hasEmptyDecimals($weight)) {
3016
                    $weight = round($weight);
3017
                }
3018
            }
3019
            $html = $score.' / '.$weight;
3020
        }
3021
3022
        // Over write score
3023
        $scoreBasedInModel = self::convertScoreToModel($percentage);
3024
        if (!empty($scoreBasedInModel)) {
3025
            $html = $scoreBasedInModel;
3026
        }
3027
3028
        // Ignore other formats and use the configuration['exercise_score_format'] value
3029
        // But also keep the round values settings.
3030
        $format = (int) api_get_setting('exercise.exercise_score_format');
3031
        if (!empty($format)) {
3032
            $html = ScoreDisplay::instance()->display_score([$score, $weight], $format);
3033
        }
3034
3035
        return Display::span($html, ['class' => 'score_exercise']);
3036
    }
3037
3038
    /**
3039
     * @param array $model
3040
     * @param float $percentage
3041
     *
3042
     * @return string
3043
     */
3044
    public static function getModelStyle($model, $percentage)
3045
    {
3046
        return '<span class="'.$model['css_class'].' p-2">'.$model['name'].'</span>';
3047
    }
3048
3049
    /**
3050
     * @param float $percentage value between 0 and 100
3051
     *
3052
     * @return string
3053
     */
3054
    public static function convertScoreToModel($percentage)
3055
    {
3056
        $model = self::getCourseScoreModel();
3057
        if (!empty($model)) {
3058
            $scoreWithGrade = [];
3059
            foreach ($model['score_list'] as $item) {
3060
                if ($percentage >= $item['min'] && $percentage <= $item['max']) {
3061
                    $scoreWithGrade = $item;
3062
                    break;
3063
                }
3064
            }
3065
3066
            if (!empty($scoreWithGrade)) {
3067
                return self::getModelStyle($scoreWithGrade, $percentage);
3068
            }
3069
        }
3070
3071
        return '';
3072
    }
3073
3074
    /**
3075
     * @return array
3076
     */
3077
    public static function getCourseScoreModel()
3078
    {
3079
        $modelList = self::getScoreModels();
3080
3081
        if (empty($modelList)) {
3082
            return [];
3083
        }
3084
3085
        $courseInfo = api_get_course_info();
3086
        if (!empty($courseInfo)) {
3087
            $scoreModelId = api_get_course_setting('score_model_id');
3088
            if (-1 != $scoreModelId) {
3089
                $modelIdList = array_column($modelList['models'], 'id');
3090
                if (in_array($scoreModelId, $modelIdList)) {
3091
                    foreach ($modelList['models'] as $item) {
3092
                        if ($item['id'] == $scoreModelId) {
3093
                            return $item;
3094
                        }
3095
                    }
3096
                }
3097
            }
3098
        }
3099
3100
        return [];
3101
    }
3102
3103
    /**
3104
     * @return array
3105
     */
3106
    public static function getScoreModels()
3107
    {
3108
        return api_get_setting('exercise.score_grade_model', true);
3109
    }
3110
3111
    /**
3112
     * @param float  $score
3113
     * @param float  $weight
3114
     * @param string $passPercentage
3115
     *
3116
     * @return bool
3117
     */
3118
    public static function isSuccessExerciseResult($score, $weight, $passPercentage)
3119
    {
3120
        $percentage = float_format(
3121
            ($score / (0 != $weight ? $weight : 1)) * 100,
3122
            1
3123
        );
3124
        if (isset($passPercentage) && !empty($passPercentage)) {
3125
            if ($percentage >= $passPercentage) {
3126
                return true;
3127
            }
3128
        }
3129
3130
        return false;
3131
    }
3132
3133
    /**
3134
     * @param string $name
3135
     * @param $weight
3136
     * @param $selected
3137
     *
3138
     * @return bool
3139
     */
3140
    public static function addScoreModelInput(
3141
        FormValidator $form,
3142
        $name,
3143
        $weight,
3144
        $selected
3145
    ) {
3146
        $model = self::getCourseScoreModel();
3147
        if (empty($model)) {
3148
            return false;
3149
        }
3150
3151
        /** @var HTML_QuickForm_select $element */
3152
        $element = $form->createElement(
3153
            'select',
3154
            $name,
3155
            get_lang('Score'),
3156
            [],
3157
            ['class' => 'exercise_mark_select']
3158
        );
3159
3160
        foreach ($model['score_list'] as $item) {
3161
            $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2);
3162
            $label = self::getModelStyle($item, $i);
3163
            $attributes = [
3164
                'class' => $item['css_class'],
3165
            ];
3166
            if ($selected == $i) {
3167
                $attributes['selected'] = 'selected';
3168
            }
3169
            $element->addOption($label, $i, $attributes);
3170
        }
3171
        $form->addElement($element);
3172
    }
3173
3174
    /**
3175
     * @return string
3176
     */
3177
    public static function getJsCode()
3178
    {
3179
        // Filling the scores with the right colors.
3180
        $models = self::getCourseScoreModel();
3181
        $cssListToString = '';
3182
        if (!empty($models)) {
3183
            $cssList = array_column($models['score_list'], 'css_class');
3184
            $cssListToString = implode(' ', $cssList);
3185
        }
3186
3187
        if (empty($cssListToString)) {
3188
            return '';
3189
        }
3190
        $js = <<<EOT
3191
3192
        function updateSelect(element) {
3193
            var spanTag = element.parent().find('span.filter-option');
3194
            var value = element.val();
3195
            var selectId = element.attr('id');
3196
            var optionClass = $('#' + selectId + ' option[value="'+value+'"]').attr('class');
3197
            spanTag.removeClass('$cssListToString');
3198
            spanTag.addClass(optionClass);
3199
        }
3200
3201
        $(function() {
3202
            // Loading values
3203
            $('.exercise_mark_select').on('loaded.bs.select', function() {
3204
                updateSelect($(this));
3205
            });
3206
            // On change
3207
            $('.exercise_mark_select').on('changed.bs.select', function() {
3208
                updateSelect($(this));
3209
            });
3210
        });
3211
EOT;
3212
3213
        return $js;
3214
    }
3215
3216
    /**
3217
     * @param float  $score
3218
     * @param float  $weight
3219
     * @param string $pass_percentage
3220
     *
3221
     * @return string
3222
     */
3223
    public static function showSuccessMessage($score, $weight, $pass_percentage)
3224
    {
3225
        $res = '';
3226
        if (self::isPassPercentageEnabled($pass_percentage)) {
3227
            $isSuccess = self::isSuccessExerciseResult(
3228
                $score,
3229
                $weight,
3230
                $pass_percentage
3231
            );
3232
3233
            if ($isSuccess) {
3234
                $html = get_lang('Congratulations you passed the test!');
3235
                $icon = Display::getMdiIcon('check-circle', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Correct'));
3236
            } else {
3237
                $html = get_lang('You didn\'t reach the minimum score');
3238
                $icon = Display::getMdiIcon('alert', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Wrong'));
3239
            }
3240
            $html = Display::tag('h4', $html);
3241
            $html .= Display::tag(
3242
                'h5',
3243
                $icon,
3244
                ['style' => 'width:40px; padding:2px 10px 0px 0px']
3245
            );
3246
            $res = $html;
3247
        }
3248
3249
        return $res;
3250
    }
3251
3252
    /**
3253
     * Return true if pass_pourcentage activated (we use the pass pourcentage feature
3254
     * return false if pass_percentage = 0 (we don't use the pass pourcentage feature.
3255
     *
3256
     * @param $value
3257
     *
3258
     * @return bool
3259
     *              In this version, pass_percentage and show_success_message are disabled if
3260
     *              pass_percentage is set to 0
3261
     */
3262
    public static function isPassPercentageEnabled($value)
3263
    {
3264
        return $value > 0;
3265
    }
3266
3267
    /**
3268
     * Converts a numeric value in a percentage example 0.66666 to 66.67 %.
3269
     *
3270
     * @param $value
3271
     *
3272
     * @return float Converted number
3273
     */
3274
    public static function convert_to_percentage($value)
3275
    {
3276
        $return = '-';
3277
        if ('' != $value) {
3278
            $return = float_format($value * 100, 1).' %';
3279
        }
3280
3281
        return $return;
3282
    }
3283
3284
    /**
3285
     * Getting all active exercises from a course from a session
3286
     * (if a session_id is provided we will show all the exercises in the course +
3287
     * all exercises in the session).
3288
     *
3289
     * @param array  $course_info
3290
     * @param int    $session_id
3291
     * @param bool   $check_publication_dates
3292
     * @param string $search                  Search exercise name
3293
     * @param bool   $search_all_sessions     Search exercises in all sessions
3294
     * @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...
3295
     *                  1 = only active exercises,
3296
     *                  2 = all exercises
3297
     *                  3 = active <> -1
3298
     *
3299
     * @return CQuiz[]
3300
     */
3301
    public static function get_all_exercises(
3302
        $course_info = null,
3303
        $session_id = 0,
3304
        $check_publication_dates = false,
3305
        $search = '',
3306
        $search_all_sessions = false,
3307
        $active = 2
3308
    ) {
3309
        $course_id = api_get_course_int_id();
3310
        if (!empty($course_info) && !empty($course_info['real_id'])) {
3311
            $course_id = $course_info['real_id'];
3312
        }
3313
3314
        if (-1 == $session_id) {
3315
            $session_id = 0;
3316
        }
3317
        $course = api_get_course_entity($course_id);
3318
        $session = api_get_session_entity($session_id);
3319
3320
        if (null === $course) {
3321
            return [];
3322
        }
3323
3324
        $repo = Container::getQuizRepository();
3325
3326
        return $repo->findAllByCourse($course, $session, (string) $search, $active)
3327
            ->getQuery()
3328
            ->getResult();
3329
    }
3330
3331
    /**
3332
     * Getting all exercises (active only or all)
3333
     * from a course from a session
3334
     * (if a session_id is provided we will show all the exercises in the
3335
     * course + all exercises in the session).
3336
     */
3337
    public static function get_all_exercises_for_course_id(
3338
        int $courseId,
3339
        int $sessionId = 0,
3340
        bool $onlyActiveExercises = true
3341
    ): array {
3342
        if ($courseId <= 0) {
3343
            return [];
3344
        }
3345
3346
        $course  = api_get_course_entity($courseId);
3347
        $session = api_get_session_entity($sessionId);
3348
3349
        $repo = Container::getQuizRepository();
3350
3351
        $qb = $repo->getResourcesByCourse($course, $session);
3352
3353
        $qb->andWhere('resourceLink.endVisibilityAt IS NULL');
3354
3355
        if ($onlyActiveExercises) {
3356
            $qb->andWhere('resourceLink.visibility = 2');
3357
        } else {
3358
            $qb->andWhere('resourceLink.visibility IN (0,2)');
3359
        }
3360
3361
        $qb->orderBy('resource.title', 'ASC');
3362
3363
        $exercises = $qb->getQuery()->getResult();
3364
3365
        $exerciseList = [];
3366
        foreach ($exercises as $exercise) {
3367
            $exerciseList[] = [
3368
                'iid' => $exercise->getIid(),
3369
                'title' => $exercise->getTitle(),
3370
            ];
3371
        }
3372
3373
        return $exerciseList;
3374
    }
3375
3376
    /**
3377
     * Gets the position of the score based in a given score (result/weight)
3378
     * and the exe_id based in the user list
3379
     * (NO Exercises in LPs ).
3380
     *
3381
     * @param float  $my_score      user score to be compared *attention*
3382
     *                              $my_score = score/weight and not just the score
3383
     * @param int    $my_exe_id     exe id of the exercise
3384
     *                              (this is necessary because if 2 students have the same score the one
3385
     *                              with the minor exe_id will have a best position, just to be fair and FIFO)
3386
     * @param int    $exercise_id
3387
     * @param string $course_code
3388
     * @param int    $session_id
3389
     * @param array  $user_list
3390
     * @param bool   $return_string
3391
     *
3392
     * @return int the position of the user between his friends in a course
3393
     *             (or course within a session)
3394
     */
3395
    public static function get_exercise_result_ranking(
3396
        $my_score,
3397
        $my_exe_id,
3398
        $exercise_id,
3399
        $course_code,
3400
        $session_id = 0,
3401
        $user_list = [],
3402
        $return_string = true
3403
    ) {
3404
        //No score given we return
3405
        if (is_null($my_score)) {
3406
            return '-';
3407
        }
3408
        if (empty($user_list)) {
3409
            return '-';
3410
        }
3411
3412
        $best_attempts = [];
3413
        foreach ($user_list as $user_data) {
3414
            $user_id = $user_data['user_id'];
3415
            $best_attempts[$user_id] = self::get_best_attempt_by_user(
3416
                $user_id,
3417
                $exercise_id,
3418
                $course_code,
3419
                $session_id
3420
            );
3421
        }
3422
3423
        if (empty($best_attempts)) {
3424
            return 1;
3425
        } else {
3426
            $position = 1;
3427
            $my_ranking = [];
3428
            foreach ($best_attempts as $user_id => $result) {
3429
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3430
                    $my_ranking[$user_id] = $result['score'] / $result['max_score'];
3431
                } else {
3432
                    $my_ranking[$user_id] = 0;
3433
                }
3434
            }
3435
            //if (!empty($my_ranking)) {
3436
            asort($my_ranking);
3437
            $position = count($my_ranking);
3438
            if (!empty($my_ranking)) {
3439
                foreach ($my_ranking as $user_id => $ranking) {
3440
                    if ($my_score >= $ranking) {
3441
                        if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) {
3442
                            $exe_id = $best_attempts[$user_id]['exe_id'];
3443
                            if ($my_exe_id < $exe_id) {
3444
                                $position--;
3445
                            }
3446
                        } else {
3447
                            $position--;
3448
                        }
3449
                    }
3450
                }
3451
            }
3452
            //}
3453
            $return_value = [
3454
                'position' => $position,
3455
                'count' => count($my_ranking),
3456
            ];
3457
3458
            if ($return_string) {
3459
                if (!empty($position) && !empty($my_ranking)) {
3460
                    $return_value = $position.'/'.count($my_ranking);
3461
                } else {
3462
                    $return_value = '-';
3463
                }
3464
            }
3465
3466
            return $return_value;
3467
        }
3468
    }
3469
3470
    /**
3471
     * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts
3472
     * (NO Exercises in LPs ) old functionality by attempt.
3473
     *
3474
     * @param   float   user score to be compared attention => score/weight
3475
     * @param   int     exe id of the exercise
3476
     * (this is necessary because if 2 students have the same score the one
3477
     * with the minor exe_id will have a best position, just to be fair and FIFO)
3478
     * @param   int     exercise id
3479
     * @param   string  course code
3480
     * @param   int     session id
3481
     * @param bool $return_string
3482
     *
3483
     * @return int the position of the user between his friends in a course (or course within a session)
3484
     */
3485
    public static function get_exercise_result_ranking_by_attempt(
3486
        $my_score,
3487
        $my_exe_id,
3488
        $exercise_id,
3489
        $courseId,
3490
        $session_id = 0,
3491
        $return_string = true
3492
    ) {
3493
        if (empty($session_id)) {
3494
            $session_id = 0;
3495
        }
3496
        if (is_null($my_score)) {
3497
            return '-';
3498
        }
3499
        $user_results = Event::get_all_exercise_results(
3500
            $exercise_id,
3501
            $courseId,
3502
            $session_id,
3503
            false
3504
        );
3505
        $position_data = [];
3506
        if (empty($user_results)) {
3507
            return 1;
3508
        } else {
3509
            $position = 1;
3510
            $my_ranking = [];
3511
            foreach ($user_results as $result) {
3512
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3513
                    $my_ranking[$result['exe_id']] = $result['score'] / $result['max_score'];
3514
                } else {
3515
                    $my_ranking[$result['exe_id']] = 0;
3516
                }
3517
            }
3518
            asort($my_ranking);
3519
            $position = count($my_ranking);
3520
            if (!empty($my_ranking)) {
3521
                foreach ($my_ranking as $exe_id => $ranking) {
3522
                    if ($my_score >= $ranking) {
3523
                        if ($my_score == $ranking) {
3524
                            if ($my_exe_id < $exe_id) {
3525
                                $position--;
3526
                            }
3527
                        } else {
3528
                            $position--;
3529
                        }
3530
                    }
3531
                }
3532
            }
3533
            $return_value = [
3534
                'position' => $position,
3535
                'count' => count($my_ranking),
3536
            ];
3537
3538
            if ($return_string) {
3539
                if (!empty($position) && !empty($my_ranking)) {
3540
                    return $position.'/'.count($my_ranking);
3541
                }
3542
            }
3543
3544
            return $return_value;
3545
        }
3546
    }
3547
3548
    /**
3549
     * Get the best attempt in a exercise (NO Exercises in LPs ).
3550
     *
3551
     * @param int $exercise_id
3552
     * @param int $courseId
3553
     * @param int $session_id
3554
     *
3555
     * @return array
3556
     */
3557
    public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id)
3558
    {
3559
        $user_results = Event::get_all_exercise_results(
3560
            $exercise_id,
3561
            $courseId,
3562
            $session_id,
3563
            false
3564
        );
3565
3566
        $best_score_data = [];
3567
        $best_score = 0;
3568
        if (!empty($user_results)) {
3569
            foreach ($user_results as $result) {
3570
                if (!empty($result['max_score']) &&
3571
                    0 != intval($result['max_score'])
3572
                ) {
3573
                    $score = $result['score'] / $result['max_score'];
3574
                    if ($score >= $best_score) {
3575
                        $best_score = $score;
3576
                        $best_score_data = $result;
3577
                    }
3578
                }
3579
            }
3580
        }
3581
3582
        return $best_score_data;
3583
    }
3584
3585
    /**
3586
     * Get the best score in a exercise (NO Exercises in LPs ).
3587
     *
3588
     * @param int $user_id
3589
     * @param int $exercise_id
3590
     * @param int $courseId
3591
     * @param int $session_id
3592
     *
3593
     * @return array
3594
     */
3595
    public static function get_best_attempt_by_user(
3596
        $user_id,
3597
        $exercise_id,
3598
        $courseId,
3599
        $session_id
3600
    ) {
3601
        $user_results = Event::get_all_exercise_results(
3602
            $exercise_id,
3603
            $courseId,
3604
            $session_id,
3605
            false,
3606
            $user_id
3607
        );
3608
        $best_score_data = [];
3609
        $best_score = 0;
3610
        if (!empty($user_results)) {
3611
            foreach ($user_results as $result) {
3612
                if (!empty($result['max_score']) && 0 != (float) $result['max_score']) {
3613
                    $score = $result['score'] / $result['max_score'];
3614
                    if ($score >= $best_score) {
3615
                        $best_score = $score;
3616
                        $best_score_data = $result;
3617
                    }
3618
                }
3619
            }
3620
        }
3621
3622
        return $best_score_data;
3623
    }
3624
3625
    /**
3626
     * Get average score (NO Exercises in LPs ).
3627
     *
3628
     * @param    int    exercise id
3629
     * @param int $courseId
3630
     * @param    int    session id
3631
     *
3632
     * @return float Average score
3633
     */
3634
    public static function get_average_score($exercise_id, $courseId, $session_id)
3635
    {
3636
        $user_results = Event::get_all_exercise_results(
3637
            $exercise_id,
3638
            $courseId,
3639
            $session_id
3640
        );
3641
        $avg_score = 0;
3642
        if (!empty($user_results)) {
3643
            foreach ($user_results as $result) {
3644
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3645
                    $score = $result['score'] / $result['max_score'];
3646
                    $avg_score += $score;
3647
                }
3648
            }
3649
            $avg_score = float_format($avg_score / count($user_results), 1);
3650
        }
3651
3652
        return $avg_score;
3653
    }
3654
3655
    /**
3656
     * Get average score by score (NO Exercises in LPs ).
3657
     *
3658
     * @param int $courseId
3659
     * @param    int    session id
3660
     *
3661
     * @return float Average score
3662
     */
3663
    public static function get_average_score_by_course($courseId, $session_id)
3664
    {
3665
        $user_results = Event::get_all_exercise_results_by_course(
3666
            $courseId,
3667
            $session_id,
3668
            false
3669
        );
3670
        $avg_score = 0;
3671
        if (!empty($user_results)) {
3672
            foreach ($user_results as $result) {
3673
                if (!empty($result['max_score']) && 0 != intval(
3674
                        $result['max_score']
3675
                    )
3676
                ) {
3677
                    $score = $result['score'] / $result['max_score'];
3678
                    $avg_score += $score;
3679
                }
3680
            }
3681
            // We assume that all max_score
3682
            $avg_score = $avg_score / count($user_results);
3683
        }
3684
3685
        return $avg_score;
3686
    }
3687
3688
    /**
3689
     * @param int $user_id
3690
     * @param int $courseId
3691
     * @param int $session_id
3692
     *
3693
     * @return float|int
3694
     */
3695
    public static function get_average_score_by_course_by_user(
3696
        $user_id,
3697
        $courseId,
3698
        $session_id
3699
    ) {
3700
        $user_results = Event::get_all_exercise_results_by_user(
3701
            $user_id,
3702
            $courseId,
3703
            $session_id
3704
        );
3705
        $avg_score = 0;
3706
        if (!empty($user_results)) {
3707
            foreach ($user_results as $result) {
3708
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3709
                    $score = $result['score'] / $result['max_score'];
3710
                    $avg_score += $score;
3711
                }
3712
            }
3713
            // We assume that all max_score
3714
            $avg_score = ($avg_score / count($user_results));
3715
        }
3716
3717
        return $avg_score;
3718
    }
3719
3720
    /**
3721
     * Get average score by score (NO Exercises in LPs ).
3722
     *
3723
     * @param int $exercise_id
3724
     * @param int $courseId
3725
     * @param int $session_id
3726
     * @param int $user_count
3727
     *
3728
     * @return float Best average score
3729
     */
3730
    public static function get_best_average_score_by_exercise(
3731
        $exercise_id,
3732
        $courseId,
3733
        $session_id,
3734
        $user_count
3735
    ) {
3736
        $user_results = Event::get_best_exercise_results_by_user(
3737
            $exercise_id,
3738
            $courseId,
3739
            $session_id
3740
        );
3741
        $avg_score = 0;
3742
        if (!empty($user_results)) {
3743
            foreach ($user_results as $result) {
3744
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3745
                    $score = $result['score'] / $result['max_score'];
3746
                    $avg_score += $score;
3747
                }
3748
            }
3749
            // We asumme that all max_score
3750
            if (!empty($user_count)) {
3751
                $avg_score = float_format($avg_score / $user_count, 1) * 100;
3752
            } else {
3753
                $avg_score = 0;
3754
            }
3755
        }
3756
3757
        return $avg_score;
3758
    }
3759
3760
    /**
3761
     * Get average score by score (NO Exercises in LPs ).
3762
     *
3763
     * @param int $exercise_id
3764
     * @param int $courseId
3765
     * @param int $session_id
3766
     *
3767
     * @return float Best average score
3768
     */
3769
    public static function getBestScoreByExercise(
3770
        $exercise_id,
3771
        $courseId,
3772
        $session_id
3773
    ) {
3774
        $user_results = Event::get_best_exercise_results_by_user(
3775
            $exercise_id,
3776
            $courseId,
3777
            $session_id
3778
        );
3779
        $avg_score = 0;
3780
        if (!empty($user_results)) {
3781
            foreach ($user_results as $result) {
3782
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3783
                    $score = $result['score'] / $result['max_score'];
3784
                    $avg_score += $score;
3785
                }
3786
            }
3787
        }
3788
3789
        return $avg_score;
3790
    }
3791
3792
    /**
3793
     * Get student results (only in completed exercises) stats by question.
3794
     *
3795
     * @throws \Doctrine\DBAL\Exception
3796
     */
3797
    public static function getStudentStatsByQuestion(
3798
        int $questionId,
3799
        int $exerciseId,
3800
        string $courseCode,
3801
        int $sessionId,
3802
        bool $onlyStudent = false
3803
    ): array
3804
    {
3805
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3806
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3807
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3808
3809
        $questionId = (int) $questionId;
3810
        $exerciseId = (int) $exerciseId;
3811
        $courseCode = Database::escape_string($courseCode);
3812
        $sessionId = (int) $sessionId;
3813
        $courseId = api_get_course_int_id($courseCode);
3814
3815
        $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average
3816
                FROM $trackExercises e ";
3817
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
3818
        if ($onlyStudent) {
3819
            $courseCondition = '';
3820
            if (empty($sessionId)) {
3821
                $courseCondition = "
3822
                INNER JOIN $courseUser c
3823
                ON (
3824
                    e.exe_user_id = c.user_id AND
3825
                    e.c_id = c.c_id AND
3826
                    c.status = ".STUDENT." AND
3827
                    relation_type <> 2
3828
                )";
3829
            } else {
3830
                $sessionRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3831
                $courseCondition = "
3832
            INNER JOIN $sessionRelCourse sc
3833
            ON (
3834
                        e.exe_user_id = sc.user_id AND
3835
                        e.c_id = sc.c_id AND
3836
                        e.session_id = sc.session_id AND
3837
                        sc.status = ".SessionEntity::STUDENT."
3838
                )";
3839
            }
3840
            $sql .= $courseCondition;
3841
        }
3842
        $sql .= "
3843
    		INNER JOIN $trackAttempt a
3844
    		ON (
3845
    		    a.exe_id = e.exe_id
3846
            )
3847
    		WHERE
3848
    		    exe_exo_id 	= $exerciseId AND
3849
                e.c_id = $courseId AND
3850
                question_id = $questionId AND
3851
                e.status = ''
3852
                $sessionCondition
3853
            LIMIT 1";
3854
        $result = Database::query($sql);
3855
        $return = [];
3856
        if ($result) {
3857
            $return = Database::fetch_assoc($result);
3858
        }
3859
3860
        return $return;
3861
    }
3862
3863
    /**
3864
     * Get the correct answer count for a fill blanks question.
3865
     *
3866
     * @param int $question_id
3867
     * @param int $exercise_id
3868
     *
3869
     * @return array
3870
     */
3871
    public static function getNumberStudentsFillBlanksAnswerCount(
3872
        $question_id,
3873
        $exercise_id
3874
    ) {
3875
        $listStudentsId = [];
3876
        $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3877
            api_get_course_id(),
3878
            true
3879
        );
3880
        foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3881
            $listStudentsId[] = $listStudentInfo['user_id'];
3882
        }
3883
3884
        $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3885
            $exercise_id,
3886
            $question_id,
3887
            $listStudentsId,
3888
            '1970-01-01',
3889
            '3000-01-01'
3890
        );
3891
3892
        $arrayCount = [];
3893
3894
        foreach ($listFillTheBlankResult as $resultCount) {
3895
            foreach ($resultCount as $index => $count) {
3896
                //this is only for declare the array index per answer
3897
                $arrayCount[$index] = 0;
3898
            }
3899
        }
3900
3901
        foreach ($listFillTheBlankResult as $resultCount) {
3902
            foreach ($resultCount as $index => $count) {
3903
                $count = (0 === $count) ? 1 : 0;
3904
                $arrayCount[$index] += $count;
3905
            }
3906
        }
3907
3908
        return $arrayCount;
3909
    }
3910
3911
    /**
3912
     * Get the number of questions with answers.
3913
     *
3914
     * @param int    $question_id
3915
     * @param int    $exercise_id
3916
     * @param string $course_code
3917
     * @param int    $session_id
3918
     * @param string $questionType
3919
     *
3920
     * @return int
3921
     */
3922
    public static function get_number_students_question_with_answer_count(
3923
        $question_id,
3924
        $exercise_id,
3925
        $course_code,
3926
        $session_id,
3927
        $questionType = ''
3928
    ) {
3929
        $track_exercises   = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3930
        $track_attempt     = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3931
        $courseUser        = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3932
        $courseTable       = Database::get_main_table(TABLE_MAIN_COURSE);
3933
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3934
3935
        $question_id = (int) $question_id;
3936
        $exercise_id = (int) $exercise_id;
3937
        $courseId    = (int) api_get_course_int_id($course_code);
3938
        $session_id  = (int) $session_id;
3939
3940
        if (in_array($questionType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION], true)) {
3941
            $listStudentsId     = [];
3942
            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(api_get_course_id(), true);
3943
            foreach ($listAllStudentInfo as $listStudentInfo) {
3944
                $listStudentsId[] = (int) $listStudentInfo['user_id'];
3945
            }
3946
3947
            $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3948
                $exercise_id,
3949
                $question_id,
3950
                $listStudentsId,
3951
                '1970-01-01',
3952
                '3000-01-01'
3953
            );
3954
3955
            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
3956
        }
3957
3958
        if (empty($session_id)) {
3959
            $courseCondition = "
3960
            INNER JOIN $courseUser cu
3961
                ON cu.c_id = c.id AND cu.user_id = exe_user_id";
3962
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3963
        } else {
3964
            $courseCondition = "
3965
            INNER JOIN $courseUserSession cu
3966
                ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3967
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3968
        }
3969
3970
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
3971
        $sql = "SELECT DISTINCT exe_user_id
3972
            FROM $track_exercises e
3973
            INNER JOIN $track_attempt a
3974
                ON (a.exe_id = e.exe_id AND a.c_id = e.c_id)
3975
            INNER JOIN $courseTable c
3976
                ON c.id = e.c_id
3977
            $courseCondition
3978
            WHERE
3979
                exe_exo_id  = $exercise_id AND
3980
                e.c_id      = $courseId AND
3981
                question_id = $question_id AND
3982
                answer <> '0' AND
3983
                e.status = ''
3984
                $courseConditionWhere
3985
                $sessionCondition
3986
    ";
3987
3988
        $result = Database::query($sql);
3989
3990
        return $result ? (int) Database::num_rows($result) : 0;
3991
    }
3992
3993
    /**
3994
     * Get number of answers to hotspot questions.
3995
     */
3996
    public static function getNumberStudentsAnswerHotspotCount(
3997
        int    $answerId,
3998
        int    $questionId,
3999
        int    $exerciseId,
4000
        string $courseCode,
4001
        int $sessionId
4002
    ): int
4003
    {
4004
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4005
        $trackHotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4006
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4007
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4008
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4009
4010
        $questionId = (int) $questionId;
4011
        $answerId = (int) $answerId;
4012
        $exerciseId = (int) $exerciseId;
4013
        $courseId = api_get_course_int_id($courseCode);
4014
        $sessionId = (int) $sessionId;
4015
4016
        if (empty($sessionId)) {
4017
            $courseCondition = "
4018
            INNER JOIN $courseUser cu
4019
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4020
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4021
        } else {
4022
            $courseCondition = "
4023
            INNER JOIN $courseUserSession cu
4024
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4025
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4026
        }
4027
4028
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
4029
        $sql = "SELECT DISTINCT exe_user_id
4030
                FROM $trackExercises e
4031
                INNER JOIN $trackHotspot a
4032
                ON (a.hotspot_exe_id = e.exe_id)
4033
                INNER JOIN $courseTable c
4034
                ON (a.c_id = c.id)
4035
                $courseCondition
4036
                WHERE
4037
                    exe_exo_id              = $exerciseId AND
4038
                    a.c_id 	= $courseId AND
4039
                    hotspot_answer_id       = $answerId AND
4040
                    hotspot_question_id     = $questionId AND
4041
                    hotspot_correct         =  1 AND
4042
                    e.status                = ''
4043
                    $courseConditionWhere
4044
                    $sessionCondition
4045
            ";
4046
        $result = Database::query($sql);
4047
        $return = 0;
4048
        if ($result) {
4049
            $return = Database::num_rows($result);
4050
        }
4051
4052
        return $return;
4053
    }
4054
4055
    /**
4056
     * @param int    $answer_id
4057
     * @param int    $question_id
4058
     * @param int    $exercise_id
4059
     * @param string $course_code
4060
     * @param int    $session_id
4061
     * @param string $question_type
4062
     * @param string $correct_answer
4063
     * @param string $current_answer
4064
     *
4065
     * @return int
4066
     */
4067
    public static function get_number_students_answer_count(
4068
        $answer_id,
4069
        $question_id,
4070
        $exercise_id,
4071
        $course_code,
4072
        $session_id,
4073
        $question_type = null,
4074
        $correct_answer = null,
4075
        $current_answer = null
4076
    ) {
4077
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4078
        $track_attempt   = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4079
        $courseTable     = Database::get_main_table(TABLE_MAIN_COURSE);
4080
        $courseUser      = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4081
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4082
4083
        $question_id = (int) $question_id;
4084
        $answer_id   = (int) $answer_id;
4085
        $exercise_id = (int) $exercise_id;
4086
        $courseId    = (int) api_get_course_int_id($course_code);
4087
        $session_id  = (int) $session_id;
4088
4089
        switch ($question_type) {
4090
            case FILL_IN_BLANKS:
4091
            case FILL_IN_BLANKS_COMBINATION:
4092
                $answer_condition = '';
4093
                $select_condition = ' e.exe_id, answer ';
4094
                break;
4095
            case MATCHING:
4096
            case MATCHING_COMBINATION:
4097
            case MATCHING_DRAGGABLE:
4098
            case MATCHING_DRAGGABLE_COMBINATION:
4099
            default:
4100
                $answer_condition = " answer = $answer_id AND ";
4101
                $select_condition = ' DISTINCT exe_user_id ';
4102
        }
4103
4104
        if (empty($session_id)) {
4105
            $courseCondition = "
4106
            INNER JOIN $courseUser cu
4107
                ON cu.c_id = e.c_id AND cu.user_id = e.exe_user_id";
4108
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4109
        } else {
4110
            $courseCondition = "
4111
            INNER JOIN $courseUserSession cu
4112
                ON (cu.c_id = e.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4113
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4114
        }
4115
4116
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
4117
        $sql = "SELECT $select_condition
4118
            FROM $track_exercises e
4119
            INNER JOIN $track_attempt a
4120
                ON (a.exe_id = e.exe_id AND a.c_id = e.c_id)
4121
            INNER JOIN $courseTable c
4122
                ON c.id = e.c_id
4123
            $courseCondition
4124
            WHERE
4125
                exe_exo_id = $exercise_id AND
4126
                e.c_id = $courseId AND
4127
                $answer_condition
4128
                question_id = $question_id AND
4129
                e.status = ''
4130
                $courseConditionWhere
4131
                $sessionCondition
4132
    ";
4133
4134
        $result = Database::query($sql);
4135
        $return = 0;
4136
        if ($result) {
4137
            switch ($question_type) {
4138
                case FILL_IN_BLANKS:
4139
                case FILL_IN_BLANKS_COMBINATION:
4140
                    $good_answers = 0;
4141
                    while ($row = Database::fetch_assoc($result)) {
4142
                        $fill_blank = self::check_fill_in_blanks(
4143
                            $correct_answer,
4144
                            $row['answer'],
4145
                            $current_answer
4146
                        );
4147
                        if (isset($fill_blank[$current_answer]) && 1 == (int) $fill_blank[$current_answer]) {
4148
                            $good_answers++;
4149
                        }
4150
                    }
4151
4152
                    return $good_answers;
4153
4154
                case MATCHING:
4155
                case MATCHING_COMBINATION:
4156
                case MATCHING_DRAGGABLE:
4157
                case MATCHING_DRAGGABLE_COMBINATION:
4158
                default:
4159
                    $return = Database::num_rows($result);
4160
            }
4161
        }
4162
4163
        return $return;
4164
    }
4165
4166
    /**
4167
     * Get the number of times an answer was selected.
4168
     */
4169
    public static function getCountOfAnswers(
4170
        int $answerId,
4171
        int $questionId,
4172
        int $exerciseId,
4173
        string $courseCode,
4174
        int $sessionId,
4175
        $questionType = null,
4176
    ): int
4177
    {
4178
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4179
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4180
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4181
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4182
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4183
4184
        $answerId = (int) $answerId;
4185
        $questionId = (int) $questionId;
4186
        $exerciseId = (int) $exerciseId;
4187
        $courseId = api_get_course_int_id($courseCode);
4188
        $sessionId = (int) $sessionId;
4189
        $return = 0;
4190
4191
        $answerCondition = match ($questionType) {
4192
            FILL_IN_BLANKS => '',
4193
            default => " answer = $answerId AND ",
4194
        };
4195
4196
        if (empty($sessionId)) {
4197
            $courseCondition = "
4198
            INNER JOIN $courseUser cu
4199
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
4200
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4201
        } else {
4202
            $courseCondition = "
4203
            INNER JOIN $courseUserSession cu
4204
            ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4205
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4206
        }
4207
4208
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
4209
        $sql = "SELECT count(a.answer) as total
4210
                FROM $trackExercises e
4211
                INNER JOIN $trackAttempt a
4212
                ON (
4213
                    a.exe_id = e.exe_id
4214
                )
4215
                INNER JOIN $courseTable c
4216
                ON c.id = e.c_id
4217
                $courseCondition
4218
                WHERE
4219
                    exe_exo_id = $exerciseId AND
4220
                    e.c_id = $courseId AND
4221
                    $answerCondition
4222
                    question_id = $questionId AND
4223
                    e.status = ''
4224
                    $courseConditionWhere
4225
                    $sessionCondition
4226
            ";
4227
        $result = Database::query($sql);
4228
        if ($result) {
4229
            $count = Database::fetch_array($result);
4230
            $return = (int) $count['total'];
4231
        }
4232
        return $return;
4233
    }
4234
4235
    /**
4236
     * @param array  $answer
4237
     * @param string $user_answer
4238
     *
4239
     * @return array
4240
     */
4241
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
4242
    {
4243
        // the question is encoded like this
4244
        // [A] B [C] D [E] F::10,10,10@1
4245
        // number 1 before the "@" means that is a switchable fill in blank question
4246
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4247
        // means that is a normal fill blank question
4248
        // first we explode the "::"
4249
        $pre_array = explode('::', $answer);
4250
        // is switchable fill blank or not
4251
        $last = count($pre_array) - 1;
4252
        $is_set_switchable = explode('@', $pre_array[$last]);
4253
        $switchable_answer_set = false;
4254
        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4255
            $switchable_answer_set = true;
4256
        }
4257
        $answer = '';
4258
        for ($k = 0; $k < $last; $k++) {
4259
            $answer .= $pre_array[$k];
4260
        }
4261
        // splits weightings that are joined with a comma
4262
        $answerWeighting = explode(',', $is_set_switchable[0]);
4263
4264
        // we save the answer because it will be modified
4265
        //$temp = $answer;
4266
        $temp = $answer;
4267
4268
        $answer = '';
4269
        $j = 0;
4270
        //initialise answer tags
4271
        $user_tags = $correct_tags = $real_text = [];
4272
        // the loop will stop at the end of the text
4273
        while (1) {
4274
            // quits the loop if there are no more blanks (detect '[')
4275
            if (false === ($pos = api_strpos($temp, '['))) {
4276
                // adds the end of the text
4277
                $answer = $temp;
4278
                $real_text[] = $answer;
4279
                break; //no more "blanks", quit the loop
4280
            }
4281
            // adds the piece of text that is before the blank
4282
            //and ends with '[' into a general storage array
4283
            $real_text[] = api_substr($temp, 0, $pos + 1);
4284
            $answer .= api_substr($temp, 0, $pos + 1);
4285
            //take the string remaining (after the last "[" we found)
4286
            $temp = api_substr($temp, $pos + 1);
4287
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4288
            if (false === ($pos = api_strpos($temp, ']'))) {
4289
                // adds the end of the text
4290
                $answer .= $temp;
4291
                break;
4292
            }
4293
4294
            $str = $user_answer;
4295
4296
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
4297
            $str = str_replace('\r\n', '', $str);
4298
            $choices = $arr[1];
4299
            $choice = [];
4300
            $check = false;
4301
            $i = 0;
4302
            foreach ($choices as $item) {
4303
                if ($current_answer === $item) {
4304
                    $check = true;
4305
                }
4306
                if ($check) {
4307
                    $choice[] = $item;
4308
                    $i++;
4309
                }
4310
                if (3 == $i) {
4311
                    break;
4312
                }
4313
            }
4314
            $tmp = api_strrpos($choice[$j], ' / ');
4315
4316
            if (false !== $tmp) {
4317
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
4318
            }
4319
4320
            $choice[$j] = trim($choice[$j]);
4321
4322
            //Needed to let characters ' and " to work as part of an answer
4323
            $choice[$j] = stripslashes($choice[$j]);
4324
4325
            $user_tags[] = api_strtolower($choice[$j]);
4326
            //put the contents of the [] answer tag into correct_tags[]
4327
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
4328
            $j++;
4329
            $temp = api_substr($temp, $pos + 1);
4330
        }
4331
4332
        $answer = '';
4333
        $real_correct_tags = $correct_tags;
4334
        $chosen_list = [];
4335
        $good_answer = [];
4336
4337
        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...
4338
            if (!$switchable_answer_set) {
4339
                //needed to parse ' and " characters
4340
                $user_tags[$i] = stripslashes($user_tags[$i]);
4341
                if ($correct_tags[$i] == $user_tags[$i]) {
4342
                    $good_answer[$correct_tags[$i]] = 1;
4343
                } elseif (!empty($user_tags[$i])) {
4344
                    $good_answer[$correct_tags[$i]] = 0;
4345
                } else {
4346
                    $good_answer[$correct_tags[$i]] = 0;
4347
                }
4348
            } else {
4349
                // switchable fill in the blanks
4350
                if (in_array($user_tags[$i], $correct_tags)) {
4351
                    $correct_tags = array_diff($correct_tags, $chosen_list);
4352
                    $good_answer[$correct_tags[$i]] = 1;
4353
                } elseif (!empty($user_tags[$i])) {
4354
                    $good_answer[$correct_tags[$i]] = 0;
4355
                } else {
4356
                    $good_answer[$correct_tags[$i]] = 0;
4357
                }
4358
            }
4359
            // adds the correct word, followed by ] to close the blank
4360
            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4361
            if (isset($real_text[$i + 1])) {
4362
                $answer .= $real_text[$i + 1];
4363
            }
4364
        }
4365
4366
        return $good_answer;
4367
    }
4368
4369
    /**
4370
     * Return an HTML select menu with the student groups.
4371
     *
4372
     * @param string $name     is the name and the id of the <select>
4373
     * @param string $default  default value for option
4374
     * @param string $onchange
4375
     *
4376
     * @return string the html code of the <select>
4377
     */
4378
    public static function displayGroupMenu($name, $default, $onchange = "")
4379
    {
4380
        // check the default value of option
4381
        $tabSelected = [$default => " selected='selected' "];
4382
        $res = "<select name='$name' id='$name' onchange='".$onchange."' >";
4383
        $res .= "<option value='-1'".$tabSelected["-1"].">-- ".get_lang('All groups')." --</option>";
4384
        $res .= "<option value='0'".$tabSelected["0"].">- ".get_lang('Not in a group')." -</option>";
4385
        $groups = GroupManager::get_group_list();
4386
        $currentCatId = 0;
4387
        $countGroups = count($groups);
4388
        for ($i = 0; $i < $countGroups; $i++) {
4389
            $category = GroupManager::get_category_from_group($groups[$i]['iid']);
4390
            if ($category['id'] != $currentCatId) {
4391
                $res .= "<option value='-1' disabled='disabled'>".$category['title']."</option>";
4392
                $currentCatId = $category['id'];
4393
            }
4394
            $res .= "<option ".$tabSelected[$groups[$i]['id']]."style='margin-left:40px' value='".
4395
                $groups[$i]["iid"]."'>".
4396
                $groups[$i]["name"].
4397
                "</option>";
4398
        }
4399
        $res .= "</select>";
4400
4401
        return $res;
4402
    }
4403
4404
    /**
4405
     * @param int $exe_id
4406
     */
4407
    public static function create_chat_exercise_session($exe_id)
4408
    {
4409
        if (!isset($_SESSION['current_exercises'])) {
4410
            $_SESSION['current_exercises'] = [];
4411
        }
4412
        $_SESSION['current_exercises'][$exe_id] = true;
4413
    }
4414
4415
    /**
4416
     * @param int $exe_id
4417
     */
4418
    public static function delete_chat_exercise_session($exe_id)
4419
    {
4420
        if (isset($_SESSION['current_exercises'])) {
4421
            $_SESSION['current_exercises'][$exe_id] = false;
4422
        }
4423
    }
4424
4425
    /**
4426
     * Display the exercise results.
4427
     *
4428
     * @param Exercise $objExercise
4429
     * @param int      $exeId
4430
     * @param bool     $save_user_result save users results (true) or just show the results (false)
4431
     * @param string   $remainingMessage
4432
     * @param bool     $allowSignature
4433
     * @param bool     $allowExportPdf
4434
     * @param bool     $isExport
4435
     */
4436
    public static function displayQuestionListByAttempt(
4437
        $objExercise,
4438
        $exeId,
4439
        $save_user_result = false,
4440
        $remainingMessage = '',
4441
        $allowSignature = false,
4442
        $allowExportPdf = false,
4443
        $isExport = false
4444
    ) {
4445
        $origin = api_get_origin();
4446
        $courseId = api_get_course_int_id();
4447
        $courseCode = api_get_course_id();
4448
        $sessionId = api_get_session_id();
4449
4450
        // Getting attempt info
4451
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
4452
4453
        // Getting question list
4454
        $question_list = [];
4455
        $studentInfo = [];
4456
        if (!empty($exercise_stat_info['data_tracking'])) {
4457
            $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
4458
            $question_list = explode(',', $exercise_stat_info['data_tracking']);
4459
        } else {
4460
            // Try getting the question list only if save result is off
4461
            if (false == $save_user_result) {
4462
                $question_list = $objExercise->get_validated_question_list();
4463
            }
4464
            if (in_array(
4465
                $objExercise->getFeedbackType(),
4466
                [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4467
            )) {
4468
                $question_list = $objExercise->get_validated_question_list();
4469
            }
4470
        }
4471
4472
        if ($objExercise->getResultAccess()) {
4473
            if (false === $objExercise->hasResultsAccess($exercise_stat_info)) {
4474
                echo Display::return_message(
4475
                    sprintf(get_lang('You have passed the %s minutes limit to see the results.'), $objExercise->getResultsAccess())
4476
                );
4477
4478
                return false;
4479
            }
4480
4481
            if (!empty($objExercise->getResultAccess())) {
4482
                $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id;
4483
                echo $objExercise->returnTimeLeftDiv();
4484
                echo $objExercise->showSimpleTimeControl(
4485
                    $objExercise->getResultAccessTimeDiff($exercise_stat_info),
4486
                    $url
4487
                );
4488
            }
4489
        }
4490
4491
        $counter = 1;
4492
        $total_score = $total_weight = 0;
4493
        $exerciseContent = null;
4494
4495
        // Hide results
4496
        $show_results = false;
4497
        $show_only_score = false;
4498
        if (in_array($objExercise->results_disabled,
4499
            [
4500
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4501
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
4502
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4503
            ]
4504
        )) {
4505
            $show_results = true;
4506
        }
4507
4508
        if (in_array(
4509
            $objExercise->results_disabled,
4510
            [
4511
                RESULT_DISABLE_SHOW_SCORE_ONLY,
4512
                RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
4513
                RESULT_DISABLE_RANKING,
4514
            ]
4515
        )
4516
        ) {
4517
            $show_only_score = true;
4518
        }
4519
4520
        // Not display expected answer, but score, and feedback
4521
        $show_all_but_expected_answer = false;
4522
        if (RESULT_DISABLE_SHOW_SCORE_ONLY == $objExercise->results_disabled &&
4523
            EXERCISE_FEEDBACK_TYPE_END == $objExercise->getFeedbackType()
4524
        ) {
4525
            $show_all_but_expected_answer = true;
4526
            $show_results = true;
4527
            $show_only_score = false;
4528
        }
4529
4530
        $showTotalScoreAndUserChoicesInLastAttempt = true;
4531
        $showTotalScore = true;
4532
        $showQuestionScore = true;
4533
        $attemptResult = [];
4534
4535
        if (in_array(
4536
            $objExercise->results_disabled,
4537
            [
4538
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
4539
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
4540
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
4541
            ])
4542
        ) {
4543
            $show_only_score = true;
4544
            $show_results = true;
4545
            $numberAttempts = 0;
4546
            if ($objExercise->attempts > 0) {
4547
                $attempts = Event::getExerciseResultsByUser(
4548
                    api_get_user_id(),
4549
                    $objExercise->id,
4550
                    $courseId,
4551
                    $sessionId,
4552
                    $exercise_stat_info['orig_lp_id'],
4553
                    $exercise_stat_info['orig_lp_item_id'],
4554
                    'desc'
4555
                );
4556
                if ($attempts) {
4557
                    $numberAttempts = count($attempts);
4558
                }
4559
4560
                if ($save_user_result) {
4561
                    $numberAttempts++;
4562
                }
4563
4564
                $showTotalScore = false;
4565
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT == $objExercise->results_disabled) {
4566
                    $showTotalScore = true;
4567
                }
4568
                $showTotalScoreAndUserChoicesInLastAttempt = false;
4569
                if ($numberAttempts >= $objExercise->attempts) {
4570
                    $showTotalScore = true;
4571
                    $show_results = true;
4572
                    $show_only_score = false;
4573
                    $showTotalScoreAndUserChoicesInLastAttempt = true;
4574
                }
4575
4576
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $objExercise->results_disabled) {
4577
                    $showTotalScore = true;
4578
                    $show_results = true;
4579
                    $show_only_score = false;
4580
                    $showTotalScoreAndUserChoicesInLastAttempt = false;
4581
                    if ($numberAttempts >= $objExercise->attempts) {
4582
                        $showTotalScoreAndUserChoicesInLastAttempt = true;
4583
                    }
4584
4585
                    // Check if the current attempt is the last.
4586
                    if (false === $save_user_result && !empty($attempts)) {
4587
                        $showTotalScoreAndUserChoicesInLastAttempt = false;
4588
                        $position = 1;
4589
                        foreach ($attempts as $attempt) {
4590
                            if ($exeId == $attempt['exe_id']) {
4591
                                break;
4592
                            }
4593
                            $position++;
4594
                        }
4595
4596
                        if ($position == $objExercise->attempts) {
4597
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
4598
                        }
4599
                    }
4600
                }
4601
            }
4602
4603
            if (RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK ==
4604
                $objExercise->results_disabled
4605
            ) {
4606
                $show_only_score = false;
4607
                $show_results = true;
4608
                $show_all_but_expected_answer = false;
4609
                $showTotalScore = false;
4610
                $showQuestionScore = false;
4611
                if ($numberAttempts >= $objExercise->attempts) {
4612
                    $showTotalScore = true;
4613
                    $showQuestionScore = true;
4614
                }
4615
            }
4616
        }
4617
4618
        // When exporting to PDF hide feedback/comment/score show warning in hotspot.
4619
        if ($allowExportPdf && $isExport) {
4620
            $showTotalScore = false;
4621
            $showQuestionScore = false;
4622
            $objExercise->feedback_type = 2;
4623
            $objExercise->hideComment = true;
4624
            $objExercise->hideNoAnswer = true;
4625
            $objExercise->results_disabled = 0;
4626
            $objExercise->hideExpectedAnswer = true;
4627
            $show_results = true;
4628
        }
4629
4630
        if ('embeddable' !== $origin &&
4631
            !empty($exercise_stat_info['exe_user_id']) &&
4632
            !empty($studentInfo)
4633
        ) {
4634
            // Shows exercise header.
4635
            echo $objExercise->showExerciseResultHeader(
4636
                $studentInfo,
4637
                $exercise_stat_info,
4638
                $save_user_result,
4639
                $allowSignature,
4640
                $allowExportPdf
4641
            );
4642
        }
4643
4644
        $question_list_answers = [];
4645
        $category_list = [];
4646
        $loadChoiceFromSession = false;
4647
        $fromDatabase = true;
4648
        $exerciseResult = null;
4649
        $exerciseResultCoordinates = null;
4650
        $delineationResults = null;
4651
        if (true === $save_user_result && in_array(
4652
            $objExercise->getFeedbackType(),
4653
            [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4654
        )) {
4655
            $loadChoiceFromSession = true;
4656
            $fromDatabase = false;
4657
            $exerciseResult = Session::read('exerciseResult');
4658
            $exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
4659
            $delineationResults = Session::read('hotspot_delineation_result');
4660
            $delineationResults = isset($delineationResults[$objExercise->id]) ? $delineationResults[$objExercise->id] : null;
4661
        }
4662
4663
        $countPendingQuestions = 0;
4664
        $result = [];
4665
        $panelsByParent = [];
4666
        // Loop over all question to show results for each of them, one by one
4667
        if (!empty($question_list)) {
4668
            foreach ($question_list as $questionId) {
4669
                // Creates a temporary Question object
4670
                $objQuestionTmp = Question::read($questionId, $objExercise->course);
4671
                // This variable came from exercise_submit_modal.php
4672
                ob_start();
4673
                $choice = null;
4674
                $delineationChoice = null;
4675
                if ($loadChoiceFromSession) {
4676
                    $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null;
4677
                    $delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null;
4678
                }
4679
4680
                // We're inside *one* question. Go through each possible answer for this question
4681
                $result = $objExercise->manage_answer(
4682
                    $exeId,
4683
                    $questionId,
4684
                    $choice,
4685
                    'exercise_result',
4686
                    $exerciseResultCoordinates,
4687
                    $save_user_result,
4688
                    $fromDatabase,
4689
                    $show_results,
4690
                    $objExercise->selectPropagateNeg(),
4691
                    $delineationChoice,
4692
                    $showTotalScoreAndUserChoicesInLastAttempt
4693
                );
4694
4695
                if (empty($result)) {
4696
                    continue;
4697
                }
4698
4699
                $total_score += $result['score'];
4700
                $total_weight += $result['weight'];
4701
4702
                $question_list_answers[] = [
4703
                    'question' => $result['open_question'],
4704
                    'answer' => $result['open_answer'],
4705
                    'answer_type' => $result['answer_type'],
4706
                    'generated_oral_file' => $result['generated_oral_file'],
4707
                ];
4708
4709
                $my_total_score = $result['score'];
4710
                $my_total_weight = $result['weight'];
4711
                $scorePassed = self::scorePassed($my_total_score, $my_total_weight);
4712
4713
                // Category report
4714
                $category_was_added_for_this_test = false;
4715
                if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
4716
                    if (!isset($category_list[$objQuestionTmp->category]['score'])) {
4717
                        $category_list[$objQuestionTmp->category]['score'] = 0;
4718
                    }
4719
                    if (!isset($category_list[$objQuestionTmp->category]['total'])) {
4720
                        $category_list[$objQuestionTmp->category]['total'] = 0;
4721
                    }
4722
                    if (!isset($category_list[$objQuestionTmp->category]['total_questions'])) {
4723
                        $category_list[$objQuestionTmp->category]['total_questions'] = 0;
4724
                    }
4725
                    if (!isset($category_list[$objQuestionTmp->category]['passed'])) {
4726
                        $category_list[$objQuestionTmp->category]['passed'] = 0;
4727
                    }
4728
                    if (!isset($category_list[$objQuestionTmp->category]['wrong'])) {
4729
                        $category_list[$objQuestionTmp->category]['wrong'] = 0;
4730
                    }
4731
                    if (!isset($category_list[$objQuestionTmp->category]['no_answer'])) {
4732
                        $category_list[$objQuestionTmp->category]['no_answer'] = 0;
4733
                    }
4734
4735
                    $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
4736
                    $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
4737
                    if ($scorePassed) {
4738
                        // Only count passed if score is not empty
4739
                        if (!empty($my_total_score)) {
4740
                            $category_list[$objQuestionTmp->category]['passed']++;
4741
                        }
4742
                    } else {
4743
                        if ($result['user_answered']) {
4744
                            $category_list[$objQuestionTmp->category]['wrong']++;
4745
                        } else {
4746
                            $category_list[$objQuestionTmp->category]['no_answer']++;
4747
                        }
4748
                    }
4749
4750
                    $category_list[$objQuestionTmp->category]['total_questions']++;
4751
                    $category_was_added_for_this_test = true;
4752
                }
4753
                if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) {
4754
                    foreach ($objQuestionTmp->category_list as $category_id) {
4755
                        $category_list[$category_id]['score'] += $my_total_score;
4756
                        $category_list[$category_id]['total'] += $my_total_weight;
4757
                        $category_was_added_for_this_test = true;
4758
                    }
4759
                }
4760
4761
                // No category for this question!
4762
                if (false == $category_was_added_for_this_test) {
4763
                    if (!isset($category_list['none']['score'])) {
4764
                        $category_list['none']['score'] = 0;
4765
                    }
4766
                    if (!isset($category_list['none']['total'])) {
4767
                        $category_list['none']['total'] = 0;
4768
                    }
4769
4770
                    $category_list['none']['score'] += $my_total_score;
4771
                    $category_list['none']['total'] += $my_total_weight;
4772
                }
4773
4774
                if (0 == $objExercise->selectPropagateNeg() && $my_total_score < 0) {
4775
                    $my_total_score = 0;
4776
                }
4777
4778
                $comnt = null;
4779
                if ($show_results) {
4780
                    $comnt = Event::get_comments($exeId, $questionId);
4781
                    $teacherAudio = self::getOralFeedbackAudio(
4782
                        $exeId,
4783
                        $questionId
4784
                    );
4785
4786
                    if (!empty($comnt) || $teacherAudio) {
4787
                        echo '<b>'.get_lang('Feedback').'</b>';
4788
                    }
4789
4790
                    if (!empty($comnt)) {
4791
                        echo self::getFeedbackText($comnt);
4792
                    }
4793
4794
                    if ($teacherAudio) {
4795
                        echo $teacherAudio;
4796
                    }
4797
                }
4798
4799
                $calculatedScore = [
4800
                    'result' => self::show_score(
4801
                        $my_total_score,
4802
                        $my_total_weight,
4803
                        false
4804
                    ),
4805
                    'pass' => $scorePassed,
4806
                    'score' => $my_total_score,
4807
                    'weight' => $my_total_weight,
4808
                    'comments' => $comnt,
4809
                    'user_answered' => $result['user_answered'],
4810
                ];
4811
4812
                $score = [];
4813
                if ($show_results) {
4814
                    $score = $calculatedScore;
4815
                }
4816
                if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
4817
                    $reviewScore = [
4818
                        'score' => $my_total_score,
4819
                        'comments' => Event::get_comments($exeId, $questionId),
4820
                    ];
4821
                    $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore);
4822
                    if (false === $check) {
4823
                        $countPendingQuestions++;
4824
                    }
4825
                }
4826
4827
                $contents = ob_get_clean();
4828
                $questionContent = '';
4829
                if ($show_results) {
4830
                    $questionContent = '<div class="question-answer-result">';
4831
                    if (false === $showQuestionScore) {
4832
                        $score = [];
4833
                    }
4834
4835
                    // Shows question title an description
4836
                    $questionContent .= $objQuestionTmp->return_header(
4837
                        $objExercise,
4838
                        $counter,
4839
                        $score
4840
                    );
4841
                }
4842
                $counter++;
4843
                $questionContent .= $contents;
4844
                if ($show_results) {
4845
                    $questionContent .= '</div>';
4846
                }
4847
4848
                $calculatedScore['question_content'] = $questionContent;
4849
                $attemptResult[] = $calculatedScore;
4850
                $parentId = intval($objQuestionTmp->parent_id ?: 0);
4851
                $panelsByParent[$parentId][] = Display::panel($questionContent);
4852
            }
4853
4854
            foreach ($panelsByParent as $pid => $panels) {
4855
                if ($pid !== 0) {
4856
                    $mediaQ = Question::read($pid, $objExercise->course);
4857
                    echo '<div class="media-group">';
4858
                    echo '<div class="media-content">';
4859
                    ob_start();
4860
                    $objExercise->manage_answer(
4861
                        $exeId,
4862
                        $pid,
4863
                        null,
4864
                        'exercise_show',
4865
                        [],
4866
                        false,
4867
                        true,
4868
                        $show_results,
4869
                        $objExercise->selectPropagateNeg()
4870
                    );
4871
                    echo ob_get_clean();
4872
                    echo '</div>';
4873
                    if (!empty($mediaQ->description)) {
4874
                        echo '<div class="media-description">'
4875
                            . $mediaQ->description
4876
                            . '</div>';
4877
                    }
4878
                    echo '<div class="media-children">';
4879
                }
4880
4881
                foreach ($panels as $panelHtml) {
4882
                    echo $panelHtml;
4883
                }
4884
4885
                if ($pid !== 0) {
4886
                    echo '</div></div>';
4887
                }
4888
            }
4889
        }
4890
4891
        // Display text when test is finished #4074 and for LP #4227
4892
        $endOfMessage = $objExercise->getFinishText($total_score, $total_weight);
4893
        if (!empty($endOfMessage)) {
4894
            echo Display::div(
4895
                $endOfMessage,
4896
                ['id' => 'quiz_end_message']
4897
            );
4898
        }
4899
4900
        $totalScoreText = null;
4901
        $certificateBlock = '';
4902
        if (($show_results || $show_only_score) && $showTotalScore) {
4903
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4904
                echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('Your results').'</h1><br />';
4905
            }
4906
            $totalScoreText .= '<div class="question_row_score">';
4907
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4908
                $totalScoreText .= self::getQuestionDiagnosisRibbon(
4909
                    $objExercise,
4910
                    $total_score,
4911
                    $total_weight,
4912
                    true
4913
                );
4914
            } else {
4915
                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
4916
                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
4917
                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->getId());
4918
4919
                    if (!empty($formula)) {
4920
                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
4921
                        $total_weight = $pluginEvaluation->getMaxScore();
4922
                    }
4923
                }
4924
4925
                $totalScoreText .= self::getTotalScoreRibbon(
4926
                    $objExercise,
4927
                    $total_score,
4928
                    $total_weight,
4929
                    true,
4930
                    $countPendingQuestions
4931
                );
4932
            }
4933
            $totalScoreText .= '</div>';
4934
4935
            if (!empty($studentInfo)) {
4936
                $certificateBlock = self::generateAndShowCertificateBlock(
4937
                    $total_score,
4938
                    $total_weight,
4939
                    $objExercise,
4940
                    $studentInfo['id'],
4941
                    $courseId,
4942
                    $sessionId
4943
                );
4944
            }
4945
        }
4946
4947
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4948
            $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults(
4949
                $exeId,
4950
                $objExercise
4951
            );
4952
            echo $chartMultiAnswer;
4953
        }
4954
4955
        if (!empty($category_list) &&
4956
            ($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled)
4957
        ) {
4958
            // Adding total
4959
            $category_list['total'] = [
4960
                'score' => $total_score,
4961
                'total' => $total_weight,
4962
            ];
4963
            echo TestCategory::get_stats_table_by_attempt($objExercise, $category_list);
4964
        }
4965
4966
        if ($show_all_but_expected_answer) {
4967
            $exerciseContent .= Display::return_message(get_lang('Note: This test has been setup to hide the expected answers.'));
4968
        }
4969
4970
        // Remove audio auto play from questions on results page - refs BT#7939
4971
        $exerciseContent = preg_replace(
4972
            ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'],
4973
            '',
4974
            $exerciseContent
4975
        );
4976
4977
        echo $certificateBlock;
4978
4979
        // Ofaj change BT#11784
4980
        if (('true' === api_get_setting('exercise.quiz_show_description_on_results_page')) &&
4981
            !empty($objExercise->description)
4982
        ) {
4983
            echo Display::div($objExercise->description, ['class' => 'exercise_description']);
4984
        }
4985
4986
        echo $exerciseContent;
4987
        if (!$show_only_score) {
4988
            echo $totalScoreText;
4989
        }
4990
4991
        if ($save_user_result) {
4992
            // Tracking of results
4993
            if ($exercise_stat_info) {
4994
                $learnpath_id = $exercise_stat_info['orig_lp_id'];
4995
                $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'];
4996
                $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'];
4997
4998
                if (api_is_allowed_to_session_edit()) {
4999
                    Event::updateEventExercise(
5000
                        $exercise_stat_info['exe_id'],
5001
                        $objExercise->getId(),
5002
                        $total_score,
5003
                        $total_weight,
5004
                        $sessionId,
5005
                        $learnpath_id,
5006
                        $learnpath_item_id,
5007
                        $learnpath_item_view_id,
5008
                        $exercise_stat_info['exe_duration'],
5009
                        $question_list
5010
                    );
5011
5012
                    $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
5013
                    if ($allowStats) {
5014
                        $objExercise->generateStats(
5015
                            $objExercise->getId(),
5016
                            api_get_course_info(),
5017
                            $sessionId
5018
                        );
5019
                    }
5020
                }
5021
            }
5022
5023
            // Send notification at the end
5024
            if (!api_is_allowed_to_edit(null, true) &&
5025
                !api_is_excluded_user_type()
5026
            ) {
5027
                $objExercise->send_mail_notification_for_exam(
5028
                    'end',
5029
                    $question_list_answers,
5030
                    $origin,
5031
                    $exeId,
5032
                    $total_score,
5033
                    $total_weight
5034
                );
5035
            }
5036
        }
5037
5038
        if (in_array(
5039
            $objExercise->selectResultsDisabled(),
5040
            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
5041
        )) {
5042
            echo Display::page_header(get_lang('Ranking'), null, 'h4');
5043
            echo self::displayResultsInRanking(
5044
                $objExercise,
5045
                api_get_user_id(),
5046
                $courseId,
5047
                $sessionId
5048
            );
5049
        }
5050
5051
        if (!empty($remainingMessage)) {
5052
            echo Display::return_message($remainingMessage, 'normal', false);
5053
        }
5054
5055
        $failedAnswersCount = 0;
5056
        $wrongQuestionHtml = '';
5057
        $all = '';
5058
        foreach ($attemptResult as $item) {
5059
            if (false === $item['pass']) {
5060
                $failedAnswersCount++;
5061
                $wrongQuestionHtml .= $item['question_content'].'<br />';
5062
            }
5063
            $all .= $item['question_content'].'<br />';
5064
        }
5065
5066
        $passed = self::isPassPercentageAttemptPassed(
5067
            $objExercise,
5068
            $total_score,
5069
            $total_weight
5070
        );
5071
5072
        $percentage = 0;
5073
        if (!empty($total_weight)) {
5074
            $percentage = ($total_score / $total_weight) * 100;
5075
        }
5076
5077
        return [
5078
            'category_list' => $category_list,
5079
            'attempts_result_list' => $attemptResult, // array of results
5080
            'exercise_passed' => $passed, // boolean
5081
            'total_answers_count' => count($attemptResult), // int
5082
            'failed_answers_count' => $failedAnswersCount, // int
5083
            'failed_answers_html' => $wrongQuestionHtml,
5084
            'all_answers_html' => $all,
5085
            'total_score' => $total_score,
5086
            'total_weight' => $total_weight,
5087
            'total_percentage' => $percentage,
5088
            'count_pending_questions' => $countPendingQuestions,
5089
        ];
5090
    }
5091
5092
    /**
5093
     * Display the ranking of results in a exercise.
5094
     *
5095
     * @param Exercise $exercise
5096
     * @param int      $currentUserId
5097
     * @param int      $courseId
5098
     * @param int      $sessionId
5099
     *
5100
     * @return string
5101
     */
5102
    public static function displayResultsInRanking($exercise, $currentUserId, $courseId, $sessionId = 0)
5103
    {
5104
        $exerciseId = $exercise->iId;
5105
        $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId);
5106
5107
        $table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered']);
5108
        $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']);
5109
        $table->setHeaderContents(0, 1, get_lang('Username'));
5110
        $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']);
5111
        $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
5112
5113
        foreach ($data as $r => $item) {
5114
            if (!isset($item[1])) {
5115
                continue;
5116
            }
5117
            $selected = $item[1]->getId() == $currentUserId;
5118
5119
            foreach ($item as $c => $value) {
5120
                $table->setCellContents($r + 1, $c, $value);
5121
5122
                $attrClass = '';
5123
5124
                if (in_array($c, [0, 2])) {
5125
                    $attrClass = 'text-right';
5126
                } elseif (3 == $c) {
5127
                    $attrClass = 'text-center';
5128
                }
5129
5130
                if ($selected) {
5131
                    $attrClass .= ' warning';
5132
                }
5133
5134
                $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]);
5135
            }
5136
        }
5137
5138
        return $table->toHtml();
5139
    }
5140
5141
    /**
5142
     * Get the ranking for results in a exercise.
5143
     * Function used internally by ExerciseLib::displayResultsInRanking.
5144
     *
5145
     * @param int $exerciseId
5146
     * @param int $courseId
5147
     * @param int $sessionId
5148
     *
5149
     * @return array
5150
     */
5151
    public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0)
5152
    {
5153
        $em = Database::getManager();
5154
5155
        $dql = 'SELECT DISTINCT u.id FROM ChamiloCoreBundle:TrackEExercise te JOIN te.user u WHERE te.quiz = :id AND te.course = :cId';
5156
        $dql .= api_get_session_condition($sessionId, true, false, 'te.session');
5157
5158
        $result = $em
5159
            ->createQuery($dql)
5160
            ->setParameters(['id' => $exerciseId, 'cId' => $courseId])
5161
            ->getScalarResult();
5162
5163
        $data = [];
5164
5165
        foreach ($result as $item) {
5166
            $attempt = self::get_best_attempt_by_user($item['id'], $exerciseId, $courseId, $sessionId);
5167
            if (!empty($attempt) && isset($attempt['score']) && isset($attempt['exe_date'])) {
5168
                $data[] = $attempt;
5169
            }
5170
        }
5171
5172
        if (empty($data)) {
5173
            return [];
5174
        }
5175
5176
        usort(
5177
            $data,
5178
            function ($a, $b) {
5179
                if ($a['score'] != $b['score']) {
5180
                    return $a['score'] > $b['score'] ? -1 : 1;
5181
                }
5182
5183
                if ($a['exe_date'] != $b['exe_date']) {
5184
                    return $a['exe_date'] < $b['exe_date'] ? -1 : 1;
5185
                }
5186
5187
                return 0;
5188
            }
5189
        );
5190
5191
        // flags to display the same position in case of tie
5192
        $lastScore = $data[0]['score'];
5193
        $position = 1;
5194
        $data = array_map(
5195
            function ($item) use (&$lastScore, &$position) {
5196
                if ($item['score'] < $lastScore) {
5197
                    $position++;
5198
                }
5199
5200
                $lastScore = $item['score'];
5201
5202
                return [
5203
                    $position,
5204
                    api_get_user_entity($item['exe_user_id']),
5205
                    self::show_score($item['score'], $item['max_score'], true, true, true),
5206
                    api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT),
5207
                ];
5208
            },
5209
            $data
5210
        );
5211
5212
        return $data;
5213
    }
5214
5215
    /**
5216
     * Get a special ribbon on top of "degree of certainty" questions (
5217
     * variation from getTotalScoreRibbon() for other question types).
5218
     *
5219
     * @param Exercise $objExercise
5220
     * @param float    $score
5221
     * @param float    $weight
5222
     * @param bool     $checkPassPercentage
5223
     *
5224
     * @return string
5225
     */
5226
    public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false)
5227
    {
5228
        $displayChartDegree = true;
5229
        $ribbon = $displayChartDegree ? '<div class="ribbon">' : '';
5230
5231
        if ($checkPassPercentage) {
5232
            $passPercentage = $objExercise->selectPassPercentage();
5233
            $isSuccess = self::isSuccessExerciseResult($score, $weight, $passPercentage);
5234
            // Color the final test score if pass_percentage activated
5235
            $ribbonTotalSuccessOrError = '';
5236
            if (self::isPassPercentageEnabled($passPercentage)) {
5237
                if ($isSuccess) {
5238
                    $ribbonTotalSuccessOrError = ' ribbon-total-success';
5239
                } else {
5240
                    $ribbonTotalSuccessOrError = ' ribbon-total-error';
5241
                }
5242
            }
5243
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total '.$ribbonTotalSuccessOrError.'">' : '';
5244
        } else {
5245
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total">' : '';
5246
        }
5247
5248
        if ($displayChartDegree) {
5249
            $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
5250
            $ribbon .= self::show_score($score, $weight, false, true);
5251
            $ribbon .= '</h3>';
5252
            $ribbon .= '</div>';
5253
        }
5254
5255
        if ($checkPassPercentage) {
5256
            $ribbon .= self::showSuccessMessage(
5257
                $score,
5258
                $weight,
5259
                $objExercise->selectPassPercentage()
5260
            );
5261
        }
5262
5263
        $ribbon .= $displayChartDegree ? '</div>' : '';
5264
5265
        return $ribbon;
5266
    }
5267
5268
    public static function isPassPercentageAttemptPassed($objExercise, $score, $weight)
5269
    {
5270
        $passPercentage = $objExercise->selectPassPercentage();
5271
5272
        return self::isSuccessExerciseResult($score, $weight, $passPercentage);
5273
    }
5274
5275
    /**
5276
     * @param float $score
5277
     * @param float $weight
5278
     * @param bool  $checkPassPercentage
5279
     * @param int   $countPendingQuestions
5280
     *
5281
     * @return string
5282
     */
5283
    public static function getTotalScoreRibbon(
5284
        Exercise $objExercise,
5285
        $score,
5286
        $weight,
5287
        $checkPassPercentage = false,
5288
        $countPendingQuestions = 0
5289
    ) {
5290
        $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score');
5291
        if (1 === $hide) {
5292
            return '';
5293
        }
5294
5295
        $passPercentage = $objExercise->selectPassPercentage();
5296
        $ribbon = '<div class="title-score">';
5297
        if ($checkPassPercentage) {
5298
            $isSuccess = self::isSuccessExerciseResult(
5299
                $score,
5300
                $weight,
5301
                $passPercentage
5302
            );
5303
            // Color the final test score if pass_percentage activated
5304
            $class = '';
5305
            if (self::isPassPercentageEnabled($passPercentage)) {
5306
                if ($isSuccess) {
5307
                    $class = ' ribbon-total-success';
5308
                } else {
5309
                    $class = ' ribbon-total-error';
5310
                }
5311
            }
5312
            $ribbon .= '<div class="total '.$class.'">';
5313
        } else {
5314
            $ribbon .= '<div class="total">';
5315
        }
5316
        $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
5317
        $ribbon .= self::show_score($score, $weight, false, true);
5318
        $ribbon .= '</h3>';
5319
        $ribbon .= '</div>';
5320
        if ($checkPassPercentage) {
5321
            $ribbon .= self::showSuccessMessage(
5322
                $score,
5323
                $weight,
5324
                $passPercentage
5325
            );
5326
        }
5327
        $ribbon .= '</div>';
5328
5329
        if (!empty($countPendingQuestions)) {
5330
            $ribbon .= '<br />';
5331
            $ribbon .= Display::return_message(
5332
                sprintf(
5333
                    get_lang('Temporary score: %s open question(s) not corrected yet.'),
5334
                    $countPendingQuestions
5335
                ),
5336
                'warning'
5337
            );
5338
        }
5339
5340
        return $ribbon;
5341
    }
5342
5343
    /**
5344
     * @param int $countLetter
5345
     *
5346
     * @return mixed
5347
     */
5348
    public static function detectInputAppropriateClass($countLetter)
5349
    {
5350
        $limits = [
5351
            0 => 'input-mini',
5352
            10 => 'input-mini',
5353
            15 => 'input-medium',
5354
            20 => 'input-xlarge',
5355
            40 => 'input-xlarge',
5356
            60 => 'input-xxlarge',
5357
            100 => 'input-xxlarge',
5358
            200 => 'input-xxlarge',
5359
        ];
5360
5361
        foreach ($limits as $size => $item) {
5362
            if ($countLetter <= $size) {
5363
                return $item;
5364
            }
5365
        }
5366
5367
        return $limits[0];
5368
    }
5369
5370
    /**
5371
     * @param int    $senderId
5372
     * @param array  $course_info
5373
     * @param string $test
5374
     * @param string $url
5375
     *
5376
     * @return string
5377
     */
5378
    public static function getEmailNotification($senderId, $course_info, $test, $url)
5379
    {
5380
        $teacher_info = api_get_user_info($senderId);
5381
        $fromName = api_get_person_name(
5382
            $teacher_info['firstname'],
5383
            $teacher_info['lastname'],
5384
            null,
5385
            PERSON_NAME_EMAIL_ADDRESS
5386
        );
5387
5388
        $params = [
5389
            'course_title' => Security::remove_XSS($course_info['name']),
5390
            'test_title' => Security::remove_XSS($test),
5391
            'url' => $url,
5392
            'teacher_name' => $fromName,
5393
        ];
5394
5395
        return Container::getTwig()->render(
5396
            '@ChamiloCore/Mailer/Exercise/result_alert_body.html.twig',
5397
            $params
5398
        );
5399
    }
5400
5401
    /**
5402
     * @return string
5403
     */
5404
    public static function getNotCorrectedYetText()
5405
    {
5406
        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');
5407
    }
5408
5409
    /**
5410
     * @param string $message
5411
     *
5412
     * @return string
5413
     */
5414
    public static function getFeedbackText($message)
5415
    {
5416
        return Display::return_message($message, 'warning', false);
5417
    }
5418
5419
    /**
5420
     * Get the recorder audio component for save a teacher audio feedback.
5421
     *
5422
     * @param int $attemptId
5423
     * @param int $questionId
5424
     *
5425
     * @return string
5426
     */
5427
    public static function getOralFeedbackForm($attemptId, $questionId)
5428
    {
5429
        $view = new Template('', false, false, false, false, false, false);
5430
        $view->assign('type', Asset::EXERCISE_FEEDBACK);
5431
        $view->assign('question_id', $questionId);
5432
        $view->assign('t_exercise_id', $attemptId);
5433
        $template = $view->get_template('exercise/oral_expression.html.twig');
5434
5435
        return $view->fetch($template);
5436
    }
5437
5438
    /**
5439
     * Retrieves the generated audio files for an oral question in an exercise attempt.
5440
     *
5441
     * @param int  $trackExerciseId The ID of the tracked exercise.
5442
     * @param int  $questionId      The ID of the question.
5443
     * @param bool $returnUrls      (Optional) If set to true, only the URLs of the audio files are returned. Default is false.
5444
     *
5445
     * @return array|string If $returnUrls is true, returns an array of URLs of the audio files. Otherwise, returns an HTML string with audio tags.
5446
     */
5447
    public static function getOralFileAudio(int $trackExerciseId, int $questionId, bool $returnUrls = false): array|string
5448
    {
5449
        /** @var TrackEExercise $trackExercise */
5450
        $trackExercise = Container::getTrackEExerciseRepository()->find($trackExerciseId);
5451
5452
        if (null === $trackExercise) {
5453
            return $returnUrls ? [] : '';
5454
        }
5455
5456
        $questionAttempt = $trackExercise->getAttemptByQuestionId($questionId);
5457
5458
        if (null === $questionAttempt) {
5459
            return $returnUrls ? [] : '';
5460
        }
5461
5462
        $basePath = rtrim(api_get_path(WEB_PATH), '/');
5463
        $assetRepo = Container::getAssetRepository();
5464
5465
        if ($returnUrls) {
5466
            $urls = [];
5467
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5468
                $urls[] = $basePath.$assetRepo->getAssetUrl($attemptFile->getAsset());
5469
            }
5470
5471
            return $urls;
5472
        } else {
5473
            $html = '';
5474
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5475
                $html .= Display::tag(
5476
                    'audio',
5477
                    '',
5478
                    [
5479
                        'src' => $basePath.$assetRepo->getAssetUrl($attemptFile->getAsset()),
5480
                        'controls' => '',
5481
                    ]
5482
                );
5483
            }
5484
5485
            return $html;
5486
        }
5487
    }
5488
5489
    /**
5490
     * Get the audio component for a teacher audio feedback.
5491
     */
5492
    public static function getOralFeedbackAudio(int $attemptId, int $questionId): string
5493
    {
5494
        /** @var TrackEExercise $tExercise */
5495
        $tExercise = Container::getTrackEExerciseRepository()->find($attemptId);
5496
5497
        if (null === $tExercise) {
5498
            return '';
5499
        }
5500
5501
        $qAttempt = $tExercise->getAttemptByQuestionId($questionId);
5502
5503
        if (null === $qAttempt) {
5504
            return '';
5505
        }
5506
5507
        $html = '';
5508
5509
        $assetRepo = Container::getAssetRepository();
5510
5511
        foreach ($qAttempt->getAttemptFeedbacks() as $attemptFeedback) {
5512
            $html .= Display::tag(
5513
                'audio',
5514
                '',
5515
                [
5516
                    'src' => $assetRepo->getAssetUrl($attemptFeedback->getAsset()),
5517
                    'controls' => '',
5518
                ]
5519
5520
            );
5521
        }
5522
5523
        return $html;
5524
    }
5525
5526
    public static function getUploadAnswerFiles(int $trackExerciseId, int $questionId, bool $returnUrls = false)
5527
    {
5528
        $trackExercise = Container::getTrackEExerciseRepository()->find($trackExerciseId);
5529
        if (!$trackExercise) { return $returnUrls ? [] : ''; }
5530
        $attempt = $trackExercise->getAttemptByQuestionId($questionId);
5531
        if (!$attempt) { return $returnUrls ? [] : ''; }
5532
5533
        $assetRepo = Container::getAssetRepository();
5534
        $basePath = rtrim(api_get_path(WEB_PATH), '/');
5535
5536
        if ($returnUrls) {
5537
            $urls = [];
5538
            foreach ($attempt->getAttemptFiles() as $af) {
5539
                $urls[] = $basePath.$assetRepo->getAssetUrl($af->getAsset());
5540
            }
5541
            return $urls;
5542
        }
5543
5544
        $html = '';
5545
        foreach ($attempt->getAttemptFiles() as $af) {
5546
            $url = $basePath.$assetRepo->getAssetUrl($af->getAsset());
5547
            $html .= Display::url(basename($url), $url, ['target' => '_blank']).'<br />';
5548
        }
5549
        return $html;
5550
    }
5551
5552
    public static function getNotificationSettings(): array
5553
    {
5554
        return [
5555
            2 => get_lang('Paranoid: E-mail teacher when a student starts an exercise'),
5556
            1 => get_lang('Aware: E-mail teacher when a student ends an exercise'), // default
5557
            3 => get_lang('Relaxed open: E-mail teacher when a student ends an exercise, only if an open question is answered'),
5558
            4 => get_lang('Relaxed audio: E-mail teacher when a student ends an exercise, only if an oral question is answered'),
5559
        ];
5560
    }
5561
5562
    /**
5563
     * Get the additional actions added in exercise_additional_teacher_modify_actions configuration.
5564
     *
5565
     * @param int $exerciseId
5566
     * @param int $iconSize
5567
     *
5568
     * @return string
5569
     */
5570
    public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL)
5571
    {
5572
        $additionalActions = api_get_setting('exercise.exercise_additional_teacher_modify_actions', true) ?: [];
5573
        $actions = [];
5574
5575
        if (is_array($additionalActions)) {
5576
            foreach ($additionalActions as $additionalAction) {
5577
                $actions[] = call_user_func(
5578
                    $additionalAction,
5579
                    $exerciseId,
5580
                    $iconSize
5581
                );
5582
            }
5583
        }
5584
5585
        return implode(PHP_EOL, $actions);
5586
    }
5587
5588
    /**
5589
     * @param int $userId
5590
     * @param int $courseId
5591
     * @param int $sessionId
5592
     *
5593
     * @return int
5594
     */
5595
    public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId)
5596
    {
5597
        $em = Database::getManager();
5598
5599
        if (empty($sessionId)) {
5600
            $sessionId = null;
5601
        }
5602
5603
        $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true);
5604
5605
        $result = $em
5606
            ->createQuery('
5607
                SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
5608
                WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
5609
                    AND ea.tms > :time
5610
            ')
5611
            ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
5612
            ->getSingleScalarResult();
5613
5614
        return $result;
5615
    }
5616
5617
    /**
5618
     * @param int $userId
5619
     * @param int $numberOfQuestions
5620
     * @param int $courseId
5621
     * @param int $sessionId
5622
     *
5623
     * @throws \Doctrine\ORM\Query\QueryException
5624
     *
5625
     * @return bool
5626
     */
5627
    public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId)
5628
    {
5629
        $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day');
5630
5631
        if ($questionsLimitPerDay <= 0) {
5632
            return false;
5633
        }
5634
5635
        $midnightTime = ChamiloHelper::getServerMidnightTime();
5636
5637
        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
5638
            $midnightTime,
5639
            $userId,
5640
            $courseId,
5641
            $sessionId
5642
        );
5643
5644
        return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
5645
    }
5646
5647
    /**
5648
     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
5649
     * By making sure it is set on one question per page and it only contains unique-answer or multiple-answer questions
5650
     * or unique-answer image. And that the exam does not have immediate feedback.
5651
     *
5652
     * @return bool
5653
     */
5654
    public static function isQuizEmbeddable(CQuiz $exercise)
5655
    {
5656
        $em = Database::getManager();
5657
5658
        if (ONE_PER_PAGE != $exercise->getType() ||
5659
            in_array($exercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
5660
        ) {
5661
            return false;
5662
        }
5663
5664
        $countAll = $em
5665
            ->createQuery('SELECT COUNT(qq)
5666
                FROM ChamiloCourseBundle:CQuizQuestion qq
5667
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5668
                   WITH qq.iid = qrq.question
5669
                WHERE qrq.quiz = :id'
5670
            )
5671
            ->setParameter('id', $exercise->getIid())
5672
            ->getSingleScalarResult();
5673
5674
        $countOfAllowed = $em
5675
            ->createQuery('SELECT COUNT(qq)
5676
                FROM ChamiloCourseBundle:CQuizQuestion qq
5677
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5678
                   WITH qq.iid = qrq.question
5679
                WHERE qrq.quiz = :id AND qq.type IN (:types)'
5680
            )
5681
            ->setParameters(
5682
                [
5683
                    'id' => $exercise->getIid(),
5684
                    'types' => [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE],
5685
                ]
5686
            )
5687
            ->getSingleScalarResult();
5688
5689
        return $countAll === $countOfAllowed;
5690
    }
5691
5692
    /**
5693
     * Generate a certificate linked to current quiz and.
5694
     * Return the HTML block with links to download and view the certificate.
5695
     *
5696
     * @param float $totalScore
5697
     * @param float $totalWeight
5698
     * @param int   $studentId
5699
     * @param int   $courseId
5700
     * @param int   $sessionId
5701
     *
5702
     * @return string
5703
     */
5704
    public static function generateAndShowCertificateBlock(
5705
        $totalScore,
5706
        $totalWeight,
5707
        Exercise $objExercise,
5708
        $studentId,
5709
        $courseId,
5710
        $sessionId = 0
5711
    ) {
5712
        if (('true' !== api_get_setting('exercise.quiz_generate_certificate_ending')) ||
5713
            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
5714
        ) {
5715
            return '';
5716
        }
5717
5718
        $repo = Container::getGradeBookCategoryRepository();
5719
        /** @var GradebookCategory $category */
5720
        $category = $repo->findOneBy(
5721
            ['course' => $courseId, 'session' => $sessionId]
5722
        );
5723
5724
        if (null === $category) {
5725
            return '';
5726
        }
5727
5728
        /*$category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
5729
        if (empty($category)) {
5730
            return '';
5731
        }*/
5732
        $categoryId = $category->getId();
5733
        /*$link = LinkFactory::load(
5734
            null,
5735
            null,
5736
            $objExercise->getId(),
5737
            null,
5738
            $courseCode,
5739
            $categoryId
5740
        );*/
5741
5742
        if (empty($category->getLinks()->count())) {
5743
            return '';
5744
        }
5745
5746
        $resourceDeletedMessage = Category::show_message_resource_delete($courseId);
5747
        if (!empty($resourceDeletedMessage) || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
5748
            return '';
5749
        }
5750
5751
        $certificate = Category::generateUserCertificate($category, $studentId);
5752
        if (!is_array($certificate)) {
5753
            return '';
5754
        }
5755
5756
        return Category::getDownloadCertificateBlock($certificate);
5757
    }
5758
5759
    /**
5760
     * @param int $exerciseId
5761
     */
5762
    public static function getExerciseTitleById($exerciseId)
5763
    {
5764
        $em = Database::getManager();
5765
5766
        return $em
5767
            ->createQuery('SELECT cq.title
5768
                FROM ChamiloCourseBundle:CQuiz cq
5769
                WHERE cq.iid = :iid'
5770
            )
5771
            ->setParameter('iid', $exerciseId)
5772
            ->getSingleScalarResult();
5773
    }
5774
5775
    /**
5776
     * @param int $exeId      ID from track_e_exercises
5777
     * @param int $userId     User ID
5778
     * @param int $exerciseId Exercise ID
5779
     * @param int $courseId   Optional. Coure ID.
5780
     *
5781
     * @return TrackEExercise|null
5782
     */
5783
    public static function recalculateResult($exeId, $userId, $exerciseId, $courseId = 0)
5784
    {
5785
        if (empty($userId) || empty($exerciseId)) {
5786
            return null;
5787
        }
5788
5789
        $em = Database::getManager();
5790
        /** @var TrackEExercise $trackedExercise */
5791
        $trackedExercise = $em->getRepository(TrackEExercise::class)->find($exeId);
5792
5793
        if (empty($trackedExercise)) {
5794
            return null;
5795
        }
5796
5797
        if ($trackedExercise->getUser()->getId() != $userId ||
5798
            $trackedExercise->getQuiz()?->getIid() != $exerciseId
5799
        ) {
5800
            return null;
5801
        }
5802
5803
        $questionList = $trackedExercise->getDataTracking();
5804
5805
        if (empty($questionList)) {
5806
            return null;
5807
        }
5808
5809
        $questionList = explode(',', $questionList);
5810
5811
        $exercise = new Exercise($courseId);
5812
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : [];
5813
5814
        if (false === $exercise->read($exerciseId)) {
5815
            return null;
5816
        }
5817
5818
        $totalScore = 0;
5819
        $totalWeight = 0;
5820
5821
        $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
5822
5823
        $formula = 'true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)
5824
            ? $pluginEvaluation->getFormulaForExercise($exerciseId)
5825
            : 0;
5826
5827
        if (empty($formula)) {
5828
            foreach ($questionList as $questionId) {
5829
                $question = Question::read($questionId, $courseInfo);
5830
5831
                if (false === $question) {
5832
                    continue;
5833
                }
5834
5835
                $totalWeight += $question->selectWeighting();
5836
5837
                // We're inside *one* question. Go through each possible answer for this question
5838
                $result = $exercise->manage_answer(
5839
                    $exeId,
5840
                    $questionId,
5841
                    [],
5842
                    'exercise_result',
5843
                    [],
5844
                    false,
5845
                    true,
5846
                    false,
5847
                    $exercise->selectPropagateNeg(),
5848
                    [],
5849
                    [],
5850
                    true
5851
                );
5852
5853
                //  Adding the new score.
5854
                $totalScore += $result['score'];
5855
            }
5856
        } else {
5857
            $totalScore = $pluginEvaluation->getResultWithFormula($exeId, $formula);
5858
            $totalWeight = $pluginEvaluation->getMaxScore();
5859
        }
5860
5861
        $trackedExercise
5862
            ->setScore($totalScore)
5863
            ->setMaxScore($totalWeight);
5864
5865
        $em->persist($trackedExercise);
5866
        $em->flush();
5867
        $lpItemId = $trackedExercise->getOrigLpItemId();
5868
        $lpId = $trackedExercise->getOrigLpId();
5869
        $lpItemViewId = $trackedExercise->getOrigLpItemViewId();
5870
        if ($lpId && $lpItemId && $lpItemViewId) {
5871
            $lpItem = $em->getRepository(CLpItem::class)->find($lpItemId);
5872
            if ($lpItem && 'quiz' === $lpItem->getItemType()) {
5873
                $lpItemView = $em->getRepository(CLpItemView::class)->find($lpItemViewId);
5874
                if ($lpItemView) {
5875
                    $lpItemView->setScore($totalScore);
5876
                    $em->persist($lpItemView);
5877
                    $em->flush();
5878
                }
5879
            }
5880
        }
5881
5882
        return $trackedExercise;
5883
    }
5884
5885
    public static function getTotalQuestionAnswered($courseId, $exerciseId, $questionId, $onlyStudents = false): int
5886
    {
5887
        $courseId = (int) $courseId;
5888
        $exerciseId = (int) $exerciseId;
5889
        $questionId = (int) $questionId;
5890
5891
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5892
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5893
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
5894
        $courseUserJoin = "";
5895
        $studentsWhere = "";
5896
        if ($onlyStudents) {
5897
            $courseUserJoin = "
5898
            INNER JOIN $courseUser cu
5899
            ON cu.c_id = te.c_id AND cu.user_id = exe_user_id";
5900
            $studentsWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
5901
        }
5902
5903
        $sql = "SELECT count(distinct (te.exe_id)) total
5904
            FROM $attemptTable t
5905
            INNER JOIN $trackTable te
5906
            ON (t.exe_id = te.exe_id)
5907
            $courseUserJoin
5908
            WHERE
5909
                te.c_id = $courseId AND
5910
                exe_exo_id = $exerciseId AND
5911
                t.question_id = $questionId AND
5912
                te.status != 'incomplete'
5913
                $studentsWhere
5914
        ";
5915
        $queryTotal = Database::query($sql);
5916
        $totalRow = Database::fetch_assoc($queryTotal);
5917
        $total = 0;
5918
        if ($totalRow) {
5919
            $total = (int) $totalRow['total'];
5920
        }
5921
5922
        return $total;
5923
    }
5924
5925
    public static function getWrongQuestionResults($courseId, $exerciseId, $sessionId = 0, $limit = 10)
5926
    {
5927
        $courseId = (int) $courseId;
5928
        $exerciseId = (int) $exerciseId;
5929
        $limit = (int) $limit;
5930
5931
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
5932
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5933
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5934
5935
        $sessionCondition = '';
5936
        if (!empty($sessionId)) {
5937
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5938
        }
5939
5940
        $sql = "SELECT q.question, question_id, count(q.iid) count
5941
                FROM $attemptTable t
5942
                INNER JOIN $questionTable q
5943
                ON (q.iid = t.question_id)
5944
                INNER JOIN $trackTable te
5945
                ON (t.exe_id = te.exe_id)
5946
                WHERE
5947
                    te.c_id = $courseId AND
5948
                    t.marks != q.ponderation AND
5949
                    exe_exo_id = $exerciseId AND
5950
                    status != 'incomplete'
5951
                    $sessionCondition
5952
                GROUP BY q.iid
5953
                ORDER BY count DESC
5954
                LIMIT $limit
5955
        ";
5956
5957
        $result = Database::query($sql);
5958
5959
        return Database::store_result($result, 'ASSOC');
5960
    }
5961
5962
    public static function getExerciseResultsCount($type, $courseId, $exerciseId, $sessionId = 0)
5963
    {
5964
        $courseId = (int) $courseId;
5965
        $exerciseId = (int) $exerciseId;
5966
5967
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5968
5969
        $sessionCondition = '';
5970
        if (!empty($sessionId)) {
5971
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5972
        }
5973
5974
        $selectCount = 'count(DISTINCT te.exe_id)';
5975
        $scoreCondition = '';
5976
        switch ($type) {
5977
            case 'correct_student':
5978
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5979
                $scoreCondition = ' AND score = max_score ';
5980
                break;
5981
            case 'wrong_student':
5982
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5983
                $scoreCondition = ' AND score != max_score ';
5984
                break;
5985
            case 'correct':
5986
                $scoreCondition = ' AND score = max_score ';
5987
                break;
5988
            case 'wrong':
5989
                $scoreCondition = ' AND score != max_score ';
5990
                break;
5991
        }
5992
5993
        $sql = "SELECT $selectCount count
5994
                FROM $trackTable te
5995
                WHERE
5996
                    c_id = $courseId AND
5997
                    exe_exo_id = $exerciseId AND
5998
                    status != 'incomplete'
5999
                    $scoreCondition
6000
                    $sessionCondition
6001
        ";
6002
        $result = Database::query($sql);
6003
        $totalRow = Database::fetch_assoc($result);
6004
        $total = 0;
6005
        if ($totalRow) {
6006
            $total = (int) $totalRow['count'];
6007
        }
6008
6009
        return $total;
6010
    }
6011
6012
    public static function parseContent($content, $stats, Exercise $exercise, $trackInfo, $currentUserId = 0)
6013
    {
6014
        $wrongAnswersCount = $stats['failed_answers_count'];
6015
        $attemptDate = substr($trackInfo['exe_date'], 0, 10);
6016
        $exerciseId = $exercise->iId;
6017
        $resultsStudentUrl = api_get_path(WEB_CODE_PATH).
6018
            'exercise/result.php?id='.$exerciseId.'&'.api_get_cidreq();
6019
        $resultsTeacherUrl = api_get_path(WEB_CODE_PATH).
6020
            'exercise/exercise_show.php?action=edit&id='.$exerciseId.'&'.api_get_cidreq();
6021
6022
        $content = str_replace(
6023
            [
6024
                '((exercise_error_count))',
6025
                '((all_answers_html))',
6026
                '((all_answers_teacher_html))',
6027
                '((exercise_title))',
6028
                '((exercise_attempt_date))',
6029
                '((link_to_test_result_page_student))',
6030
                '((link_to_test_result_page_teacher))',
6031
            ],
6032
            [
6033
                $wrongAnswersCount,
6034
                $stats['all_answers_html'],
6035
                $stats['all_answers_teacher_html'],
6036
                $exercise->get_formated_title(),
6037
                $attemptDate,
6038
                $resultsStudentUrl,
6039
                $resultsTeacherUrl,
6040
            ],
6041
            $content
6042
        );
6043
6044
        $currentUserId = empty($currentUserId) ? api_get_user_id() : (int) $currentUserId;
6045
6046
        $content = AnnouncementManager::parseContent(
6047
            $currentUserId,
6048
            $content,
6049
            api_get_course_id(),
6050
            api_get_session_id()
6051
        );
6052
6053
        return $content;
6054
    }
6055
6056
    public static function sendNotification(
6057
        $currentUserId,
6058
        $objExercise,
6059
        $exercise_stat_info,
6060
        $courseInfo,
6061
        $attemptCountToSend,
6062
        $stats,
6063
        $statsTeacher
6064
    ) {
6065
        $notifications = api_get_configuration_value('exercise_finished_notification_settings');
6066
        if (empty($notifications)) {
6067
            return false;
6068
        }
6069
6070
        $studentId = $exercise_stat_info['exe_user_id'];
6071
        $exerciseExtraFieldValue = new ExtraFieldValue('exercise');
6072
        $wrongAnswersCount = $stats['failed_answers_count'];
6073
        $exercisePassed = $stats['exercise_passed'];
6074
        $countPendingQuestions = $stats['count_pending_questions'];
6075
        $stats['all_answers_teacher_html'] = $statsTeacher['all_answers_html'];
6076
6077
        // If there are no pending questions (Open questions).
6078
        if (0 === $countPendingQuestions) {
6079
            /*$extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6080
                $objExercise->iId,
6081
                'signature_mandatory'
6082
            );
6083
6084
            if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) {
6085
                if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) {
6086
                    $signature = ExerciseSignaturePlugin::getSignature($studentId, $exercise_stat_info);
6087
                    if (false !== $signature) {
6088
                        //return false;
6089
                    }
6090
                }
6091
            }*/
6092
6093
            // Notifications.
6094
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6095
                $objExercise->iId,
6096
                'notifications'
6097
            );
6098
            $exerciseNotification = '';
6099
            if ($extraFieldData && isset($extraFieldData['value'])) {
6100
                $exerciseNotification = $extraFieldData['value'];
6101
            }
6102
6103
            $subject = sprintf(get_lang('Failure on attempt %s at %s'), $attemptCountToSend, $courseInfo['title']);
6104
            if ($exercisePassed) {
6105
                $subject = sprintf(get_lang('Validation of exercise at %s'), $courseInfo['title']);
6106
            }
6107
6108
            if ($exercisePassed) {
6109
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6110
                    $objExercise->iId,
6111
                    'MailSuccess'
6112
                );
6113
            } else {
6114
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6115
                    $objExercise->iId,
6116
                    'MailAttempt'.$attemptCountToSend
6117
                );
6118
            }
6119
6120
            // Blocking exercise.
6121
            $blockPercentageExtra = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6122
                $objExercise->iId,
6123
                'blocking_percentage'
6124
            );
6125
            $blockPercentage = false;
6126
            if ($blockPercentageExtra && isset($blockPercentageExtra['value']) && $blockPercentageExtra['value']) {
6127
                $blockPercentage = $blockPercentageExtra['value'];
6128
            }
6129
            if ($blockPercentage) {
6130
                $passBlock = $stats['total_percentage'] > $blockPercentage;
6131
                if (false === $passBlock) {
6132
                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6133
                        $objExercise->iId,
6134
                        'MailIsBlockByPercentage'
6135
                    );
6136
                }
6137
            }
6138
6139
            $extraFieldValueUser = new ExtraFieldValue('user');
6140
6141
            if ($extraFieldData && isset($extraFieldData['value'])) {
6142
                $content = $extraFieldData['value'];
6143
                $content = self::parseContent($content, $stats, $objExercise, $exercise_stat_info, $studentId);
6144
                //if (false === $exercisePassed) {
6145
                if (0 !== $wrongAnswersCount) {
6146
                    $content .= $stats['failed_answers_html'];
6147
                }
6148
6149
                $sendMessage = true;
6150
                if (!empty($exerciseNotification)) {
6151
                    foreach ($notifications as $name => $notificationList) {
6152
                        if ($exerciseNotification !== $name) {
6153
                            continue;
6154
                        }
6155
                        foreach ($notificationList as $notificationName => $attemptData) {
6156
                            if ('student_check' === $notificationName) {
6157
                                $sendMsgIfInList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : '';
6158
                                if (!empty($sendMsgIfInList)) {
6159
                                    foreach ($sendMsgIfInList as $skipVariable => $skipValues) {
6160
                                        $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
6161
                                            $studentId,
6162
                                            $skipVariable
6163
                                        );
6164
6165
                                        if (empty($userExtraFieldValue)) {
6166
                                            $sendMessage = false;
6167
                                            break;
6168
                                        } else {
6169
                                            $sendMessage = false;
6170
                                            if (isset($userExtraFieldValue['value']) &&
6171
                                                in_array($userExtraFieldValue['value'], $skipValues)
6172
                                            ) {
6173
                                                $sendMessage = true;
6174
                                                break;
6175
                                            }
6176
                                        }
6177
                                    }
6178
                                }
6179
                                break;
6180
                            }
6181
                        }
6182
                    }
6183
                }
6184
6185
                // Send to student.
6186
                if ($sendMessage) {
6187
                    MessageManager::send_message($currentUserId, $subject, $content);
6188
                }
6189
            }
6190
6191
            if (!empty($exerciseNotification)) {
6192
                foreach ($notifications as $name => $notificationList) {
6193
                    if ($exerciseNotification !== $name) {
6194
                        continue;
6195
                    }
6196
                    foreach ($notificationList as $attemptData) {
6197
                        $skipNotification = false;
6198
                        $skipNotificationList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : [];
6199
                        if (!empty($skipNotificationList)) {
6200
                            foreach ($skipNotificationList as $skipVariable => $skipValues) {
6201
                                $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
6202
                                    $studentId,
6203
                                    $skipVariable
6204
                                );
6205
6206
                                if (empty($userExtraFieldValue)) {
6207
                                    $skipNotification = true;
6208
                                    break;
6209
                                } else {
6210
                                    if (isset($userExtraFieldValue['value'])) {
6211
                                        if (!in_array($userExtraFieldValue['value'], $skipValues)) {
6212
                                            $skipNotification = true;
6213
                                            break;
6214
                                        }
6215
                                    } else {
6216
                                        $skipNotification = true;
6217
                                        break;
6218
                                    }
6219
                                }
6220
                            }
6221
                        }
6222
6223
                        if ($skipNotification) {
6224
                            continue;
6225
                        }
6226
6227
                        $email = isset($attemptData['email']) ? $attemptData['email'] : '';
6228
                        $emailList = explode(',', $email);
6229
                        if (empty($emailList)) {
6230
                            continue;
6231
                        }
6232
                        $attempts = isset($attemptData['attempts']) ? $attemptData['attempts'] : [];
6233
                        foreach ($attempts as $attempt) {
6234
                            $sendMessage = false;
6235
                            if (isset($attempt['attempt']) && $attemptCountToSend !== (int) $attempt['attempt']) {
6236
                                continue;
6237
                            }
6238
6239
                            if (!isset($attempt['status'])) {
6240
                                continue;
6241
                            }
6242
6243
                            if ($blockPercentage && isset($attempt['is_block_by_percentage'])) {
6244
                                if ($attempt['is_block_by_percentage']) {
6245
                                    if ($passBlock) {
6246
                                        continue;
6247
                                    }
6248
                                } else {
6249
                                    if (false === $passBlock) {
6250
                                        continue;
6251
                                    }
6252
                                }
6253
                            }
6254
6255
                            switch ($attempt['status']) {
6256
                                case 'passed':
6257
                                    if ($exercisePassed) {
6258
                                        $sendMessage = true;
6259
                                    }
6260
                                    break;
6261
                                case 'failed':
6262
                                    if (false === $exercisePassed) {
6263
                                        $sendMessage = true;
6264
                                    }
6265
                                    break;
6266
                                case 'all':
6267
                                    $sendMessage = true;
6268
                                    break;
6269
                            }
6270
6271
                            if ($sendMessage) {
6272
                                $attachments = [];
6273
                                if (isset($attempt['add_pdf']) && $attempt['add_pdf']) {
6274
                                    // Get pdf content
6275
                                    $pdfExtraData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6276
                                        $objExercise->iId,
6277
                                        $attempt['add_pdf']
6278
                                    );
6279
6280
                                    if ($pdfExtraData && isset($pdfExtraData['value'])) {
6281
                                        $pdfContent = self::parseContent(
6282
                                            $pdfExtraData['value'],
6283
                                            $stats,
6284
                                            $objExercise,
6285
                                            $exercise_stat_info,
6286
                                            $studentId
6287
                                        );
6288
6289
                                        @$pdf = new PDF();
6290
                                        $filename = get_lang('Test');
6291
                                        $pdfPath = @$pdf->content_to_pdf(
6292
                                            "<html><body>$pdfContent</body></html>",
6293
                                            null,
6294
                                            $filename,
6295
                                            api_get_course_id(),
6296
                                            'F',
6297
                                            false,
6298
                                            null,
6299
                                            false,
6300
                                            true
6301
                                        );
6302
                                        $attachments[] = ['filename' => $filename, 'path' => $pdfPath];
6303
                                    }
6304
                                }
6305
6306
                                $content = isset($attempt['content_default']) ? $attempt['content_default'] : '';
6307
                                if (isset($attempt['content'])) {
6308
                                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6309
                                        $objExercise->iId,
6310
                                        $attempt['content']
6311
                                    );
6312
                                    if ($extraFieldData && isset($extraFieldData['value']) && !empty($extraFieldData['value'])) {
6313
                                        $content = $extraFieldData['value'];
6314
                                    }
6315
                                }
6316
6317
                                if (!empty($content)) {
6318
                                    $content = self::parseContent(
6319
                                        $content,
6320
                                        $stats,
6321
                                        $objExercise,
6322
                                        $exercise_stat_info,
6323
                                        $studentId
6324
                                    );
6325
                                    foreach ($emailList as $email) {
6326
                                        if (empty($email)) {
6327
                                            continue;
6328
                                        }
6329
                                        api_mail_html(
6330
                                            null,
6331
                                            $email,
6332
                                            $subject,
6333
                                            $content,
6334
                                            null,
6335
                                            null,
6336
                                            [],
6337
                                            $attachments
6338
                                        );
6339
                                    }
6340
                                }
6341
6342
                                if (isset($attempt['post_actions'])) {
6343
                                    foreach ($attempt['post_actions'] as $action => $params) {
6344
                                        switch ($action) {
6345
                                            case 'subscribe_student_to_courses':
6346
                                                foreach ($params as $code) {
6347
                                                    $courseInfo = api_get_course_info($code);
6348
                                                    CourseManager::subscribeUser(
6349
                                                        $currentUserId,
6350
                                                        $courseInfo['real_id']
6351
                                                    );
6352
                                                    break;
6353
                                                }
6354
                                                break;
6355
                                        }
6356
                                    }
6357
                                }
6358
                            }
6359
                        }
6360
                    }
6361
                }
6362
            }
6363
        }
6364
    }
6365
6366
    /**
6367
     * Delete an exercise attempt.
6368
     *
6369
     * Log the exe_id deleted with the exe_user_id related.
6370
     *
6371
     * @param int $exeId
6372
     */
6373
    public static function deleteExerciseAttempt($exeId)
6374
    {
6375
        $exeId = (int) $exeId;
6376
6377
        $trackExerciseInfo = self::get_exercise_track_exercise_info($exeId);
6378
6379
        if (empty($trackExerciseInfo)) {
6380
            return;
6381
        }
6382
6383
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6384
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6385
6386
        Database::query("DELETE FROM $tblTrackAttempt WHERE exe_id = $exeId");
6387
        Database::query("DELETE FROM $tblTrackExercises WHERE exe_id = $exeId");
6388
6389
        Event::addEvent(
6390
            LOG_EXERCISE_ATTEMPT_DELETE,
6391
            LOG_EXERCISE_ATTEMPT,
6392
            $exeId,
6393
            api_get_utc_datetime()
6394
        );
6395
        Event::addEvent(
6396
            LOG_EXERCISE_ATTEMPT_DELETE,
6397
            LOG_EXERCISE_AND_USER_ID,
6398
            $exeId.'-'.$trackExerciseInfo['exe_user_id'],
6399
            api_get_utc_datetime()
6400
        );
6401
    }
6402
6403
    public static function scorePassed($score, $total)
6404
    {
6405
        $compareResult = bccomp($score, $total, 3);
6406
        $scorePassed = 1 === $compareResult || 0 === $compareResult;
6407
        if (false === $scorePassed) {
6408
            $epsilon = 0.00001;
6409
            if (abs($score - $total) < $epsilon) {
6410
                $scorePassed = true;
6411
            }
6412
        }
6413
6414
        return $scorePassed;
6415
    }
6416
6417
    /**
6418
     * Returns the HTML for a specific exercise attempt, ready for PDF generation.
6419
     */
6420
    public static function getAttemptPdfHtml(int $exeId, int $courseId, int $sessionId): string
6421
    {
6422
        $_GET = [
6423
            'id'           => $exeId,
6424
            'action'       => 'export',
6425
            'export_type'  => 'all_results',
6426
            'cid'          => $courseId,
6427
            'sid'          => $sessionId,
6428
            'gid'          => 0,
6429
            'gradebook'    => 0,
6430
            'origin'       => '',
6431
        ];
6432
        $_REQUEST = $_GET + $_REQUEST;
6433
6434
        ob_start();
6435
        include __DIR__ . '/../../exercise/exercise_show.php';
6436
        return ob_get_clean();
6437
    }
6438
6439
    /**
6440
     * Generates and saves a PDF for a single exercise attempt
6441
     */
6442
    public static function saveFileExerciseResultPdfDirect(
6443
        int    $exeId,
6444
        int    $courseId,
6445
        int    $sessionId,
6446
        string $exportFolderPath
6447
    ): void {
6448
        // Retrieve the HTML for this attempt and convert it to PDF
6449
        $html = self::getAttemptPdfHtml($exeId, $courseId, $sessionId);
6450
6451
        // Determine filename and path based on user information
6452
        $track   = self::get_exercise_track_exercise_info($exeId);
6453
        $userId  = $track['exe_user_id'] ?? 0;
6454
        $user    = api_get_user_info($userId);
6455
        $pdfName = api_replace_dangerous_char(
6456
            ($user['firstname'] ?? 'user') . '_' .
6457
            ($user['lastname']  ?? 'unknown') .
6458
            '_attemptId' . $exeId . '.pdf'
6459
        );
6460
        $filePath = rtrim($exportFolderPath, '/') . '/' . $pdfName;
6461
6462
        if (file_exists($filePath)) {
6463
            return;
6464
        }
6465
6466
        // Ensure the directory exists
6467
        $dir = dirname($filePath);
6468
        if (!is_dir($dir)) {
6469
            mkdir($dir, 0755, true);
6470
        }
6471
6472
        // Use Chamilo's PDF class to generate and save the file
6473
        $params = [
6474
            'filename'    => $pdfName,
6475
            'course_code' => api_get_course_id(),
6476
        ];
6477
        $pdf = new PDF('A4', 'P', $params);
6478
        $pdf->html_to_pdf_with_template(
6479
            $html,
6480
            true,
6481
            false,
6482
            true,
6483
            [],
6484
            'F',
6485
            $filePath
6486
        );
6487
    }
6488
6489
    /**
6490
     * Exports all results of an exercise to a ZIP archive by generating PDFs on disk and then sending the ZIP to the browser.
6491
     */
6492
    public static function exportExerciseAllResultsZip(
6493
        int   $sessionId,
6494
        int   $courseId,
6495
        int   $exerciseId,
6496
        array $filterDates = [],
6497
        string $mainPath    = ''
6498
    ) {
6499
        $em = Container::getEntityManager();
6500
6501
        /** @var CourseEntity|null $course */
6502
        $course = $em->getRepository(CourseEntity::class)->find($courseId);
6503
        /** @var CQuiz|null $quiz */
6504
        $quiz   = $em->getRepository(CQuiz::class)->findOneBy(['iid' => $exerciseId]);
6505
        $session = null;
6506
6507
        if (!$course) {
6508
            Display::addFlash(Display::return_message(get_lang('Course not found'), 'warning', false));
6509
            return false;
6510
        }
6511
        if (!$quiz) {
6512
            Display::addFlash(Display::return_message(get_lang('Test not found'), 'warning', false));
6513
            return false;
6514
        }
6515
        if ($sessionId > 0) {
6516
            $session = $em->getRepository(SessionEntity::class)->find($sessionId);
6517
            if (!$session) {
6518
                Display::addFlash(Display::return_message(get_lang('Session not found'), 'warning', false));
6519
                return false;
6520
            }
6521
        }
6522
6523
        // Fetch exe_ids with Doctrine, accepting NULL/0 session when $sessionId == 0
6524
        $exeIds = self::findAttemptExeIdsForExport($course, $quiz, $session, $filterDates);
6525
6526
        // Optional: hard fallback with native SQL to catch legacy session_id=0 rows if needed
6527
        if (empty($exeIds) && $sessionId === 0) {
6528
            $exeIds = self::findAttemptExeIdsFallbackSql($courseId, $exerciseId, $filterDates);
6529
        }
6530
6531
        if (empty($exeIds)) {
6532
            Display::addFlash(
6533
                Display::return_message(
6534
                    get_lang('No result found for export in this test.'),
6535
                    'warning',
6536
                    false
6537
                )
6538
            );
6539
            return false;
6540
        }
6541
6542
        // Prepare a temporary folder for the PDFs
6543
        $exportName       = 'S' . (int)($sessionId) . '-C' . (int)($courseId) . '-T' . (int)($exerciseId);
6544
        $baseDir          = api_get_path(SYS_ARCHIVE_PATH);
6545
        $exportFolderPath = $baseDir . 'pdfexport-' . $exportName;
6546
        if (is_dir($exportFolderPath)) {
6547
            rmdirr($exportFolderPath);
6548
        }
6549
        mkdir($exportFolderPath, 0755, true);
6550
6551
        // Generate a PDF for each attempt
6552
        foreach ($exeIds as $exeId) {
6553
            self::saveFileExerciseResultPdfDirect(
6554
                (int)$exeId,
6555
                (int)$courseId,
6556
                (int)$sessionId,
6557
                $exportFolderPath
6558
            );
6559
        }
6560
6561
        // Create the ZIP archive containing all generated PDFs
6562
        $zipFilePath = $baseDir . 'pdfexport-' . $exportName . '.zip';
6563
        $zip = new \ZipArchive();
6564
        if ($zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
6565
            throw new \Exception('Failed to create ZIP file');
6566
        }
6567
        $files = new RecursiveIteratorIterator(
6568
            new RecursiveDirectoryIterator($exportFolderPath),
6569
            RecursiveIteratorIterator::LEAVES_ONLY
6570
        );
6571
        foreach ($files as $file) {
6572
            if (!$file->isDir()) {
6573
                $filePath     = $file->getRealPath();
6574
                $relativePath = substr($filePath, strlen($exportFolderPath) + 1);
6575
                $zip->addFile($filePath, $relativePath);
6576
            }
6577
        }
6578
        $zip->close();
6579
        rmdirr($exportFolderPath);
6580
6581
        // Send the ZIP file to the browser or move it to mainPath
6582
        if (!empty($mainPath)) {
6583
            @rename($zipFilePath, $mainPath . '/pdfexport-' . $exportName . '.zip');
6584
            return true;
6585
        }
6586
6587
        session_write_close();
6588
        while (ob_get_level()) {
6589
            @ob_end_clean();
6590
        }
6591
6592
        header('Content-Description: File Transfer');
6593
        header('Content-Type: application/zip');
6594
        header('Content-Disposition: attachment; filename="pdfexport-' . $exportName . '.zip"');
6595
        header('Content-Transfer-Encoding: binary');
6596
        header('Expires: 0');
6597
        header('Cache-Control: must-revalidate');
6598
        header('Pragma: public');
6599
        header('Content-Length: ' . filesize($zipFilePath));
6600
6601
        readfile($zipFilePath);
6602
        @unlink($zipFilePath);
6603
        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...
6604
    }
6605
6606
    /**
6607
     * Return exe_ids for export using Doctrine (handles NULL/0 sessions safely).
6608
     */
6609
    private static function findAttemptExeIdsForExport(
6610
        CourseEntity $course,
6611
        CQuiz $quiz,
6612
        ?SessionEntity $session,
6613
        array $filterDates
6614
    ): array {
6615
        $em = Container::getEntityManager();
6616
6617
        $qb = $em->createQueryBuilder()
6618
            ->select('te.exeId AS exeId')
6619
            ->from(TrackEExercise::class, 'te')
6620
            ->where('te.course = :course')
6621
            ->andWhere('te.quiz = :quiz')
6622
            ->setParameter('course', $course)
6623
            ->setParameter('quiz', $quiz);
6624
6625
        // Session filter:
6626
        if ($session) {
6627
            $qb->andWhere('te.session = :session')->setParameter('session', $session);
6628
        } else {
6629
            // Accept both NULL and legacy "0" values
6630
            // IDENTITY() extracts the FK raw value to match 0 if present
6631
            $qb->andWhere('(te.session IS NULL OR IDENTITY(te.session) = 0)');
6632
        }
6633
6634
        // Date filters on exeDate
6635
        if (!empty($filterDates['start_date'])) {
6636
            $qb->andWhere('te.exeDate >= :start')
6637
                ->setParameter('start', new DateTime($filterDates['start_date']));
6638
        }
6639
        if (!empty($filterDates['end_date'])) {
6640
            $qb->andWhere('te.exeDate <= :end')
6641
                ->setParameter('end', new DateTime($filterDates['end_date']));
6642
        }
6643
6644
        $qb->orderBy('te.exeDate', 'DESC')->setMaxResults(5000);
6645
6646
        $rows = $qb->getQuery()->getScalarResult();
6647
        $exeIds = array_map(static fn($r) => (int)$r['exeId'], $rows);
6648
6649
6650
        return array_values(array_unique($exeIds));
6651
    }
6652
6653
    /**
6654
     * Fallback with native SQL for very legacy rows (session_id=0 and column names).
6655
     */
6656
    private static function findAttemptExeIdsFallbackSql(
6657
        int $courseId,
6658
        int $quizIid,
6659
        array $filterDates
6660
    ): array {
6661
        $conn = Container::getEntityManager()->getConnection();
6662
6663
        $sql = 'SELECT te.exe_id
6664
            FROM track_e_exercises te
6665
            WHERE te.c_id = :cid
6666
              AND te.exe_exo_id = :iid
6667
              AND (te.session_id IS NULL OR te.session_id = 0)';
6668
6669
        $params = ['cid' => $courseId, 'iid' => $quizIid];
6670
        $types  = [];
6671
6672
        if (!empty($filterDates['start_date'])) {
6673
            $sql .= ' AND te.exe_date >= :start';
6674
            $params['start'] = $filterDates['start_date'];
6675
        }
6676
        if (!empty($filterDates['end_date'])) {
6677
            $sql .= ' AND te.exe_date <= :end';
6678
            $params['end'] = $filterDates['end_date'];
6679
        }
6680
6681
        $sql .= ' ORDER BY te.exe_date DESC LIMIT 5000';
6682
6683
        $rows = $conn->fetchAllAssociative($sql, $params, $types);
6684
        $exeIds = array_map(static fn($r) => (int)$r['exe_id'], $rows);
6685
6686
        return $exeIds;
6687
    }
6688
6689
    /**
6690
     * Calculates the overall score for Combination-type questions.
6691
     */
6692
    public static function getUserQuestionScoreGlobal(
6693
        int   $answerType,
6694
        array $listCorrectAnswers,
6695
        int   $exeId,
6696
        int   $questionId,
6697
        float $questionWeighting,
6698
        array $choice = [],
6699
        int $nbrAnswers = 0
6700
    ): float
6701
    {
6702
        $nbrCorrect = 0;
6703
        $nbrOptions = 0;
6704
        $choice = is_array($choice) ? $choice : [];
6705
        switch ($answerType) {
6706
            case FILL_IN_BLANKS_COMBINATION:
6707
                if (!empty($listCorrectAnswers)) {
6708
                    if (!empty($listCorrectAnswers['student_score']) && is_array($listCorrectAnswers['student_score'])) {
6709
                        foreach ($listCorrectAnswers['student_score'] as $val) {
6710
                            if ((int) $val === 1) {
6711
                                $nbrCorrect++;
6712
                            }
6713
                        }
6714
                    }
6715
                    if (!empty($listCorrectAnswers['words_count'])) {
6716
                        $nbrOptions = (int) $listCorrectAnswers['words_count'];
6717
                    } elseif (!empty($listCorrectAnswers['words']) && is_array($listCorrectAnswers['words'])) {
6718
                        $nbrOptions = count($listCorrectAnswers['words']);
6719
                    }
6720
                }
6721
                break;
6722
6723
            case HOT_SPOT_COMBINATION:
6724
                if (!empty($listCorrectAnswers) && is_array($listCorrectAnswers) && is_array($choice)) {
6725
                    foreach ($listCorrectAnswers as $idx => $val) {
6726
                        if (isset($choice[$idx]) && (int) $choice[$idx] === 1) {
6727
                            $nbrCorrect++;
6728
                        }
6729
                    }
6730
                } else {
6731
                    $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
6732
                    $exeIdEsc = Database::escape_string($exeId);
6733
                    $qIdEsc   = Database::escape_string($questionId);
6734
                    $sql = "SELECT COUNT(hotspot_id) AS ct
6735
                        FROM $TBL_TRACK_HOTSPOT
6736
                        WHERE hotspot_exe_id = '$exeIdEsc'
6737
                          AND hotspot_question_id = '$qIdEsc'
6738
                          AND hotspot_correct = 1";
6739
                    $result = Database::query($sql);
6740
                    $nbrCorrect = (int) Database::result($result, 0, 0);
6741
                }
6742
                $nbrOptions = (int) $nbrAnswers;
6743
                break;
6744
6745
            case MATCHING_COMBINATION:
6746
            case MATCHING_DRAGGABLE_COMBINATION:
6747
                if (isset($listCorrectAnswers['form_values'])) {
6748
                    if (isset($listCorrectAnswers['form_values']['correct'])) {
6749
                        $nbrCorrect = count($listCorrectAnswers['form_values']['correct']);
6750
                        $nbrOptions = (int) $listCorrectAnswers['form_values']['count_options'];
6751
                    }
6752
                } else {
6753
                    if (isset($listCorrectAnswers['from_database'])) {
6754
                        if (isset($listCorrectAnswers['from_database']['correct'])) {
6755
                            $nbrCorrect = count($listCorrectAnswers['from_database']['correct']);
6756
                            $nbrOptions = (int) $listCorrectAnswers['from_database']['count_options'];
6757
                        }
6758
                    }
6759
                }
6760
                break;
6761
        }
6762
6763
        $questionScore = 0.0;
6764
        if ($nbrOptions > 0 && $nbrCorrect === $nbrOptions) {
6765
            $questionScore = (float) $questionWeighting;
6766
        }
6767
6768
        return $questionScore;
6769
    }
6770
}
6771