Passed
Push — master ( 600633...6695e4 )
by
unknown
08:28
created

ExerciseLib::exercise_time_control_is_valid()   B

Complexity

Conditions 9
Paths 11

Size

Total Lines 48
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 25
c 1
b 0
f 0
nc 11
nop 3
dl 0
loc 48
rs 8.0555
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\Asset;
6
use Chamilo\CoreBundle\Entity\GradebookCategory;
7
use Chamilo\CoreBundle\Entity\TrackEExercise;
8
use Chamilo\CoreBundle\Enums\ActionIcon;
9
use Chamilo\CoreBundle\Framework\Container;
10
use Chamilo\CoreBundle\Helpers\ChamiloHelper;
11
use Chamilo\CourseBundle\Entity\CLpItem;
12
use Chamilo\CourseBundle\Entity\CLpItemView;
13
use Chamilo\CourseBundle\Entity\CQuiz;
14
use ChamiloSession as Session;
15
16
/**
17
 * Class ExerciseLib
18
 * shows a question and its answers.
19
 *
20
 * @author Olivier Brouckaert <[email protected]>
21
 * @author Hubert Borderiou 2011-10-21
22
 * @author ivantcholakov2009-07-20
23
 * @author Julio Montoya
24
 */
25
class ExerciseLib
26
{
27
    /**
28
     * Shows a question.
29
     *
30
     * @param Exercise $exercise
31
     * @param int      $questionId     $questionId question id
32
     * @param bool     $only_questions if true only show the questions, no exercise title
33
     * @param bool     $origin         i.e = learnpath
34
     * @param string   $current_item   current item from the list of questions
35
     * @param bool     $show_title
36
     * @param bool     $freeze
37
     * @param array    $user_choice
38
     * @param bool     $show_comment
39
     * @param bool     $show_answers
40
     *
41
     * @throws \Exception
42
     *
43
     * @return bool|int
44
     */
45
    public static function showQuestion(
46
        $exercise,
47
        $questionId,
48
        $only_questions = false,
49
        $origin = false,
50
        $current_item = '',
51
        $show_title = true,
52
        $freeze = false,
53
        $user_choice = [],
54
        $show_comment = false,
55
        $show_answers = false,
56
        $show_icon = false
57
    ) {
58
        $course_id = $exercise->course_id;
59
        $exerciseId = $exercise->iId;
60
61
        if (empty($course_id)) {
62
            return '';
63
        }
64
        $course = $exercise->course;
65
66
        // Change false to true in the following line to enable answer hinting
67
        $debug_mark_answer = $show_answers;
68
        // Reads question information
69
        if (!$objQuestionTmp = Question::read($questionId, $course)) {
70
            // Question not found
71
            return false;
72
        }
73
74
        if (EXERCISE_FEEDBACK_TYPE_END != $exercise->getFeedbackType()) {
75
            $show_comment = false;
76
        }
77
78
        $answerType = $objQuestionTmp->selectType();
79
80
        if (MEDIA_QUESTION === $answerType) {
81
            $mediaHtml = $objQuestionTmp->selectDescription();
82
            if (!empty($mediaHtml)) {
83
                echo '<div class="media-content wysiwyg">'. $mediaHtml .'</div>';
84
            }
85
            return 0;
86
        }
87
88
        if (PAGE_BREAK === $answerType) {
89
            $description = $objQuestionTmp->selectDescription();
90
            if (!$only_questions && !empty($description)) {
91
                echo '<div class="page-break-content wysiwyg">'
92
                    . $description .
93
                    '</div>';
94
            }
95
            return 0;
96
        }
97
98
        $s = '';
99
        if (HOT_SPOT != $answerType &&
100
            HOT_SPOT_DELINEATION != $answerType &&
101
             HOT_SPOT_COMBINATION != $answerType &&
102
            ANNOTATION != $answerType
103
        ) {
104
            // Question is not a hotspot
105
            if (!$only_questions) {
106
                $questionDescription = $objQuestionTmp->selectDescription();
107
                if ($show_title) {
108
                    if ($exercise->display_category_name) {
109
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
110
                    }
111
                    $titleToDisplay = $objQuestionTmp->getTitleToDisplay($exercise, $current_item);
112
                    if (READING_COMPREHENSION == $answerType) {
113
                        // In READING_COMPREHENSION, the title of the question
114
                        // contains the question itself, which can only be
115
                        // shown at the end of the given time, so hide for now
116
                        $titleToDisplay = Display::div(
117
                            $current_item.'. '.get_lang('Reading comprehension'),
118
                            ['class' => 'question_title']
119
                        );
120
                    }
121
                    echo $titleToDisplay;
122
                }
123
124
                if (!empty($questionDescription) && READING_COMPREHENSION != $answerType) {
125
                    echo Display::div(
126
                        $questionDescription,
127
                        ['class' => 'question_description wysiwyg']
128
                    );
129
                }
130
            }
131
132
            if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, UPLOAD_ANSWER]) && $freeze) {
133
                return '';
134
            }
135
136
            echo '<div class="question_options type-'.$answerType.'">';
137
            // construction of the Answer object (also gets all answers details)
138
            $objAnswerTmp = new Answer($questionId, $course_id, $exercise);
139
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
140
            $quizQuestionOptions = Question::readQuestionOption($questionId, $course_id);
141
            $selectableOptions = [];
142
143
            for ($i = 1; $i <= $objAnswerTmp->nbrAnswers; $i++) {
144
                $selectableOptions[$objAnswerTmp->iid[$i]] = $objAnswerTmp->answer[$i];
145
            }
146
147
            // For "matching" type here, we need something a little bit special
148
            // because the match between the suggestions and the answers cannot be
149
            // done easily (suggestions and answers are in the same table), so we
150
            // have to go through answers first (elems with "correct" value to 0).
151
            $select_items = [];
152
            //This will contain the number of answers on the left side. We call them
153
            // suggestions here, for the sake of comprehensions, while the ones
154
            // on the right side are called answers
155
            $num_suggestions = 0;
156
            switch ($answerType) {
157
                case MATCHING:
158
                case MATCHING_COMBINATION:
159
                case DRAGGABLE:
160
                case MATCHING_DRAGGABLE:
161
                case MATCHING_DRAGGABLE_COMBINATION:
162
                    if (DRAGGABLE == $answerType) {
163
                        $isVertical = 'v' === $objQuestionTmp->extra;
164
                        $s .= '<p class="small">'
165
                            .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.')
166
                            .'</p>
167
                            <div class="w-full ui-widget ui-helper-clearfix">
168
                                <div class="clearfix">
169
                                    <ul class="exercise-draggable-answer '.($isVertical ? 'vertical' : 'list-inline w-full').'"
170
                                        id="question-'.$questionId.'" data-question="'.$questionId.'">
171
                            ';
172
                    } else {
173
                        $s .= '<div id="drag'.$questionId.'_question" class="drag_question">
174
                               <table class="table table-hover table-striped data_table">';
175
                    }
176
177
                    // Iterate through answers.
178
                    $x = 1;
179
                    // Mark letters for each answer.
180
                    $letter = 'A';
181
                    $answer_matching = [];
182
                    $cpt1 = [];
183
                    for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
184
                        $answerCorrect = $objAnswerTmp->isCorrect($answerId);
185
                        $numAnswer = $objAnswerTmp->selectAutoId($answerId);
186
                        if (0 == $answerCorrect) {
187
                            // options (A, B, C, ...) that will be put into the list-box
188
                            // have the "correct" field set to 0 because they are answer
189
                            $cpt1[$x] = $letter;
190
                            $answer_matching[$x] = $objAnswerTmp->selectAnswerByAutoId($numAnswer);
191
                            $x++;
192
                            $letter++;
193
                        }
194
                    }
195
196
                    $i = 1;
197
                    $select_items[0]['id'] = 0;
198
                    $select_items[0]['letter'] = '--';
199
                    $select_items[0]['answer'] = '';
200
                    foreach ($answer_matching as $id => $value) {
201
                        $select_items[$i]['id'] = $value['iid'];
202
                        $select_items[$i]['letter'] = $cpt1[$id];
203
                        $select_items[$i]['answer'] = $value['answer'];
204
                        $i++;
205
                    }
206
207
                    $user_choice_array_position = [];
208
                    if (!empty($user_choice)) {
209
                        foreach ($user_choice as $item) {
210
                            $user_choice_array_position[$item['position']] = $item['answer'];
211
                        }
212
                    }
213
                    $num_suggestions = ($nbrAnswers - $x) + 1;
214
                    break;
215
                case FREE_ANSWER:
216
                    $fck_content = isset($user_choice[0]) && !empty($user_choice[0]['answer']) ? $user_choice[0]['answer'] : null;
217
                    $form = new FormValidator('free_choice_'.$questionId);
218
                    $config = [
219
                        'ToolbarSet' => 'TestFreeAnswer',
220
                    ];
221
                    $form->addHtmlEditor(
222
                        'choice['.$questionId.']',
223
                        null,
224
                        false,
225
                        false,
226
                        $config
227
                    );
228
                    $form->setDefaults(["choice[".$questionId."]" => $fck_content]);
229
                    $s .= $form->returnForm();
230
                    break;
231
                case UPLOAD_ANSWER:
232
                    $url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=upload_answer&question_id='.$questionId;
233
                    $multipleForm = new FormValidator(
234
                        'drag_drop',
235
                        'post',
236
                        '#',
237
                        '',
238
                        ['enctype' => 'multipart/form-data', 'id' => 'drag_drop']
239
                    );
240
241
                    $iconDelete = Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_SMALL);
242
                    $multipleForm->addMultipleUpload($url);
243
244
                    $s .= '<script>
245
                        function setRemoveLink(dataContext) {
246
                            var removeLink = $("<a>", {
247
                                html: "&nbsp;'.addslashes($iconDelete).'",
248
                                href: "#",
249
                                click: function(e) {
250
                                  e.preventDefault();
251
                                  dataContext.parent().remove();
252
                                }
253
                            });
254
                            dataContext.append(removeLink);
255
                        }
256
257
                        $(function() {
258
                            $("#input_file_upload").bind("fileuploaddone", function (e, data) {
259
                                $.each(data.result.files, function (index, file) {
260
                                    // El backend ahora devuelve asset_id y url
261
                                    if (file.asset_id) {
262
                                        var input = $("<input>", {
263
                                            type: "hidden",
264
                                            name: "uploadAsset['.$questionId.'][]",
265
                                            value: file.asset_id
266
                                        });
267
                                        $(data.context.children()[index]).parent().append(input);
268
                                        // set the remove link
269
                                        setRemoveLink($(data.context.children()[index]).parent());
270
                                    }
271
                                });
272
                            });
273
                        });
274
                    </script>';
275
                    $sessionKey = 'upload_answer_assets_'.$questionId;
276
                    $assetIds = (array) ChamiloSession::read($sessionKey);
277
278
                    if (!empty($assetIds)) {
279
                        $icon = Display::return_icon('file_txt.gif');
280
                        $default = "";
281
                        $assetRepo = Container::getAssetRepository();
282
                        $basePath = rtrim(api_get_path(WEB_PATH), "/");
283
284
                        foreach ($assetIds as $id) {
285
                            try {
286
                                $asset = $assetRepo->find(\Symfony\Component\Uid\Uuid::fromRfc4122((string)$id));
287
                            } catch (\Throwable $e) {
288
                                $asset = null;
289
                            }
290
                            if (!$asset) { continue; }
291
292
                            $title = Security::remove_XSS($asset->getTitle());
293
                            $urlAsset = $basePath.$assetRepo->getAssetUrl($asset);
294
295
                            $default .= Display::tag(
296
                                "a",
297
                                Display::div(
298
                                    Display::div($icon, ['class' => 'col-sm-4'])
299
                                    . Display::div($title, ['class' => 'col-sm-5 file_name'])
300
                                    . Display::tag("input", "", [
301
                                        "type" => "hidden",
302
                                        "name" => "uploadAsset['.$questionId.'][]",
303
                                        "value" => (string)$id
304
                                    ])
305
                                    . Display::div("", ["class" => "col-sm-3"]),
306
                                    ["class" => "row"]
307
                                ),
308
                                ["target" => "_blank", "class" => "panel-image", "href" => $urlAsset]
309
                            );
310
                        }
311
312
                        $s .= '<script>
313
                            $(function() {
314
                                if ($("#files").length > 0) {
315
                                    $("#files").html("'.addslashes($default).'");
316
                                    var links = $("#files").children();
317
                                    links.each(function(index) {
318
                                        var dataContext = $(links[index]).find(".row");
319
                                        setRemoveLink(dataContext);
320
                                    });
321
                                }
322
                            });
323
                        </script>';
324
                    }
325
326
                    $s .= $multipleForm->returnForm();
327
                    break;
328
                case ORAL_EXPRESSION:
329
                    // Add nanog
330
                    //@todo pass this as a parameter
331
                    global $exercise_stat_info;
332
                    if (!empty($exercise_stat_info)) {
333
                        echo $objQuestionTmp->returnRecorder((int) $exercise_stat_info['exe_id']);
334
                        $generatedFile = self::getOralFileAudio($exercise_stat_info['exe_id'], $questionId);
335
                        if (!empty($generatedFile)) {
336
                            echo $generatedFile;
337
                        }
338
                    }
339
340
                    $form = new FormValidator('free_choice_'.$questionId);
341
                    $config = ['ToolbarSet' => 'TestFreeAnswer'];
342
343
                    $form->addHtml('<div id="'.'hide_description_'.$questionId.'_options" style="display: none;">');
344
                    $form->addHtmlEditor(
345
                        "choice[$questionId]",
346
                        null,
347
                        false,
348
                        false,
349
                        $config
350
                    );
351
                    $form->addHtml('</div>');
352
                    $s .= $form->returnForm();
353
                    break;
354
                case MULTIPLE_ANSWER_DROPDOWN:
355
                case MULTIPLE_ANSWER_DROPDOWN_COMBINATION:
356
                    if ($debug_mark_answer) {
357
                        $s .= '<p><strong>'
358
                            .(
359
                                MULTIPLE_ANSWER_DROPDOWN == $answerType
360
                                    ? '<span class="pull-right">'.get_lang('Score').'</span>'
361
                                    : ''
362
                            )
363
                            .get_lang('Correct answer').'</strong></p>';
364
                    }
365
                    break;
366
            }
367
368
            // Now navigate through the possible answers, using the max number of
369
            // answers for the question as a limiter
370
            $lines_count = 1; // a counter for matching-type answers
371
            if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType ||
372
                MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE == $answerType
373
            ) {
374
                $header = Display::tag('th', get_lang('Options'));
375
                foreach ($objQuestionTmp->options as $item) {
376
                    if (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
377
                        if (in_array($item, $objQuestionTmp->options)) {
378
                            $header .= Display::tag('th', get_lang($item));
379
                        } else {
380
                            $header .= Display::tag('th', $item);
381
                        }
382
                    } else {
383
                        $header .= Display::tag('th', $item);
384
                    }
385
                }
386
                if ($show_comment) {
387
                    $header .= Display::tag('th', get_lang('Feedback'));
388
                }
389
                $s .= '<table class="table table-hover table-striped">';
390
                $s .= Display::tag(
391
                    'tr',
392
                    $header,
393
                    ['style' => 'text-align:left;']
394
                );
395
            } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
396
                $header = Display::tag('th', get_lang('Options'), ['width' => '50%']);
397
                echo "
398
                <script>
399
                    function RadioValidator(question_id, answer_id)
400
                    {
401
                        var ShowAlert = '';
402
                        var typeRadioB = '';
403
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
404
405
                        for (i = 0; i < AllFormElements.length; i++) {
406
                            if (AllFormElements[i].type == 'radio') {
407
                                var ThisRadio = AllFormElements[i].name;
408
                                var ThisChecked = 'No';
409
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
410
411
                                for (x = 0; x < AllRadioOptions.length; x++) {
412
                                     if (AllRadioOptions[x].checked && ThisChecked == 'No') {
413
                                         ThisChecked = 'Yes';
414
                                         break;
415
                                     }
416
                                }
417
418
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
419
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
420
                                    ShowAlert = ShowAlert + ThisRadio;
421
                                }
422
                            }
423
                        }
424
                        if (ShowAlert != '') {
425
426
                        } else {
427
                            $('.question-validate-btn').removeAttr('disabled');
428
                        }
429
                    }
430
431
                    function handleRadioRow(event, question_id, answer_id) {
432
                        var t = event.target;
433
                        if (t && t.tagName == 'INPUT')
434
                            return;
435
                        while (t && t.tagName != 'TD') {
436
                            t = t.parentElement;
437
                        }
438
                        var r = t.getElementsByTagName('INPUT')[0];
439
                        r.click();
440
                        RadioValidator(question_id, answer_id);
441
                    }
442
443
                    $(function() {
444
                        var ShowAlert = '';
445
                        var typeRadioB = '';
446
                        var question_id = $('input[name=question_id]').val();
447
                        var AllFormElements = window.document.getElementById('exercise_form').elements;
448
449
                        for (i = 0; i < AllFormElements.length; i++) {
450
                            if (AllFormElements[i].type == 'radio') {
451
                                var ThisRadio = AllFormElements[i].name;
452
                                var ThisChecked = 'No';
453
                                var AllRadioOptions = document.getElementsByName(ThisRadio);
454
455
                                for (x = 0; x < AllRadioOptions.length; x++) {
456
                                    if (AllRadioOptions[x].checked && ThisChecked == 'No') {
457
                                        ThisChecked = \"Yes\";
458
                                        break;
459
                                    }
460
                                }
461
462
                                var AlreadySearched = ShowAlert.indexOf(ThisRadio);
463
                                if (ThisChecked == 'No' && AlreadySearched == -1) {
464
                                    ShowAlert = ShowAlert + ThisRadio;
465
                                }
466
                            }
467
                        }
468
469
                        if (ShowAlert != '') {
470
                             $('.question-validate-btn').attr('disabled', 'disabled');
471
                        } else {
472
                            $('.question-validate-btn').removeAttr('disabled');
473
                        }
474
                    });
475
                </script>";
476
477
                foreach ($objQuestionTmp->optionsTitle as $item) {
478
                    if (in_array($item, $objQuestionTmp->optionsTitle)) {
479
                        $properties = [];
480
                        if ('Answers' === $item) {
481
                            $properties['colspan'] = 2;
482
                            $properties['style'] = 'background-color: #F56B2A; color: #ffffff;';
483
                        } elseif ('DegreeOfCertaintyThatMyAnswerIsCorrect' === $item) {
484
                            $properties['colspan'] = 6;
485
                            $properties['style'] = 'background-color: #330066; color: #ffffff;';
486
                        }
487
                        $header .= Display::tag('th', get_lang($item), $properties);
488
                    } else {
489
                        $header .= Display::tag('th', $item);
490
                    }
491
                }
492
493
                if ($show_comment) {
494
                    $header .= Display::tag('th', get_lang('Feedback'));
495
                }
496
497
                $s .= '<table class="table table-hover table-striped data_table">';
498
                $s .= Display::tag('tr', $header, ['style' => 'text-align:left;']);
499
500
                // ajout de la 2eme ligne d'entête pour true/falss et les pourcentages de certitude
501
                $header1 = Display::tag('th', '&nbsp;');
502
                $cpt1 = 0;
503
                foreach ($objQuestionTmp->options as $item) {
504
                    $colorBorder1 = ($cpt1 == (count($objQuestionTmp->options) - 1))
505
                        ? '' : 'border-right: solid #FFFFFF 1px;';
506
                    if ('True' === $item || 'False' === $item) {
507
                        $header1 .= Display::tag(
508
                            'th',
509
                            get_lang($item),
510
                            ['style' => 'background-color: #F7C9B4; color: black;'.$colorBorder1]
511
                        );
512
                    } else {
513
                        $header1 .= Display::tag(
514
                            'th',
515
                            $item,
516
                            ['style' => 'background-color: #e6e6ff; color: black;padding:5px; '.$colorBorder1]
517
                        );
518
                    }
519
                    $cpt1++;
520
                }
521
                if ($show_comment) {
522
                    $header1 .= Display::tag('th', '&nbsp;');
523
                }
524
525
                $s .= Display::tag('tr', $header1);
526
527
                // add explanation
528
                $header2 = Display::tag('th', '&nbsp;');
529
                $descriptionList = [
530
                    get_lang('I don\'t know the answer and I\'ve picked at random'),
531
                    get_lang('I am very unsure'),
532
                    get_lang('I am unsure'),
533
                    get_lang('I am pretty sure'),
534
                    get_lang('I am almost 100% sure'),
535
                    get_lang('I am totally sure'),
536
                ];
537
                $counter2 = 0;
538
                foreach ($objQuestionTmp->options as $item) {
539
                    if ('True' === $item || 'False' === $item) {
540
                        $header2 .= Display::tag('td',
541
                            '&nbsp;',
542
                            ['style' => 'background-color: #F7E1D7; color: black;border-right: solid #FFFFFF 1px;']);
543
                    } else {
544
                        $color_border2 = ($counter2 == (count($objQuestionTmp->options) - 1)) ?
545
                            '' : 'border-right: solid #FFFFFF 1px;font-size:11px;';
546
                        $header2 .= Display::tag(
547
                            'td',
548
                            nl2br($descriptionList[$counter2]),
549
                            ['style' => 'background-color: #EFEFFC; color: black; width: 110px; text-align:center;
550
                                vertical-align: top; padding:5px; '.$color_border2]);
551
                        $counter2++;
552
                    }
553
                }
554
                if ($show_comment) {
555
                    $header2 .= Display::tag('th', '&nbsp;');
556
                }
557
                $s .= Display::tag('tr', $header2);
558
            }
559
560
            if ($show_comment) {
561
                if (in_array(
562
                    $answerType,
563
                    [
564
                        MULTIPLE_ANSWER,
565
                        MULTIPLE_ANSWER_COMBINATION,
566
                        UNIQUE_ANSWER,
567
                        UNIQUE_ANSWER_IMAGE,
568
                        UNIQUE_ANSWER_NO_OPTION,
569
                        GLOBAL_MULTIPLE_ANSWER,
570
                    ]
571
                )) {
572
                    $header = Display::tag('th', get_lang('Options'));
573
                    if (EXERCISE_FEEDBACK_TYPE_END == $exercise->getFeedbackType()) {
574
                        $header .= Display::tag('th', get_lang('Feedback'));
575
                    }
576
                    $s .= '<table class="table table-hover table-striped">';
577
                    $s .= Display::tag(
578
                        'tr',
579
                        $header,
580
                        ['style' => 'text-align:left;']
581
                    );
582
                }
583
            }
584
585
            $matching_correct_answer = 0;
586
            $userChoiceList = [];
587
            if (!empty($user_choice)) {
588
                foreach ($user_choice as $item) {
589
                    $userChoiceList[] = $item['answer'];
590
                }
591
            }
592
593
            $hidingClass = '';
594
            if (READING_COMPREHENSION == $answerType) {
595
                /** @var ReadingComprehension */
596
                $objQuestionTmp->setExerciseType($exercise->selectType());
597
                $objQuestionTmp->processText($objQuestionTmp->selectDescription());
598
                $hidingClass = 'hide-reading-answers';
599
                $s .= Display::div(
600
                    $objQuestionTmp->selectTitle(),
601
                    ['class' => 'question_title '.$hidingClass]
602
                );
603
            }
604
605
            for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
606
                $answer = $objAnswerTmp->selectAnswer($answerId);
607
                $answerCorrect = $objAnswerTmp->isCorrect($answerId);
608
                $numAnswer = $objAnswerTmp->selectAutoId($answerId);
609
                $comment = $objAnswerTmp->selectComment($answerId);
610
                $attributes = [];
611
612
                switch ($answerType) {
613
                    case UNIQUE_ANSWER:
614
                    case UNIQUE_ANSWER_NO_OPTION:
615
                    case UNIQUE_ANSWER_IMAGE:
616
                    case READING_COMPREHENSION:
617
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
618
                        if (isset($user_choice[0]['answer']) && $user_choice[0]['answer'] == $numAnswer) {
619
                            $attributes = [
620
                                'id' => $input_id,
621
                                'checked' => 1,
622
                                'selected' => 1,
623
                            ];
624
                        } else {
625
                            $attributes = ['id' => $input_id];
626
                        }
627
628
                        if ($debug_mark_answer) {
629
                            if ($answerCorrect) {
630
                                $attributes['checked'] = 1;
631
                                $attributes['selected'] = 1;
632
                            }
633
                        }
634
635
                        if ($show_comment) {
636
                            $s .= '<tr><td>';
637
                        }
638
639
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
640
                            if ($show_comment) {
641
                                if (empty($comment)) {
642
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
643
                                            class="exercise-unique-answer-image text-center">';
644
                                } else {
645
                                    $s .= '<div id="answer'.$questionId.$numAnswer.'"
646
                                            class="exercise-unique-answer-image col-xs-6 col-sm-12 text-center">';
647
                                }
648
                            } else {
649
                                $s .= '<div id="answer'.$questionId.$numAnswer.'"
650
                                        class="exercise-unique-answer-image col-xs-6 col-md-3 text-center">';
651
                            }
652
                        }
653
654
                        if (UNIQUE_ANSWER_IMAGE != $answerType) {
655
                            $userStatus = STUDENT;
656
                            // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
657
                            // see BT#18242
658
                            if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
659
                                $userStatus = COURSEMANAGERLOWSECURITY;
660
                            }
661
                            $answer = Security::remove_XSS($answer, $userStatus);
662
                        }
663
                        $s .= Display::input(
664
                            'hidden',
665
                            'choice2['.$questionId.']',
666
                            '0'
667
                        );
668
669
                        $answer_input = null;
670
                        $attributes['class'] = 'checkradios';
671
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
672
                            $attributes['class'] = '';
673
                            $attributes['style'] = 'display: none;';
674
                            $answer = '<div class="thumbnail">'.$answer.'</div>';
675
                        }
676
677
                        $answer_input .= '<label class="radio '.$hidingClass.'">';
678
                        $answer_input .= Display::input(
679
                            'radio',
680
                            'choice['.$questionId.']',
681
                            $numAnswer,
682
                            $attributes
683
                        );
684
                        $answer_input .= $answer;
685
                        $answer_input .= '</label>';
686
687
                        if (UNIQUE_ANSWER_IMAGE == $answerType) {
688
                            $answer_input .= "</div>";
689
                        }
690
691
                        if ($show_comment) {
692
                            $s .= $answer_input;
693
                            $s .= '</td>';
694
                            $s .= '<td>';
695
                            $s .= $comment;
696
                            $s .= '</td>';
697
                            $s .= '</tr>';
698
                        } else {
699
                            $s .= $answer_input;
700
                        }
701
                        break;
702
                    case MULTIPLE_ANSWER:
703
                    case MULTIPLE_ANSWER_TRUE_FALSE:
704
                    case GLOBAL_MULTIPLE_ANSWER:
705
                    case MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY:
706
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
707
                        $userStatus = STUDENT;
708
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
709
                        // see BT#18242
710
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
711
                            $userStatus = COURSEMANAGERLOWSECURITY;
712
                        }
713
                        $answer = Security::remove_XSS($answer, $userStatus);
714
715
                        if (in_array($numAnswer, $userChoiceList)) {
716
                            $attributes = [
717
                                'id' => $input_id,
718
                                'checked' => 1,
719
                                'selected' => 1,
720
                            ];
721
                        } else {
722
                            $attributes = ['id' => $input_id];
723
                        }
724
725
                        if ($debug_mark_answer) {
726
                            if ($answerCorrect) {
727
                                $attributes['checked'] = 1;
728
                                $attributes['selected'] = 1;
729
                            }
730
                        }
731
732
                        if (MULTIPLE_ANSWER == $answerType || GLOBAL_MULTIPLE_ANSWER == $answerType) {
733
                            $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
734
                            $attributes['class'] = 'checkradios';
735
                            $answer_input = '<label class="checkbox">';
736
                            $answer_input .= Display::input(
737
                                'checkbox',
738
                                'choice['.$questionId.']['.$numAnswer.']',
739
                                $numAnswer,
740
                                $attributes
741
                            );
742
                            $answer_input .= $answer;
743
                            $answer_input .= '</label>';
744
745
                            if ($show_comment) {
746
                                $s .= '<tr><td>';
747
                                $s .= $answer_input;
748
                                $s .= '</td>';
749
                                $s .= '<td>';
750
                                $s .= $comment;
751
                                $s .= '</td>';
752
                                $s .= '</tr>';
753
                            } else {
754
                                $s .= $answer_input;
755
                            }
756
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE == $answerType) {
757
                            $myChoice = [];
758
                            if (!empty($userChoiceList)) {
759
                                foreach ($userChoiceList as $item) {
760
                                    $item = explode(':', $item);
761
                                    if (!empty($item)) {
762
                                        $myChoice[$item[0]] = isset($item[1]) ? $item[1] : '';
763
                                    }
764
                                }
765
                            }
766
767
                            $s .= '<tr>';
768
                            $s .= Display::tag('td', $answer);
769
770
                            if (!empty($quizQuestionOptions)) {
771
                                $j = 1;
772
                                foreach ($quizQuestionOptions as $id => $item) {
773
                                    if (isset($myChoice[$numAnswer]) && $item['iid'] == $myChoice[$numAnswer]) {
774
                                        $attributes = [
775
                                            'checked' => 1,
776
                                            'selected' => 1,
777
                                        ];
778
                                    } else {
779
                                        $attributes = [];
780
                                    }
781
782
                                    if ($debug_mark_answer) {
783
                                        if ($j == $answerCorrect) {
784
                                            $attributes['checked'] = 1;
785
                                            $attributes['selected'] = 1;
786
                                        }
787
                                    }
788
                                    $s .= Display::tag(
789
                                        'td',
790
                                        Display::input(
791
                                            'radio',
792
                                            'choice['.$questionId.']['.$numAnswer.']',
793
                                            $item['iid'],
794
                                            $attributes
795
                                        ),
796
                                        ['style' => '']
797
                                    );
798
                                    $j++;
799
                                }
800
                            }
801
802
                            if ($show_comment) {
803
                                $s .= '<td>';
804
                                $s .= $comment;
805
                                $s .= '</td>';
806
                            }
807
                            $s .= '</tr>';
808
                        } elseif (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $answerType) {
809
                            $myChoice = [];
810
                            if (!empty($userChoiceList)) {
811
                                foreach ($userChoiceList as $item) {
812
                                    $item = explode(':', $item);
813
                                    $myChoice[$item[0]] = $item[1];
814
                                }
815
                            }
816
                            $myChoiceDegreeCertainty = [];
817
                            if (!empty($userChoiceList)) {
818
                                foreach ($userChoiceList as $item) {
819
                                    $item = explode(':', $item);
820
                                    $myChoiceDegreeCertainty[$item[0]] = $item[2];
821
                                }
822
                            }
823
                            $s .= '<tr>';
824
                            $s .= Display::tag('td', $answer);
825
826
                            if (!empty($quizQuestionOptions)) {
827
                                $j = 1;
828
                                foreach ($quizQuestionOptions as $id => $item) {
829
                                    if (isset($myChoice[$numAnswer]) && $id == $myChoice[$numAnswer]) {
830
                                        $attributes = ['checked' => 1, 'selected' => 1];
831
                                    } else {
832
                                        $attributes = [];
833
                                    }
834
                                    $attributes['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
835
836
                                    // radio button selection
837
                                    if (isset($myChoiceDegreeCertainty[$numAnswer]) &&
838
                                        $id == $myChoiceDegreeCertainty[$numAnswer]
839
                                    ) {
840
                                        $attributes1 = ['checked' => 1, 'selected' => 1];
841
                                    } else {
842
                                        $attributes1 = [];
843
                                    }
844
845
                                    $attributes1['onChange'] = 'RadioValidator('.$questionId.', '.$numAnswer.')';
846
847
                                    if ($debug_mark_answer) {
848
                                        if ($j == $answerCorrect) {
849
                                            $attributes['checked'] = 1;
850
                                            $attributes['selected'] = 1;
851
                                        }
852
                                    }
853
854
                                    if ('True' == $item['name'] || 'False' == $item['name']) {
855
                                        $s .= Display::tag('td',
856
                                            Display::input('radio',
857
                                                'choice['.$questionId.']['.$numAnswer.']',
858
                                                $id,
859
                                                $attributes
860
                                            ),
861
                                            ['style' => 'text-align:center; background-color:#F7E1D7;',
862
                                                'onclick' => 'handleRadioRow(event, '.
863
                                                    $questionId.', '.
864
                                                    $numAnswer.')',
865
                                            ]
866
                                        );
867
                                    } else {
868
                                        $s .= Display::tag('td',
869
                                            Display::input('radio',
870
                                                'choiceDegreeCertainty['.$questionId.']['.$numAnswer.']',
871
                                                $id,
872
                                                $attributes1
873
                                            ),
874
                                            ['style' => 'text-align:center; background-color:#EFEFFC;',
875
                                                'onclick' => 'handleRadioRow(event, '.
876
                                                    $questionId.', '.
877
                                                    $numAnswer.')',
878
                                            ]
879
                                        );
880
                                    }
881
                                    $j++;
882
                                }
883
                            }
884
885
                            if ($show_comment) {
886
                                $s .= '<td>';
887
                                $s .= $comment;
888
                                $s .= '</td>';
889
                            }
890
                            $s .= '</tr>';
891
                        }
892
                        break;
893
                    case MULTIPLE_ANSWER_COMBINATION:
894
                        // multiple answers
895
                        $input_id = 'choice-'.$questionId.'-'.$answerId;
896
897
                        if (in_array($numAnswer, $userChoiceList)) {
898
                            $attributes = [
899
                                'id' => $input_id,
900
                                'checked' => 1,
901
                                'selected' => 1,
902
                            ];
903
                        } else {
904
                            $attributes = ['id' => $input_id];
905
                        }
906
907
                        if ($debug_mark_answer) {
908
                            if ($answerCorrect) {
909
                                $attributes['checked'] = 1;
910
                                $attributes['selected'] = 1;
911
                            }
912
                        }
913
914
                        $userStatus = STUDENT;
915
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
916
                        // see BT#18242
917
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
918
                            $userStatus = COURSEMANAGERLOWSECURITY;
919
                        }
920
                        $answer = Security::remove_XSS($answer, $userStatus);
921
                        $answer_input = '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
922
                        $answer_input .= '<label class="checkbox">';
923
                        $answer_input .= Display::input(
924
                            'checkbox',
925
                            'choice['.$questionId.']['.$numAnswer.']',
926
                            1,
927
                            $attributes
928
                        );
929
                        $answer_input .= $answer;
930
                        $answer_input .= '</label>';
931
932
                        if ($show_comment) {
933
                            $s .= '<tr>';
934
                            $s .= '<td>';
935
                            $s .= $answer_input;
936
                            $s .= '</td>';
937
                            $s .= '<td>';
938
                            $s .= $comment;
939
                            $s .= '</td>';
940
                            $s .= '</tr>';
941
                        } else {
942
                            $s .= $answer_input;
943
                        }
944
                        break;
945
                    case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
946
                        $s .= '<input type="hidden" name="choice2['.$questionId.']" value="0" />';
947
                        $myChoice = [];
948
                        if (!empty($userChoiceList)) {
949
                            foreach ($userChoiceList as $item) {
950
                                $item = explode(':', $item);
951
                                if (isset($item[1]) && isset($item[0])) {
952
                                    $myChoice[$item[0]] = $item[1];
953
                                }
954
                            }
955
                        }
956
                        $userStatus = STUDENT;
957
                        // Allows to do a remove_XSS in question of exersice with user status COURSEMANAGER
958
                        // see BT#18242
959
                        if (api_get_configuration_value('question_exercise_html_strict_filtering')) {
960
                            $userStatus = COURSEMANAGERLOWSECURITY;
961
                        }
962
                        $answer = Security::remove_XSS($answer, $userStatus);
963
                        $s .= '<tr>';
964
                        $s .= Display::tag('td', $answer);
965
                        foreach ($objQuestionTmp->options as $key => $item) {
966
                            if (isset($myChoice[$numAnswer]) && $key == $myChoice[$numAnswer]) {
967
                                $attributes = [
968
                                    'checked' => 1,
969
                                    'selected' => 1,
970
                                ];
971
                            } else {
972
                                $attributes = [];
973
                            }
974
975
                            if ($debug_mark_answer) {
976
                                if ($key == $answerCorrect) {
977
                                    $attributes['checked'] = 1;
978
                                    $attributes['selected'] = 1;
979
                                }
980
                            }
981
                            $s .= Display::tag(
982
                                'td',
983
                                Display::input(
984
                                    'radio',
985
                                    'choice['.$questionId.']['.$numAnswer.']',
986
                                    $key,
987
                                    $attributes
988
                                )
989
                            );
990
                        }
991
992
                        if ($show_comment) {
993
                            $s .= '<td>';
994
                            $s .= $comment;
995
                            $s .= '</td>';
996
                        }
997
                        $s .= '</tr>';
998
                        break;
999
                    case FILL_IN_BLANKS:
1000
                    case FILL_IN_BLANKS_COMBINATION:
1001
                        // display the question, with field empty, for student to fill it,
1002
                        // or filled to display the answer in the Question preview of the exercise/admin.php page
1003
                        $displayForStudent = true;
1004
                        $listAnswerInfo = FillBlanks::getAnswerInfo($answer);
1005
                        // Correct answers
1006
                        $correctAnswerList = $listAnswerInfo['words'];
1007
                        // Student's answer
1008
                        $studentAnswerList = [];
1009
                        if (isset($user_choice[0]['answer'])) {
1010
                            $arrayStudentAnswer = FillBlanks::getAnswerInfo(
1011
                                $user_choice[0]['answer'],
1012
                                true
1013
                            );
1014
                            $studentAnswerList = $arrayStudentAnswer['student_answer'];
1015
                        }
1016
1017
                        // If the question must be shown with the answer (in page exercise/admin.php)
1018
                        // for teacher preview set the student-answer to the correct answer
1019
                        if ($debug_mark_answer) {
1020
                            $studentAnswerList = $correctAnswerList;
1021
                            $displayForStudent = false;
1022
                        }
1023
1024
                        if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
1025
                            $answer = '';
1026
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
1027
                                // display the common word
1028
                                $answer .= $listAnswerInfo['common_words'][$i];
1029
                                // display the blank word
1030
                                $correctItem = $listAnswerInfo['words'][$i];
1031
                                if (isset($studentAnswerList[$i])) {
1032
                                    // If student already started this test and answered this question,
1033
                                    // fill the blank with his previous answers
1034
                                    // may be "" if student viewed the question, but did not fill the blanks
1035
                                    $correctItem = $studentAnswerList[$i];
1036
                                }
1037
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
1038
                                $answer .= FillBlanks::getFillTheBlankHtml(
1039
                                    $current_item,
1040
                                    $questionId,
1041
                                    $correctItem,
1042
                                    $attributes,
1043
                                    $answer,
1044
                                    $listAnswerInfo,
1045
                                    $displayForStudent,
1046
                                    $i
1047
                                );
1048
                            }
1049
                            // display the last common word
1050
                            $answer .= $listAnswerInfo['common_words'][$i];
1051
                        } else {
1052
                            // display empty [input] with the right width for student to fill it
1053
                            $answer = '';
1054
                            for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
1055
                                // display the common words
1056
                                $answer .= $listAnswerInfo['common_words'][$i];
1057
                                // display the blank word
1058
                                $attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
1059
                                $answer .= FillBlanks::getFillTheBlankHtml(
1060
                                    $current_item,
1061
                                    $questionId,
1062
                                    '',
1063
                                    $attributes,
1064
                                    $answer,
1065
                                    $listAnswerInfo,
1066
                                    $displayForStudent,
1067
                                    $i
1068
                                );
1069
                            }
1070
                            // display the last common word
1071
                            $answer .= $listAnswerInfo['common_words'][$i];
1072
                        }
1073
                        $s .= $answer;
1074
                        break;
1075
                    case CALCULATED_ANSWER:
1076
                        /*
1077
                         * In the CALCULATED_ANSWER test
1078
                         * you mustn't have [ and ] in the textarea
1079
                         * you mustn't have @@ in the textarea
1080
                         * the text to find mustn't be empty or contains only spaces
1081
                         * the text to find mustn't contains HTML tags
1082
                         * the text to find mustn't contains char "
1083
                         */
1084
                        if (null !== $origin) {
1085
                            global $exe_id;
1086
                            $exe_id = (int) $exe_id;
1087
                            $trackAttempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
1088
                            $sql = "SELECT answer FROM $trackAttempts
1089
                                    WHERE exe_id = $exe_id AND question_id= $questionId";
1090
                            $rsLastAttempt = Database::query($sql);
1091
                            $rowLastAttempt = Database::fetch_array($rsLastAttempt);
1092
1093
                            $answer = null;
1094
                            if (isset($rowLastAttempt['answer'])) {
1095
                                $answer = $rowLastAttempt['answer'];
1096
                            }
1097
1098
                            if (empty($answer)) {
1099
                                $_SESSION['calculatedAnswerId'][$questionId] = mt_rand(
1100
                                    1,
1101
                                    $nbrAnswers
1102
                                );
1103
                                $answer = $objAnswerTmp->selectAnswer(
1104
                                    $_SESSION['calculatedAnswerId'][$questionId]
1105
                                );
1106
                            }
1107
                        }
1108
1109
                        [$answer] = explode('@@', $answer);
1110
                        // $correctAnswerList array of array with correct anwsers 0=> [0=>[\p] 1=>[plop]]
1111
                        api_preg_match_all(
1112
                            '/\[[^]]+\]/',
1113
                            $answer,
1114
                            $correctAnswerList
1115
                        );
1116
1117
                        // get student answer to display it if student go back
1118
                        // to previous calculated answer question in a test
1119
                        if (isset($user_choice[0]['answer'])) {
1120
                            api_preg_match_all(
1121
                                '/\[[^]]+\]/',
1122
                                $answer,
1123
                                $studentAnswerList
1124
                            );
1125
                            $studentAnswerListToClean = $studentAnswerList[0];
1126
                            $studentAnswerList = [];
1127
1128
                            $maxStudents = count($studentAnswerListToClean);
1129
                            for ($i = 0; $i < $maxStudents; $i++) {
1130
                                $answerCorrected = $studentAnswerListToClean[$i];
1131
                                $answerCorrected = api_preg_replace(
1132
                                    '| / <font color="green"><b>.*$|',
1133
                                    '',
1134
                                    $answerCorrected
1135
                                );
1136
                                $answerCorrected = api_preg_replace(
1137
                                    '/^\[/',
1138
                                    '',
1139
                                    $answerCorrected
1140
                                );
1141
                                $answerCorrected = api_preg_replace(
1142
                                    '|^<font color="red"><s>|',
1143
                                    '',
1144
                                    $answerCorrected
1145
                                );
1146
                                $answerCorrected = api_preg_replace(
1147
                                    '|</s></font>$|',
1148
                                    '',
1149
                                    $answerCorrected
1150
                                );
1151
                                $answerCorrected = '['.$answerCorrected.']';
1152
                                $studentAnswerList[] = $answerCorrected;
1153
                            }
1154
                        }
1155
1156
                        // If display preview of answer in test view for exemple,
1157
                        // set the student answer to the correct answers
1158
                        if ($debug_mark_answer) {
1159
                            // contain the rights answers surronded with brackets
1160
                            $studentAnswerList = $correctAnswerList[0];
1161
                        }
1162
1163
                        /*
1164
                        Split the response by bracket
1165
                        tabComments is an array with text surrounding the text to find
1166
                        we add a space before and after the answerQuestion to be sure to
1167
                        have a block of text before and after [xxx] patterns
1168
                        so we have n text to find ([xxx]) and n+1 block of texts before,
1169
                        between and after the text to find
1170
                        */
1171
                        $tabComments = api_preg_split(
1172
                            '/\[[^]]+\]/',
1173
                            ' '.$answer.' '
1174
                        );
1175
                        if (!empty($correctAnswerList) && !empty($studentAnswerList)) {
1176
                            $answer = '';
1177
                            $i = 0;
1178
                            foreach ($studentAnswerList as $studentItem) {
1179
                                // Remove surronding brackets
1180
                                $studentResponse = api_substr(
1181
                                    $studentItem,
1182
                                    1,
1183
                                    api_strlen($studentItem) - 2
1184
                                );
1185
                                $size = strlen($studentItem);
1186
                                $attributes['class'] = self::detectInputAppropriateClass($size);
1187
                                $answer .= $tabComments[$i].
1188
                                    Display::input(
1189
                                        'text',
1190
                                        "choice[$questionId][]",
1191
                                        $studentResponse,
1192
                                        $attributes
1193
                                    );
1194
                                $i++;
1195
                            }
1196
                            $answer .= $tabComments[$i];
1197
                        } else {
1198
                            // display exercise with empty input fields
1199
                            // every [xxx] are replaced with an empty input field
1200
                            foreach ($correctAnswerList[0] as $item) {
1201
                                $size = strlen($item);
1202
                                $attributes['class'] = self::detectInputAppropriateClass($size);
1203
                                if (EXERCISE_FEEDBACK_TYPE_POPUP == $exercise->getFeedbackType()) {
1204
                                    $attributes['id'] = "question_$questionId";
1205
                                    $attributes['class'] .= ' checkCalculatedQuestionOnEnter ';
1206
                                }
1207
1208
                                $answer = str_replace(
1209
                                    $item,
1210
                                    Display::input(
1211
                                        'text',
1212
                                        "choice[$questionId][]",
1213
                                        '',
1214
                                        $attributes
1215
                                    ),
1216
                                    $answer
1217
                                );
1218
                            }
1219
                        }
1220
                        if (null !== $origin) {
1221
                            $s = $answer;
1222
                            break;
1223
                        } else {
1224
                            $s .= $answer;
1225
                        }
1226
                        break;
1227
                    case MATCHING:
1228
                    case MATCHING_COMBINATION:
1229
                        // matching type, showing suggestions and answers
1230
                        // TODO: replace $answerId by $numAnswer
1231
                        if (0 != $answerCorrect) {
1232
                            // only show elements to be answered (not the contents of
1233
                            // the select boxes, who are correct = 0)
1234
                            $s .= '<tr><td width="45%" valign="top">';
1235
                            $parsed_answer = $answer;
1236
                            // Left part questions
1237
                            $s .= '<p class="indent">'.$lines_count.'.&nbsp;'.$parsed_answer.'</p></td>';
1238
                            // Middle part (matches selects)
1239
                            // Id of select is # question + # of option
1240
                            $s .= '<td width="10%" valign="top" align="center">
1241
                                <div class="select-matching">
1242
                                <select
1243
                                    class="form-control"
1244
                                    id="choice_id_'.$current_item.'_'.$lines_count.'"
1245
                                    name="choice['.$questionId.']['.$numAnswer.']">';
1246
1247
                            // fills the list-box
1248
                            foreach ($select_items as $key => $val) {
1249
                                // set $debug_mark_answer to true at function start to
1250
                                // show the correct answer with a suffix '-x'
1251
                                $selected = '';
1252
                                if ($debug_mark_answer) {
1253
                                    if ($val['id'] == $answerCorrect) {
1254
                                        $selected = 'selected="selected"';
1255
                                    }
1256
                                }
1257
                                //$user_choice_array_position
1258
                                if (isset($user_choice_array_position[$numAnswer]) &&
1259
                                    $val['id'] == $user_choice_array_position[$numAnswer]
1260
                                ) {
1261
                                    $selected = 'selected="selected"';
1262
                                }
1263
                                $s .= '<option value="'.$val['id'].'" '.$selected.'>'.$val['letter'].'</option>';
1264
                            }
1265
1266
                            $s .= '</select></div></td><td width="5%" class="separate">&nbsp;</td>';
1267
                            $s .= '<td width="40%" valign="top" >';
1268
                            if (isset($select_items[$lines_count])) {
1269
                                $s .= '<div class="text-right">
1270
                                        <p class="indent">'.
1271
                                    $select_items[$lines_count]['letter'].'.&nbsp; '.
1272
                                    $select_items[$lines_count]['answer'].'
1273
                                        </p>
1274
                                        </div>';
1275
                            } else {
1276
                                $s .= '&nbsp;';
1277
                            }
1278
                            $s .= '</td>';
1279
                            $s .= '</tr>';
1280
                            $lines_count++;
1281
                            // If the left side of the "matching" has been completely
1282
                            // shown but the right side still has values to show...
1283
                            if (($lines_count - 1) == $num_suggestions) {
1284
                                // if it remains answers to shown at the right side
1285
                                while (isset($select_items[$lines_count])) {
1286
                                    $s .= '<tr>
1287
                                      <td colspan="2"></td>
1288
                                      <td valign="top">';
1289
                                    $s .= '<b>'.$select_items[$lines_count]['letter'].'.</b> '.
1290
                                        $select_items[$lines_count]['answer'];
1291
                                    $s .= "</td>
1292
                                </tr>";
1293
                                    $lines_count++;
1294
                                }
1295
                            }
1296
                            $matching_correct_answer++;
1297
                        }
1298
                        break;
1299
                    case DRAGGABLE:
1300
                        if ($answerCorrect) {
1301
                            $windowId = $questionId.'_'.$lines_count;
1302
                            $s .= '<li class="touch-items" id="'.$windowId.'">';
1303
                            $s .= Display::div(
1304
                                $answer,
1305
                                [
1306
                                    'id' => "window_$windowId",
1307
                                    'class' => "window{$questionId}_question_draggable exercise-draggable-answer-option",
1308
                                ]
1309
                            );
1310
1311
                            $draggableSelectOptions = [];
1312
                            $selectedValue = 0;
1313
                            $selectedIndex = 0;
1314
                            if ($user_choice) {
1315
                                foreach ($user_choice as $userChoiceKey => $chosen) {
1316
                                    $userChoiceKey++;
1317
                                    if ($lines_count != $userChoiceKey) {
1318
                                        continue;
1319
                                    }
1320
                                    /*if ($answerCorrect != $chosen['answer']) {
1321
                                        continue;
1322
                                    }*/
1323
                                    $selectedValue = $chosen['answer'];
1324
                                }
1325
                            }
1326
                            foreach ($select_items as $key => $select_item) {
1327
                                $draggableSelectOptions[$select_item['id']] = $select_item['letter'];
1328
                            }
1329
1330
                            foreach ($draggableSelectOptions as $value => $text) {
1331
                                if ($value == $selectedValue) {
1332
                                    break;
1333
                                }
1334
                                $selectedIndex++;
1335
                            }
1336
1337
                            $s .= Display::select(
1338
                                "choice[$questionId][$numAnswer]",
1339
                                $draggableSelectOptions,
1340
                                $selectedValue,
1341
                                [
1342
                                    'id' => "window_{$windowId}_select",
1343
                                    'class' => 'select_option hidden',
1344
                                ],
1345
                                false
1346
                            );
1347
1348
                            if ($selectedValue && $selectedIndex) {
1349
                                $s .= "
1350
                                    <script>
1351
                                        $(function() {
1352
                                            DraggableAnswer.deleteItem(
1353
                                                $('#{$questionId}_$lines_count'),
1354
                                                $('#drop_{$questionId}_{$selectedIndex}')
1355
                                            );
1356
                                        });
1357
                                    </script>
1358
                                ";
1359
                            }
1360
1361
                            if (isset($select_items[$lines_count])) {
1362
                                $s .= Display::div(
1363
                                    Display::tag(
1364
                                        'b',
1365
                                        $select_items[$lines_count]['letter']
1366
                                    ).$select_items[$lines_count]['answer'],
1367
                                    [
1368
                                        'id' => "window_{$windowId}_answer",
1369
                                        'class' => 'hidden',
1370
                                    ]
1371
                                );
1372
                            } else {
1373
                                $s .= '&nbsp;';
1374
                            }
1375
1376
                            $lines_count++;
1377
                            if (($lines_count - 1) == $num_suggestions) {
1378
                                while (isset($select_items[$lines_count])) {
1379
                                    $s .= Display::tag('b', $select_items[$lines_count]['letter']);
1380
                                    $s .= $select_items[$lines_count]['answer'];
1381
                                    $lines_count++;
1382
                                }
1383
                            }
1384
1385
                            $matching_correct_answer++;
1386
                            $s .= '</li>';
1387
                        }
1388
                        break;
1389
                    case MATCHING_DRAGGABLE:
1390
                    case MATCHING_DRAGGABLE_COMBINATION:
1391
                        if (1 == $answerId) {
1392
                            echo $objAnswerTmp->getJs();
1393
                        }
1394
                        if (0 != $answerCorrect) {
1395
                            $windowId = "{$questionId}_{$lines_count}";
1396
                            $s .= <<<HTML
1397
                            <tr>
1398
                                <td width="45%">
1399
                                    <div id="window_{$windowId}"
1400
                                        class="window window_left_question window{$questionId}_question">
1401
                                        <strong>$lines_count.</strong>
1402
                                        $answer
1403
                                    </div>
1404
                                </td>
1405
                                <td width="10%">
1406
HTML;
1407
1408
                            $draggableSelectOptions = [];
1409
                            $selectedValue = 0;
1410
                            $selectedIndex = 0;
1411
1412
                            if ($user_choice) {
1413
                                foreach ($user_choice as $chosen) {
1414
                                    if ($numAnswer == $chosen['position']) {
1415
                                        $selectedValue = $chosen['answer'];
1416
                                        break;
1417
                                    }
1418
                                }
1419
                            }
1420
1421
                            foreach ($select_items as $key => $selectItem) {
1422
                                $draggableSelectOptions[$selectItem['id']] = $selectItem['letter'];
1423
                            }
1424
1425
                            foreach ($draggableSelectOptions as $value => $text) {
1426
                                if ($value == $selectedValue) {
1427
                                    break;
1428
                                }
1429
                                $selectedIndex++;
1430
                            }
1431
1432
                            $s .= Display::select(
1433
                                "choice[$questionId][$numAnswer]",
1434
                                $draggableSelectOptions,
1435
                                $selectedValue,
1436
                                [
1437
                                    'id' => "window_{$windowId}_select",
1438
                                    'class' => 'hidden',
1439
                                ],
1440
                                false
1441
                            );
1442
1443
                            if (!empty($answerCorrect) && !empty($selectedValue)) {
1444
                                // Show connect if is not freeze (question preview)
1445
                                if (!$freeze) {
1446
                                    $s .= "
1447
                                        <script>
1448
                                            $(function() {
1449
                                                MatchingDraggable.instances['$questionId'].connect({
1450
                                                    source: 'window_$windowId',
1451
                                                    target: 'window_{$questionId}_{$selectedIndex}_answer',
1452
                                                    endpoint: ['Dot', {radius: 12}],
1453
                                                    anchors: ['RightMiddle', 'LeftMiddle'],
1454
                                                    paintStyle: {stroke: '#8A8888', strokeWidth: 8},
1455
                                                    connector: [
1456
                                                        MatchingDraggable.connectorType,
1457
                                                        {curvines: MatchingDraggable.curviness}
1458
                                                    ]
1459
                                                });
1460
                                            });
1461
                                        </script>
1462
                                    ";
1463
                                }
1464
                            }
1465
1466
                            $s .= '</td><td width="45%">';
1467
                            if (isset($select_items[$lines_count])) {
1468
                                $s .= <<<HTML
1469
                                <div id="window_{$windowId}_answer" class="window window_right_question">
1470
                                    <strong>{$select_items[$lines_count]['letter']}.</strong>
1471
                                    {$select_items[$lines_count]['answer']}
1472
                                </div>
1473
HTML;
1474
                            } else {
1475
                                $s .= '&nbsp;';
1476
                            }
1477
1478
                            $s .= '</td></tr>';
1479
                            $lines_count++;
1480
                            if (($lines_count - 1) == $num_suggestions) {
1481
                                while (isset($select_items[$lines_count])) {
1482
                                    $s .= <<<HTML
1483
                                    <tr>
1484
                                        <td colspan="2"></td>
1485
                                        <td>
1486
                                            <strong>{$select_items[$lines_count]['letter']}</strong>
1487
                                            {$select_items[$lines_count]['answer']}
1488
                                        </td>
1489
                                    </tr>
1490
HTML;
1491
                                    $lines_count++;
1492
                                }
1493
                            }
1494
                            $matching_correct_answer++;
1495
                        }
1496
                        break;
1497
                    case MULTIPLE_ANSWER_DROPDOWN:
1498
                    case MULTIPLE_ANSWER_DROPDOWN_COMBINATION:
1499
                        if ($debug_mark_answer && $answerCorrect) {
1500
                            $s .= '<p>'
1501
                                .(
1502
                                    MULTIPLE_ANSWER_DROPDOWN == $answerType
1503
                                        ? '<span class="pull-right">'.$objAnswerTmp->weighting[$answerId].'</span>'
1504
                                        : ''
1505
                                )
1506
                                .Display::returnFontAwesomeIcon('check-square-o', '', true);
1507
                            $s .= Security::remove_XSS($objAnswerTmp->answer[$answerId]).'</p>';
1508
                        }
1509
                        break;
1510
                }
1511
            }
1512
1513
            if (in_array($answerType, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION]) && !$debug_mark_answer) {
1514
                $userChoiceList = array_unique($userChoiceList);
1515
                $input_id = "choice-$questionId";
1516
                $clear_id = "clear-$questionId";
1517
1518
                $s .= Display::input('hidden', "choice2[$questionId]", '0')
1519
                    .'<div class="mb-4">'
1520
                    .'<div class="flex items-center justify-between mb-2">'
1521
                    .'<label for="'.$input_id.'" class="text-sm font-medium text-gray-90">'
1522
                    .get_lang('Please select an option')
1523
                    .'</label>'
1524
                    .'<button type="button" id="'.$clear_id.'" '
1525
                    .'class="inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium '
1526
                    .'bg-primary text-white hover:opacity-90 border border-primary">'
1527
                    .'<span class="fa fa-times" aria-hidden="true"></span>'
1528
                    .'<span>'.get_lang('Clear').'</span>'
1529
                    .'</button>'
1530
                    .'</div>'
1531
                    .Display::select(
1532
                        "choice[$questionId][]",
1533
                        $selectableOptions,
1534
                        $userChoiceList,
1535
                        [
1536
                            'id'       => $input_id,
1537
                            'multiple' => 'multiple',
1538
                            'class'    => 'w-full', // full width before Select2 mounts
1539
                        ],
1540
                        false
1541
                    )
1542
                    .'</div>'
1543
                    .'<script>
1544
            $(function () {
1545
                var $el = $("#'.$input_id.'");
1546
                if (!$.fn.select2) return;
1547
1548
                $el.select2({
1549
                    width: "100%",
1550
                    placeholder: { id: "-2", text: "'.get_lang('None').'" },
1551
                    allowClear: true,
1552
                    selectOnClose: false,
1553
                    containerCssClass: "select2-tw",
1554
                    selectionCssClass: "select2-tw",
1555
                    dropdownCssClass: "select2-tw-dd"
1556
                });
1557
1558
                $("#'.$clear_id.'").on("click", function(e){
1559
                    e.preventDefault();
1560
                    $el.val(null).trigger("change");
1561
                });
1562
            });
1563
        </script>';
1564
            }
1565
1566
            if ($show_comment) {
1567
                $s .= '</table>';
1568
            } elseif (in_array(
1569
                $answerType,
1570
                [
1571
                    MATCHING,
1572
                    MATCHING_COMBINATION,
1573
                    MATCHING_DRAGGABLE,
1574
                    MATCHING_DRAGGABLE_COMBINATION,
1575
                    UNIQUE_ANSWER_NO_OPTION,
1576
                    MULTIPLE_ANSWER_TRUE_FALSE,
1577
                    MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE,
1578
                    MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY,
1579
                ]
1580
            )) {
1581
                $s .= '</table>';
1582
            }
1583
1584
            if (DRAGGABLE == $answerType) {
1585
                $isVertical = 'v' == $objQuestionTmp->extra;
1586
                $s .= "</ul></div>";
1587
                $counterAnswer = 1;
1588
                $s .= '<div class="question-answer__items question-answer__items--'.($isVertical ? 'vertical' : 'horizontal').'">';
1589
                for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
1590
                    $answerCorrect = $objAnswerTmp->isCorrect($answerId);
1591
                    $windowId = $questionId.'_'.$counterAnswer;
1592
                    if ($answerCorrect) {
1593
                        $s .= '<div class="droppable-item '.($isVertical ? 'w-full' : '').' flex items-center justify-between p-4 mb-4 bg-gray-200 rounded-md">';
1594
                        $s .= '<span class="number text-lg font-bold">'.$counterAnswer.'</span>';
1595
                        $s .= '<div id="drop_'.$windowId.'" class="droppable border-2 border-dashed border-gray-400 p-4 bg-white rounded-md"></div>';
1596
                        $s .= '</div>';
1597
                        $counterAnswer++;
1598
                    }
1599
                }
1600
1601
                $s .= '</div>';
1602
//                $s .= '</div>';
1603
            }
1604
1605
            if (in_array($answerType, [MATCHING, MATCHING_COMBINATION, MATCHING_DRAGGABLE, MATCHING_DRAGGABLE_COMBINATION])) {
1606
                $s .= '</div>'; //drag_question
1607
            }
1608
1609
            $s .= '</div>'; //question_options row
1610
1611
            // destruction of the Answer object
1612
            unset($objAnswerTmp);
1613
            // destruction of the Question object
1614
            unset($objQuestionTmp);
1615
            if ('export' == $origin) {
1616
                return $s;
1617
            }
1618
            echo $s;
1619
        } elseif (in_array($answerType, [HOT_SPOT, HOT_SPOT_DELINEATION, HOT_SPOT_COMBINATION])) {
1620
            global $exe_id;
1621
            $questionDescription = $objQuestionTmp->selectDescription();
1622
            // Get the answers, make a list
1623
            $objAnswerTmp = new Answer($questionId, $course_id);
1624
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
1625
1626
            // get answers of hotpost
1627
            $answers_hotspot = [];
1628
            for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
1629
                $answers = $objAnswerTmp->selectAnswerByAutoId(
1630
                    $objAnswerTmp->selectAutoId($answerId)
1631
                );
1632
                $answers_hotspot[$answers['iid']] = $objAnswerTmp->selectAnswer(
1633
                    $answerId
1634
                );
1635
            }
1636
1637
            $answerList = '';
1638
            $hotspotColor = 0;
1639
            if (HOT_SPOT_DELINEATION != $answerType) {
1640
                $answerList = '
1641
        <div class="p-4 rounded-md border border-gray-25">
1642
            <h5 class="font-bold text-lg mb-2 text-primary">'.get_lang('Image zones').'</h5>
1643
            <ol class="list-decimal ml-6 space-y-2 text-primary">
1644
        ';
1645
1646
                if (!empty($answers_hotspot)) {
1647
                    Session::write("hotspot_ordered$questionId", array_keys($answers_hotspot));
1648
                    foreach ($answers_hotspot as $value) {
1649
                        $answerList .= '<li class="flex items-center space-x-2">';
1650
                        if ($freeze) {
1651
                            $answerList .= '<span class="text-support-5 fa fa-square" aria-hidden="true"></span>';
1652
                        }
1653
                        $answerList .= '<span>'.$value.'</span>';
1654
                        $answerList .= '</li>';
1655
                        $hotspotColor++;
1656
                    }
1657
                }
1658
1659
                $answerList .= '
1660
                        </ol>
1661
                    </div>
1662
                ';
1663
            }
1664
            if ($freeze) {
1665
                $relPath = api_get_path(WEB_CODE_PATH);
1666
                echo "
1667
        <div class=\"flex space-x-4\">
1668
            <div class=\"w-3/4\">
1669
                <div id=\"hotspot-preview-$questionId\" class=\"bg-gray-10 w-full bg-center bg-no-repeat bg-contain border border-gray-25\"></div>
1670
            </div>
1671
            <div class=\"w-1/4\">
1672
                $answerList
1673
            </div>
1674
        </div>
1675
        <script>
1676
            new ".(in_array($answerType, [HOT_SPOT, HOT_SPOT_COMBINATION]) ? "HotspotQuestion" : "DelineationQuestion")."({
1677
                questionId: $questionId,
1678
                exerciseId: $exerciseId,
1679
                exeId: 0,
1680
                selector: '#hotspot-preview-$questionId',
1681
                for: 'preview',
1682
                relPath: '$relPath'
1683
            });
1684
        </script>
1685
    ";
1686
                return;
1687
            }
1688
1689
            if (!$only_questions) {
1690
                if ($show_title) {
1691
                    if ($exercise->display_category_name) {
1692
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
1693
                    }
1694
                    echo $objQuestionTmp->getTitleToDisplay($exercise, $current_item);
1695
                }
1696
1697
                //@todo I need to the get the feedback type
1698
                echo <<<HOTSPOT
1699
        <input type="hidden" name="hidden_hotspot_id" value="$questionId" />
1700
        <div class="exercise_questions">
1701
            $questionDescription
1702
            <div class="flex space-x-4">
1703
HOTSPOT;
1704
            }
1705
1706
            $relPath = api_get_path(WEB_CODE_PATH);
1707
            $s .= "<div class=\"w-3/4\">
1708
           <div class=\"hotspot-image bg-gray-10 border border-gray-25 bg-center bg-no-repeat bg-contain\"></div>
1709
            <script>
1710
                $(function() {
1711
                    new ".(HOT_SPOT_DELINEATION == $answerType ? 'DelineationQuestion' : 'HotspotQuestion')."({
1712
                        questionId: $questionId,
1713
                        exerciseId: $exerciseId,
1714
                        exeId: 0,
1715
                        selector: '#question_div_' + $questionId + ' .hotspot-image',
1716
                        for: 'user',
1717
                        relPath: '$relPath'
1718
                    });
1719
                });
1720
            </script>
1721
        </div>
1722
        <div class=\"w-1/4\">
1723
            $answerList
1724
        </div>
1725
    ";
1726
1727
            echo <<<HOTSPOT
1728
        $s
1729
    </div>
1730
</div>
1731
HOTSPOT;
1732
        } elseif (ANNOTATION == $answerType) {
1733
            global $exe_id;
1734
            $relPath = api_get_path(WEB_CODE_PATH);
1735
            if (api_is_platform_admin() || api_is_course_admin()) {
1736
                $questionRepo = Container::getQuestionRepository();
1737
                $questionEntity = $questionRepo->find($questionId);
1738
                if ($freeze) {
1739
                    echo '
1740
            <div id="annotation-canvas-'.$questionId.'" class="annotation-canvas center-block"></div>
1741
            <script>
1742
                AnnotationQuestion({
1743
                    questionId: '.(int)$questionId.',
1744
                    exerciseId: 0,
1745
                    relPath: \''.$relPath.'\',
1746
                    courseId: '.(int)$course_id.',
1747
                    mode: "preview"
1748
                });
1749
            </script>
1750
        ';
1751
                    return 0;
1752
                }
1753
            }
1754
1755
            if (!$only_questions) {
1756
                if ($show_title) {
1757
                    if ($exercise->display_category_name) {
1758
                        TestCategory::displayCategoryAndTitle($objQuestionTmp->id);
1759
                    }
1760
                    echo $objQuestionTmp->getTitleToDisplay($exercise, $current_item);
1761
                }
1762
1763
                echo '
1764
                    <input type="hidden" name="hidden_hotspot_id" value="'.$questionId.'" />
1765
                    <div class="exercise_questions">
1766
                        '.$objQuestionTmp->selectDescription().'
1767
                        <div class="row">
1768
                            <div class="col-sm-8 col-md-9">
1769
                                <div id="annotation-canvas-'.$questionId.'" class="annotation-canvas center-block">
1770
                                </div>
1771
                                <script>
1772
                                    AnnotationQuestion({
1773
                                        questionId: '.$questionId.',
1774
                                        exerciseId: '.$exerciseId.',
1775
                                        relPath: \''.$relPath.'\',
1776
                                        courseId: '.$course_id.',
1777
                                    });
1778
                                </script>
1779
                            </div>
1780
                            <div class="col-sm-4 col-md-3">
1781
                                <div class="well well-sm" id="annotation-toolbar-'.$questionId.'">
1782
                                    <div class="btn-toolbar">
1783
                                        <div class="btn-group" data-toggle="buttons">
1784
                                            <label class="btn btn--plain active"
1785
                                                aria-label="'.get_lang('Add annotation path').'">
1786
                                                <input
1787
                                                    type="radio" value="0"
1788
                                                    name="'.$questionId.'-options" autocomplete="off" checked>
1789
                                                <span class="fas fa-pencil-alt" aria-hidden="true"></span>
1790
                                            </label>
1791
                                            <label class="btn btn--plain"
1792
                                                aria-label="'.get_lang('Add annotation text').'">
1793
                                                <input
1794
                                                    type="radio" value="1"
1795
                                                    name="'.$questionId.'-options" autocomplete="off">
1796
                                                <span class="fa fa-font fa-fw" aria-hidden="true"></span>
1797
                                            </label>
1798
                                        </div>
1799
                                    </div>
1800
                                    <ul class="list-unstyled"></ul>
1801
                                </div>
1802
                            </div>
1803
                        </div>
1804
                    </div>
1805
                ';
1806
            }
1807
            $objAnswerTmp = new Answer($questionId);
1808
            $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
1809
            unset($objAnswerTmp, $objQuestionTmp);
1810
        }
1811
1812
            return $nbrAnswers;
1813
    }
1814
1815
    /**
1816
     * Displays a table listing the quizzes where a question is used.
1817
     */
1818
    public static function showTestsWhereQuestionIsUsed(int $questionId, int $excludeTestId = 0): void
1819
    {
1820
        $em = Database::getManager();
1821
        $quizRepo = $em->getRepository(CQuiz::class);
1822
        $quizzes = $quizRepo->findQuizzesUsingQuestion($questionId, $excludeTestId);
1823
1824
        if (empty($quizzes)) {
1825
            echo '';
1826
            return;
1827
        }
1828
1829
        $result = [];
1830
1831
        foreach ($quizzes as $quiz) {
1832
            $link = $quiz->getFirstResourceLink();
1833
            $course = $link?->getCourse();
1834
            $session = $link?->getSession();
1835
            $courseId = $course?->getId() ?? 0;
1836
            $sessionId = $session?->getId() ?? 0;
1837
1838
            $url = api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.
1839
                'cid='.$courseId.'&sid='.$sessionId.'&gid=0&gradebook=0&origin='.
1840
                '&exerciseId='.$quiz->getIid().'&r=1';
1841
1842
1843
            $result[] = [
1844
                $course?->getTitle() ?? '-',
1845
                $session?->getTitle() ?? '-',
1846
                $quiz->getTitle(),
1847
                '<a href="'.$url.'">'.Display::getMdiIcon(
1848
                    'order-bool-ascending-variant',
1849
                    'ch-tool-icon',
1850
                    null,
1851
                    ICON_SIZE_SMALL,
1852
                    get_lang('Edit')
1853
                ).'</a>',
1854
            ];
1855
        }
1856
1857
        $headers = [
1858
            get_lang('Course'),
1859
            get_lang('Session'),
1860
            get_lang('Test'),
1861
            get_lang('Link to test edition'),
1862
        ];
1863
1864
        $title = Display::div(
1865
            get_lang('Question also used in the following tests'),
1866
            ['class' => 'section-title', 'style' => 'margin-top: 25px; border-bottom: none']
1867
        );
1868
1869
        echo $title.Display::table($headers, $result);
1870
    }
1871
1872
    /**
1873
     * @param int $exeId
1874
     *
1875
     * @return array
1876
     */
1877
    public static function get_exercise_track_exercise_info($exeId)
1878
    {
1879
        $quizTable = Database::get_course_table(TABLE_QUIZ_TEST);
1880
        $trackExerciseTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
1881
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
1882
        $exeId = (int) $exeId;
1883
        $result = [];
1884
        if (!empty($exeId)) {
1885
            $sql = " SELECT q.*, tee.*
1886
                FROM $quizTable as q
1887
                INNER JOIN $trackExerciseTable as tee
1888
                ON q.iid = tee.exe_exo_id
1889
                WHERE
1890
                    tee.exe_id = $exeId";
1891
1892
            $sqlResult = Database::query($sql);
1893
            if (Database::num_rows($sqlResult)) {
1894
                $result = Database::fetch_assoc($sqlResult);
1895
                $result['duration_formatted'] = '';
1896
                if (!empty($result['exe_duration'])) {
1897
                    $time = api_format_time($result['exe_duration'], 'js');
1898
                    $result['duration_formatted'] = $time;
1899
                }
1900
            }
1901
        }
1902
1903
        return $result;
1904
    }
1905
1906
    /**
1907
     * Validates the time control key.
1908
     *
1909
     * @param int $lp_id
1910
     * @param int $lp_item_id
1911
     *
1912
     * @return bool
1913
     */
1914
    public static function exercise_time_control_is_valid(Exercise $exercise, $lp_id = 0, $lp_item_id = 0)
1915
    {
1916
        $exercise_id = $exercise->getId();
1917
        $expiredTime = $exercise->expired_time;
1918
1919
        // If the exercise has no time control configured, it's valid.
1920
        if (empty($expiredTime)) {
1921
            return true;
1922
        }
1923
1924
        // Build a stable session key for the LP/exercise context
1925
        $current_expired_time_key = self::get_time_control_key(
1926
            $exercise_id,
1927
            $lp_id,
1928
            $lp_item_id
1929
        );
1930
1931
        // If the key isn't present, time control cannot be validated -> not valid
1932
        if (!isset($_SESSION['expired_time'][$current_expired_time_key])) {
1933
            return false;
1934
        }
1935
1936
        // Normalize the stored value (can be DateTime, unix timestamp, or string)
1937
        $raw = $_SESSION['expired_time'][$current_expired_time_key];
1938
        if ($raw instanceof \DateTimeInterface) {
1939
            $expiredAtStr = $raw->format('Y-m-d H:i:s');
1940
        } elseif (is_int($raw) || ctype_digit((string) $raw)) {
1941
            // Treat numeric as unix timestamp (UTC)
1942
            $expiredAtStr = gmdate('Y-m-d H:i:s', (int) $raw);
1943
        } else {
1944
            // Assume parsable datetime string
1945
            $expiredAtStr = (string) $raw;
1946
        }
1947
1948
        // Compute remaining time (defensive: handle parse failure as already expired)
1949
        $expired_time = api_strtotime($expiredAtStr, 'UTC');
1950
        if ($expired_time === false || $expired_time === null) {
1951
            return false;
1952
        }
1953
1954
        $current_time = time();
1955
        $total_time_allowed = $expired_time + 30; // small grace period
1956
1957
        if ($total_time_allowed < $current_time) {
1958
            return false;
1959
        }
1960
1961
        return true;
1962
    }
1963
1964
    /**
1965
     * Deletes the time control token.
1966
     *
1967
     * @param int $exercise_id
1968
     * @param int $lp_id
1969
     * @param int $lp_item_id
1970
     */
1971
    public static function exercise_time_control_delete(
1972
        $exercise_id,
1973
        $lp_id = 0,
1974
        $lp_item_id = 0
1975
    ) {
1976
        $current_expired_time_key = self::get_time_control_key(
1977
            $exercise_id,
1978
            $lp_id,
1979
            $lp_item_id
1980
        );
1981
        unset($_SESSION['expired_time'][$current_expired_time_key]);
1982
    }
1983
1984
    /**
1985
     * Generates the time control key.
1986
     *
1987
     * @param int $exercise_id
1988
     * @param int $lp_id
1989
     * @param int $lp_item_id
1990
     *
1991
     * @return string
1992
     */
1993
    public static function get_time_control_key(
1994
        $exercise_id,
1995
        $lp_id = 0,
1996
        $lp_item_id = 0
1997
    ) {
1998
        $exercise_id = (int) $exercise_id;
1999
        $lp_id = (int) $lp_id;
2000
        $lp_item_id = (int) $lp_item_id;
2001
2002
        return
2003
            api_get_course_int_id().'_'.
2004
            api_get_session_id().'_'.
2005
            $exercise_id.'_'.
2006
            api_get_user_id().'_'.
2007
            $lp_id.'_'.
2008
            $lp_item_id;
2009
    }
2010
2011
    /**
2012
     * Get session time control.
2013
     *
2014
     * @param int $exercise_id
2015
     * @param int $lp_id
2016
     * @param int $lp_item_id
2017
     *
2018
     * @return int
2019
     */
2020
    public static function get_session_time_control_key(
2021
        $exercise_id,
2022
        $lp_id = 0,
2023
        $lp_item_id = 0
2024
    ) {
2025
        $return_value = 0;
2026
        $time_control_key = self::get_time_control_key(
2027
            $exercise_id,
2028
            $lp_id,
2029
            $lp_item_id
2030
        );
2031
        if (isset($_SESSION['expired_time']) && isset($_SESSION['expired_time'][$time_control_key])) {
2032
            $return_value = $_SESSION['expired_time'][$time_control_key];
2033
        }
2034
2035
        return $return_value;
2036
    }
2037
2038
    /**
2039
     * Gets count of exam results.
2040
     *
2041
     * @param int   $exerciseId
2042
     * @param array $conditions
2043
     * @param int   $courseId
2044
     * @param bool  $showSession
2045
     *
2046
     * @return array
2047
     */
2048
    public static function get_count_exam_results($exerciseId, $conditions, $courseId, $showSession = false)
2049
    {
2050
        $count = self::get_exam_results_data(
2051
            null,
2052
            null,
2053
            null,
2054
            null,
2055
            $exerciseId,
2056
            $conditions,
2057
            true,
2058
            $courseId,
2059
            $showSession
2060
        );
2061
2062
        return $count;
2063
    }
2064
2065
    /**
2066
     * Gets the exam'data results.
2067
     *
2068
     * @todo this function should be moved in a library  + no global calls
2069
     *
2070
     * @param int    $from
2071
     * @param int    $number_of_items
2072
     * @param int    $column
2073
     * @param string $direction
2074
     * @param int    $exercise_id
2075
     * @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...
2076
     * @param bool   $get_count
2077
     * @param int    $courseId
2078
     * @param bool   $showSessionField
2079
     * @param bool   $showExerciseCategories
2080
     * @param array  $userExtraFieldsToAdd
2081
     * @param bool   $useCommaAsDecimalPoint
2082
     * @param bool   $roundValues
2083
     * @param bool   $getOnyIds
2084
     *
2085
     * @return array
2086
     */
2087
    public static function get_exam_results_data(
2088
        $from,
2089
        $number_of_items,
2090
        $column,
2091
        $direction,
2092
        $exercise_id,
2093
        $extra_where_conditions = null,
2094
        $get_count = false,
2095
        $courseId = null,
2096
        $showSessionField = false,
2097
        $showExerciseCategories = false,
2098
        $userExtraFieldsToAdd = [],
2099
        $useCommaAsDecimalPoint = false,
2100
        $roundValues = false,
2101
        $getOnyIds = false
2102
    ) {
2103
        //@todo replace all this globals
2104
        global $filter;
2105
        $courseId = (int) $courseId;
2106
        $course = api_get_course_entity($courseId);
2107
        if (null === $course) {
2108
            return [];
2109
        }
2110
2111
        $sessionId = api_get_session_id();
2112
        $exercise_id = (int) $exercise_id;
2113
2114
        $is_allowedToEdit =
2115
            api_is_allowed_to_edit(null, true) ||
2116
            api_is_allowed_to_edit(true) ||
2117
            api_is_drh() ||
2118
            api_is_student_boss() ||
2119
            api_is_session_admin();
2120
        $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
2121
        $TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
2122
        $TBL_GROUP_REL_USER = Database::get_course_table(TABLE_GROUP_USER);
2123
        $TBL_GROUP = Database::get_course_table(TABLE_GROUP);
2124
        $TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2125
        $tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
2126
2127
        $session_id_and = '';
2128
        $sessionCondition = '';
2129
        if (!$showSessionField) {
2130
            $session_id_and = api_get_session_condition($sessionId, true, false, 'te.session_id');
2131
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'ttte.session_id');
2132
        }
2133
2134
        $exercise_where = '';
2135
        if (!empty($exercise_id)) {
2136
            $exercise_where .= ' AND te.exe_exo_id = '.$exercise_id.'  ';
2137
        }
2138
2139
        // sql for chamilo-type tests for teacher / tutor view
2140
        $sql_inner_join_tbl_track_exercices = "
2141
        (
2142
            SELECT DISTINCT ttte.*, if(tr.exe_id,1, 0) as revised
2143
            FROM $TBL_TRACK_EXERCISES ttte
2144
            LEFT JOIN $tblTrackAttemptQualify tr
2145
            ON (ttte.exe_id = tr.exe_id) AND tr.author > 0
2146
            WHERE
2147
                c_id = $courseId AND
2148
                exe_exo_id = $exercise_id
2149
                $sessionCondition
2150
        )";
2151
2152
        if ($is_allowedToEdit) {
2153
            //@todo fix to work with COURSE_RELATION_TYPE_RRHH in both queries
2154
            // Hack in order to filter groups
2155
            $sql_inner_join_tbl_user = '';
2156
            if (strpos($extra_where_conditions, 'group_id')) {
2157
                $sql_inner_join_tbl_user = "
2158
                (
2159
                    SELECT
2160
                        u.id as user_id,
2161
                        firstname,
2162
                        lastname,
2163
                        official_code,
2164
                        email,
2165
                        username,
2166
                        g.name as group_name,
2167
                        g.id as group_id
2168
                    FROM $TBL_USER u
2169
                    INNER JOIN $TBL_GROUP_REL_USER gru
2170
                    ON (gru.user_id = u.id AND gru.c_id= $courseId )
2171
                    INNER JOIN $TBL_GROUP g
2172
                    ON (gru.group_id = g.id AND g.c_id= $courseId )
2173
                    WHERE u.active <> ".USER_SOFT_DELETED."
2174
                )";
2175
            }
2176
2177
            if (strpos($extra_where_conditions, 'group_all')) {
2178
                $extra_where_conditions = str_replace(
2179
                    "AND (  group_id = 'group_all'  )",
2180
                    '',
2181
                    $extra_where_conditions
2182
                );
2183
                $extra_where_conditions = str_replace(
2184
                    "AND group_id = 'group_all'",
2185
                    '',
2186
                    $extra_where_conditions
2187
                );
2188
                $extra_where_conditions = str_replace(
2189
                    "group_id = 'group_all' AND",
2190
                    '',
2191
                    $extra_where_conditions
2192
                );
2193
2194
                $sql_inner_join_tbl_user = "
2195
                (
2196
                    SELECT
2197
                        u.id as user_id,
2198
                        firstname,
2199
                        lastname,
2200
                        official_code,
2201
                        email,
2202
                        username,
2203
                        '' as group_name,
2204
                        '' as group_id
2205
                    FROM $TBL_USER u
2206
                    WHERE u.active <> ".USER_SOFT_DELETED."
2207
                )";
2208
                $sql_inner_join_tbl_user = null;
2209
            }
2210
2211
            if (strpos($extra_where_conditions, 'group_none')) {
2212
                $extra_where_conditions = str_replace(
2213
                    "AND (  group_id = 'group_none'  )",
2214
                    "AND (  group_id is null  )",
2215
                    $extra_where_conditions
2216
                );
2217
                $extra_where_conditions = str_replace(
2218
                    "AND group_id = 'group_none'",
2219
                    "AND (  group_id is null  )",
2220
                    $extra_where_conditions
2221
                );
2222
                $sql_inner_join_tbl_user = "
2223
            (
2224
                SELECT
2225
                    u.id as user_id,
2226
                    firstname,
2227
                    lastname,
2228
                    official_code,
2229
                    email,
2230
                    username,
2231
                    g.name as group_name,
2232
                    g.iid as group_id
2233
                FROM $TBL_USER u
2234
                LEFT OUTER JOIN $TBL_GROUP_REL_USER gru
2235
                ON (gru.user_id = u.id AND gru.c_id= $courseId )
2236
                LEFT OUTER JOIN $TBL_GROUP g
2237
                ON (gru.group_id = g.id AND g.c_id = $courseId )
2238
                WHERE u.active <> ".USER_SOFT_DELETED."
2239
            )";
2240
            }
2241
2242
            // All
2243
            $is_empty_sql_inner_join_tbl_user = false;
2244
            if (empty($sql_inner_join_tbl_user)) {
2245
                $is_empty_sql_inner_join_tbl_user = true;
2246
                $sql_inner_join_tbl_user = "
2247
            (
2248
                SELECT u.id as user_id, firstname, lastname, email, username, ' ' as group_name, '' as group_id, official_code
2249
                FROM $TBL_USER u
2250
                WHERE u.active <> ".USER_SOFT_DELETED." AND u.status NOT IN(".api_get_users_status_ignored_in_reports('string').")
2251
            )";
2252
            }
2253
2254
            $sqlFromOption = " , $TBL_GROUP_REL_USER AS gru ";
2255
            $sqlWhereOption = "  AND gru.c_id = $courseId AND gru.user_id = user.id ";
2256
            $first_and_last_name = api_is_western_name_order() ? "firstname, lastname" : "lastname, firstname";
2257
2258
            if ($get_count) {
2259
                $sql_select = 'SELECT count(te.exe_id) ';
2260
            } else {
2261
                $sql_select = "SELECT DISTINCT
2262
                user.user_id,
2263
                $first_and_last_name,
2264
                official_code,
2265
                ce.title,
2266
                username,
2267
                te.score,
2268
                te.max_score,
2269
                te.exe_date,
2270
                te.exe_id,
2271
                te.session_id,
2272
                email as exemail,
2273
                te.start_date,
2274
                ce.expired_time,
2275
                steps_counter,
2276
                exe_user_id,
2277
                te.exe_duration,
2278
                te.status as completion_status,
2279
                propagate_neg,
2280
                revised,
2281
                group_name,
2282
                user.group_id AS group_id,
2283
                orig_lp_id,
2284
                te.user_ip";
2285
            }
2286
2287
            $sql = " $sql_select
2288
            FROM $TBL_EXERCISES AS ce
2289
            INNER JOIN $sql_inner_join_tbl_track_exercices AS te
2290
            ON (te.exe_exo_id = ce.iid)
2291
            INNER JOIN $sql_inner_join_tbl_user AS user
2292
            ON (user.user_id = exe_user_id)
2293
            INNER JOIN resource_node rn
2294
                ON rn.id = ce.resource_node_id
2295
            INNER JOIN resource_link rl
2296
                ON rl.resource_node_id = rn.id
2297
            WHERE
2298
                te.c_id = $courseId $session_id_and AND
2299
                rl.deleted_at IS NULL
2300
                $exercise_where
2301
                $extra_where_conditions
2302
            ";
2303
        }
2304
2305
        if (empty($sql)) {
2306
            return false;
2307
        }
2308
2309
        if ($get_count) {
2310
            $resx = Database::query($sql);
2311
            $rowx = Database::fetch_row($resx, 'ASSOC');
2312
2313
            return $rowx[0];
2314
        }
2315
2316
        $teacher_list = CourseManager::get_teacher_list_from_course_code($course->getCode());
2317
        $teacher_id_list = [];
2318
        if (!empty($teacher_list)) {
2319
            foreach ($teacher_list as $teacher) {
2320
                $teacher_id_list[] = $teacher['user_id'];
2321
            }
2322
        }
2323
2324
        $scoreDisplay = new ScoreDisplay();
2325
        $decimalSeparator = '.';
2326
        $thousandSeparator = ',';
2327
2328
        if ($useCommaAsDecimalPoint) {
2329
            $decimalSeparator = ',';
2330
            $thousandSeparator = '';
2331
        }
2332
2333
        $listInfo = [];
2334
        $column = !empty($column) ? Database::escape_string($column) : null;
2335
        $from = (int) $from;
2336
        $number_of_items = (int) $number_of_items;
2337
        $direction = !in_array(strtolower(trim($direction)), ['asc', 'desc']) ? 'asc' : $direction;
2338
2339
        if (!empty($column)) {
2340
            $sql .= " ORDER BY `$column` $direction ";
2341
        }
2342
2343
        if (!$getOnyIds) {
2344
            $sql .= " LIMIT $from, $number_of_items";
2345
        }
2346
2347
        $results = [];
2348
        $resx = Database::query($sql);
2349
        while ($rowx = Database::fetch_assoc($resx)) {
2350
            $results[] = $rowx;
2351
        }
2352
2353
        $group_list = GroupManager::get_group_list(null, $course);
2354
        $clean_group_list = [];
2355
        if (!empty($group_list)) {
2356
            foreach ($group_list as $group) {
2357
                $clean_group_list[$group['iid']] = $group['title'];
2358
            }
2359
        }
2360
2361
        $lp_list_obj = new LearnpathList(api_get_user_id());
2362
        $lp_list = $lp_list_obj->get_flat_list();
2363
        $oldIds = array_column($lp_list, 'lp_old_id', 'iid');
2364
2365
        if (is_array($results)) {
2366
            $users_array_id = [];
2367
            $from_gradebook = false;
2368
            if (isset($_GET['gradebook']) && 'view' === $_GET['gradebook']) {
2369
                $from_gradebook = true;
2370
            }
2371
            $sizeof = count($results);
2372
            $locked = api_resource_is_locked_by_gradebook(
2373
                $exercise_id,
2374
                LINK_EXERCISE
2375
            );
2376
2377
            $timeNow = strtotime(api_get_utc_datetime());
2378
            // Looping results
2379
            for ($i = 0; $i < $sizeof; $i++) {
2380
                $revised = $results[$i]['revised'];
2381
                if ('incomplete' === $results[$i]['completion_status']) {
2382
                    // If the exercise was incomplete, we need to determine
2383
                    // if it is still into the time allowed, or if its
2384
                    // allowed time has expired and it can be closed
2385
                    // (it's "unclosed")
2386
                    $minutes = $results[$i]['expired_time'];
2387
                    if (0 == $minutes) {
2388
                        // There's no time limit, so obviously the attempt
2389
                        // can still be "ongoing", but the teacher should
2390
                        // be able to choose to close it, so mark it as
2391
                        // "unclosed" instead of "ongoing"
2392
                        $revised = 2;
2393
                    } else {
2394
                        $allowedSeconds = $minutes * 60;
2395
                        $timeAttemptStarted = strtotime($results[$i]['start_date']);
2396
                        $secondsSinceStart = $timeNow - $timeAttemptStarted;
2397
                        if ($secondsSinceStart > $allowedSeconds) {
2398
                            $revised = 2; // mark as "unclosed"
2399
                        } else {
2400
                            $revised = 3; // mark as "ongoing"
2401
                        }
2402
                    }
2403
                }
2404
2405
                if ($from_gradebook && ($is_allowedToEdit)) {
2406
                    if (in_array(
2407
                        $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'],
2408
                        $users_array_id
2409
                    )) {
2410
                        continue;
2411
                    }
2412
                    $users_array_id[] = $results[$i]['username'].$results[$i]['firstname'].$results[$i]['lastname'];
2413
                }
2414
2415
                $lp_obj = isset($results[$i]['orig_lp_id']) && isset($lp_list[$results[$i]['orig_lp_id']]) ? $lp_list[$results[$i]['orig_lp_id']] : null;
2416
                if (empty($lp_obj)) {
2417
                    // Try to get the old id (id instead of iid)
2418
                    $lpNewId = isset($results[$i]['orig_lp_id']) && isset($oldIds[$results[$i]['orig_lp_id']]) ? $oldIds[$results[$i]['orig_lp_id']] : null;
2419
                    if ($lpNewId) {
2420
                        $lp_obj = isset($lp_list[$lpNewId]) ? $lp_list[$lpNewId] : null;
2421
                    }
2422
                }
2423
                $lp_name = null;
2424
                if ($lp_obj) {
2425
                    $url = api_get_path(WEB_CODE_PATH).
2426
                        'lp/lp_controller.php?'.api_get_cidreq().'&action=view&lp_id='.$results[$i]['orig_lp_id'];
2427
                    $lp_name = Display::url(
2428
                        $lp_obj['lp_name'],
2429
                        $url,
2430
                        ['target' => '_blank']
2431
                    );
2432
                }
2433
2434
                // Add all groups by user
2435
                $group_name_list = '';
2436
                if ($is_empty_sql_inner_join_tbl_user) {
2437
                    $group_list = GroupManager::get_group_ids(
2438
                        api_get_course_int_id(),
2439
                        $results[$i]['user_id']
2440
                    );
2441
2442
                    foreach ($group_list as $id) {
2443
                        if (isset($clean_group_list[$id])) {
2444
                            $group_name_list .= $clean_group_list[$id].'<br/>';
2445
                        }
2446
                    }
2447
                    $results[$i]['group_name'] = $group_name_list;
2448
                }
2449
2450
                $results[$i]['exe_duration'] = !empty($results[$i]['exe_duration']) ? round($results[$i]['exe_duration'] / 60) : 0;
2451
                $id = $results[$i]['exe_id'];
2452
                $dt = api_convert_and_format_date($results[$i]['max_score']);
2453
2454
                // we filter the results if we have the permission to
2455
                $result_disabled = 0;
2456
                if (isset($results[$i]['results_disabled'])) {
2457
                    $result_disabled = (int) $results[$i]['results_disabled'];
2458
                }
2459
                if (0 == $result_disabled) {
2460
                    $my_res = $results[$i]['score'];
2461
                    $my_total = $results[$i]['max_score'];
2462
                    $results[$i]['start_date'] = api_get_local_time($results[$i]['start_date']);
2463
                    $results[$i]['exe_date'] = api_get_local_time($results[$i]['exe_date']);
2464
2465
                    if (!$results[$i]['propagate_neg'] && $my_res < 0) {
2466
                        $my_res = 0;
2467
                    }
2468
2469
                    $score = self::show_score(
2470
                        $my_res,
2471
                        $my_total,
2472
                        true,
2473
                        true,
2474
                        false,
2475
                        false,
2476
                        $decimalSeparator,
2477
                        $thousandSeparator,
2478
                        $roundValues
2479
                    );
2480
2481
                    $actions = '<div class="pull-right">';
2482
                    if ($is_allowedToEdit) {
2483
                        if (isset($teacher_id_list)) {
2484
                            if (in_array(
2485
                                $results[$i]['exe_user_id'],
2486
                                $teacher_id_list
2487
                            )) {
2488
                                $actions .= Display::getMdiIcon('human-male-board', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Trainer'));
2489
                            }
2490
                        }
2491
                        $revisedLabel = '';
2492
                        switch ($revised) {
2493
                            case 0:
2494
                                $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=qualify&id=$id'>".
2495
                                    Display::getMdiIcon(ActionIcon::GRADE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Grade activity')
2496
                                    );
2497
                                $actions .= '</a>';
2498
                                $revisedLabel = Display::label(
2499
                                    get_lang('Not validated'),
2500
                                    'info'
2501
                                );
2502
                                break;
2503
                            case 1:
2504
                                $actions .= "<a href='exercise_show.php?".api_get_cidreq()."&action=edit&id=$id'>".
2505
                                    Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Edit'));
2506
                                $actions .= '</a>';
2507
                                $revisedLabel = Display::label(
2508
                                    get_lang('Validated'),
2509
                                    'success'
2510
                                );
2511
                                break;
2512
                            case 2: //finished but not marked as such
2513
                                $actions .= '<a href="exercise_report.php?'
2514
                                    .api_get_cidreq()
2515
                                    .'&exerciseId='
2516
                                    .$exercise_id
2517
                                    .'&a=close&id='
2518
                                    .$id
2519
                                    .'">'.
2520
                                    Display::getMdiIcon(ActionIcon::LOCK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Mark attempt as closed'));
2521
                                $actions .= '</a>';
2522
                                $revisedLabel = Display::label(
2523
                                    get_lang('Unclosed'),
2524
                                    'warning'
2525
                                );
2526
                                break;
2527
                            case 3: //still ongoing
2528
                                $actions .= Display::getMdiIcon('clock', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Attempt still going on. Please wait.'));
2529
                                $actions .= '';
2530
                                $revisedLabel = Display::label(
2531
                                    get_lang('Ongoing'),
2532
                                    'danger'
2533
                                );
2534
                                break;
2535
                        }
2536
2537
                        if (2 == $filter) {
2538
                            $actions .= ' <a href="exercise_history.php?'.api_get_cidreq().'&exe_id='.$id.'">'.
2539
                                Display::getMdiIcon('clipboard-text-clock', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('View changes history')
2540
                                ).'</a>';
2541
                        }
2542
2543
                        // Admin can always delete the attempt
2544
                        if ((false == $locked || api_is_platform_admin()) && !api_is_student_boss()) {
2545
                            $ip = Tracking::get_ip_from_user_event(
2546
                                $results[$i]['exe_user_id'],
2547
                                api_get_utc_datetime(),
2548
                                false
2549
                            );
2550
                            $actions .= '<a href="http://www.whatsmyip.org/ip-geo-location/?ip='.$ip.'" target="_blank">'
2551
                                .Display::getMdiIcon('information', 'ch-tool-icon', null, ICON_SIZE_SMALL, $ip)
2552
                                .'</a>';
2553
2554
                            $recalculateUrl = api_get_path(WEB_CODE_PATH).'exercise/recalculate.php?'.
2555
                                api_get_cidreq().'&'.
2556
                                http_build_query([
2557
                                    'id' => $id,
2558
                                    'exercise' => $exercise_id,
2559
                                    'user' => $results[$i]['exe_user_id'],
2560
                                ]);
2561
                            $actions .= Display::url(
2562
                                Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Recalculate results')),
2563
                                $recalculateUrl,
2564
                                [
2565
                                    'data-exercise' => $exercise_id,
2566
                                    'data-user' => $results[$i]['exe_user_id'],
2567
                                    'data-id' => $id,
2568
                                    'class' => 'exercise-recalculate',
2569
                                ]
2570
                            );
2571
2572
                            $exportPdfUrl = api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'.
2573
                                api_get_cidreq().'&exerciseId='.$exercise_id.'&action=export_pdf&attemptId='.$id.'&userId='.(int) $results[$i]['exe_user_id'];
2574
                            $actions .= '<a href="'.$exportPdfUrl.'" target="_blank">'
2575
                                .Display::getMdiIcon(ActionIcon::EXPORT_PDF, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Export to PDF'))
2576
                                .'</a>';
2577
2578
                            $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'];
2579
                            $emailLink = '<a href="'.$sendMailUrl.'">'
2580
                                .Display::getMdiIcon(ActionIcon::SEND_SINGLE_EMAIL, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Send by e-mail'))
2581
                                .'</a>';
2582
2583
                            $filterByUser = isset($_GET['filter_by_user']) ? (int) $_GET['filter_by_user'] : 0;
2584
                            $delete_link = '<a
2585
                            href="exercise_report.php?'.api_get_cidreq().'&filter_by_user='.$filterByUser.'&filter='.$filter.'&exerciseId='.$exercise_id.'&delete=delete&did='.$id.'"
2586
                            onclick=
2587
                            "javascript:if(!confirm(\''.sprintf(addslashes(get_lang('Delete attempt?')), $results[$i]['username'], $dt).'\')) return false;"
2588
                            >';
2589
                            $delete_link .= Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_SMALL, addslashes(get_lang('Delete'))).'</a>';
2590
2591
                            if (api_is_drh() && !api_is_platform_admin()) {
2592
                                $delete_link = null;
2593
                            }
2594
                            if (api_is_session_admin()) {
2595
                                $delete_link = '';
2596
                            }
2597
                            if (3 == $revised) {
2598
                                $delete_link = null;
2599
                            }
2600
                            if (1 !== $revised) {
2601
                                $emailLink = '';
2602
                            }
2603
                            $actions .= $delete_link;
2604
                            $actions .= $emailLink;
2605
                        }
2606
                    } else {
2607
                        $attempt_url = api_get_path(WEB_CODE_PATH).'exercise/result.php?'.api_get_cidreq().'&id='.$results[$i]['exe_id'].'&sid='.$sessionId;
2608
                        $attempt_link = Display::url(
2609
                            get_lang('Show'),
2610
                            $attempt_url,
2611
                            [
2612
                                'class' => 'ajax btn btn--plain',
2613
                                'data-title' => get_lang('Show'),
2614
                            ]
2615
                        );
2616
                        $actions .= $attempt_link;
2617
                    }
2618
                    $actions .= '</div>';
2619
2620
                    if (!empty($userExtraFieldsToAdd)) {
2621
                        foreach ($userExtraFieldsToAdd as $variable) {
2622
                            $extraFieldValue = new ExtraFieldValue('user');
2623
                            $values = $extraFieldValue->get_values_by_handler_and_field_variable(
2624
                                $results[$i]['user_id'],
2625
                                $variable
2626
                            );
2627
                            if (isset($values['value'])) {
2628
                                $results[$i][$variable] = $values['value'];
2629
                            }
2630
                        }
2631
                    }
2632
2633
                    $exeId = $results[$i]['exe_id'];
2634
                    $results[$i]['id'] = $exeId;
2635
                    $category_list = [];
2636
                    if ($is_allowedToEdit) {
2637
                        $sessionName = '';
2638
                        $sessionStartAccessDate = '';
2639
                        if (!empty($results[$i]['session_id'])) {
2640
                            $sessionInfo = api_get_session_info($results[$i]['session_id']);
2641
                            if (!empty($sessionInfo)) {
2642
                                $sessionName = $sessionInfo['title'];
2643
                                $sessionStartAccessDate = api_get_local_time($sessionInfo['access_start_date']);
2644
                            }
2645
                        }
2646
2647
                        $objExercise = new Exercise($courseId);
2648
                        if ($showExerciseCategories) {
2649
                            // Getting attempt info
2650
                            $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
2651
                            if (!empty($exercise_stat_info['data_tracking'])) {
2652
                                $question_list = explode(',', $exercise_stat_info['data_tracking']);
2653
                                if (!empty($question_list)) {
2654
                                    foreach ($question_list as $questionId) {
2655
                                        $objQuestionTmp = Question::read($questionId, $objExercise->course);
2656
                                        // We're inside *one* question. Go through each possible answer for this question
2657
                                        $result = $objExercise->manage_answer(
2658
                                            $exeId,
2659
                                            $questionId,
2660
                                            null,
2661
                                            'exercise_result',
2662
                                            false,
2663
                                            false,
2664
                                            true,
2665
                                            false,
2666
                                            $objExercise->selectPropagateNeg(),
2667
                                            null,
2668
                                            true
2669
                                        );
2670
2671
                                        $my_total_score = $result['score'];
2672
                                        $my_total_weight = $result['weight'];
2673
2674
                                        // Category report
2675
                                        $category_was_added_for_this_test = false;
2676
                                        if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
2677
                                            if (!isset($category_list[$objQuestionTmp->category]['score'])) {
2678
                                                $category_list[$objQuestionTmp->category]['score'] = 0;
2679
                                            }
2680
                                            if (!isset($category_list[$objQuestionTmp->category]['total'])) {
2681
                                                $category_list[$objQuestionTmp->category]['total'] = 0;
2682
                                            }
2683
                                            $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
2684
                                            $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
2685
                                            $category_was_added_for_this_test = true;
2686
                                        }
2687
2688
                                        if (isset($objQuestionTmp->category_list) &&
2689
                                            !empty($objQuestionTmp->category_list)
2690
                                        ) {
2691
                                            foreach ($objQuestionTmp->category_list as $category_id) {
2692
                                                $category_list[$category_id]['score'] += $my_total_score;
2693
                                                $category_list[$category_id]['total'] += $my_total_weight;
2694
                                                $category_was_added_for_this_test = true;
2695
                                            }
2696
                                        }
2697
2698
                                        // No category for this question!
2699
                                        if (false == $category_was_added_for_this_test) {
2700
                                            if (!isset($category_list['none']['score'])) {
2701
                                                $category_list['none']['score'] = 0;
2702
                                            }
2703
                                            if (!isset($category_list['none']['total'])) {
2704
                                                $category_list['none']['total'] = 0;
2705
                                            }
2706
2707
                                            $category_list['none']['score'] += $my_total_score;
2708
                                            $category_list['none']['total'] += $my_total_weight;
2709
                                        }
2710
                                    }
2711
                                }
2712
                            }
2713
                        }
2714
2715
                        foreach ($category_list as $categoryId => $result) {
2716
                            $scoreToDisplay = self::show_score(
2717
                                $result['score'],
2718
                                $result['total'],
2719
                                true,
2720
                                true,
2721
                                false,
2722
                                false,
2723
                                $decimalSeparator,
2724
                                $thousandSeparator,
2725
                                $roundValues
2726
                            );
2727
                            $results[$i]['category_'.$categoryId] = $scoreToDisplay;
2728
                            $results[$i]['category_'.$categoryId.'_score_percentage'] = self::show_score(
2729
                                $result['score'],
2730
                                $result['total'],
2731
                                true,
2732
                                true,
2733
                                true, // $show_only_percentage = false
2734
                                true, // hide % sign
2735
                                $decimalSeparator,
2736
                                $thousandSeparator,
2737
                                $roundValues
2738
                            );
2739
                            $results[$i]['category_'.$categoryId.'_only_score'] = $result['score'];
2740
                            $results[$i]['category_'.$categoryId.'_total'] = $result['total'];
2741
                        }
2742
                        $results[$i]['session'] = $sessionName;
2743
                        $results[$i]['session_access_start_date'] = $sessionStartAccessDate;
2744
                        $results[$i]['status'] = $revisedLabel;
2745
                        $results[$i]['score'] = $score;
2746
                        $results[$i]['score_percentage'] = self::show_score(
2747
                            $my_res,
2748
                            $my_total,
2749
                            true,
2750
                            true,
2751
                            true,
2752
                            true,
2753
                            $decimalSeparator,
2754
                            $thousandSeparator,
2755
                            $roundValues
2756
                        );
2757
2758
                        if ($roundValues) {
2759
                            $whole = floor($my_res); // 1
2760
                            $fraction = $my_res - $whole; // .25
2761
                            if ($fraction >= 0.5) {
2762
                                $onlyScore = ceil($my_res);
2763
                            } else {
2764
                                $onlyScore = round($my_res);
2765
                            }
2766
                        } else {
2767
                            $onlyScore = $scoreDisplay->format_score(
2768
                                $my_res,
2769
                                false,
2770
                                $decimalSeparator,
2771
                                $thousandSeparator
2772
                            );
2773
                        }
2774
2775
                        $results[$i]['only_score'] = $onlyScore;
2776
2777
                        if ($roundValues) {
2778
                            $whole = floor($my_total); // 1
2779
                            $fraction = $my_total - $whole; // .25
2780
                            if ($fraction >= 0.5) {
2781
                                $onlyTotal = ceil($my_total);
2782
                            } else {
2783
                                $onlyTotal = round($my_total);
2784
                            }
2785
                        } else {
2786
                            $onlyTotal = $scoreDisplay->format_score(
2787
                                $my_total,
2788
                                false,
2789
                                $decimalSeparator,
2790
                                $thousandSeparator
2791
                            );
2792
                        }
2793
                        $results[$i]['total'] = $onlyTotal;
2794
                        $results[$i]['lp'] = $lp_name;
2795
                        $results[$i]['actions'] = $actions;
2796
                        $listInfo[] = $results[$i];
2797
                    } else {
2798
                        $results[$i]['status'] = $revisedLabel;
2799
                        $results[$i]['score'] = $score;
2800
                        $results[$i]['actions'] = $actions;
2801
                        $listInfo[] = $results[$i];
2802
                    }
2803
                }
2804
            }
2805
        }
2806
2807
        return $listInfo;
2808
    }
2809
2810
    /**
2811
     * Returns email content for a specific attempt.
2812
     */
2813
    public static function getEmailContentForAttempt(int $attemptId): array
2814
    {
2815
        $trackExerciseInfo = self::get_exercise_track_exercise_info($attemptId);
2816
2817
        if (empty($trackExerciseInfo)) {
2818
            return [
2819
                'to' => '',
2820
                'subject' => 'No exercise info found',
2821
                'message' => 'Attempt ID not found or invalid.',
2822
            ];
2823
        }
2824
2825
        $studentId = $trackExerciseInfo['exe_user_id'];
2826
        $courseInfo = api_get_course_info();
2827
        $teacherId = api_get_user_id();
2828
2829
        if (
2830
            empty($trackExerciseInfo['orig_lp_id']) ||
2831
            empty($trackExerciseInfo['orig_lp_item_id'])
2832
        ) {
2833
            $url = api_get_path(WEB_CODE_PATH).'exercise/result.php?id='.$trackExerciseInfo['exe_id'].'&'.api_get_cidreq()
2834
                .'&show_headers=1&id_session='.api_get_session_id();
2835
        } else {
2836
            $url = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?action=view&item_id='
2837
                .$trackExerciseInfo['orig_lp_item_id'].'&lp_id='.$trackExerciseInfo['orig_lp_id'].'&'.api_get_cidreq()
2838
                .'&id_session='.api_get_session_id();
2839
        }
2840
2841
        $message = self::getEmailNotification(
2842
            $teacherId,
2843
            $courseInfo,
2844
            $trackExerciseInfo['title'],
2845
            $url
2846
        );
2847
2848
        return [
2849
            'to' => $studentId,
2850
            'subject' => get_lang('Corrected test result'),
2851
            'message' => $message,
2852
        ];
2853
    }
2854
2855
    /**
2856
     * Sends the exercise result email to the student.
2857
     */
2858
    public static function sendExerciseResultByEmail(int $attemptId): void
2859
    {
2860
        $content = self::getEmailContentForAttempt($attemptId);
2861
2862
        if (empty($content['to'])) {
2863
            return;
2864
        }
2865
2866
        MessageManager::send_message_simple(
2867
            $content['to'],
2868
            $content['subject'],
2869
            $content['message'],
2870
            api_get_user_id()
2871
        );
2872
    }
2873
2874
    /**
2875
     * Returns all reviewed attempts for a given exercise and session.
2876
     */
2877
    public static function getReviewedAttemptsInfo(int $exerciseId, int $sessionId): array
2878
    {
2879
        $courseId = api_get_course_int_id();
2880
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
2881
        $qualifyTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
2882
2883
        $sessionCondition = api_get_session_condition($sessionId, true, false, 't.session_id');
2884
2885
        $sql = "
2886
            SELECT DISTINCT t.exe_id
2887
            FROM $trackTable t
2888
            INNER JOIN $qualifyTable q ON (t.exe_id = q.exe_id AND q.author > 0)
2889
            WHERE
2890
                t.c_id = $courseId AND
2891
                t.exe_exo_id = $exerciseId
2892
                $sessionCondition
2893
        ";
2894
2895
        return Database::store_result(Database::query($sql));
2896
    }
2897
2898
    /**
2899
     * @param $score
2900
     * @param $weight
2901
     *
2902
     * @return array
2903
     */
2904
    public static function convertScoreToPlatformSetting($score, $weight)
2905
    {
2906
        $maxNote = api_get_setting('exercise_max_score');
2907
        $minNote = api_get_setting('exercise_min_score');
2908
2909
        if ('' != $maxNote && '' != $minNote) {
2910
            if (!empty($weight) && (float) $weight !== (float) 0) {
2911
                $score = $minNote + ($maxNote - $minNote) * $score / $weight;
2912
            } else {
2913
                $score = $minNote;
2914
            }
2915
            $weight = $maxNote;
2916
        }
2917
2918
        return ['score' => $score, 'weight' => $weight];
2919
    }
2920
2921
    /**
2922
     * Converts the score with the exercise_max_note and exercise_min_score
2923
     * the platform settings + formats the results using the float_format function.
2924
     *
2925
     * @param float  $score
2926
     * @param float  $weight
2927
     * @param bool   $show_percentage       show percentage or not
2928
     * @param bool   $use_platform_settings use or not the platform settings
2929
     * @param bool   $show_only_percentage
2930
     * @param bool   $hidePercentageSign    hide "%" sign
2931
     * @param string $decimalSeparator
2932
     * @param string $thousandSeparator
2933
     * @param bool   $roundValues           This option rounds the float values into a int using ceil()
2934
     * @param bool   $removeEmptyDecimals
2935
     *
2936
     * @return string an html with the score modified
2937
     */
2938
    public static function show_score(
2939
        $score,
2940
        $weight,
2941
        $show_percentage = true,
2942
        $use_platform_settings = true,
2943
        $show_only_percentage = false,
2944
        $hidePercentageSign = false,
2945
        $decimalSeparator = '.',
2946
        $thousandSeparator = ',',
2947
        $roundValues = false,
2948
        $removeEmptyDecimals = false
2949
    ) {
2950
        if (is_null($score) && is_null($weight)) {
2951
            return '-';
2952
        }
2953
2954
        $decimalSeparator = empty($decimalSeparator) ? '.' : $decimalSeparator;
2955
        $thousandSeparator = empty($thousandSeparator) ? ',' : $thousandSeparator;
2956
2957
        if ($use_platform_settings) {
2958
            $result = self::convertScoreToPlatformSetting($score, $weight);
2959
            $score = $result['score'];
2960
            $weight = $result['weight'];
2961
        }
2962
2963
        $percentage = (100 * $score) / (0 != $weight ? $weight : 1);
2964
        // Formats values
2965
        $percentage = float_format($percentage, 1);
2966
        $score = float_format($score, 1);
2967
        $weight = float_format($weight, 1);
2968
2969
        if ($roundValues) {
2970
            $whole = floor($percentage); // 1
2971
            $fraction = $percentage - $whole; // .25
2972
2973
            // Formats values
2974
            if ($fraction >= 0.5) {
2975
                $percentage = ceil($percentage);
2976
            } else {
2977
                $percentage = round($percentage);
2978
            }
2979
2980
            $whole = floor($score); // 1
2981
            $fraction = $score - $whole; // .25
2982
            if ($fraction >= 0.5) {
2983
                $score = ceil($score);
2984
            } else {
2985
                $score = round($score);
2986
            }
2987
2988
            $whole = floor($weight); // 1
2989
            $fraction = $weight - $whole; // .25
2990
            if ($fraction >= 0.5) {
2991
                $weight = ceil($weight);
2992
            } else {
2993
                $weight = round($weight);
2994
            }
2995
        } else {
2996
            // Formats values
2997
            $percentage = float_format($percentage, 1, $decimalSeparator, $thousandSeparator);
2998
            $score = float_format($score, 1, $decimalSeparator, $thousandSeparator);
2999
            $weight = float_format($weight, 1, $decimalSeparator, $thousandSeparator);
3000
        }
3001
3002
        if ($show_percentage) {
3003
            $percentageSign = ' %';
3004
            if ($hidePercentageSign) {
3005
                $percentageSign = '';
3006
            }
3007
            $html = $percentage."$percentageSign ($score / $weight)";
3008
            if ($show_only_percentage) {
3009
                $html = $percentage.$percentageSign;
3010
            }
3011
        } else {
3012
            if ($removeEmptyDecimals) {
3013
                if (ScoreDisplay::hasEmptyDecimals($weight)) {
3014
                    $weight = round($weight);
3015
                }
3016
            }
3017
            $html = $score.' / '.$weight;
3018
        }
3019
3020
        // Over write score
3021
        $scoreBasedInModel = self::convertScoreToModel($percentage);
3022
        if (!empty($scoreBasedInModel)) {
3023
            $html = $scoreBasedInModel;
3024
        }
3025
3026
        // Ignore other formats and use the configuration['exercise_score_format'] value
3027
        // But also keep the round values settings.
3028
        $format = (int) api_get_setting('exercise.exercise_score_format');
3029
        if (!empty($format)) {
3030
            $html = ScoreDisplay::instance()->display_score([$score, $weight], $format);
3031
        }
3032
3033
        return Display::span($html, ['class' => 'score_exercise']);
3034
    }
3035
3036
    /**
3037
     * @param array $model
3038
     * @param float $percentage
3039
     *
3040
     * @return string
3041
     */
3042
    public static function getModelStyle($model, $percentage)
3043
    {
3044
        return '<span class="'.$model['css_class'].' p-2">'.$model['name'].'</span>';
3045
    }
3046
3047
    /**
3048
     * @param float $percentage value between 0 and 100
3049
     *
3050
     * @return string
3051
     */
3052
    public static function convertScoreToModel($percentage)
3053
    {
3054
        $model = self::getCourseScoreModel();
3055
        if (!empty($model)) {
3056
            $scoreWithGrade = [];
3057
            foreach ($model['score_list'] as $item) {
3058
                if ($percentage >= $item['min'] && $percentage <= $item['max']) {
3059
                    $scoreWithGrade = $item;
3060
                    break;
3061
                }
3062
            }
3063
3064
            if (!empty($scoreWithGrade)) {
3065
                return self::getModelStyle($scoreWithGrade, $percentage);
3066
            }
3067
        }
3068
3069
        return '';
3070
    }
3071
3072
    /**
3073
     * @return array
3074
     */
3075
    public static function getCourseScoreModel()
3076
    {
3077
        $modelList = self::getScoreModels();
3078
3079
        if (empty($modelList)) {
3080
            return [];
3081
        }
3082
3083
        $courseInfo = api_get_course_info();
3084
        if (!empty($courseInfo)) {
3085
            $scoreModelId = api_get_course_setting('score_model_id');
3086
            if (-1 != $scoreModelId) {
3087
                $modelIdList = array_column($modelList['models'], 'id');
3088
                if (in_array($scoreModelId, $modelIdList)) {
3089
                    foreach ($modelList['models'] as $item) {
3090
                        if ($item['id'] == $scoreModelId) {
3091
                            return $item;
3092
                        }
3093
                    }
3094
                }
3095
            }
3096
        }
3097
3098
        return [];
3099
    }
3100
3101
    /**
3102
     * @return array
3103
     */
3104
    public static function getScoreModels()
3105
    {
3106
        return api_get_setting('exercise.score_grade_model', true);
3107
    }
3108
3109
    /**
3110
     * @param float  $score
3111
     * @param float  $weight
3112
     * @param string $passPercentage
3113
     *
3114
     * @return bool
3115
     */
3116
    public static function isSuccessExerciseResult($score, $weight, $passPercentage)
3117
    {
3118
        $percentage = float_format(
3119
            ($score / (0 != $weight ? $weight : 1)) * 100,
3120
            1
3121
        );
3122
        if (isset($passPercentage) && !empty($passPercentage)) {
3123
            if ($percentage >= $passPercentage) {
3124
                return true;
3125
            }
3126
        }
3127
3128
        return false;
3129
    }
3130
3131
    /**
3132
     * @param string $name
3133
     * @param $weight
3134
     * @param $selected
3135
     *
3136
     * @return bool
3137
     */
3138
    public static function addScoreModelInput(
3139
        FormValidator $form,
3140
        $name,
3141
        $weight,
3142
        $selected
3143
    ) {
3144
        $model = self::getCourseScoreModel();
3145
        if (empty($model)) {
3146
            return false;
3147
        }
3148
3149
        /** @var HTML_QuickForm_select $element */
3150
        $element = $form->createElement(
3151
            'select',
3152
            $name,
3153
            get_lang('Score'),
3154
            [],
3155
            ['class' => 'exercise_mark_select']
3156
        );
3157
3158
        foreach ($model['score_list'] as $item) {
3159
            $i = api_number_format($item['score_to_qualify'] / 100 * $weight, 2);
3160
            $label = self::getModelStyle($item, $i);
3161
            $attributes = [
3162
                'class' => $item['css_class'],
3163
            ];
3164
            if ($selected == $i) {
3165
                $attributes['selected'] = 'selected';
3166
            }
3167
            $element->addOption($label, $i, $attributes);
3168
        }
3169
        $form->addElement($element);
3170
    }
3171
3172
    /**
3173
     * @return string
3174
     */
3175
    public static function getJsCode()
3176
    {
3177
        // Filling the scores with the right colors.
3178
        $models = self::getCourseScoreModel();
3179
        $cssListToString = '';
3180
        if (!empty($models)) {
3181
            $cssList = array_column($models['score_list'], 'css_class');
3182
            $cssListToString = implode(' ', $cssList);
3183
        }
3184
3185
        if (empty($cssListToString)) {
3186
            return '';
3187
        }
3188
        $js = <<<EOT
3189
3190
        function updateSelect(element) {
3191
            var spanTag = element.parent().find('span.filter-option');
3192
            var value = element.val();
3193
            var selectId = element.attr('id');
3194
            var optionClass = $('#' + selectId + ' option[value="'+value+'"]').attr('class');
3195
            spanTag.removeClass('$cssListToString');
3196
            spanTag.addClass(optionClass);
3197
        }
3198
3199
        $(function() {
3200
            // Loading values
3201
            $('.exercise_mark_select').on('loaded.bs.select', function() {
3202
                updateSelect($(this));
3203
            });
3204
            // On change
3205
            $('.exercise_mark_select').on('changed.bs.select', function() {
3206
                updateSelect($(this));
3207
            });
3208
        });
3209
EOT;
3210
3211
        return $js;
3212
    }
3213
3214
    /**
3215
     * @param float  $score
3216
     * @param float  $weight
3217
     * @param string $pass_percentage
3218
     *
3219
     * @return string
3220
     */
3221
    public static function showSuccessMessage($score, $weight, $pass_percentage)
3222
    {
3223
        $res = '';
3224
        if (self::isPassPercentageEnabled($pass_percentage)) {
3225
            $isSuccess = self::isSuccessExerciseResult(
3226
                $score,
3227
                $weight,
3228
                $pass_percentage
3229
            );
3230
3231
            if ($isSuccess) {
3232
                $html = get_lang('Congratulations you passed the test!');
3233
                $icon = Display::getMdiIcon('check-circle', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Correct'));
3234
            } else {
3235
                $html = get_lang('You didn\'t reach the minimum score');
3236
                $icon = Display::getMdiIcon('alert', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Wrong'));
3237
            }
3238
            $html = Display::tag('h4', $html);
3239
            $html .= Display::tag(
3240
                'h5',
3241
                $icon,
3242
                ['style' => 'width:40px; padding:2px 10px 0px 0px']
3243
            );
3244
            $res = $html;
3245
        }
3246
3247
        return $res;
3248
    }
3249
3250
    /**
3251
     * Return true if pass_pourcentage activated (we use the pass pourcentage feature
3252
     * return false if pass_percentage = 0 (we don't use the pass pourcentage feature.
3253
     *
3254
     * @param $value
3255
     *
3256
     * @return bool
3257
     *              In this version, pass_percentage and show_success_message are disabled if
3258
     *              pass_percentage is set to 0
3259
     */
3260
    public static function isPassPercentageEnabled($value)
3261
    {
3262
        return $value > 0;
3263
    }
3264
3265
    /**
3266
     * Converts a numeric value in a percentage example 0.66666 to 66.67 %.
3267
     *
3268
     * @param $value
3269
     *
3270
     * @return float Converted number
3271
     */
3272
    public static function convert_to_percentage($value)
3273
    {
3274
        $return = '-';
3275
        if ('' != $value) {
3276
            $return = float_format($value * 100, 1).' %';
3277
        }
3278
3279
        return $return;
3280
    }
3281
3282
    /**
3283
     * Getting all active exercises from a course from a session
3284
     * (if a session_id is provided we will show all the exercises in the course +
3285
     * all exercises in the session).
3286
     *
3287
     * @param array  $course_info
3288
     * @param int    $session_id
3289
     * @param bool   $check_publication_dates
3290
     * @param string $search                  Search exercise name
3291
     * @param bool   $search_all_sessions     Search exercises in all sessions
3292
     * @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...
3293
     *                  1 = only active exercises,
3294
     *                  2 = all exercises
3295
     *                  3 = active <> -1
3296
     *
3297
     * @return CQuiz[]
3298
     */
3299
    public static function get_all_exercises(
3300
        $course_info = null,
3301
        $session_id = 0,
3302
        $check_publication_dates = false,
3303
        $search = '',
3304
        $search_all_sessions = false,
3305
        $active = 2
3306
    ) {
3307
        $course_id = api_get_course_int_id();
3308
        if (!empty($course_info) && !empty($course_info['real_id'])) {
3309
            $course_id = $course_info['real_id'];
3310
        }
3311
3312
        if (-1 == $session_id) {
3313
            $session_id = 0;
3314
        }
3315
        $course = api_get_course_entity($course_id);
3316
        $session = api_get_session_entity($session_id);
3317
3318
        if (null === $course) {
3319
            return [];
3320
        }
3321
3322
        $repo = Container::getQuizRepository();
3323
3324
        return $repo->findAllByCourse($course, $session, (string) $search, $active)
3325
            ->getQuery()
3326
            ->getResult();
3327
    }
3328
3329
    /**
3330
     * Getting all exercises (active only or all)
3331
     * from a course from a session
3332
     * (if a session_id is provided we will show all the exercises in the
3333
     * course + all exercises in the session).
3334
     */
3335
    public static function get_all_exercises_for_course_id(
3336
        int $courseId,
3337
        int $sessionId = 0,
3338
        bool $onlyActiveExercises = true
3339
    ): array {
3340
        if ($courseId <= 0) {
3341
            return [];
3342
        }
3343
3344
        $course  = api_get_course_entity($courseId);
3345
        $session = api_get_session_entity($sessionId);
3346
3347
        $repo = Container::getQuizRepository();
3348
3349
        $qb = $repo->getResourcesByCourse($course, $session);
3350
3351
        $qb->andWhere('resourceLink.endVisibilityAt IS NULL');
3352
3353
        if ($onlyActiveExercises) {
3354
            $qb->andWhere('resourceLink.visibility = 2');
3355
        } else {
3356
            $qb->andWhere('resourceLink.visibility IN (0,2)');
3357
        }
3358
3359
        $qb->orderBy('resource.title', 'ASC');
3360
3361
        $exercises = $qb->getQuery()->getResult();
3362
3363
        $exerciseList = [];
3364
        foreach ($exercises as $exercise) {
3365
            $exerciseList[] = [
3366
                'iid' => $exercise->getIid(),
3367
                'title' => $exercise->getTitle(),
3368
            ];
3369
        }
3370
3371
        return $exerciseList;
3372
    }
3373
3374
    /**
3375
     * Gets the position of the score based in a given score (result/weight)
3376
     * and the exe_id based in the user list
3377
     * (NO Exercises in LPs ).
3378
     *
3379
     * @param float  $my_score      user score to be compared *attention*
3380
     *                              $my_score = score/weight and not just the score
3381
     * @param int    $my_exe_id     exe id of the exercise
3382
     *                              (this is necessary because if 2 students have the same score the one
3383
     *                              with the minor exe_id will have a best position, just to be fair and FIFO)
3384
     * @param int    $exercise_id
3385
     * @param string $course_code
3386
     * @param int    $session_id
3387
     * @param array  $user_list
3388
     * @param bool   $return_string
3389
     *
3390
     * @return int the position of the user between his friends in a course
3391
     *             (or course within a session)
3392
     */
3393
    public static function get_exercise_result_ranking(
3394
        $my_score,
3395
        $my_exe_id,
3396
        $exercise_id,
3397
        $course_code,
3398
        $session_id = 0,
3399
        $user_list = [],
3400
        $return_string = true
3401
    ) {
3402
        //No score given we return
3403
        if (is_null($my_score)) {
3404
            return '-';
3405
        }
3406
        if (empty($user_list)) {
3407
            return '-';
3408
        }
3409
3410
        $best_attempts = [];
3411
        foreach ($user_list as $user_data) {
3412
            $user_id = $user_data['user_id'];
3413
            $best_attempts[$user_id] = self::get_best_attempt_by_user(
3414
                $user_id,
3415
                $exercise_id,
3416
                $course_code,
3417
                $session_id
3418
            );
3419
        }
3420
3421
        if (empty($best_attempts)) {
3422
            return 1;
3423
        } else {
3424
            $position = 1;
3425
            $my_ranking = [];
3426
            foreach ($best_attempts as $user_id => $result) {
3427
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3428
                    $my_ranking[$user_id] = $result['score'] / $result['max_score'];
3429
                } else {
3430
                    $my_ranking[$user_id] = 0;
3431
                }
3432
            }
3433
            //if (!empty($my_ranking)) {
3434
            asort($my_ranking);
3435
            $position = count($my_ranking);
3436
            if (!empty($my_ranking)) {
3437
                foreach ($my_ranking as $user_id => $ranking) {
3438
                    if ($my_score >= $ranking) {
3439
                        if ($my_score == $ranking && isset($best_attempts[$user_id]['exe_id'])) {
3440
                            $exe_id = $best_attempts[$user_id]['exe_id'];
3441
                            if ($my_exe_id < $exe_id) {
3442
                                $position--;
3443
                            }
3444
                        } else {
3445
                            $position--;
3446
                        }
3447
                    }
3448
                }
3449
            }
3450
            //}
3451
            $return_value = [
3452
                'position' => $position,
3453
                'count' => count($my_ranking),
3454
            ];
3455
3456
            if ($return_string) {
3457
                if (!empty($position) && !empty($my_ranking)) {
3458
                    $return_value = $position.'/'.count($my_ranking);
3459
                } else {
3460
                    $return_value = '-';
3461
                }
3462
            }
3463
3464
            return $return_value;
3465
        }
3466
    }
3467
3468
    /**
3469
     * Gets the position of the score based in a given score (result/weight) and the exe_id based in all attempts
3470
     * (NO Exercises in LPs ) old functionality by attempt.
3471
     *
3472
     * @param   float   user score to be compared attention => score/weight
3473
     * @param   int     exe id of the exercise
3474
     * (this is necessary because if 2 students have the same score the one
3475
     * with the minor exe_id will have a best position, just to be fair and FIFO)
3476
     * @param   int     exercise id
3477
     * @param   string  course code
3478
     * @param   int     session id
3479
     * @param bool $return_string
3480
     *
3481
     * @return int the position of the user between his friends in a course (or course within a session)
3482
     */
3483
    public static function get_exercise_result_ranking_by_attempt(
3484
        $my_score,
3485
        $my_exe_id,
3486
        $exercise_id,
3487
        $courseId,
3488
        $session_id = 0,
3489
        $return_string = true
3490
    ) {
3491
        if (empty($session_id)) {
3492
            $session_id = 0;
3493
        }
3494
        if (is_null($my_score)) {
3495
            return '-';
3496
        }
3497
        $user_results = Event::get_all_exercise_results(
3498
            $exercise_id,
3499
            $courseId,
3500
            $session_id,
3501
            false
3502
        );
3503
        $position_data = [];
3504
        if (empty($user_results)) {
3505
            return 1;
3506
        } else {
3507
            $position = 1;
3508
            $my_ranking = [];
3509
            foreach ($user_results as $result) {
3510
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3511
                    $my_ranking[$result['exe_id']] = $result['score'] / $result['max_score'];
3512
                } else {
3513
                    $my_ranking[$result['exe_id']] = 0;
3514
                }
3515
            }
3516
            asort($my_ranking);
3517
            $position = count($my_ranking);
3518
            if (!empty($my_ranking)) {
3519
                foreach ($my_ranking as $exe_id => $ranking) {
3520
                    if ($my_score >= $ranking) {
3521
                        if ($my_score == $ranking) {
3522
                            if ($my_exe_id < $exe_id) {
3523
                                $position--;
3524
                            }
3525
                        } else {
3526
                            $position--;
3527
                        }
3528
                    }
3529
                }
3530
            }
3531
            $return_value = [
3532
                'position' => $position,
3533
                'count' => count($my_ranking),
3534
            ];
3535
3536
            if ($return_string) {
3537
                if (!empty($position) && !empty($my_ranking)) {
3538
                    return $position.'/'.count($my_ranking);
3539
                }
3540
            }
3541
3542
            return $return_value;
3543
        }
3544
    }
3545
3546
    /**
3547
     * Get the best attempt in a exercise (NO Exercises in LPs ).
3548
     *
3549
     * @param int $exercise_id
3550
     * @param int $courseId
3551
     * @param int $session_id
3552
     *
3553
     * @return array
3554
     */
3555
    public static function get_best_attempt_in_course($exercise_id, $courseId, $session_id)
3556
    {
3557
        $user_results = Event::get_all_exercise_results(
3558
            $exercise_id,
3559
            $courseId,
3560
            $session_id,
3561
            false
3562
        );
3563
3564
        $best_score_data = [];
3565
        $best_score = 0;
3566
        if (!empty($user_results)) {
3567
            foreach ($user_results as $result) {
3568
                if (!empty($result['max_score']) &&
3569
                    0 != intval($result['max_score'])
3570
                ) {
3571
                    $score = $result['score'] / $result['max_score'];
3572
                    if ($score >= $best_score) {
3573
                        $best_score = $score;
3574
                        $best_score_data = $result;
3575
                    }
3576
                }
3577
            }
3578
        }
3579
3580
        return $best_score_data;
3581
    }
3582
3583
    /**
3584
     * Get the best score in a exercise (NO Exercises in LPs ).
3585
     *
3586
     * @param int $user_id
3587
     * @param int $exercise_id
3588
     * @param int $courseId
3589
     * @param int $session_id
3590
     *
3591
     * @return array
3592
     */
3593
    public static function get_best_attempt_by_user(
3594
        $user_id,
3595
        $exercise_id,
3596
        $courseId,
3597
        $session_id
3598
    ) {
3599
        $user_results = Event::get_all_exercise_results(
3600
            $exercise_id,
3601
            $courseId,
3602
            $session_id,
3603
            false,
3604
            $user_id
3605
        );
3606
        $best_score_data = [];
3607
        $best_score = 0;
3608
        if (!empty($user_results)) {
3609
            foreach ($user_results as $result) {
3610
                if (!empty($result['max_score']) && 0 != (float) $result['max_score']) {
3611
                    $score = $result['score'] / $result['max_score'];
3612
                    if ($score >= $best_score) {
3613
                        $best_score = $score;
3614
                        $best_score_data = $result;
3615
                    }
3616
                }
3617
            }
3618
        }
3619
3620
        return $best_score_data;
3621
    }
3622
3623
    /**
3624
     * Get average score (NO Exercises in LPs ).
3625
     *
3626
     * @param    int    exercise id
3627
     * @param int $courseId
3628
     * @param    int    session id
3629
     *
3630
     * @return float Average score
3631
     */
3632
    public static function get_average_score($exercise_id, $courseId, $session_id)
3633
    {
3634
        $user_results = Event::get_all_exercise_results(
3635
            $exercise_id,
3636
            $courseId,
3637
            $session_id
3638
        );
3639
        $avg_score = 0;
3640
        if (!empty($user_results)) {
3641
            foreach ($user_results as $result) {
3642
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3643
                    $score = $result['score'] / $result['max_score'];
3644
                    $avg_score += $score;
3645
                }
3646
            }
3647
            $avg_score = float_format($avg_score / count($user_results), 1);
3648
        }
3649
3650
        return $avg_score;
3651
    }
3652
3653
    /**
3654
     * Get average score by score (NO Exercises in LPs ).
3655
     *
3656
     * @param int $courseId
3657
     * @param    int    session id
3658
     *
3659
     * @return float Average score
3660
     */
3661
    public static function get_average_score_by_course($courseId, $session_id)
3662
    {
3663
        $user_results = Event::get_all_exercise_results_by_course(
3664
            $courseId,
3665
            $session_id,
3666
            false
3667
        );
3668
        $avg_score = 0;
3669
        if (!empty($user_results)) {
3670
            foreach ($user_results as $result) {
3671
                if (!empty($result['max_score']) && 0 != intval(
3672
                        $result['max_score']
3673
                    )
3674
                ) {
3675
                    $score = $result['score'] / $result['max_score'];
3676
                    $avg_score += $score;
3677
                }
3678
            }
3679
            // We assume that all max_score
3680
            $avg_score = $avg_score / count($user_results);
3681
        }
3682
3683
        return $avg_score;
3684
    }
3685
3686
    /**
3687
     * @param int $user_id
3688
     * @param int $courseId
3689
     * @param int $session_id
3690
     *
3691
     * @return float|int
3692
     */
3693
    public static function get_average_score_by_course_by_user(
3694
        $user_id,
3695
        $courseId,
3696
        $session_id
3697
    ) {
3698
        $user_results = Event::get_all_exercise_results_by_user(
3699
            $user_id,
3700
            $courseId,
3701
            $session_id
3702
        );
3703
        $avg_score = 0;
3704
        if (!empty($user_results)) {
3705
            foreach ($user_results as $result) {
3706
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3707
                    $score = $result['score'] / $result['max_score'];
3708
                    $avg_score += $score;
3709
                }
3710
            }
3711
            // We assume that all max_score
3712
            $avg_score = ($avg_score / count($user_results));
3713
        }
3714
3715
        return $avg_score;
3716
    }
3717
3718
    /**
3719
     * Get average score by score (NO Exercises in LPs ).
3720
     *
3721
     * @param int $exercise_id
3722
     * @param int $courseId
3723
     * @param int $session_id
3724
     * @param int $user_count
3725
     *
3726
     * @return float Best average score
3727
     */
3728
    public static function get_best_average_score_by_exercise(
3729
        $exercise_id,
3730
        $courseId,
3731
        $session_id,
3732
        $user_count
3733
    ) {
3734
        $user_results = Event::get_best_exercise_results_by_user(
3735
            $exercise_id,
3736
            $courseId,
3737
            $session_id
3738
        );
3739
        $avg_score = 0;
3740
        if (!empty($user_results)) {
3741
            foreach ($user_results as $result) {
3742
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3743
                    $score = $result['score'] / $result['max_score'];
3744
                    $avg_score += $score;
3745
                }
3746
            }
3747
            // We asumme that all max_score
3748
            if (!empty($user_count)) {
3749
                $avg_score = float_format($avg_score / $user_count, 1) * 100;
3750
            } else {
3751
                $avg_score = 0;
3752
            }
3753
        }
3754
3755
        return $avg_score;
3756
    }
3757
3758
    /**
3759
     * Get average score by score (NO Exercises in LPs ).
3760
     *
3761
     * @param int $exercise_id
3762
     * @param int $courseId
3763
     * @param int $session_id
3764
     *
3765
     * @return float Best average score
3766
     */
3767
    public static function getBestScoreByExercise(
3768
        $exercise_id,
3769
        $courseId,
3770
        $session_id
3771
    ) {
3772
        $user_results = Event::get_best_exercise_results_by_user(
3773
            $exercise_id,
3774
            $courseId,
3775
            $session_id
3776
        );
3777
        $avg_score = 0;
3778
        if (!empty($user_results)) {
3779
            foreach ($user_results as $result) {
3780
                if (!empty($result['max_score']) && 0 != intval($result['max_score'])) {
3781
                    $score = $result['score'] / $result['max_score'];
3782
                    $avg_score += $score;
3783
                }
3784
            }
3785
        }
3786
3787
        return $avg_score;
3788
    }
3789
3790
    /**
3791
     * Get student results (only in completed exercises) stats by question.
3792
     *
3793
     * @throws \Doctrine\DBAL\Exception
3794
     */
3795
    public static function getStudentStatsByQuestion(
3796
        int $questionId,
3797
        int $exerciseId,
3798
        string $courseCode,
3799
        int $sessionId,
3800
        bool $onlyStudent = false
3801
    ): array
3802
    {
3803
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3804
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3805
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3806
3807
        $questionId = (int) $questionId;
3808
        $exerciseId = (int) $exerciseId;
3809
        $courseCode = Database::escape_string($courseCode);
3810
        $sessionId = (int) $sessionId;
3811
        $courseId = api_get_course_int_id($courseCode);
3812
3813
        $sql = "SELECT MAX(marks) as max, MIN(marks) as min, AVG(marks) as average
3814
                FROM $trackExercises e ";
3815
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
3816
        if ($onlyStudent) {
3817
            $courseCondition = '';
3818
            if (empty($sessionId)) {
3819
                $courseCondition = "
3820
                INNER JOIN $courseUser c
3821
                ON (
3822
                    e.exe_user_id = c.user_id AND
3823
                    e.c_id = c.c_id AND
3824
                    c.status = ".STUDENT." AND
3825
                    relation_type <> 2
3826
                )";
3827
            } else {
3828
                $sessionRelCourse = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3829
                $courseCondition = "
3830
            INNER JOIN $sessionRelCourse sc
3831
            ON (
3832
                        e.exe_user_id = sc.user_id AND
3833
                        e.c_id = sc.c_id AND
3834
                        e.session_id = sc.session_id AND
3835
                        sc.status = ".SessionEntity::STUDENT."
3836
                )";
3837
            }
3838
            $sql .= $courseCondition;
3839
        }
3840
        $sql .= "
3841
    		INNER JOIN $trackAttempt a
3842
    		ON (
3843
    		    a.exe_id = e.exe_id
3844
            )
3845
    		WHERE
3846
    		    exe_exo_id 	= $exerciseId AND
3847
                e.c_id = $courseId AND
3848
                question_id = $questionId AND
3849
                e.status = ''
3850
                $sessionCondition
3851
            LIMIT 1";
3852
        $result = Database::query($sql);
3853
        $return = [];
3854
        if ($result) {
3855
            $return = Database::fetch_assoc($result);
3856
        }
3857
3858
        return $return;
3859
    }
3860
3861
    /**
3862
     * Get the correct answer count for a fill blanks question.
3863
     *
3864
     * @param int $question_id
3865
     * @param int $exercise_id
3866
     *
3867
     * @return array
3868
     */
3869
    public static function getNumberStudentsFillBlanksAnswerCount(
3870
        $question_id,
3871
        $exercise_id
3872
    ) {
3873
        $listStudentsId = [];
3874
        $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
3875
            api_get_course_id(),
3876
            true
3877
        );
3878
        foreach ($listAllStudentInfo as $i => $listStudentInfo) {
3879
            $listStudentsId[] = $listStudentInfo['user_id'];
3880
        }
3881
3882
        $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3883
            $exercise_id,
3884
            $question_id,
3885
            $listStudentsId,
3886
            '1970-01-01',
3887
            '3000-01-01'
3888
        );
3889
3890
        $arrayCount = [];
3891
3892
        foreach ($listFillTheBlankResult as $resultCount) {
3893
            foreach ($resultCount as $index => $count) {
3894
                //this is only for declare the array index per answer
3895
                $arrayCount[$index] = 0;
3896
            }
3897
        }
3898
3899
        foreach ($listFillTheBlankResult as $resultCount) {
3900
            foreach ($resultCount as $index => $count) {
3901
                $count = (0 === $count) ? 1 : 0;
3902
                $arrayCount[$index] += $count;
3903
            }
3904
        }
3905
3906
        return $arrayCount;
3907
    }
3908
3909
    /**
3910
     * Get the number of questions with answers.
3911
     *
3912
     * @param int    $question_id
3913
     * @param int    $exercise_id
3914
     * @param string $course_code
3915
     * @param int    $session_id
3916
     * @param string $questionType
3917
     *
3918
     * @return int
3919
     */
3920
    public static function get_number_students_question_with_answer_count(
3921
        $question_id,
3922
        $exercise_id,
3923
        $course_code,
3924
        $session_id,
3925
        $questionType = ''
3926
    ) {
3927
        $track_exercises   = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
3928
        $track_attempt     = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
3929
        $courseUser        = Database::get_main_table(TABLE_MAIN_COURSE_USER);
3930
        $courseTable       = Database::get_main_table(TABLE_MAIN_COURSE);
3931
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
3932
3933
        $question_id = (int) $question_id;
3934
        $exercise_id = (int) $exercise_id;
3935
        $courseId    = (int) api_get_course_int_id($course_code);
3936
        $session_id  = (int) $session_id;
3937
3938
        if (in_array($questionType, [FILL_IN_BLANKS, FILL_IN_BLANKS_COMBINATION], true)) {
3939
            $listStudentsId     = [];
3940
            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(api_get_course_id(), true);
3941
            foreach ($listAllStudentInfo as $listStudentInfo) {
3942
                $listStudentsId[] = (int) $listStudentInfo['user_id'];
3943
            }
3944
3945
            $listFillTheBlankResult = FillBlanks::getFillTheBlankResult(
3946
                $exercise_id,
3947
                $question_id,
3948
                $listStudentsId,
3949
                '1970-01-01',
3950
                '3000-01-01'
3951
            );
3952
3953
            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
3954
        }
3955
3956
        if (empty($session_id)) {
3957
            $courseCondition = "
3958
            INNER JOIN $courseUser cu
3959
                ON cu.c_id = c.id AND cu.user_id = exe_user_id";
3960
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
3961
        } else {
3962
            $courseCondition = "
3963
            INNER JOIN $courseUserSession cu
3964
                ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
3965
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
3966
        }
3967
3968
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
3969
        $sql = "SELECT DISTINCT exe_user_id
3970
            FROM $track_exercises e
3971
            INNER JOIN $track_attempt a
3972
                ON (a.exe_id = e.exe_id AND a.c_id = e.c_id)
3973
            INNER JOIN $courseTable c
3974
                ON c.id = e.c_id
3975
            $courseCondition
3976
            WHERE
3977
                exe_exo_id  = $exercise_id AND
3978
                e.c_id      = $courseId AND
3979
                question_id = $question_id AND
3980
                answer <> '0' AND
3981
                e.status = ''
3982
                $courseConditionWhere
3983
                $sessionCondition
3984
    ";
3985
3986
        $result = Database::query($sql);
3987
3988
        return $result ? (int) Database::num_rows($result) : 0;
3989
    }
3990
3991
    /**
3992
     * Get number of answers to hotspot questions.
3993
     */
3994
    public static function getNumberStudentsAnswerHotspotCount(
3995
        int    $answerId,
3996
        int    $questionId,
3997
        int    $exerciseId,
3998
        string $courseCode,
3999
        int $sessionId
4000
    ): int
4001
    {
4002
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4003
        $trackHotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
4004
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4005
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4006
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4007
4008
        $questionId = (int) $questionId;
4009
        $answerId = (int) $answerId;
4010
        $exerciseId = (int) $exerciseId;
4011
        $courseId = api_get_course_int_id($courseCode);
4012
        $sessionId = (int) $sessionId;
4013
4014
        if (empty($sessionId)) {
4015
            $courseCondition = "
4016
            INNER JOIN $courseUser cu
4017
            ON cu.c_id = c.id AND cu.user_id  = exe_user_id";
4018
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4019
        } else {
4020
            $courseCondition = "
4021
            INNER JOIN $courseUserSession cu
4022
            ON (cu.c_id = c.id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4023
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4024
        }
4025
4026
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
4027
        $sql = "SELECT DISTINCT exe_user_id
4028
                FROM $trackExercises e
4029
                INNER JOIN $trackHotspot a
4030
                ON (a.hotspot_exe_id = e.exe_id)
4031
                INNER JOIN $courseTable c
4032
                ON (a.c_id = c.id)
4033
                $courseCondition
4034
                WHERE
4035
                    exe_exo_id              = $exerciseId AND
4036
                    a.c_id 	= $courseId AND
4037
                    hotspot_answer_id       = $answerId AND
4038
                    hotspot_question_id     = $questionId AND
4039
                    hotspot_correct         =  1 AND
4040
                    e.status                = ''
4041
                    $courseConditionWhere
4042
                    $sessionCondition
4043
            ";
4044
        $result = Database::query($sql);
4045
        $return = 0;
4046
        if ($result) {
4047
            $return = Database::num_rows($result);
4048
        }
4049
4050
        return $return;
4051
    }
4052
4053
    /**
4054
     * @param int    $answer_id
4055
     * @param int    $question_id
4056
     * @param int    $exercise_id
4057
     * @param string $course_code
4058
     * @param int    $session_id
4059
     * @param string $question_type
4060
     * @param string $correct_answer
4061
     * @param string $current_answer
4062
     *
4063
     * @return int
4064
     */
4065
    public static function get_number_students_answer_count(
4066
        $answer_id,
4067
        $question_id,
4068
        $exercise_id,
4069
        $course_code,
4070
        $session_id,
4071
        $question_type = null,
4072
        $correct_answer = null,
4073
        $current_answer = null
4074
    ) {
4075
        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4076
        $track_attempt   = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4077
        $courseTable     = Database::get_main_table(TABLE_MAIN_COURSE);
4078
        $courseUser      = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4079
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4080
4081
        $question_id = (int) $question_id;
4082
        $answer_id   = (int) $answer_id;
4083
        $exercise_id = (int) $exercise_id;
4084
        $courseId    = (int) api_get_course_int_id($course_code);
4085
        $session_id  = (int) $session_id;
4086
4087
        switch ($question_type) {
4088
            case FILL_IN_BLANKS:
4089
            case FILL_IN_BLANKS_COMBINATION:
4090
                $answer_condition = '';
4091
                $select_condition = ' e.exe_id, answer ';
4092
                break;
4093
            case MATCHING:
4094
            case MATCHING_COMBINATION:
4095
            case MATCHING_DRAGGABLE:
4096
            case MATCHING_DRAGGABLE_COMBINATION:
4097
            default:
4098
                $answer_condition = " answer = $answer_id AND ";
4099
                $select_condition = ' DISTINCT exe_user_id ';
4100
        }
4101
4102
        if (empty($session_id)) {
4103
            $courseCondition = "
4104
            INNER JOIN $courseUser cu
4105
                ON cu.c_id = e.c_id AND cu.user_id = e.exe_user_id";
4106
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4107
        } else {
4108
            $courseCondition = "
4109
            INNER JOIN $courseUserSession cu
4110
                ON (cu.c_id = e.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4111
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4112
        }
4113
4114
        $sessionCondition = api_get_session_condition($session_id, true, false, 'e.session_id');
4115
        $sql = "SELECT $select_condition
4116
            FROM $track_exercises e
4117
            INNER JOIN $track_attempt a
4118
                ON (a.exe_id = e.exe_id AND a.c_id = e.c_id)
4119
            INNER JOIN $courseTable c
4120
                ON c.id = e.c_id
4121
            $courseCondition
4122
            WHERE
4123
                exe_exo_id = $exercise_id AND
4124
                e.c_id = $courseId AND
4125
                $answer_condition
4126
                question_id = $question_id AND
4127
                e.status = ''
4128
                $courseConditionWhere
4129
                $sessionCondition
4130
    ";
4131
4132
        $result = Database::query($sql);
4133
        $return = 0;
4134
        if ($result) {
4135
            switch ($question_type) {
4136
                case FILL_IN_BLANKS:
4137
                case FILL_IN_BLANKS_COMBINATION:
4138
                    $good_answers = 0;
4139
                    while ($row = Database::fetch_assoc($result)) {
4140
                        $fill_blank = self::check_fill_in_blanks(
4141
                            $correct_answer,
4142
                            $row['answer'],
4143
                            $current_answer
4144
                        );
4145
                        if (isset($fill_blank[$current_answer]) && 1 == (int) $fill_blank[$current_answer]) {
4146
                            $good_answers++;
4147
                        }
4148
                    }
4149
4150
                    return $good_answers;
4151
4152
                case MATCHING:
4153
                case MATCHING_COMBINATION:
4154
                case MATCHING_DRAGGABLE:
4155
                case MATCHING_DRAGGABLE_COMBINATION:
4156
                default:
4157
                    $return = Database::num_rows($result);
4158
            }
4159
        }
4160
4161
        return $return;
4162
    }
4163
4164
    /**
4165
     * Get the number of times an answer was selected.
4166
     */
4167
    public static function getCountOfAnswers(
4168
        int $answerId,
4169
        int $questionId,
4170
        int $exerciseId,
4171
        string $courseCode,
4172
        int $sessionId,
4173
        $questionType = null,
4174
    ): int
4175
    {
4176
        $trackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
4177
        $trackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
4178
        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
4179
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
4180
        $courseUserSession = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
4181
4182
        $answerId = (int) $answerId;
4183
        $questionId = (int) $questionId;
4184
        $exerciseId = (int) $exerciseId;
4185
        $courseId = api_get_course_int_id($courseCode);
4186
        $sessionId = (int) $sessionId;
4187
        $return = 0;
4188
4189
        $answerCondition = match ($questionType) {
4190
            FILL_IN_BLANKS => '',
4191
            default => " answer = $answerId AND ",
4192
        };
4193
4194
        if (empty($sessionId)) {
4195
            $courseCondition = "
4196
            INNER JOIN $courseUser cu
4197
            ON cu.c_id = c.id AND cu.user_id = exe_user_id";
4198
            $courseConditionWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
4199
        } else {
4200
            $courseCondition = "
4201
            INNER JOIN $courseUserSession cu
4202
            ON (cu.c_id = a.c_id AND cu.user_id = e.exe_user_id AND e.session_id = cu.session_id)";
4203
            $courseConditionWhere = ' AND cu.status = '.SessionEntity::STUDENT;
4204
        }
4205
4206
        $sessionCondition = api_get_session_condition($sessionId, true, false, 'e.session_id');
4207
        $sql = "SELECT count(a.answer) as total
4208
                FROM $trackExercises e
4209
                INNER JOIN $trackAttempt a
4210
                ON (
4211
                    a.exe_id = e.exe_id
4212
                )
4213
                INNER JOIN $courseTable c
4214
                ON c.id = e.c_id
4215
                $courseCondition
4216
                WHERE
4217
                    exe_exo_id = $exerciseId AND
4218
                    e.c_id = $courseId AND
4219
                    $answerCondition
4220
                    question_id = $questionId AND
4221
                    e.status = ''
4222
                    $courseConditionWhere
4223
                    $sessionCondition
4224
            ";
4225
        $result = Database::query($sql);
4226
        if ($result) {
4227
            $count = Database::fetch_array($result);
4228
            $return = (int) $count['total'];
4229
        }
4230
        return $return;
4231
    }
4232
4233
    /**
4234
     * @param array  $answer
4235
     * @param string $user_answer
4236
     *
4237
     * @return array
4238
     */
4239
    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
4240
    {
4241
        // the question is encoded like this
4242
        // [A] B [C] D [E] F::10,10,10@1
4243
        // number 1 before the "@" means that is a switchable fill in blank question
4244
        // [A] B [C] D [E] F::10,10,10@ or  [A] B [C] D [E] F::10,10,10
4245
        // means that is a normal fill blank question
4246
        // first we explode the "::"
4247
        $pre_array = explode('::', $answer);
4248
        // is switchable fill blank or not
4249
        $last = count($pre_array) - 1;
4250
        $is_set_switchable = explode('@', $pre_array[$last]);
4251
        $switchable_answer_set = false;
4252
        if (isset($is_set_switchable[1]) && 1 == $is_set_switchable[1]) {
4253
            $switchable_answer_set = true;
4254
        }
4255
        $answer = '';
4256
        for ($k = 0; $k < $last; $k++) {
4257
            $answer .= $pre_array[$k];
4258
        }
4259
        // splits weightings that are joined with a comma
4260
        $answerWeighting = explode(',', $is_set_switchable[0]);
4261
4262
        // we save the answer because it will be modified
4263
        //$temp = $answer;
4264
        $temp = $answer;
4265
4266
        $answer = '';
4267
        $j = 0;
4268
        //initialise answer tags
4269
        $user_tags = $correct_tags = $real_text = [];
4270
        // the loop will stop at the end of the text
4271
        while (1) {
4272
            // quits the loop if there are no more blanks (detect '[')
4273
            if (false === ($pos = api_strpos($temp, '['))) {
4274
                // adds the end of the text
4275
                $answer = $temp;
4276
                $real_text[] = $answer;
4277
                break; //no more "blanks", quit the loop
4278
            }
4279
            // adds the piece of text that is before the blank
4280
            //and ends with '[' into a general storage array
4281
            $real_text[] = api_substr($temp, 0, $pos + 1);
4282
            $answer .= api_substr($temp, 0, $pos + 1);
4283
            //take the string remaining (after the last "[" we found)
4284
            $temp = api_substr($temp, $pos + 1);
4285
            // quit the loop if there are no more blanks, and update $pos to the position of next ']'
4286
            if (false === ($pos = api_strpos($temp, ']'))) {
4287
                // adds the end of the text
4288
                $answer .= $temp;
4289
                break;
4290
            }
4291
4292
            $str = $user_answer;
4293
4294
            preg_match_all('#\[([^[]*)\]#', $str, $arr);
4295
            $str = str_replace('\r\n', '', $str);
4296
            $choices = $arr[1];
4297
            $choice = [];
4298
            $check = false;
4299
            $i = 0;
4300
            foreach ($choices as $item) {
4301
                if ($current_answer === $item) {
4302
                    $check = true;
4303
                }
4304
                if ($check) {
4305
                    $choice[] = $item;
4306
                    $i++;
4307
                }
4308
                if (3 == $i) {
4309
                    break;
4310
                }
4311
            }
4312
            $tmp = api_strrpos($choice[$j], ' / ');
4313
4314
            if (false !== $tmp) {
4315
                $choice[$j] = api_substr($choice[$j], 0, $tmp);
4316
            }
4317
4318
            $choice[$j] = trim($choice[$j]);
4319
4320
            //Needed to let characters ' and " to work as part of an answer
4321
            $choice[$j] = stripslashes($choice[$j]);
4322
4323
            $user_tags[] = api_strtolower($choice[$j]);
4324
            //put the contents of the [] answer tag into correct_tags[]
4325
            $correct_tags[] = api_strtolower(api_substr($temp, 0, $pos));
4326
            $j++;
4327
            $temp = api_substr($temp, $pos + 1);
4328
        }
4329
4330
        $answer = '';
4331
        $real_correct_tags = $correct_tags;
4332
        $chosen_list = [];
4333
        $good_answer = [];
4334
4335
        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...
4336
            if (!$switchable_answer_set) {
4337
                //needed to parse ' and " characters
4338
                $user_tags[$i] = stripslashes($user_tags[$i]);
4339
                if ($correct_tags[$i] == $user_tags[$i]) {
4340
                    $good_answer[$correct_tags[$i]] = 1;
4341
                } elseif (!empty($user_tags[$i])) {
4342
                    $good_answer[$correct_tags[$i]] = 0;
4343
                } else {
4344
                    $good_answer[$correct_tags[$i]] = 0;
4345
                }
4346
            } else {
4347
                // switchable fill in the blanks
4348
                if (in_array($user_tags[$i], $correct_tags)) {
4349
                    $correct_tags = array_diff($correct_tags, $chosen_list);
4350
                    $good_answer[$correct_tags[$i]] = 1;
4351
                } elseif (!empty($user_tags[$i])) {
4352
                    $good_answer[$correct_tags[$i]] = 0;
4353
                } else {
4354
                    $good_answer[$correct_tags[$i]] = 0;
4355
                }
4356
            }
4357
            // adds the correct word, followed by ] to close the blank
4358
            $answer .= ' / <font color="green"><b>'.$real_correct_tags[$i].'</b></font>]';
4359
            if (isset($real_text[$i + 1])) {
4360
                $answer .= $real_text[$i + 1];
4361
            }
4362
        }
4363
4364
        return $good_answer;
4365
    }
4366
4367
    /**
4368
     * Return an HTML select menu with the student groups.
4369
     *
4370
     * @param string $name     is the name and the id of the <select>
4371
     * @param string $default  default value for option
4372
     * @param string $onchange
4373
     *
4374
     * @return string the html code of the <select>
4375
     */
4376
    public static function displayGroupMenu($name, $default, $onchange = "")
4377
    {
4378
        // check the default value of option
4379
        $tabSelected = [$default => " selected='selected' "];
4380
        $res = "<select name='$name' id='$name' onchange='".$onchange."' >";
4381
        $res .= "<option value='-1'".$tabSelected["-1"].">-- ".get_lang('All groups')." --</option>";
4382
        $res .= "<option value='0'".$tabSelected["0"].">- ".get_lang('Not in a group')." -</option>";
4383
        $groups = GroupManager::get_group_list();
4384
        $currentCatId = 0;
4385
        $countGroups = count($groups);
4386
        for ($i = 0; $i < $countGroups; $i++) {
4387
            $category = GroupManager::get_category_from_group($groups[$i]['iid']);
4388
            if ($category['id'] != $currentCatId) {
4389
                $res .= "<option value='-1' disabled='disabled'>".$category['title']."</option>";
4390
                $currentCatId = $category['id'];
4391
            }
4392
            $res .= "<option ".$tabSelected[$groups[$i]['id']]."style='margin-left:40px' value='".
4393
                $groups[$i]["iid"]."'>".
4394
                $groups[$i]["name"].
4395
                "</option>";
4396
        }
4397
        $res .= "</select>";
4398
4399
        return $res;
4400
    }
4401
4402
    /**
4403
     * @param int $exe_id
4404
     */
4405
    public static function create_chat_exercise_session($exe_id)
4406
    {
4407
        if (!isset($_SESSION['current_exercises'])) {
4408
            $_SESSION['current_exercises'] = [];
4409
        }
4410
        $_SESSION['current_exercises'][$exe_id] = true;
4411
    }
4412
4413
    /**
4414
     * @param int $exe_id
4415
     */
4416
    public static function delete_chat_exercise_session($exe_id)
4417
    {
4418
        if (isset($_SESSION['current_exercises'])) {
4419
            $_SESSION['current_exercises'][$exe_id] = false;
4420
        }
4421
    }
4422
4423
    /**
4424
     * Display the exercise results.
4425
     *
4426
     * @param Exercise $objExercise
4427
     * @param int      $exeId
4428
     * @param bool     $save_user_result save users results (true) or just show the results (false)
4429
     * @param string   $remainingMessage
4430
     * @param bool     $allowSignature
4431
     * @param bool     $allowExportPdf
4432
     * @param bool     $isExport
4433
     */
4434
    public static function displayQuestionListByAttempt(
4435
        $objExercise,
4436
        $exeId,
4437
        $save_user_result = false,
4438
        $remainingMessage = '',
4439
        $allowSignature = false,
4440
        $allowExportPdf = false,
4441
        $isExport = false
4442
    ) {
4443
        $origin = api_get_origin();
4444
        $courseId = api_get_course_int_id();
4445
        $courseCode = api_get_course_id();
4446
        $sessionId = api_get_session_id();
4447
4448
        // Getting attempt info
4449
        $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
4450
4451
        // Getting question list
4452
        $question_list = [];
4453
        $studentInfo = [];
4454
        if (!empty($exercise_stat_info['data_tracking'])) {
4455
            $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
4456
            $question_list = explode(',', $exercise_stat_info['data_tracking']);
4457
        } else {
4458
            // Try getting the question list only if save result is off
4459
            if (false == $save_user_result) {
4460
                $question_list = $objExercise->get_validated_question_list();
4461
            }
4462
            if (in_array(
4463
                $objExercise->getFeedbackType(),
4464
                [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4465
            )) {
4466
                $question_list = $objExercise->get_validated_question_list();
4467
            }
4468
        }
4469
4470
        if ($objExercise->getResultAccess()) {
4471
            if (false === $objExercise->hasResultsAccess($exercise_stat_info)) {
4472
                echo Display::return_message(
4473
                    sprintf(get_lang('You have passed the %s minutes limit to see the results.'), $objExercise->getResultsAccess())
4474
                );
4475
4476
                return false;
4477
            }
4478
4479
            if (!empty($objExercise->getResultAccess())) {
4480
                $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id;
4481
                echo $objExercise->returnTimeLeftDiv();
4482
                echo $objExercise->showSimpleTimeControl(
4483
                    $objExercise->getResultAccessTimeDiff($exercise_stat_info),
4484
                    $url
4485
                );
4486
            }
4487
        }
4488
4489
        $counter = 1;
4490
        $total_score = $total_weight = 0;
4491
        $exerciseContent = null;
4492
4493
        // Hide results
4494
        $show_results = false;
4495
        $show_only_score = false;
4496
        if (in_array($objExercise->results_disabled,
4497
            [
4498
                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
4499
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
4500
                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
4501
            ]
4502
        )) {
4503
            $show_results = true;
4504
        }
4505
4506
        if (in_array(
4507
            $objExercise->results_disabled,
4508
            [
4509
                RESULT_DISABLE_SHOW_SCORE_ONLY,
4510
                RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
4511
                RESULT_DISABLE_RANKING,
4512
            ]
4513
        )
4514
        ) {
4515
            $show_only_score = true;
4516
        }
4517
4518
        // Not display expected answer, but score, and feedback
4519
        $show_all_but_expected_answer = false;
4520
        if (RESULT_DISABLE_SHOW_SCORE_ONLY == $objExercise->results_disabled &&
4521
            EXERCISE_FEEDBACK_TYPE_END == $objExercise->getFeedbackType()
4522
        ) {
4523
            $show_all_but_expected_answer = true;
4524
            $show_results = true;
4525
            $show_only_score = false;
4526
        }
4527
4528
        $showTotalScoreAndUserChoicesInLastAttempt = true;
4529
        $showTotalScore = true;
4530
        $showQuestionScore = true;
4531
        $attemptResult = [];
4532
4533
        if (in_array(
4534
            $objExercise->results_disabled,
4535
            [
4536
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
4537
                RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
4538
                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
4539
            ])
4540
        ) {
4541
            $show_only_score = true;
4542
            $show_results = true;
4543
            $numberAttempts = 0;
4544
            if ($objExercise->attempts > 0) {
4545
                $attempts = Event::getExerciseResultsByUser(
4546
                    api_get_user_id(),
4547
                    $objExercise->id,
4548
                    $courseId,
4549
                    $sessionId,
4550
                    $exercise_stat_info['orig_lp_id'],
4551
                    $exercise_stat_info['orig_lp_item_id'],
4552
                    'desc'
4553
                );
4554
                if ($attempts) {
4555
                    $numberAttempts = count($attempts);
4556
                }
4557
4558
                if ($save_user_result) {
4559
                    $numberAttempts++;
4560
                }
4561
4562
                $showTotalScore = false;
4563
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT == $objExercise->results_disabled) {
4564
                    $showTotalScore = true;
4565
                }
4566
                $showTotalScoreAndUserChoicesInLastAttempt = false;
4567
                if ($numberAttempts >= $objExercise->attempts) {
4568
                    $showTotalScore = true;
4569
                    $show_results = true;
4570
                    $show_only_score = false;
4571
                    $showTotalScoreAndUserChoicesInLastAttempt = true;
4572
                }
4573
4574
                if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $objExercise->results_disabled) {
4575
                    $showTotalScore = true;
4576
                    $show_results = true;
4577
                    $show_only_score = false;
4578
                    $showTotalScoreAndUserChoicesInLastAttempt = false;
4579
                    if ($numberAttempts >= $objExercise->attempts) {
4580
                        $showTotalScoreAndUserChoicesInLastAttempt = true;
4581
                    }
4582
4583
                    // Check if the current attempt is the last.
4584
                    if (false === $save_user_result && !empty($attempts)) {
4585
                        $showTotalScoreAndUserChoicesInLastAttempt = false;
4586
                        $position = 1;
4587
                        foreach ($attempts as $attempt) {
4588
                            if ($exeId == $attempt['exe_id']) {
4589
                                break;
4590
                            }
4591
                            $position++;
4592
                        }
4593
4594
                        if ($position == $objExercise->attempts) {
4595
                            $showTotalScoreAndUserChoicesInLastAttempt = true;
4596
                        }
4597
                    }
4598
                }
4599
            }
4600
4601
            if (RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK ==
4602
                $objExercise->results_disabled
4603
            ) {
4604
                $show_only_score = false;
4605
                $show_results = true;
4606
                $show_all_but_expected_answer = false;
4607
                $showTotalScore = false;
4608
                $showQuestionScore = false;
4609
                if ($numberAttempts >= $objExercise->attempts) {
4610
                    $showTotalScore = true;
4611
                    $showQuestionScore = true;
4612
                }
4613
            }
4614
        }
4615
4616
        // When exporting to PDF hide feedback/comment/score show warning in hotspot.
4617
        if ($allowExportPdf && $isExport) {
4618
            $showTotalScore = false;
4619
            $showQuestionScore = false;
4620
            $objExercise->feedback_type = 2;
4621
            $objExercise->hideComment = true;
4622
            $objExercise->hideNoAnswer = true;
4623
            $objExercise->results_disabled = 0;
4624
            $objExercise->hideExpectedAnswer = true;
4625
            $show_results = true;
4626
        }
4627
4628
        if ('embeddable' !== $origin &&
4629
            !empty($exercise_stat_info['exe_user_id']) &&
4630
            !empty($studentInfo)
4631
        ) {
4632
            // Shows exercise header.
4633
            echo $objExercise->showExerciseResultHeader(
4634
                $studentInfo,
4635
                $exercise_stat_info,
4636
                $save_user_result,
4637
                $allowSignature,
4638
                $allowExportPdf
4639
            );
4640
        }
4641
4642
        $question_list_answers = [];
4643
        $category_list = [];
4644
        $loadChoiceFromSession = false;
4645
        $fromDatabase = true;
4646
        $exerciseResult = null;
4647
        $exerciseResultCoordinates = null;
4648
        $delineationResults = null;
4649
        if (true === $save_user_result && in_array(
4650
            $objExercise->getFeedbackType(),
4651
            [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
4652
        )) {
4653
            $loadChoiceFromSession = true;
4654
            $fromDatabase = false;
4655
            $exerciseResult = Session::read('exerciseResult');
4656
            $exerciseResultCoordinates = Session::read('exerciseResultCoordinates');
4657
            $delineationResults = Session::read('hotspot_delineation_result');
4658
            $delineationResults = isset($delineationResults[$objExercise->id]) ? $delineationResults[$objExercise->id] : null;
4659
        }
4660
4661
        $countPendingQuestions = 0;
4662
        $result = [];
4663
        $panelsByParent = [];
4664
        // Loop over all question to show results for each of them, one by one
4665
        if (!empty($question_list)) {
4666
            foreach ($question_list as $questionId) {
4667
                // Creates a temporary Question object
4668
                $objQuestionTmp = Question::read($questionId, $objExercise->course);
4669
                // This variable came from exercise_submit_modal.php
4670
                ob_start();
4671
                $choice = null;
4672
                $delineationChoice = null;
4673
                if ($loadChoiceFromSession) {
4674
                    $choice = isset($exerciseResult[$questionId]) ? $exerciseResult[$questionId] : null;
4675
                    $delineationChoice = isset($delineationResults[$questionId]) ? $delineationResults[$questionId] : null;
4676
                }
4677
4678
                // We're inside *one* question. Go through each possible answer for this question
4679
                $result = $objExercise->manage_answer(
4680
                    $exeId,
4681
                    $questionId,
4682
                    $choice,
4683
                    'exercise_result',
4684
                    $exerciseResultCoordinates,
4685
                    $save_user_result,
4686
                    $fromDatabase,
4687
                    $show_results,
4688
                    $objExercise->selectPropagateNeg(),
4689
                    $delineationChoice,
4690
                    $showTotalScoreAndUserChoicesInLastAttempt
4691
                );
4692
4693
                if (empty($result)) {
4694
                    continue;
4695
                }
4696
4697
                $total_score += $result['score'];
4698
                $total_weight += $result['weight'];
4699
4700
                $question_list_answers[] = [
4701
                    'question' => $result['open_question'],
4702
                    'answer' => $result['open_answer'],
4703
                    'answer_type' => $result['answer_type'],
4704
                    'generated_oral_file' => $result['generated_oral_file'],
4705
                ];
4706
4707
                $my_total_score = $result['score'];
4708
                $my_total_weight = $result['weight'];
4709
                $scorePassed = self::scorePassed($my_total_score, $my_total_weight);
4710
4711
                // Category report
4712
                $category_was_added_for_this_test = false;
4713
                if (isset($objQuestionTmp->category) && !empty($objQuestionTmp->category)) {
4714
                    if (!isset($category_list[$objQuestionTmp->category]['score'])) {
4715
                        $category_list[$objQuestionTmp->category]['score'] = 0;
4716
                    }
4717
                    if (!isset($category_list[$objQuestionTmp->category]['total'])) {
4718
                        $category_list[$objQuestionTmp->category]['total'] = 0;
4719
                    }
4720
                    if (!isset($category_list[$objQuestionTmp->category]['total_questions'])) {
4721
                        $category_list[$objQuestionTmp->category]['total_questions'] = 0;
4722
                    }
4723
                    if (!isset($category_list[$objQuestionTmp->category]['passed'])) {
4724
                        $category_list[$objQuestionTmp->category]['passed'] = 0;
4725
                    }
4726
                    if (!isset($category_list[$objQuestionTmp->category]['wrong'])) {
4727
                        $category_list[$objQuestionTmp->category]['wrong'] = 0;
4728
                    }
4729
                    if (!isset($category_list[$objQuestionTmp->category]['no_answer'])) {
4730
                        $category_list[$objQuestionTmp->category]['no_answer'] = 0;
4731
                    }
4732
4733
                    $category_list[$objQuestionTmp->category]['score'] += $my_total_score;
4734
                    $category_list[$objQuestionTmp->category]['total'] += $my_total_weight;
4735
                    if ($scorePassed) {
4736
                        // Only count passed if score is not empty
4737
                        if (!empty($my_total_score)) {
4738
                            $category_list[$objQuestionTmp->category]['passed']++;
4739
                        }
4740
                    } else {
4741
                        if ($result['user_answered']) {
4742
                            $category_list[$objQuestionTmp->category]['wrong']++;
4743
                        } else {
4744
                            $category_list[$objQuestionTmp->category]['no_answer']++;
4745
                        }
4746
                    }
4747
4748
                    $category_list[$objQuestionTmp->category]['total_questions']++;
4749
                    $category_was_added_for_this_test = true;
4750
                }
4751
                if (isset($objQuestionTmp->category_list) && !empty($objQuestionTmp->category_list)) {
4752
                    foreach ($objQuestionTmp->category_list as $category_id) {
4753
                        $category_list[$category_id]['score'] += $my_total_score;
4754
                        $category_list[$category_id]['total'] += $my_total_weight;
4755
                        $category_was_added_for_this_test = true;
4756
                    }
4757
                }
4758
4759
                // No category for this question!
4760
                if (false == $category_was_added_for_this_test) {
4761
                    if (!isset($category_list['none']['score'])) {
4762
                        $category_list['none']['score'] = 0;
4763
                    }
4764
                    if (!isset($category_list['none']['total'])) {
4765
                        $category_list['none']['total'] = 0;
4766
                    }
4767
4768
                    $category_list['none']['score'] += $my_total_score;
4769
                    $category_list['none']['total'] += $my_total_weight;
4770
                }
4771
4772
                if (0 == $objExercise->selectPropagateNeg() && $my_total_score < 0) {
4773
                    $my_total_score = 0;
4774
                }
4775
4776
                $comnt = null;
4777
                if ($show_results) {
4778
                    $comnt = Event::get_comments($exeId, $questionId);
4779
                    $teacherAudio = self::getOralFeedbackAudio(
4780
                        $exeId,
4781
                        $questionId
4782
                    );
4783
4784
                    if (!empty($comnt) || $teacherAudio) {
4785
                        echo '<b>'.get_lang('Feedback').'</b>';
4786
                    }
4787
4788
                    if (!empty($comnt)) {
4789
                        echo self::getFeedbackText($comnt);
4790
                    }
4791
4792
                    if ($teacherAudio) {
4793
                        echo $teacherAudio;
4794
                    }
4795
                }
4796
4797
                $calculatedScore = [
4798
                    'result' => self::show_score(
4799
                        $my_total_score,
4800
                        $my_total_weight,
4801
                        false
4802
                    ),
4803
                    'pass' => $scorePassed,
4804
                    'score' => $my_total_score,
4805
                    'weight' => $my_total_weight,
4806
                    'comments' => $comnt,
4807
                    'user_answered' => $result['user_answered'],
4808
                ];
4809
4810
                $score = [];
4811
                if ($show_results) {
4812
                    $score = $calculatedScore;
4813
                }
4814
                if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
4815
                    $reviewScore = [
4816
                        'score' => $my_total_score,
4817
                        'comments' => Event::get_comments($exeId, $questionId),
4818
                    ];
4819
                    $check = $objQuestionTmp->isQuestionWaitingReview($reviewScore);
4820
                    if (false === $check) {
4821
                        $countPendingQuestions++;
4822
                    }
4823
                }
4824
4825
                $contents = ob_get_clean();
4826
                $questionContent = '';
4827
                if ($show_results) {
4828
                    $questionContent = '<div class="question-answer-result">';
4829
                    if (false === $showQuestionScore) {
4830
                        $score = [];
4831
                    }
4832
4833
                    // Shows question title an description
4834
                    $questionContent .= $objQuestionTmp->return_header(
4835
                        $objExercise,
4836
                        $counter,
4837
                        $score
4838
                    );
4839
                }
4840
                $counter++;
4841
                $questionContent .= $contents;
4842
                if ($show_results) {
4843
                    $questionContent .= '</div>';
4844
                }
4845
4846
                $calculatedScore['question_content'] = $questionContent;
4847
                $attemptResult[] = $calculatedScore;
4848
                $parentId = intval($objQuestionTmp->parent_id ?: 0);
4849
                $panelsByParent[$parentId][] = Display::panel($questionContent);
4850
            }
4851
4852
            foreach ($panelsByParent as $pid => $panels) {
4853
                if ($pid !== 0) {
4854
                    $mediaQ = Question::read($pid, $objExercise->course);
4855
                    echo '<div class="media-group">';
4856
                    echo '<div class="media-content">';
4857
                    ob_start();
4858
                    $objExercise->manage_answer(
4859
                        $exeId,
4860
                        $pid,
4861
                        null,
4862
                        'exercise_show',
4863
                        [],
4864
                        false,
4865
                        true,
4866
                        $show_results,
4867
                        $objExercise->selectPropagateNeg()
4868
                    );
4869
                    echo ob_get_clean();
4870
                    echo '</div>';
4871
                    if (!empty($mediaQ->description)) {
4872
                        echo '<div class="media-description">'
4873
                            . $mediaQ->description
4874
                            . '</div>';
4875
                    }
4876
                    echo '<div class="media-children">';
4877
                }
4878
4879
                foreach ($panels as $panelHtml) {
4880
                    echo $panelHtml;
4881
                }
4882
4883
                if ($pid !== 0) {
4884
                    echo '</div></div>';
4885
                }
4886
            }
4887
        }
4888
4889
        // Display text when test is finished #4074 and for LP #4227
4890
        $endOfMessage = $objExercise->getFinishText($total_score, $total_weight);
4891
        if (!empty($endOfMessage)) {
4892
            echo Display::div(
4893
                $endOfMessage,
4894
                ['id' => 'quiz_end_message']
4895
            );
4896
        }
4897
4898
        $totalScoreText = null;
4899
        $certificateBlock = '';
4900
        if (($show_results || $show_only_score) && $showTotalScore) {
4901
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4902
                echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('Your results').'</h1><br />';
4903
            }
4904
            $totalScoreText .= '<div class="question_row_score">';
4905
            if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4906
                $totalScoreText .= self::getQuestionDiagnosisRibbon(
4907
                    $objExercise,
4908
                    $total_score,
4909
                    $total_weight,
4910
                    true
4911
                );
4912
            } else {
4913
                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
4914
                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
4915
                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->getId());
4916
4917
                    if (!empty($formula)) {
4918
                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
4919
                        $total_weight = $pluginEvaluation->getMaxScore();
4920
                    }
4921
                }
4922
4923
                $totalScoreText .= self::getTotalScoreRibbon(
4924
                    $objExercise,
4925
                    $total_score,
4926
                    $total_weight,
4927
                    true,
4928
                    $countPendingQuestions
4929
                );
4930
            }
4931
            $totalScoreText .= '</div>';
4932
4933
            if (!empty($studentInfo)) {
4934
                $certificateBlock = self::generateAndShowCertificateBlock(
4935
                    $total_score,
4936
                    $total_weight,
4937
                    $objExercise,
4938
                    $studentInfo['id'],
4939
                    $courseId,
4940
                    $sessionId
4941
                );
4942
            }
4943
        }
4944
4945
        if (MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY == $result['answer_type']) {
4946
            $chartMultiAnswer = MultipleAnswerTrueFalseDegreeCertainty::displayStudentsChartResults(
4947
                $exeId,
4948
                $objExercise
4949
            );
4950
            echo $chartMultiAnswer;
4951
        }
4952
4953
        if (!empty($category_list) &&
4954
            ($show_results || $show_only_score || RESULT_DISABLE_RADAR == $objExercise->results_disabled)
4955
        ) {
4956
            // Adding total
4957
            $category_list['total'] = [
4958
                'score' => $total_score,
4959
                'total' => $total_weight,
4960
            ];
4961
            echo TestCategory::get_stats_table_by_attempt($objExercise, $category_list);
4962
        }
4963
4964
        if ($show_all_but_expected_answer) {
4965
            $exerciseContent .= Display::return_message(get_lang('Note: This test has been setup to hide the expected answers.'));
4966
        }
4967
4968
        // Remove audio auto play from questions on results page - refs BT#7939
4969
        $exerciseContent = preg_replace(
4970
            ['/autoplay[\=\".+\"]+/', '/autostart[\=\".+\"]+/'],
4971
            '',
4972
            $exerciseContent
4973
        );
4974
4975
        echo $certificateBlock;
4976
4977
        // Ofaj change BT#11784
4978
        if (('true' === api_get_setting('exercise.quiz_show_description_on_results_page')) &&
4979
            !empty($objExercise->description)
4980
        ) {
4981
            echo Display::div($objExercise->description, ['class' => 'exercise_description']);
4982
        }
4983
4984
        echo $exerciseContent;
4985
        if (!$show_only_score) {
4986
            echo $totalScoreText;
4987
        }
4988
4989
        if ($save_user_result) {
4990
            // Tracking of results
4991
            if ($exercise_stat_info) {
4992
                $learnpath_id = $exercise_stat_info['orig_lp_id'];
4993
                $learnpath_item_id = $exercise_stat_info['orig_lp_item_id'];
4994
                $learnpath_item_view_id = $exercise_stat_info['orig_lp_item_view_id'];
4995
4996
                if (api_is_allowed_to_session_edit()) {
4997
                    Event::updateEventExercise(
4998
                        $exercise_stat_info['exe_id'],
4999
                        $objExercise->getId(),
5000
                        $total_score,
5001
                        $total_weight,
5002
                        $sessionId,
5003
                        $learnpath_id,
5004
                        $learnpath_item_id,
5005
                        $learnpath_item_view_id,
5006
                        $exercise_stat_info['exe_duration'],
5007
                        $question_list
5008
                    );
5009
5010
                    $allowStats = ('true' === api_get_setting('gradebook.allow_gradebook_stats'));
5011
                    if ($allowStats) {
5012
                        $objExercise->generateStats(
5013
                            $objExercise->getId(),
5014
                            api_get_course_info(),
5015
                            $sessionId
5016
                        );
5017
                    }
5018
                }
5019
            }
5020
5021
            // Send notification at the end
5022
            if (!api_is_allowed_to_edit(null, true) &&
5023
                !api_is_excluded_user_type()
5024
            ) {
5025
                $objExercise->send_mail_notification_for_exam(
5026
                    'end',
5027
                    $question_list_answers,
5028
                    $origin,
5029
                    $exeId,
5030
                    $total_score,
5031
                    $total_weight
5032
                );
5033
            }
5034
        }
5035
5036
        if (in_array(
5037
            $objExercise->selectResultsDisabled(),
5038
            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
5039
        )) {
5040
            echo Display::page_header(get_lang('Ranking'), null, 'h4');
5041
            echo self::displayResultsInRanking(
5042
                $objExercise,
5043
                api_get_user_id(),
5044
                $courseId,
5045
                $sessionId
5046
            );
5047
        }
5048
5049
        if (!empty($remainingMessage)) {
5050
            echo Display::return_message($remainingMessage, 'normal', false);
5051
        }
5052
5053
        $failedAnswersCount = 0;
5054
        $wrongQuestionHtml = '';
5055
        $all = '';
5056
        foreach ($attemptResult as $item) {
5057
            if (false === $item['pass']) {
5058
                $failedAnswersCount++;
5059
                $wrongQuestionHtml .= $item['question_content'].'<br />';
5060
            }
5061
            $all .= $item['question_content'].'<br />';
5062
        }
5063
5064
        $passed = self::isPassPercentageAttemptPassed(
5065
            $objExercise,
5066
            $total_score,
5067
            $total_weight
5068
        );
5069
5070
        $percentage = 0;
5071
        if (!empty($total_weight)) {
5072
            $percentage = ($total_score / $total_weight) * 100;
5073
        }
5074
5075
        return [
5076
            'category_list' => $category_list,
5077
            'attempts_result_list' => $attemptResult, // array of results
5078
            'exercise_passed' => $passed, // boolean
5079
            'total_answers_count' => count($attemptResult), // int
5080
            'failed_answers_count' => $failedAnswersCount, // int
5081
            'failed_answers_html' => $wrongQuestionHtml,
5082
            'all_answers_html' => $all,
5083
            'total_score' => $total_score,
5084
            'total_weight' => $total_weight,
5085
            'total_percentage' => $percentage,
5086
            'count_pending_questions' => $countPendingQuestions,
5087
        ];
5088
    }
5089
5090
    /**
5091
     * Display the ranking of results in a exercise.
5092
     *
5093
     * @param Exercise $exercise
5094
     * @param int      $currentUserId
5095
     * @param int      $courseId
5096
     * @param int      $sessionId
5097
     *
5098
     * @return string
5099
     */
5100
    public static function displayResultsInRanking($exercise, $currentUserId, $courseId, $sessionId = 0)
5101
    {
5102
        $exerciseId = $exercise->iId;
5103
        $data = self::exerciseResultsInRanking($exerciseId, $courseId, $sessionId);
5104
5105
        $table = new HTML_Table(['class' => 'table table-hover table-striped table-bordered']);
5106
        $table->setHeaderContents(0, 0, get_lang('Position'), ['class' => 'text-right']);
5107
        $table->setHeaderContents(0, 1, get_lang('Username'));
5108
        $table->setHeaderContents(0, 2, get_lang('Score'), ['class' => 'text-right']);
5109
        $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
5110
5111
        foreach ($data as $r => $item) {
5112
            if (!isset($item[1])) {
5113
                continue;
5114
            }
5115
            $selected = $item[1]->getId() == $currentUserId;
5116
5117
            foreach ($item as $c => $value) {
5118
                $table->setCellContents($r + 1, $c, $value);
5119
5120
                $attrClass = '';
5121
5122
                if (in_array($c, [0, 2])) {
5123
                    $attrClass = 'text-right';
5124
                } elseif (3 == $c) {
5125
                    $attrClass = 'text-center';
5126
                }
5127
5128
                if ($selected) {
5129
                    $attrClass .= ' warning';
5130
                }
5131
5132
                $table->setCellAttributes($r + 1, $c, ['class' => $attrClass]);
5133
            }
5134
        }
5135
5136
        return $table->toHtml();
5137
    }
5138
5139
    /**
5140
     * Get the ranking for results in a exercise.
5141
     * Function used internally by ExerciseLib::displayResultsInRanking.
5142
     *
5143
     * @param int $exerciseId
5144
     * @param int $courseId
5145
     * @param int $sessionId
5146
     *
5147
     * @return array
5148
     */
5149
    public static function exerciseResultsInRanking($exerciseId, $courseId, $sessionId = 0)
5150
    {
5151
        $em = Database::getManager();
5152
5153
        $dql = 'SELECT DISTINCT u.id FROM ChamiloCoreBundle:TrackEExercise te JOIN te.user u WHERE te.quiz = :id AND te.course = :cId';
5154
        $dql .= api_get_session_condition($sessionId, true, false, 'te.session');
5155
5156
        $result = $em
5157
            ->createQuery($dql)
5158
            ->setParameters(['id' => $exerciseId, 'cId' => $courseId])
5159
            ->getScalarResult();
5160
5161
        $data = [];
5162
5163
        foreach ($result as $item) {
5164
            $attempt = self::get_best_attempt_by_user($item['id'], $exerciseId, $courseId, $sessionId);
5165
            if (!empty($attempt) && isset($attempt['score']) && isset($attempt['exe_date'])) {
5166
                $data[] = $attempt;
5167
            }
5168
        }
5169
5170
        if (empty($data)) {
5171
            return [];
5172
        }
5173
5174
        usort(
5175
            $data,
5176
            function ($a, $b) {
5177
                if ($a['score'] != $b['score']) {
5178
                    return $a['score'] > $b['score'] ? -1 : 1;
5179
                }
5180
5181
                if ($a['exe_date'] != $b['exe_date']) {
5182
                    return $a['exe_date'] < $b['exe_date'] ? -1 : 1;
5183
                }
5184
5185
                return 0;
5186
            }
5187
        );
5188
5189
        // flags to display the same position in case of tie
5190
        $lastScore = $data[0]['score'];
5191
        $position = 1;
5192
        $data = array_map(
5193
            function ($item) use (&$lastScore, &$position) {
5194
                if ($item['score'] < $lastScore) {
5195
                    $position++;
5196
                }
5197
5198
                $lastScore = $item['score'];
5199
5200
                return [
5201
                    $position,
5202
                    api_get_user_entity($item['exe_user_id']),
5203
                    self::show_score($item['score'], $item['max_score'], true, true, true),
5204
                    api_convert_and_format_date($item['exe_date'], DATE_TIME_FORMAT_SHORT),
5205
                ];
5206
            },
5207
            $data
5208
        );
5209
5210
        return $data;
5211
    }
5212
5213
    /**
5214
     * Get a special ribbon on top of "degree of certainty" questions (
5215
     * variation from getTotalScoreRibbon() for other question types).
5216
     *
5217
     * @param Exercise $objExercise
5218
     * @param float    $score
5219
     * @param float    $weight
5220
     * @param bool     $checkPassPercentage
5221
     *
5222
     * @return string
5223
     */
5224
    public static function getQuestionDiagnosisRibbon($objExercise, $score, $weight, $checkPassPercentage = false)
5225
    {
5226
        $displayChartDegree = true;
5227
        $ribbon = $displayChartDegree ? '<div class="ribbon">' : '';
5228
5229
        if ($checkPassPercentage) {
5230
            $passPercentage = $objExercise->selectPassPercentage();
5231
            $isSuccess = self::isSuccessExerciseResult($score, $weight, $passPercentage);
5232
            // Color the final test score if pass_percentage activated
5233
            $ribbonTotalSuccessOrError = '';
5234
            if (self::isPassPercentageEnabled($passPercentage)) {
5235
                if ($isSuccess) {
5236
                    $ribbonTotalSuccessOrError = ' ribbon-total-success';
5237
                } else {
5238
                    $ribbonTotalSuccessOrError = ' ribbon-total-error';
5239
                }
5240
            }
5241
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total '.$ribbonTotalSuccessOrError.'">' : '';
5242
        } else {
5243
            $ribbon .= $displayChartDegree ? '<div class="rib rib-total">' : '';
5244
        }
5245
5246
        if ($displayChartDegree) {
5247
            $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
5248
            $ribbon .= self::show_score($score, $weight, false, true);
5249
            $ribbon .= '</h3>';
5250
            $ribbon .= '</div>';
5251
        }
5252
5253
        if ($checkPassPercentage) {
5254
            $ribbon .= self::showSuccessMessage(
5255
                $score,
5256
                $weight,
5257
                $objExercise->selectPassPercentage()
5258
            );
5259
        }
5260
5261
        $ribbon .= $displayChartDegree ? '</div>' : '';
5262
5263
        return $ribbon;
5264
    }
5265
5266
    public static function isPassPercentageAttemptPassed($objExercise, $score, $weight)
5267
    {
5268
        $passPercentage = $objExercise->selectPassPercentage();
5269
5270
        return self::isSuccessExerciseResult($score, $weight, $passPercentage);
5271
    }
5272
5273
    /**
5274
     * @param float $score
5275
     * @param float $weight
5276
     * @param bool  $checkPassPercentage
5277
     * @param int   $countPendingQuestions
5278
     *
5279
     * @return string
5280
     */
5281
    public static function getTotalScoreRibbon(
5282
        Exercise $objExercise,
5283
        $score,
5284
        $weight,
5285
        $checkPassPercentage = false,
5286
        $countPendingQuestions = 0
5287
    ) {
5288
        $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score');
5289
        if (1 === $hide) {
5290
            return '';
5291
        }
5292
5293
        $passPercentage = $objExercise->selectPassPercentage();
5294
        $ribbon = '<div class="title-score">';
5295
        if ($checkPassPercentage) {
5296
            $isSuccess = self::isSuccessExerciseResult(
5297
                $score,
5298
                $weight,
5299
                $passPercentage
5300
            );
5301
            // Color the final test score if pass_percentage activated
5302
            $class = '';
5303
            if (self::isPassPercentageEnabled($passPercentage)) {
5304
                if ($isSuccess) {
5305
                    $class = ' ribbon-total-success';
5306
                } else {
5307
                    $class = ' ribbon-total-error';
5308
                }
5309
            }
5310
            $ribbon .= '<div class="total '.$class.'">';
5311
        } else {
5312
            $ribbon .= '<div class="total">';
5313
        }
5314
        $ribbon .= '<h3>'.get_lang('Score for the test').':&nbsp;';
5315
        $ribbon .= self::show_score($score, $weight, false, true);
5316
        $ribbon .= '</h3>';
5317
        $ribbon .= '</div>';
5318
        if ($checkPassPercentage) {
5319
            $ribbon .= self::showSuccessMessage(
5320
                $score,
5321
                $weight,
5322
                $passPercentage
5323
            );
5324
        }
5325
        $ribbon .= '</div>';
5326
5327
        if (!empty($countPendingQuestions)) {
5328
            $ribbon .= '<br />';
5329
            $ribbon .= Display::return_message(
5330
                sprintf(
5331
                    get_lang('Temporary score: %s open question(s) not corrected yet.'),
5332
                    $countPendingQuestions
5333
                ),
5334
                'warning'
5335
            );
5336
        }
5337
5338
        return $ribbon;
5339
    }
5340
5341
    /**
5342
     * @param int $countLetter
5343
     *
5344
     * @return mixed
5345
     */
5346
    public static function detectInputAppropriateClass($countLetter)
5347
    {
5348
        $limits = [
5349
            0 => 'input-mini',
5350
            10 => 'input-mini',
5351
            15 => 'input-medium',
5352
            20 => 'input-xlarge',
5353
            40 => 'input-xlarge',
5354
            60 => 'input-xxlarge',
5355
            100 => 'input-xxlarge',
5356
            200 => 'input-xxlarge',
5357
        ];
5358
5359
        foreach ($limits as $size => $item) {
5360
            if ($countLetter <= $size) {
5361
                return $item;
5362
            }
5363
        }
5364
5365
        return $limits[0];
5366
    }
5367
5368
    /**
5369
     * @param int    $senderId
5370
     * @param array  $course_info
5371
     * @param string $test
5372
     * @param string $url
5373
     *
5374
     * @return string
5375
     */
5376
    public static function getEmailNotification($senderId, $course_info, $test, $url)
5377
    {
5378
        $teacher_info = api_get_user_info($senderId);
5379
        $fromName = api_get_person_name(
5380
            $teacher_info['firstname'],
5381
            $teacher_info['lastname'],
5382
            null,
5383
            PERSON_NAME_EMAIL_ADDRESS
5384
        );
5385
5386
        $params = [
5387
            'course_title' => Security::remove_XSS($course_info['name']),
5388
            'test_title' => Security::remove_XSS($test),
5389
            'url' => $url,
5390
            'teacher_name' => $fromName,
5391
        ];
5392
5393
        return Container::getTwig()->render(
5394
            '@ChamiloCore/Mailer/Exercise/result_alert_body.html.twig',
5395
            $params
5396
        );
5397
    }
5398
5399
    /**
5400
     * @return string
5401
     */
5402
    public static function getNotCorrectedYetText()
5403
    {
5404
        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');
5405
    }
5406
5407
    /**
5408
     * @param string $message
5409
     *
5410
     * @return string
5411
     */
5412
    public static function getFeedbackText($message)
5413
    {
5414
        return Display::return_message($message, 'warning', false);
5415
    }
5416
5417
    /**
5418
     * Get the recorder audio component for save a teacher audio feedback.
5419
     *
5420
     * @param int $attemptId
5421
     * @param int $questionId
5422
     *
5423
     * @return string
5424
     */
5425
    public static function getOralFeedbackForm($attemptId, $questionId)
5426
    {
5427
        $view = new Template('', false, false, false, false, false, false);
5428
        $view->assign('type', Asset::EXERCISE_FEEDBACK);
5429
        $view->assign('question_id', $questionId);
5430
        $view->assign('t_exercise_id', $attemptId);
5431
        $template = $view->get_template('exercise/oral_expression.html.twig');
5432
5433
        return $view->fetch($template);
5434
    }
5435
5436
    /**
5437
     * Retrieves the generated audio files for an oral question in an exercise attempt.
5438
     *
5439
     * @param int  $trackExerciseId The ID of the tracked exercise.
5440
     * @param int  $questionId      The ID of the question.
5441
     * @param bool $returnUrls      (Optional) If set to true, only the URLs of the audio files are returned. Default is false.
5442
     *
5443
     * @return array|string If $returnUrls is true, returns an array of URLs of the audio files. Otherwise, returns an HTML string with audio tags.
5444
     */
5445
    public static function getOralFileAudio(int $trackExerciseId, int $questionId, bool $returnUrls = false): array|string
5446
    {
5447
        /** @var TrackEExercise $trackExercise */
5448
        $trackExercise = Container::getTrackEExerciseRepository()->find($trackExerciseId);
5449
5450
        if (null === $trackExercise) {
5451
            return $returnUrls ? [] : '';
5452
        }
5453
5454
        $questionAttempt = $trackExercise->getAttemptByQuestionId($questionId);
5455
5456
        if (null === $questionAttempt) {
5457
            return $returnUrls ? [] : '';
5458
        }
5459
5460
        $basePath = rtrim(api_get_path(WEB_PATH), '/');
5461
        $assetRepo = Container::getAssetRepository();
5462
5463
        if ($returnUrls) {
5464
            $urls = [];
5465
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5466
                $urls[] = $basePath.$assetRepo->getAssetUrl($attemptFile->getAsset());
5467
            }
5468
5469
            return $urls;
5470
        } else {
5471
            $html = '';
5472
            foreach ($questionAttempt->getAttemptFiles() as $attemptFile) {
5473
                $html .= Display::tag(
5474
                    'audio',
5475
                    '',
5476
                    [
5477
                        'src' => $basePath.$assetRepo->getAssetUrl($attemptFile->getAsset()),
5478
                        'controls' => '',
5479
                    ]
5480
                );
5481
            }
5482
5483
            return $html;
5484
        }
5485
    }
5486
5487
    /**
5488
     * Get the audio component for a teacher audio feedback.
5489
     */
5490
    public static function getOralFeedbackAudio(int $attemptId, int $questionId): string
5491
    {
5492
        /** @var TrackEExercise $tExercise */
5493
        $tExercise = Container::getTrackEExerciseRepository()->find($attemptId);
5494
5495
        if (null === $tExercise) {
5496
            return '';
5497
        }
5498
5499
        $qAttempt = $tExercise->getAttemptByQuestionId($questionId);
5500
5501
        if (null === $qAttempt) {
5502
            return '';
5503
        }
5504
5505
        $html = '';
5506
5507
        $assetRepo = Container::getAssetRepository();
5508
5509
        foreach ($qAttempt->getAttemptFeedbacks() as $attemptFeedback) {
5510
            $html .= Display::tag(
5511
                'audio',
5512
                '',
5513
                [
5514
                    'src' => $assetRepo->getAssetUrl($attemptFeedback->getAsset()),
5515
                    'controls' => '',
5516
                ]
5517
5518
            );
5519
        }
5520
5521
        return $html;
5522
    }
5523
5524
    public static function getUploadAnswerFiles(int $trackExerciseId, int $questionId, bool $returnUrls = false)
5525
    {
5526
        $trackExercise = Container::getTrackEExerciseRepository()->find($trackExerciseId);
5527
        if (!$trackExercise) { return $returnUrls ? [] : ''; }
5528
        $attempt = $trackExercise->getAttemptByQuestionId($questionId);
5529
        if (!$attempt) { return $returnUrls ? [] : ''; }
5530
5531
        $assetRepo = Container::getAssetRepository();
5532
        $basePath = rtrim(api_get_path(WEB_PATH), '/');
5533
5534
        if ($returnUrls) {
5535
            $urls = [];
5536
            foreach ($attempt->getAttemptFiles() as $af) {
5537
                $urls[] = $basePath.$assetRepo->getAssetUrl($af->getAsset());
5538
            }
5539
            return $urls;
5540
        }
5541
5542
        $html = '';
5543
        foreach ($attempt->getAttemptFiles() as $af) {
5544
            $url = $basePath.$assetRepo->getAssetUrl($af->getAsset());
5545
            $html .= Display::url(basename($url), $url, ['target' => '_blank']).'<br />';
5546
        }
5547
        return $html;
5548
    }
5549
5550
    public static function getNotificationSettings(): array
5551
    {
5552
        return [
5553
            2 => get_lang('Paranoid: E-mail teacher when a student starts an exercise'),
5554
            1 => get_lang('Aware: E-mail teacher when a student ends an exercise'), // default
5555
            3 => get_lang('Relaxed open: E-mail teacher when a student ends an exercise, only if an open question is answered'),
5556
            4 => get_lang('Relaxed audio: E-mail teacher when a student ends an exercise, only if an oral question is answered'),
5557
        ];
5558
    }
5559
5560
    /**
5561
     * Get the additional actions added in exercise_additional_teacher_modify_actions configuration.
5562
     *
5563
     * @param int $exerciseId
5564
     * @param int $iconSize
5565
     *
5566
     * @return string
5567
     */
5568
    public static function getAdditionalTeacherActions($exerciseId, $iconSize = ICON_SIZE_SMALL)
5569
    {
5570
        $additionalActions = api_get_setting('exercise.exercise_additional_teacher_modify_actions', true) ?: [];
5571
        $actions = [];
5572
5573
        if (is_array($additionalActions)) {
5574
            foreach ($additionalActions as $additionalAction) {
5575
                $actions[] = call_user_func(
5576
                    $additionalAction,
5577
                    $exerciseId,
5578
                    $iconSize
5579
                );
5580
            }
5581
        }
5582
5583
        return implode(PHP_EOL, $actions);
5584
    }
5585
5586
    /**
5587
     * @param int $userId
5588
     * @param int $courseId
5589
     * @param int $sessionId
5590
     *
5591
     * @return int
5592
     */
5593
    public static function countAnsweredQuestionsByUserAfterTime(DateTime $time, $userId, $courseId, $sessionId)
5594
    {
5595
        $em = Database::getManager();
5596
5597
        if (empty($sessionId)) {
5598
            $sessionId = null;
5599
        }
5600
5601
        $time = api_get_utc_datetime($time->format('Y-m-d H:i:s'), false, true);
5602
5603
        $result = $em
5604
            ->createQuery('
5605
                SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
5606
                WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
5607
                    AND ea.tms > :time
5608
            ')
5609
            ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
5610
            ->getSingleScalarResult();
5611
5612
        return $result;
5613
    }
5614
5615
    /**
5616
     * @param int $userId
5617
     * @param int $numberOfQuestions
5618
     * @param int $courseId
5619
     * @param int $sessionId
5620
     *
5621
     * @throws \Doctrine\ORM\Query\QueryException
5622
     *
5623
     * @return bool
5624
     */
5625
    public static function isQuestionsLimitPerDayReached($userId, $numberOfQuestions, $courseId, $sessionId)
5626
    {
5627
        $questionsLimitPerDay = (int) api_get_course_setting('quiz_question_limit_per_day');
5628
5629
        if ($questionsLimitPerDay <= 0) {
5630
            return false;
5631
        }
5632
5633
        $midnightTime = ChamiloHelper::getServerMidnightTime();
5634
5635
        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
5636
            $midnightTime,
5637
            $userId,
5638
            $courseId,
5639
            $sessionId
5640
        );
5641
5642
        return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
5643
    }
5644
5645
    /**
5646
     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
5647
     * By making sure it is set on one question per page and it only contains unique-answer or multiple-answer questions
5648
     * or unique-answer image. And that the exam does not have immediate feedback.
5649
     *
5650
     * @return bool
5651
     */
5652
    public static function isQuizEmbeddable(CQuiz $exercise)
5653
    {
5654
        $em = Database::getManager();
5655
5656
        if (ONE_PER_PAGE != $exercise->getType() ||
5657
            in_array($exercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
5658
        ) {
5659
            return false;
5660
        }
5661
5662
        $countAll = $em
5663
            ->createQuery('SELECT COUNT(qq)
5664
                FROM ChamiloCourseBundle:CQuizQuestion qq
5665
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5666
                   WITH qq.iid = qrq.question
5667
                WHERE qrq.quiz = :id'
5668
            )
5669
            ->setParameter('id', $exercise->getIid())
5670
            ->getSingleScalarResult();
5671
5672
        $countOfAllowed = $em
5673
            ->createQuery('SELECT COUNT(qq)
5674
                FROM ChamiloCourseBundle:CQuizQuestion qq
5675
                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
5676
                   WITH qq.iid = qrq.question
5677
                WHERE qrq.quiz = :id AND qq.type IN (:types)'
5678
            )
5679
            ->setParameters(
5680
                [
5681
                    'id' => $exercise->getIid(),
5682
                    'types' => [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE],
5683
                ]
5684
            )
5685
            ->getSingleScalarResult();
5686
5687
        return $countAll === $countOfAllowed;
5688
    }
5689
5690
    /**
5691
     * Generate a certificate linked to current quiz and.
5692
     * Return the HTML block with links to download and view the certificate.
5693
     *
5694
     * @param float $totalScore
5695
     * @param float $totalWeight
5696
     * @param int   $studentId
5697
     * @param int   $courseId
5698
     * @param int   $sessionId
5699
     *
5700
     * @return string
5701
     */
5702
    public static function generateAndShowCertificateBlock(
5703
        $totalScore,
5704
        $totalWeight,
5705
        Exercise $objExercise,
5706
        $studentId,
5707
        $courseId,
5708
        $sessionId = 0
5709
    ) {
5710
        if (('true' !== api_get_setting('exercise.quiz_generate_certificate_ending')) ||
5711
            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
5712
        ) {
5713
            return '';
5714
        }
5715
5716
        $repo = Container::getGradeBookCategoryRepository();
5717
        /** @var GradebookCategory $category */
5718
        $category = $repo->findOneBy(
5719
            ['course' => $courseId, 'session' => $sessionId]
5720
        );
5721
5722
        if (null === $category) {
5723
            return '';
5724
        }
5725
5726
        /*$category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
5727
        if (empty($category)) {
5728
            return '';
5729
        }*/
5730
        $categoryId = $category->getId();
5731
        /*$link = LinkFactory::load(
5732
            null,
5733
            null,
5734
            $objExercise->getId(),
5735
            null,
5736
            $courseCode,
5737
            $categoryId
5738
        );*/
5739
5740
        if (empty($category->getLinks()->count())) {
5741
            return '';
5742
        }
5743
5744
        $resourceDeletedMessage = Category::show_message_resource_delete($courseId);
5745
        if (!empty($resourceDeletedMessage) || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
5746
            return '';
5747
        }
5748
5749
        $certificate = Category::generateUserCertificate($category, $studentId);
5750
        if (!is_array($certificate)) {
5751
            return '';
5752
        }
5753
5754
        return Category::getDownloadCertificateBlock($certificate);
5755
    }
5756
5757
    /**
5758
     * @param int $exerciseId
5759
     */
5760
    public static function getExerciseTitleById($exerciseId)
5761
    {
5762
        $em = Database::getManager();
5763
5764
        return $em
5765
            ->createQuery('SELECT cq.title
5766
                FROM ChamiloCourseBundle:CQuiz cq
5767
                WHERE cq.iid = :iid'
5768
            )
5769
            ->setParameter('iid', $exerciseId)
5770
            ->getSingleScalarResult();
5771
    }
5772
5773
    /**
5774
     * @param int $exeId      ID from track_e_exercises
5775
     * @param int $userId     User ID
5776
     * @param int $exerciseId Exercise ID
5777
     * @param int $courseId   Optional. Coure ID.
5778
     *
5779
     * @return TrackEExercise|null
5780
     */
5781
    public static function recalculateResult($exeId, $userId, $exerciseId, $courseId = 0)
5782
    {
5783
        if (empty($userId) || empty($exerciseId)) {
5784
            return null;
5785
        }
5786
5787
        $em = Database::getManager();
5788
        /** @var TrackEExercise $trackedExercise */
5789
        $trackedExercise = $em->getRepository(TrackEExercise::class)->find($exeId);
5790
5791
        if (empty($trackedExercise)) {
5792
            return null;
5793
        }
5794
5795
        if ($trackedExercise->getUser()->getId() != $userId ||
5796
            $trackedExercise->getQuiz()?->getIid() != $exerciseId
5797
        ) {
5798
            return null;
5799
        }
5800
5801
        $questionList = $trackedExercise->getDataTracking();
5802
5803
        if (empty($questionList)) {
5804
            return null;
5805
        }
5806
5807
        $questionList = explode(',', $questionList);
5808
5809
        $exercise = new Exercise($courseId);
5810
        $courseInfo = $courseId ? api_get_course_info_by_id($courseId) : [];
5811
5812
        if (false === $exercise->read($exerciseId)) {
5813
            return null;
5814
        }
5815
5816
        $totalScore = 0;
5817
        $totalWeight = 0;
5818
5819
        $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
5820
5821
        $formula = 'true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)
5822
            ? $pluginEvaluation->getFormulaForExercise($exerciseId)
5823
            : 0;
5824
5825
        if (empty($formula)) {
5826
            foreach ($questionList as $questionId) {
5827
                $question = Question::read($questionId, $courseInfo);
5828
5829
                if (false === $question) {
5830
                    continue;
5831
                }
5832
5833
                $totalWeight += $question->selectWeighting();
5834
5835
                // We're inside *one* question. Go through each possible answer for this question
5836
                $result = $exercise->manage_answer(
5837
                    $exeId,
5838
                    $questionId,
5839
                    [],
5840
                    'exercise_result',
5841
                    [],
5842
                    false,
5843
                    true,
5844
                    false,
5845
                    $exercise->selectPropagateNeg(),
5846
                    [],
5847
                    [],
5848
                    true
5849
                );
5850
5851
                //  Adding the new score.
5852
                $totalScore += $result['score'];
5853
            }
5854
        } else {
5855
            $totalScore = $pluginEvaluation->getResultWithFormula($exeId, $formula);
5856
            $totalWeight = $pluginEvaluation->getMaxScore();
5857
        }
5858
5859
        $trackedExercise
5860
            ->setScore($totalScore)
5861
            ->setMaxScore($totalWeight);
5862
5863
        $em->persist($trackedExercise);
5864
        $em->flush();
5865
        $lpItemId = $trackedExercise->getOrigLpItemId();
5866
        $lpId = $trackedExercise->getOrigLpId();
5867
        $lpItemViewId = $trackedExercise->getOrigLpItemViewId();
5868
        if ($lpId && $lpItemId && $lpItemViewId) {
5869
            $lpItem = $em->getRepository(CLpItem::class)->find($lpItemId);
5870
            if ($lpItem && 'quiz' === $lpItem->getItemType()) {
5871
                $lpItemView = $em->getRepository(CLpItemView::class)->find($lpItemViewId);
5872
                if ($lpItemView) {
5873
                    $lpItemView->setScore($totalScore);
5874
                    $em->persist($lpItemView);
5875
                    $em->flush();
5876
                }
5877
            }
5878
        }
5879
5880
        return $trackedExercise;
5881
    }
5882
5883
    public static function getTotalQuestionAnswered($courseId, $exerciseId, $questionId, $onlyStudents = false): int
5884
    {
5885
        $courseId = (int) $courseId;
5886
        $exerciseId = (int) $exerciseId;
5887
        $questionId = (int) $questionId;
5888
5889
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5890
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5891
        $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
5892
        $courseUserJoin = "";
5893
        $studentsWhere = "";
5894
        if ($onlyStudents) {
5895
            $courseUserJoin = "
5896
            INNER JOIN $courseUser cu
5897
            ON cu.c_id = te.c_id AND cu.user_id = exe_user_id";
5898
            $studentsWhere = " AND relation_type <> 2 AND cu.status = ".STUDENT;
5899
        }
5900
5901
        $sql = "SELECT count(distinct (te.exe_id)) total
5902
            FROM $attemptTable t
5903
            INNER JOIN $trackTable te
5904
            ON (t.exe_id = te.exe_id)
5905
            $courseUserJoin
5906
            WHERE
5907
                te.c_id = $courseId AND
5908
                exe_exo_id = $exerciseId AND
5909
                t.question_id = $questionId AND
5910
                te.status != 'incomplete'
5911
                $studentsWhere
5912
        ";
5913
        $queryTotal = Database::query($sql);
5914
        $totalRow = Database::fetch_assoc($queryTotal);
5915
        $total = 0;
5916
        if ($totalRow) {
5917
            $total = (int) $totalRow['total'];
5918
        }
5919
5920
        return $total;
5921
    }
5922
5923
    public static function getWrongQuestionResults($courseId, $exerciseId, $sessionId = 0, $limit = 10)
5924
    {
5925
        $courseId = (int) $courseId;
5926
        $exerciseId = (int) $exerciseId;
5927
        $limit = (int) $limit;
5928
5929
        $questionTable = Database::get_course_table(TABLE_QUIZ_QUESTION);
5930
        $attemptTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
5931
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5932
5933
        $sessionCondition = '';
5934
        if (!empty($sessionId)) {
5935
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5936
        }
5937
5938
        $sql = "SELECT q.question, question_id, count(q.iid) count
5939
                FROM $attemptTable t
5940
                INNER JOIN $questionTable q
5941
                ON (q.iid = t.question_id)
5942
                INNER JOIN $trackTable te
5943
                ON (t.exe_id = te.exe_id)
5944
                WHERE
5945
                    te.c_id = $courseId AND
5946
                    t.marks != q.ponderation AND
5947
                    exe_exo_id = $exerciseId AND
5948
                    status != 'incomplete'
5949
                    $sessionCondition
5950
                GROUP BY q.iid
5951
                ORDER BY count DESC
5952
                LIMIT $limit
5953
        ";
5954
5955
        $result = Database::query($sql);
5956
5957
        return Database::store_result($result, 'ASSOC');
5958
    }
5959
5960
    public static function getExerciseResultsCount($type, $courseId, $exerciseId, $sessionId = 0)
5961
    {
5962
        $courseId = (int) $courseId;
5963
        $exerciseId = (int) $exerciseId;
5964
5965
        $trackTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
5966
5967
        $sessionCondition = '';
5968
        if (!empty($sessionId)) {
5969
            $sessionCondition = api_get_session_condition($sessionId, true, false, 'te.session_id');
5970
        }
5971
5972
        $selectCount = 'count(DISTINCT te.exe_id)';
5973
        $scoreCondition = '';
5974
        switch ($type) {
5975
            case 'correct_student':
5976
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5977
                $scoreCondition = ' AND score = max_score ';
5978
                break;
5979
            case 'wrong_student':
5980
                $selectCount = 'count(DISTINCT te.exe_user_id)';
5981
                $scoreCondition = ' AND score != max_score ';
5982
                break;
5983
            case 'correct':
5984
                $scoreCondition = ' AND score = max_score ';
5985
                break;
5986
            case 'wrong':
5987
                $scoreCondition = ' AND score != max_score ';
5988
                break;
5989
        }
5990
5991
        $sql = "SELECT $selectCount count
5992
                FROM $trackTable te
5993
                WHERE
5994
                    c_id = $courseId AND
5995
                    exe_exo_id = $exerciseId AND
5996
                    status != 'incomplete'
5997
                    $scoreCondition
5998
                    $sessionCondition
5999
        ";
6000
        $result = Database::query($sql);
6001
        $totalRow = Database::fetch_assoc($result);
6002
        $total = 0;
6003
        if ($totalRow) {
6004
            $total = (int) $totalRow['count'];
6005
        }
6006
6007
        return $total;
6008
    }
6009
6010
    public static function parseContent($content, $stats, Exercise $exercise, $trackInfo, $currentUserId = 0)
6011
    {
6012
        $wrongAnswersCount = $stats['failed_answers_count'];
6013
        $attemptDate = substr($trackInfo['exe_date'], 0, 10);
6014
        $exerciseId = $exercise->iId;
6015
        $resultsStudentUrl = api_get_path(WEB_CODE_PATH).
6016
            'exercise/result.php?id='.$exerciseId.'&'.api_get_cidreq();
6017
        $resultsTeacherUrl = api_get_path(WEB_CODE_PATH).
6018
            'exercise/exercise_show.php?action=edit&id='.$exerciseId.'&'.api_get_cidreq();
6019
6020
        $content = str_replace(
6021
            [
6022
                '((exercise_error_count))',
6023
                '((all_answers_html))',
6024
                '((all_answers_teacher_html))',
6025
                '((exercise_title))',
6026
                '((exercise_attempt_date))',
6027
                '((link_to_test_result_page_student))',
6028
                '((link_to_test_result_page_teacher))',
6029
            ],
6030
            [
6031
                $wrongAnswersCount,
6032
                $stats['all_answers_html'],
6033
                $stats['all_answers_teacher_html'],
6034
                $exercise->get_formated_title(),
6035
                $attemptDate,
6036
                $resultsStudentUrl,
6037
                $resultsTeacherUrl,
6038
            ],
6039
            $content
6040
        );
6041
6042
        $currentUserId = empty($currentUserId) ? api_get_user_id() : (int) $currentUserId;
6043
6044
        $content = AnnouncementManager::parseContent(
6045
            $currentUserId,
6046
            $content,
6047
            api_get_course_id(),
6048
            api_get_session_id()
6049
        );
6050
6051
        return $content;
6052
    }
6053
6054
    public static function sendNotification(
6055
        $currentUserId,
6056
        $objExercise,
6057
        $exercise_stat_info,
6058
        $courseInfo,
6059
        $attemptCountToSend,
6060
        $stats,
6061
        $statsTeacher
6062
    ) {
6063
        $notifications = api_get_configuration_value('exercise_finished_notification_settings');
6064
        if (empty($notifications)) {
6065
            return false;
6066
        }
6067
6068
        $studentId = $exercise_stat_info['exe_user_id'];
6069
        $exerciseExtraFieldValue = new ExtraFieldValue('exercise');
6070
        $wrongAnswersCount = $stats['failed_answers_count'];
6071
        $exercisePassed = $stats['exercise_passed'];
6072
        $countPendingQuestions = $stats['count_pending_questions'];
6073
        $stats['all_answers_teacher_html'] = $statsTeacher['all_answers_html'];
6074
6075
        // If there are no pending questions (Open questions).
6076
        if (0 === $countPendingQuestions) {
6077
            /*$extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6078
                $objExercise->iId,
6079
                'signature_mandatory'
6080
            );
6081
6082
            if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) {
6083
                if (ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) {
6084
                    $signature = ExerciseSignaturePlugin::getSignature($studentId, $exercise_stat_info);
6085
                    if (false !== $signature) {
6086
                        //return false;
6087
                    }
6088
                }
6089
            }*/
6090
6091
            // Notifications.
6092
            $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6093
                $objExercise->iId,
6094
                'notifications'
6095
            );
6096
            $exerciseNotification = '';
6097
            if ($extraFieldData && isset($extraFieldData['value'])) {
6098
                $exerciseNotification = $extraFieldData['value'];
6099
            }
6100
6101
            $subject = sprintf(get_lang('Failure on attempt %s at %s'), $attemptCountToSend, $courseInfo['title']);
6102
            if ($exercisePassed) {
6103
                $subject = sprintf(get_lang('Validation of exercise at %s'), $courseInfo['title']);
6104
            }
6105
6106
            if ($exercisePassed) {
6107
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6108
                    $objExercise->iId,
6109
                    'MailSuccess'
6110
                );
6111
            } else {
6112
                $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6113
                    $objExercise->iId,
6114
                    'MailAttempt'.$attemptCountToSend
6115
                );
6116
            }
6117
6118
            // Blocking exercise.
6119
            $blockPercentageExtra = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6120
                $objExercise->iId,
6121
                'blocking_percentage'
6122
            );
6123
            $blockPercentage = false;
6124
            if ($blockPercentageExtra && isset($blockPercentageExtra['value']) && $blockPercentageExtra['value']) {
6125
                $blockPercentage = $blockPercentageExtra['value'];
6126
            }
6127
            if ($blockPercentage) {
6128
                $passBlock = $stats['total_percentage'] > $blockPercentage;
6129
                if (false === $passBlock) {
6130
                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6131
                        $objExercise->iId,
6132
                        'MailIsBlockByPercentage'
6133
                    );
6134
                }
6135
            }
6136
6137
            $extraFieldValueUser = new ExtraFieldValue('user');
6138
6139
            if ($extraFieldData && isset($extraFieldData['value'])) {
6140
                $content = $extraFieldData['value'];
6141
                $content = self::parseContent($content, $stats, $objExercise, $exercise_stat_info, $studentId);
6142
                //if (false === $exercisePassed) {
6143
                if (0 !== $wrongAnswersCount) {
6144
                    $content .= $stats['failed_answers_html'];
6145
                }
6146
6147
                $sendMessage = true;
6148
                if (!empty($exerciseNotification)) {
6149
                    foreach ($notifications as $name => $notificationList) {
6150
                        if ($exerciseNotification !== $name) {
6151
                            continue;
6152
                        }
6153
                        foreach ($notificationList as $notificationName => $attemptData) {
6154
                            if ('student_check' === $notificationName) {
6155
                                $sendMsgIfInList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : '';
6156
                                if (!empty($sendMsgIfInList)) {
6157
                                    foreach ($sendMsgIfInList as $skipVariable => $skipValues) {
6158
                                        $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
6159
                                            $studentId,
6160
                                            $skipVariable
6161
                                        );
6162
6163
                                        if (empty($userExtraFieldValue)) {
6164
                                            $sendMessage = false;
6165
                                            break;
6166
                                        } else {
6167
                                            $sendMessage = false;
6168
                                            if (isset($userExtraFieldValue['value']) &&
6169
                                                in_array($userExtraFieldValue['value'], $skipValues)
6170
                                            ) {
6171
                                                $sendMessage = true;
6172
                                                break;
6173
                                            }
6174
                                        }
6175
                                    }
6176
                                }
6177
                                break;
6178
                            }
6179
                        }
6180
                    }
6181
                }
6182
6183
                // Send to student.
6184
                if ($sendMessage) {
6185
                    MessageManager::send_message($currentUserId, $subject, $content);
6186
                }
6187
            }
6188
6189
            if (!empty($exerciseNotification)) {
6190
                foreach ($notifications as $name => $notificationList) {
6191
                    if ($exerciseNotification !== $name) {
6192
                        continue;
6193
                    }
6194
                    foreach ($notificationList as $attemptData) {
6195
                        $skipNotification = false;
6196
                        $skipNotificationList = isset($attemptData['send_notification_if_user_in_extra_field']) ? $attemptData['send_notification_if_user_in_extra_field'] : [];
6197
                        if (!empty($skipNotificationList)) {
6198
                            foreach ($skipNotificationList as $skipVariable => $skipValues) {
6199
                                $userExtraFieldValue = $extraFieldValueUser->get_values_by_handler_and_field_variable(
6200
                                    $studentId,
6201
                                    $skipVariable
6202
                                );
6203
6204
                                if (empty($userExtraFieldValue)) {
6205
                                    $skipNotification = true;
6206
                                    break;
6207
                                } else {
6208
                                    if (isset($userExtraFieldValue['value'])) {
6209
                                        if (!in_array($userExtraFieldValue['value'], $skipValues)) {
6210
                                            $skipNotification = true;
6211
                                            break;
6212
                                        }
6213
                                    } else {
6214
                                        $skipNotification = true;
6215
                                        break;
6216
                                    }
6217
                                }
6218
                            }
6219
                        }
6220
6221
                        if ($skipNotification) {
6222
                            continue;
6223
                        }
6224
6225
                        $email = isset($attemptData['email']) ? $attemptData['email'] : '';
6226
                        $emailList = explode(',', $email);
6227
                        if (empty($emailList)) {
6228
                            continue;
6229
                        }
6230
                        $attempts = isset($attemptData['attempts']) ? $attemptData['attempts'] : [];
6231
                        foreach ($attempts as $attempt) {
6232
                            $sendMessage = false;
6233
                            if (isset($attempt['attempt']) && $attemptCountToSend !== (int) $attempt['attempt']) {
6234
                                continue;
6235
                            }
6236
6237
                            if (!isset($attempt['status'])) {
6238
                                continue;
6239
                            }
6240
6241
                            if ($blockPercentage && isset($attempt['is_block_by_percentage'])) {
6242
                                if ($attempt['is_block_by_percentage']) {
6243
                                    if ($passBlock) {
6244
                                        continue;
6245
                                    }
6246
                                } else {
6247
                                    if (false === $passBlock) {
6248
                                        continue;
6249
                                    }
6250
                                }
6251
                            }
6252
6253
                            switch ($attempt['status']) {
6254
                                case 'passed':
6255
                                    if ($exercisePassed) {
6256
                                        $sendMessage = true;
6257
                                    }
6258
                                    break;
6259
                                case 'failed':
6260
                                    if (false === $exercisePassed) {
6261
                                        $sendMessage = true;
6262
                                    }
6263
                                    break;
6264
                                case 'all':
6265
                                    $sendMessage = true;
6266
                                    break;
6267
                            }
6268
6269
                            if ($sendMessage) {
6270
                                $attachments = [];
6271
                                if (isset($attempt['add_pdf']) && $attempt['add_pdf']) {
6272
                                    // Get pdf content
6273
                                    $pdfExtraData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6274
                                        $objExercise->iId,
6275
                                        $attempt['add_pdf']
6276
                                    );
6277
6278
                                    if ($pdfExtraData && isset($pdfExtraData['value'])) {
6279
                                        $pdfContent = self::parseContent(
6280
                                            $pdfExtraData['value'],
6281
                                            $stats,
6282
                                            $objExercise,
6283
                                            $exercise_stat_info,
6284
                                            $studentId
6285
                                        );
6286
6287
                                        @$pdf = new PDF();
6288
                                        $filename = get_lang('Test');
6289
                                        $pdfPath = @$pdf->content_to_pdf(
6290
                                            "<html><body>$pdfContent</body></html>",
6291
                                            null,
6292
                                            $filename,
6293
                                            api_get_course_id(),
6294
                                            'F',
6295
                                            false,
6296
                                            null,
6297
                                            false,
6298
                                            true
6299
                                        );
6300
                                        $attachments[] = ['filename' => $filename, 'path' => $pdfPath];
6301
                                    }
6302
                                }
6303
6304
                                $content = isset($attempt['content_default']) ? $attempt['content_default'] : '';
6305
                                if (isset($attempt['content'])) {
6306
                                    $extraFieldData = $exerciseExtraFieldValue->get_values_by_handler_and_field_variable(
6307
                                        $objExercise->iId,
6308
                                        $attempt['content']
6309
                                    );
6310
                                    if ($extraFieldData && isset($extraFieldData['value']) && !empty($extraFieldData['value'])) {
6311
                                        $content = $extraFieldData['value'];
6312
                                    }
6313
                                }
6314
6315
                                if (!empty($content)) {
6316
                                    $content = self::parseContent(
6317
                                        $content,
6318
                                        $stats,
6319
                                        $objExercise,
6320
                                        $exercise_stat_info,
6321
                                        $studentId
6322
                                    );
6323
                                    foreach ($emailList as $email) {
6324
                                        if (empty($email)) {
6325
                                            continue;
6326
                                        }
6327
                                        api_mail_html(
6328
                                            null,
6329
                                            $email,
6330
                                            $subject,
6331
                                            $content,
6332
                                            null,
6333
                                            null,
6334
                                            [],
6335
                                            $attachments
6336
                                        );
6337
                                    }
6338
                                }
6339
6340
                                if (isset($attempt['post_actions'])) {
6341
                                    foreach ($attempt['post_actions'] as $action => $params) {
6342
                                        switch ($action) {
6343
                                            case 'subscribe_student_to_courses':
6344
                                                foreach ($params as $code) {
6345
                                                    $courseInfo = api_get_course_info($code);
6346
                                                    CourseManager::subscribeUser(
6347
                                                        $currentUserId,
6348
                                                        $courseInfo['real_id']
6349
                                                    );
6350
                                                    break;
6351
                                                }
6352
                                                break;
6353
                                        }
6354
                                    }
6355
                                }
6356
                            }
6357
                        }
6358
                    }
6359
                }
6360
            }
6361
        }
6362
    }
6363
6364
    /**
6365
     * Delete an exercise attempt.
6366
     *
6367
     * Log the exe_id deleted with the exe_user_id related.
6368
     *
6369
     * @param int $exeId
6370
     */
6371
    public static function deleteExerciseAttempt($exeId)
6372
    {
6373
        $exeId = (int) $exeId;
6374
6375
        $trackExerciseInfo = self::get_exercise_track_exercise_info($exeId);
6376
6377
        if (empty($trackExerciseInfo)) {
6378
            return;
6379
        }
6380
6381
        $tblTrackExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
6382
        $tblTrackAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
6383
6384
        Database::query("DELETE FROM $tblTrackAttempt WHERE exe_id = $exeId");
6385
        Database::query("DELETE FROM $tblTrackExercises WHERE exe_id = $exeId");
6386
6387
        Event::addEvent(
6388
            LOG_EXERCISE_ATTEMPT_DELETE,
6389
            LOG_EXERCISE_ATTEMPT,
6390
            $exeId,
6391
            api_get_utc_datetime()
6392
        );
6393
        Event::addEvent(
6394
            LOG_EXERCISE_ATTEMPT_DELETE,
6395
            LOG_EXERCISE_AND_USER_ID,
6396
            $exeId.'-'.$trackExerciseInfo['exe_user_id'],
6397
            api_get_utc_datetime()
6398
        );
6399
    }
6400
6401
    public static function scorePassed($score, $total)
6402
    {
6403
        $compareResult = bccomp($score, $total, 3);
6404
        $scorePassed = 1 === $compareResult || 0 === $compareResult;
6405
        if (false === $scorePassed) {
6406
            $epsilon = 0.00001;
6407
            if (abs($score - $total) < $epsilon) {
6408
                $scorePassed = true;
6409
            }
6410
        }
6411
6412
        return $scorePassed;
6413
    }
6414
6415
    /**
6416
     * Returns the HTML for a specific exercise attempt, ready for PDF generation.
6417
     */
6418
    public static function getAttemptPdfHtml(int $exeId, int $courseId, int $sessionId): string
6419
    {
6420
        $_GET = [
6421
            'id'           => $exeId,
6422
            'action'       => 'export',
6423
            'export_type'  => 'all_results',
6424
            'cid'          => $courseId,
6425
            'sid'          => $sessionId,
6426
            'gid'          => 0,
6427
            'gradebook'    => 0,
6428
            'origin'       => '',
6429
        ];
6430
        $_REQUEST = $_GET + $_REQUEST;
6431
6432
        ob_start();
6433
        include __DIR__ . '/../../exercise/exercise_show.php';
6434
        return ob_get_clean();
6435
    }
6436
6437
    /**
6438
     * Generates and saves a PDF for a single exercise attempt
6439
     */
6440
    public static function saveFileExerciseResultPdfDirect(
6441
        int    $exeId,
6442
        int    $courseId,
6443
        int    $sessionId,
6444
        string $exportFolderPath
6445
    ): void {
6446
        // Retrieve the HTML for this attempt and convert it to PDF
6447
        $html = self::getAttemptPdfHtml($exeId, $courseId, $sessionId);
6448
6449
        // Determine filename and path based on user information
6450
        $track   = self::get_exercise_track_exercise_info($exeId);
6451
        $userId  = $track['exe_user_id'] ?? 0;
6452
        $user    = api_get_user_info($userId);
6453
        $pdfName = api_replace_dangerous_char(
6454
            ($user['firstname'] ?? 'user') . '_' .
6455
            ($user['lastname']  ?? 'unknown') .
6456
            '_attemptId' . $exeId . '.pdf'
6457
        );
6458
        $filePath = rtrim($exportFolderPath, '/') . '/' . $pdfName;
6459
6460
        if (file_exists($filePath)) {
6461
            return;
6462
        }
6463
6464
        // Ensure the directory exists
6465
        $dir = dirname($filePath);
6466
        if (!is_dir($dir)) {
6467
            mkdir($dir, 0755, true);
6468
        }
6469
6470
        // Use Chamilo's PDF class to generate and save the file
6471
        $params = [
6472
            'filename'    => $pdfName,
6473
            'course_code' => api_get_course_id(),
6474
        ];
6475
        $pdf = new PDF('A4', 'P', $params);
6476
        $pdf->html_to_pdf_with_template(
6477
            $html,
6478
            true,
6479
            false,
6480
            true,
6481
            [],
6482
            'F',
6483
            $filePath
6484
        );
6485
    }
6486
6487
    /**
6488
     * Exports all results of an exercise to a ZIP archive by generating PDFs on disk and then sending the ZIP to the browser.
6489
     */
6490
    public static function exportExerciseAllResultsZip(
6491
        int   $sessionId,
6492
        int   $courseId,
6493
        int   $exerciseId,
6494
        array $filterDates = [],
6495
        string $mainPath    = ''
6496
    ) {
6497
        // Retrieve all attempt records for this exercise
6498
        $exerciseObj = new Exercise($courseId);
6499
        $results     = $exerciseObj->getExerciseAndResult($courseId, $sessionId, $exerciseId);
6500
        if (empty($results)) {
6501
            Display::addFlash(
6502
                Display::return_message(
6503
                    get_lang('No result found for export in this test.'),
6504
                    'warning',
6505
                    false
6506
                )
6507
            );
6508
            return false;
6509
        }
6510
6511
        // Prepare a temporary folder for the PDFs
6512
        $exportName       = 'S' . $sessionId . '-C' . $courseId . '-T' . $exerciseId;
6513
        $baseDir          = api_get_path(SYS_ARCHIVE_PATH);
6514
        $exportFolderPath = $baseDir . 'pdfexport-' . $exportName;
6515
        if (is_dir($exportFolderPath)) {
6516
            rmdirr($exportFolderPath);
6517
        }
6518
        mkdir($exportFolderPath, 0755, true);
6519
6520
        // Generate a PDF for each attempt
6521
        foreach ($results as $row) {
6522
            $exeId = (int) $row['exe_id'];
6523
            self::saveFileExerciseResultPdfDirect(
6524
                $exeId,
6525
                $courseId,
6526
                $sessionId,
6527
                $exportFolderPath
6528
            );
6529
        }
6530
6531
        // Create the ZIP archive containing all generated PDFs
6532
        $zipFilePath = $baseDir . 'pdfexport-' . $exportName . '.zip';
6533
        $zip = new \ZipArchive();
6534
        if ($zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
6535
            throw new \Exception('Failed to create ZIP file');
6536
        }
6537
        $files = new RecursiveIteratorIterator(
6538
            new RecursiveDirectoryIterator($exportFolderPath),
6539
            RecursiveIteratorIterator::LEAVES_ONLY
6540
        );
6541
        foreach ($files as $file) {
6542
            if (!$file->isDir()) {
6543
                $filePath     = $file->getRealPath();
6544
                $relativePath = substr($filePath, strlen($exportFolderPath) + 1);
6545
                $zip->addFile($filePath, $relativePath);
6546
            }
6547
        }
6548
        $zip->close();
6549
        rmdirr($exportFolderPath);
6550
6551
        // Send the ZIP file to the browser or move it to mainPath
6552
        if (!empty($mainPath)) {
6553
            @rename($zipFilePath, $mainPath . '/pdfexport-' . $exportName . '.zip');
6554
        } else {
6555
            // close session and clear output buffers
6556
            session_write_close();
6557
            while (ob_get_level()) {
6558
                @ob_end_clean();
6559
            }
6560
6561
            // send download headers
6562
            header('Content-Description: File Transfer');
6563
            header('Content-Type: application/zip');
6564
            header('Content-Disposition: attachment; filename="pdfexport-' . $exportName . '.zip"');
6565
            header('Content-Transfer-Encoding: binary');
6566
            header('Expires: 0');
6567
            header('Cache-Control: must-revalidate');
6568
            header('Pragma: public');
6569
            header('Content-Length: ' . filesize($zipFilePath));
6570
6571
            // output the file and remove it
6572
            readfile($zipFilePath);
6573
            @unlink($zipFilePath);
6574
            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...
6575
        }
6576
    }
6577
6578
    /**
6579
     * Calculates the overall score for Combination-type questions.
6580
     */
6581
    public static function getUserQuestionScoreGlobal(
6582
        int   $answerType,
6583
        array $listCorrectAnswers,
6584
        int   $exeId,
6585
        int   $questionId,
6586
        float $questionWeighting,
6587
        array $choice = [],
6588
        int $nbrAnswers = 0
6589
    ): float
6590
    {
6591
        $nbrCorrect = 0;
6592
        $nbrOptions = 0;
6593
        $choice = is_array($choice) ? $choice : [];
6594
        switch ($answerType) {
6595
            case FILL_IN_BLANKS_COMBINATION:
6596
                if (!empty($listCorrectAnswers)) {
6597
                    if (!empty($listCorrectAnswers['student_score']) && is_array($listCorrectAnswers['student_score'])) {
6598
                        foreach ($listCorrectAnswers['student_score'] as $val) {
6599
                            if ((int) $val === 1) {
6600
                                $nbrCorrect++;
6601
                            }
6602
                        }
6603
                    }
6604
                    if (!empty($listCorrectAnswers['words_count'])) {
6605
                        $nbrOptions = (int) $listCorrectAnswers['words_count'];
6606
                    } elseif (!empty($listCorrectAnswers['words']) && is_array($listCorrectAnswers['words'])) {
6607
                        $nbrOptions = count($listCorrectAnswers['words']);
6608
                    }
6609
                }
6610
                break;
6611
6612
            case HOT_SPOT_COMBINATION:
6613
                if (!empty($listCorrectAnswers) && is_array($listCorrectAnswers) && is_array($choice)) {
6614
                    foreach ($listCorrectAnswers as $idx => $val) {
6615
                        if (isset($choice[$idx]) && (int) $choice[$idx] === 1) {
6616
                            $nbrCorrect++;
6617
                        }
6618
                    }
6619
                } else {
6620
                    $TBL_TRACK_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
6621
                    $exeIdEsc = Database::escape_string($exeId);
6622
                    $qIdEsc   = Database::escape_string($questionId);
6623
                    $sql = "SELECT COUNT(hotspot_id) AS ct
6624
                        FROM $TBL_TRACK_HOTSPOT
6625
                        WHERE hotspot_exe_id = '$exeIdEsc'
6626
                          AND hotspot_question_id = '$qIdEsc'
6627
                          AND hotspot_correct = 1";
6628
                    $result = Database::query($sql);
6629
                    $nbrCorrect = (int) Database::result($result, 0, 0);
6630
                }
6631
                $nbrOptions = (int) $nbrAnswers;
6632
                break;
6633
6634
            case MATCHING_COMBINATION:
6635
            case MATCHING_DRAGGABLE_COMBINATION:
6636
                if (isset($listCorrectAnswers['form_values'])) {
6637
                    if (isset($listCorrectAnswers['form_values']['correct'])) {
6638
                        $nbrCorrect = count($listCorrectAnswers['form_values']['correct']);
6639
                        $nbrOptions = (int) $listCorrectAnswers['form_values']['count_options'];
6640
                    }
6641
                } else {
6642
                    if (isset($listCorrectAnswers['from_database'])) {
6643
                        if (isset($listCorrectAnswers['from_database']['correct'])) {
6644
                            $nbrCorrect = count($listCorrectAnswers['from_database']['correct']);
6645
                            $nbrOptions = (int) $listCorrectAnswers['from_database']['count_options'];
6646
                        }
6647
                    }
6648
                }
6649
                break;
6650
        }
6651
6652
        $questionScore = 0.0;
6653
        if ($nbrOptions > 0 && $nbrCorrect === $nbrOptions) {
6654
            $questionScore = (float) $questionWeighting;
6655
        }
6656
6657
        return $questionScore;
6658
    }
6659
}
6660